実用的なhookモジュールを持ってきて


hookモードは私たちの日常のコードでよく使われています.例えば、一連のイベントを定義し、必要に応じてこれらのイベントの処理関数を1つずつ処理するように通知します.
本質的にhookモードは責任チェーンモードと類似しており、hookは特定のイベントとイベントハンドラの間の一対の多関係を確立し、イベントが処理する必要がある場合、イベントハンドラチェーンに沿って1つずつ実行する.しかし、異なる設計では異なる処理動作がある可能性があり、本明細書では、チェーンの前の処理プログラムは、次の処理プログラムに渡して処理する必要があるかどうか、または最終結果を直接返す必要があるかどうかを決定する権利がある.
ここでは開閉の原則に従います.
「拡張」、「修正」の順に選択します.
これは、hookモードでは、新しいイベント処理のニーズがあれば、hookフレームワークを変更する必要がなく、イベント処理関数を登録して既定の動作で処理すればよいことを意味します.
次に、hookフレームワークの作成方法とどのような機能インタフェースがあるかを見てみましょう.
function createHook() {
  const _hooks = {};
  
  function register(key, fn) {...}
  
  function isRegistered(key) {...}
  
  function syncExec(key, ...args) {...}
  
  function asyncExec(key, ...args) {...}
  
  function unregisterAll(key) {...}
  
  return {
    register,
    isRegistered,
    syncExec,
    asyncExec,
    unregisterAll
  };
}

以上がこのhookフレームワークの概要であり、const hook = createHook();を使用してhookオブジェクトのインスタンスを作成して使用します.次に、各機能の実装と使用を逐一分析します.

register(key, fn)


この関数の役割は、イベント処理を登録することです.keyはイベント名文字列であり、fnはイベント処理関数です.ここで注意しなければならないのはfnが高次関数である必要があり、フォーマットは以下の通りである.
const fn = function (next) {
  return function (...args) {
    return next({...});
    // return {...}
  }
};

ここに注入されるnextは、nextを呼び出すと次の処理関数処理が呼び出され、必要でなければ処理結果を直接返すことができるので、位置の前の処理関数に選択権があります.
register関数に戻ります.
function register(key, fn) {
  if (!_hooks[key]) {
    _hooks[key] = [];
  }
  _hooks[key].push(fn);

  return function unregister() {
    const fns = _hooks[key];
    const idx = fns.indexOf(fn);
    fns.splice(idx, 1);
    if (fns.length === 0) {
      delete _hooks[key];
    }
  };
}

登録ロジックは簡単で、_hooksはイベント名とイベント処理関数の関係を記録するオブジェクトであり、複数あり、登録順に実行されるため、各key属性は配列である.
ここで注目すべきは、register関数は、fnの参照を記録する必要がなく、後でログアウトする必要がある場合はunregisterを保持し、適切なときに使用することを目的として、イベント処理をログアウトするためのunregister関数を返します.

isRegistered(key)


isRegisteredは、イベントに対応する処理関数があるかどうかを判断するためのもので、内容は簡単です.
function isRegistered(key) {
  return (_hooks[key] || []).length > 0;
}

syncExec(key, ...args)


syncExecは、あるイベントを同期して実行するための処理関数チェーンであり、処理結果を返します.使用例は次のとおりです.
const hook = createHook();

//        
hook.register('process-test', function (next) {
  return function (num) {
    return next(num + 1);
  };
});
//        
hook.register('process-test', function (next) {
  return function (num) {
    return next(num + 2);
  };
});

const rst = hook.syncExec('process-test', 1);
// rst      4

例に示すように、各処理関数はnextによって自分の処理結果を次の処理関数に渡すか、直接returnの最終結果を渡す.
次にsyncExecの具体的な実装を見てみましょう.
function syncExec(key, ...args) {
  const fns = (_hooks[key] || []).slice();

  let idx = 0;
  const next = function (...args) {
    if (idx >= fns.length) {
      return (args.length > 1 ? args : args[0]);
    } else {
      const fn = fns[idx++].call(this, next.bind(this));
      return fn.apply(this, args);
    }
  };

  return next.apply(this, args);
}

まず,あるkeyに対応する処理関数配列を取得する際にslice()を用いてコピーすることに注目すべきであり,これは実行中に配列が変化することを防止するためである.次にnext関数を構築してイベント処理関数に注入します(イベント処理関数が高次関数であることを覚えていますか?)、次に実行するイベント処理関数のインデックスidxを閉パケットで記録する.そして、最初のnextの呼び出しにより、処理チェーン全体が動作し始めます.
ここでは、結果を返すフォーマット処理という特殊な論理があります.nextが複数のパラメータを渡す場合、返される結果は配列であり、すべてのパラメータを含む.nextが単一のパラメータを渡している場合、返される結果はそのパラメータです.

asyncExec(key, ...args)


同期実行がある以上、非同期実行が必要です.asyncExecの使用例は次のとおりです.
const hook = createHook();

hook.register('process-test', function (next) {
  return function (obj) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(next({ count: obj.count + 1 }));
      }, 100);
    });
  };
});
hook.register('process-test', function (next) {
  return function (obj) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(next({ count: obj.count + 2 }));
      }, 100);
    });
  };
});

hook.asyncExec('process-test', { count: 66 }).then(rst => {
  console.log(rst);
  // rst === 69
});

イベント処理関数を非同期で実行する必要がある場合、asyncExecを呼び出して処理することができ、イベント処理関数では、上記の最初の処理関数のような同期結果が返されてもreturn next({ count: obj.count + 1 });を実行することができる.
次にasyncExecがどのように実現されているかを見てみましょう.
function asyncExec(key, ...args) {
  const fns = (_hooks[key] || []).slice();
  let idx = 0;

  const next = function (...args) {
    if (idx >= fns.length) {
      return Promise.resolve(args.length > 1 ? args : args[0]);
    } else {
      try {
        const fn = fns[idx++].call(this, next.bind(this));
        const rst = fn.apply(this, args);
        return Promise.resolve(rst);
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };

  return next.apply(this, args);
}

ここでの核心思想はresolveのパラメータがPromiseであれば、元のPromiseの状態は奥のPromiseによって決まり、慣れていない子供靴はPromiseを復習することができます.ここでもう一つ注意すべき点は,イベント処理関数が実行されると投げ出される異常を処理することである.そして結果を返す処理についてもsyncExecと同様の論理である.

unregisterAll(key)


最後のunregisterAllは簡単です.あるイベントのすべての処理関数をログアウトします.
function unregisterAll(key) {
  delete _hooks[key];
}

まとめ


以上がhookモジュール全体の解析であり,ソースコードはここにある.より詳細な使用例は、ユニットテストを参照してください.
「なぜHookをClassと書かずにビジネスシステムでhookを継承して使うのか、これでHook機能をビジネス対象に継承できるのではないか」と聞く子供靴もあると思います.
ここでは主に別の設計原則に従うことを考慮している:多用組合せ、少用継承.コンビネーション方式を使用すると、より大きな弾力性がある場合があります.hookを自分のビジネスオブジェクトに継承したい場合は、簡単に次のようにすることができます.
const app = {...};
const hook = createHook();

// mixin
Object.assign(app, hook);

本編に疑問やアドバイスがあれば、ここで提出してください.
starと私のJSブログへようこそ:Javascriptより小声で