JS 中的钩子(Hook)实现
1. Hook 简介
Hook 这个词很多人都听过,就算不知道,也直接或间接地用到过。它通常是系统或框架开放出来供上层应用开发者执行代码的一种方式。例如,Vue 的生命周期钩子,本质就是框架内部在对应时机调用了组件定义的钩子函数;此外,Webpack 所使用 tapable
更是将 hook
的应用发挥的淋漓尽致,tapable
最值得称赞的就是,它对 hook
做了很好的抽象和分类。
2. Hook 的表现形式
对于开发者,Hook 通常以钩子函数形式存在。开发者注册钩子函数,系统或者框架决定在什么时候调用钩子函数,某种意义上,它和事件回调函数有异曲同工之妙。从数据结构的设计上,我们可以使用键值对(散列表,JS中的普通对象)来表示系统提供的钩子,其中,键代表钩子名称,值是钩子函数数组。
例如,下面的代码包含一个init
钩子,这个钩子注册了3个钩子函数:fn1, fn2, fn3
。
const hooks = {
init: [fn1, fn2, fn3]
}
如果我们将 Hook 看作是一种和栈
,队列
一样的抽象数据类型(ADT),那么 Hook 的操作集合包含注册(Register)
和 调用(Call)
。简单实现就是:
// 注册钩子
function regHook(hookName, hookFn) {
if (!hooks[hookName]) hooks[hookName] = []
hooks[hookName].push(hookFn)
}
// 调用钩子
function callHook(hookName, ...args) {
hooks[hookName].forEach(fn => fn(...args))
}
3. Hook 的分类
3.1 串行和并行
根据钩子函数的执行顺序,可以分为:
- 串行钩子:根据注册顺序调用钩子,后面的钩子必须等到前面的钩子执行完后才能调用,串行钩子可以是同步的,也可以是异步的
- 并行钩子:按顺序调用钩子,但可同时执行,即后面的钩子不用等到前面的钩子执行完成,显然,并行钩子必须是异步的
3.2 同步和异步
根据钩子函数的执行方式,可以分为:
- 同步钩子: 钩子执行会阻塞主线程,钩子函数返回即代表钩子执行结束
-
异步钩子: 钩子执行不会阻塞主线程,钩子函数返回不代表钩子执行结束,需要使用回调函数或者使用
promise.then
来获取钩子执行结束的消息
4. Hook 调用
注册钩子比较简单,只需将钩子函数按顺序加入钩子函数数组即可。而调用钩子,需要根据钩子类型来采取不同调用方法。
4.1 同步钩子的调用
同步钩子的调用是最简单,按顺序调用一遍钩子函数即可,并且只有串行执行。
function callSync(hookName, ...args) {
hooks[hookName].forEach(fn => fn(...args))
}
4.2 异步钩子的调用
异步钩子的调用要分为串行和并行。
4.2 异步串行
4.2.1 基于回调函数
function callAsyncSeries(hookName, done, ...args) {
const fns = hooks[hookName]
let i = fns.length
let next = done
while(i) {
let fn = fns[--i]
let _next = next
next = () => fn(...args, _next)
}
next()
}
使用示例:
regHook('asyncSer', (a, b, done) => { setTimeout(() => { console.log('timout 1', a, b); done() }, 1000) })
regHook('asyncSer', (a, b, done) => { setTimeout(() => { console.log('timout 2', a, b); done() }, 2000) })
regHook('asyncSer', (a, b, done) => { setTimeout(() => { console.log('timout 3', a, b); done() }, 3000) })
callAsyncSeries('asyncSer', () => { console.log('done') }, 'aa', 'bb')
// timout 1 aa bb
// timout 2 aa bb
// timout 3 aa bb
// done
4.2.2 基于 Promise
function callPromiseSeries(hookName, ...args) {
return new Promise(resolve => {
const fns = hooks[hookName]
let i = fns.length
let next = resolve
while(i) {
let fn = fns[--i]
let _next = next
next = () => fn(...args).then(_next)
}
next()
})
}
使用示例:
regHook('promiseSer', (a, b) => {
return new Promise(resolve =>
setTimeout(() => { console.log('promiseSer 1', a, b); resolve() }, 2000)
)
})
regHook('promiseSer', (a, b) => {
return new Promise(resolve =>
setTimeout(() => { console.log('promiseSer 2', a, b); resolve() }, 3000)
)
})
regHook('promiseSer', (a, b) => {
return new Promise(resolve =>
setTimeout(() => { console.log('promiseSer 3', a, b); resolve() }, 1000)
)
})
callPromiseSeries('promiseSer', 'aa', 'bb').then(() => { console.log('done') })
// promiseSer 1 aa bb
// promiseSer 2 aa bb
// promiseSer 3 aa bb
// done
4.3 异步并行钩子的调用
4.3.1 基于回调函数
function callAsyncParallel(hookName, done, ...args) {
const fns = hooks[hookName]
let count = fns.length
let _done = () => {
count--
if (count === 0) {
done()
}
}
fns.forEach(fn => fn(...args, _done))
}
// 限制并发数
function callAsyncParallelN(hookName, done, N, ...args) {
const fns = hooks[hookName]
let count = fns.length
let cur = 0
let limit = N < fns.length ? N : fns.length
let _done = () => {
count--
if (count === 0) {
done()
} else if (cur < fns.length) {
fns[cur++](...args, _done)
}
}
for (; cur < limit; cur++) {
fns[cur](...args, _done)
}
}
使用示例:
regHook('asyncParallel', (a, b, done) => { setTimeout(() => { console.log('asyncParallel 1', a, b); done() }, 1000) })
regHook('asyncParallel', (a, b, done) => { setTimeout(() => { console.log('asyncParallel 2', a, b); done() }, 1000) })
regHook('asyncParallel', (a, b, done) => { setTimeout(() => { console.log('asyncParallel 3', a, b); done() }, 1000) })
callAsyncParallel('asyncParallel', () => { console.log('done') }, 'aa', 'bb')
callAsyncParallelN('asyncParallel', () => { console.log('done') }, 2, 'aa', 'bb')
4.3.2 基于 Promise
function callPromiseParallel(hookName, ...args) {
return new Promise(resolve => {
const fns = hooks[hookName]
let count = fns.length
let _done = () => {
count--
if (count === 0) {
resolve()
}
}
fns.forEach(fn => fn(...args).then(_done))
})
}
// 限制并发数
function callPromiseParallelN(hookName, N, ...args) {
return new Promise(resolve => {
const fns = hooks[hookName]
let count = fns.length
let cur = 0
let limit = N < fns.length ? N : fns.length
let _done = () => {
count--
if (count === 0) {
resolve()
} else {
if (cur < fns.length) {
fns[cur++](...args).then(_done)
}
}
}
for (; cur < limit; cur++) {
fns[cur](...args).then(_done)
}
})
}
使用示例:
regHook('promiseParallel', (a, b) => {
return new Promise(resolve =>
setTimeout(() => { console.log('promiseParallel 1', a, b); resolve() }, 1000)
)
})
regHook('promiseParallel', (a, b) => {
return new Promise(resolve =>
setTimeout(() => { console.log('promiseParallel 2', a, b); resolve() }, 1000)
)
})
regHook('promiseParallel', (a, b) => {
return new Promise(resolve =>
setTimeout(() => { console.log('promiseParallel 3', a, b); resolve() }, 1000)
)
})
callPromiseParallel('promiseParallel', 'aa', 'bb').then(() => { console.log('done') })
callPromiseParallelN('promiseParallel', 2, 'aa', 'bb').then(() => { console.log('done') })
5. 代码封装
5.1 同步钩子
class Hook {
constructor() {
this.hookFns = []
}
reg(fn) {
this.hookFns.push(fn)
}
call(...args) {
this.hookFns.forEach(fn => fn(...args))
}
}
5.2 异步回调钩子
class AsyncHook extends Hook {
call(...args, done) {
const fns = this.hookFns
let i = fns.length
let next = done
while(i) {
let fn = fns[--i]
let _next = next
next = () => fn(...args, _next)
}
next()
}
callParallel(...args, done) {
const fns = this.hookFns
let count = fns.length
let _done = () => {
count--
if (count === 0) {
done()
}
}
fns.forEach(fn => fn(...args, _done))
}
callParallelN(...args, done) {
const fns = this.hookFns
let count = fns.length
let cur = 0
let limit = N < fns.length ? N : fns.length
let _done = () => {
count--
if (count === 0) {
done()
} else if (cur < fns.length) {
fns[cur++](...args, _done)
}
}
for (; cur < limit; cur++) {
fns[cur](...args, _done)
}
}
}
5.3 异步 Promise 钩子
class PromiseHook extends Hook {
call(...args) {
return new Promise(resolve => {
const fns = this.hookFns
let i = fns.length
let next = resolve
while(i) {
let fn = fns[--i]
let _next = next
next = () => fn(...args).then(_next)
}
next()
})
}
callParallel(...args) {
return new Promise(resolve => {
const fns = this.hookFns
let count = fns.length
let _done = () => {
count--
if (count === 0) {
resolve()
}
}
fns.forEach(fn => fn(...args).then(_done))
})
}
callParallelN(...args) {
return new Promise(resolve => {
const fns = this.hookFns
let count = fns.length
let cur = 0
let limit = N < fns.length ? N : fns.length
let _done = () => {
count--
if (count === 0) {
resolve()
} else {
if (cur < fns.length) {
fns[cur++](...args).then(_done)
}
}
}
for (; cur < limit; cur++) {
fns[cur](...args).then(_done)
}
})
}
}
上一篇: 理解HTTP方法(GET/POST)与g Hook函数的运用
下一篇: Lua 模块与包 |
推荐阅读
-
实现分布式锁的一般方法有哪些?如何使用 Redis 设计分布式锁?可以使用 zk 设计分布式锁吗?这两种实现分布式锁的方法中,哪一种更有效?
-
Nest JS 权限控制 rabc 0 的实现
-
Java Swing 中实现为窗体添加背景图像的两种方法说明
-
使用 ganache 实现 Web3js 与区块链交互的步骤及问题解决方案:未找到命令 "express "等。
-
请求幂等性的 java 实现 java 中的幂等性
-
在多种不同的 Common Lisp 实现(CLISP、CCL、SBCL)中执行 shell 命令的功能
-
js 实现输入的 ID 号,以得出生日、年龄和性别
-
在 Mac 主机上的 Docker 容器中实现图形用户界面显示(运行图形用户界面应用程序)
-
任何前端都无法抗拒的炫酷效果,带你一步步用 Three.js Shader 实现!
-
JavaScript 在各种源代码中的实现(前端面试笔试必备)