Inside unjs/hookable
•4 min read
參考版本為 v5.5.3
hookable 為 UnJS 團隊釋出的套件,我認為其優點有:
- 實作 typesafe awaitable hooking system 較輕鬆(例如:不用自己處理 generic constraints)
- 可以參考 Nuxt, UnJS 團隊,在其他專案如何使用、設計,像是:nuxt, unhead, nitro
不過若是沒有非同步需求,且有效能考量,event emitter 或許是比較好的選擇,這部分可以參考 issue#64
Internal data structure
hookable 實作上,是用 object 儲存 hook, hook callbacks 之間一對多的關係
type HookCallback = (...arguments_: any) => Promise<void> | void;
class Hookable<
HooksT extends Record<string, any> = Record<string, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>
> {
private _hooks: { [key: string]: HookCallback[] };
// skip
constructor() {
this._hooks = {};
// skip
}
hook<NameT extends HookNameT>(
name: NameT,
function_: InferCallback<HooksT, NameT>,
options: { allowDeprecated?: boolean } = {}
) {
// skip
this._hooks[name] = this._hooks[name] || [];
this._hooks[name].push(function_);
// skip
}
// skip
}
Awaitable hook callbacks
簡單說明下方程式碼:
foo
,bar
為回傳 promise 的 asynchronous function- 註冊
foo
,bar
至hello
hook - 調用
await callHook('hello')
查看 log 順序 - 從 log 順序可以得知,hook callback 會依照順序,等到當前的 promise resolved,再執行下一個 callback
function foo() {
return new Promise((resolve) => {
console.log('foo started');
setTimeout(() => {
console.log('foo resolved');
resolve();
}, 200);
});
}
function bar() {
return new Promise((resolve) => {
console.log('bar started');
setTimeout(() => {
console.log('bar resolved');
resolve();
}, 200);
});
}
const hooks = createHooks();
hooks.hook('hello', foo);
hooks.hook('hello', bar);
async function run() {
await hooks.callHook('hello');
console.log('hook finished');
}
run();
// "foo started"
// "foo resolved"
// "bar started"
// "bar resolved"
// "hook finished"
對應到 hookable 實作,當調用 hooks.callHook(event)
,最終是透過 serialTaskCaller(this.hooks[name], arguments_)
來執行 hook callbacks
class Hookable<
HooksT extends Record<string, any> = Record<string, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>
> {
private _hooks: { [key: string]: HookCallback[] };
// skip
constructor() {
this._hooks = {};
// skip
}
hook<NameT extends HookNameT>(
name: NameT,
function_: InferCallback<HooksT, NameT>,
options: { allowDeprecated?: boolean } = {}
) {
// skip
this._hooks[name] = this._hooks[name] || [];
this._hooks[name].push(function_);
// skip
}
// skip
callHook<NameT extends HookNameT>(
name: NameT,
...arguments_: Parameters<InferCallback<HooksT, NameT>>
): Promise<any> {
arguments_.unshift(name);
return this.callHookWith(serialTaskCaller, name, ...arguments_);
}
callHookWith<
NameT extends HookNameT,
CallFunction extends (
hooks: HookCallback[],
arguments_: Parameters<InferCallback<HooksT, NameT>>
) => any
>(
caller: CallFunction,
name: NameT,
...arguments_: Parameters<InferCallback<HooksT, NameT>>
): ReturnType<CallFunction> {
// skip
const result = caller(
name in this._hooks ? [...this._hooks[name]] : [],
arguments_
);
// skip
return result;
}
}
接著看到 serialTaskCaller
,為了讓非同步的 log 更好被追蹤,在調用 hook callbacks 前,先透過 console.createTask 建立關聯。第 2-6 行的 createTask
是為了兼容不同瀏覽器或較舊的版本,因為 console.createTask
可能不存在,更多細節可參考 pull#69
// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces
type CreateTask = typeof console.createTask;
const defaultTask: ReturnType<CreateTask> = { run: (function_) => function_() };
const _createTask: CreateTask = () => defaultTask;
const createTask =
typeof console.createTask !== "undefined" ? console.createTask : _createTask;
function serialTaskCaller(hooks: HookCallback[], args: any[]) {
const name = args.shift();
const task = createTask(name);
// skip
}
serialTaskCaller
調用的 hook callbacks 的流程如下:
- 第一個 hook callback 是透過
Promise.resolve
產生一個已實現的 promise - 委派 fulfill callback 給已實現的 promise,在 fufill callback 內會調用下一個 hook callback
hooks.reduce
會回傳最後一個 hook callback 對應的 promise,當最後一個 promise 的狀態變更為已實現,即代表所有 hook callbacks 依序執行完成
// skip
function serialTaskCaller(hooks: HookCallback[], args: any[]) {
const name = args.shift();
const task = createTask(name);
// eslint-disable-next-line unicorn/no-array-reduce
return hooks.reduce(
(promise, hookFunction) =>
promise.then(() => task.run(() => hookFunction(...args))),
Promise.resolve()
);
}