Webpack源码解析
// 使用webpack版本
"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
Webpack 与 Tapable
Webpack的构建过程分为配置初始化、内容编译、输出编译后的内容,这三个过程可以看做成一个事件驱动型的事件流工作机制,通过事件将一个个构建任务串联起来,而实现这一切的就是 Tapable。Webpack 的两大核心模块负责编译的 Compiler 和负责创建bundles的 Compilation 都是 Tapable 的子类,并且实例内部的生命周期也是通过 Tapable 库提供的钩子类实现的:
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
emit: new AsyncSeriesHook(["compilation"]),
assetEmitted: new AsyncSeriesHook(["file", "content"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
contextModuleFactory: new SyncHook(["contextModulefactory"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
watchRun: new AsyncSeriesHook(["compiler"]),
failed: new SyncHook(["error"]),
invalid: new SyncHook(["filename", "changeTime"]),
watchClose: new SyncHook([]),
infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
environment: new SyncHook([]),
afterEnvironment: new SyncHook([]),
afterPlugins: new SyncHook(["compiler"]),
afterResolvers: new SyncHook(["compiler"]),
entryOption: new SyncBailHook(["context", "entry"]),
};
}
}
Tapable是什么
这个小型库是 webpack 的一个核心工具,但也可用于其他地方, 以提供类似的插件接口。 在 webpack 中的许多对象都扩展自 Tapable 类。 它对外暴露了 tap,tapAsync 和 tapPromise 等方法, 插件可以使用这些方法向 webpack 中注入自定义构建的步骤,这些步骤将在构建过程中触发。
首先 Tapable
是一个库,它是实现 webpack
插件化的核心,从 Compiler
内部可以看出 Tapable
提供的方法都是用于其生命周期实例化的,因此也可以称之为钩子类 HOOK。
HOOK 本质是 Tapable 实例对象,它可以分为 同步钩子 和 异步钩子 两大类,异步钩子又可以分为并行和串行两类。
Tapable导出的钩子类有:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
AsyncParallelHook,
AsyncParallelBailHook,
} = require('tapable');
hook按事件回调的运行机制分为四种:
- Hook : 普通钩子 , 监听器之间互相独立不干扰
- BailHook: 熔断钩子, 某个监听返回非 undefined 时后续不执行
- WaterfallHook: 漫布钩子 , 如果上一个回调的返回值不为undefined,那么就将下一个的回调的第一个参数替换为这个值
- LoopHook: 循环钩子 , 如果当前未返回 undefined 则一直执行
hook按触发事件的方式分:
类型 | 描述 |
---|---|
Sync | Sync开头的Hook类只能用 tap 方法注册事件回调,这类事件回调会同步执行;如果使用 tapAsync 或者 tapPromise 方法注册会报错,SyncHook重写了Hook |
AsyncSeries | Async 开头的 Hook 类,没法用 call 方法触发事件,必须用 callAsync 或者 promise 方法触发;这两个方法都能触发 tap 、 tapAsync 和 tapPromise 注册的事件回调。AsyncSeries 按照顺序执行,当前事件回调如果是异步的,那么会等到异步执行完毕才会执行下一个事件回调。 |
AsyncParalle | 而 AsyncParalle 会并行执行所有的事件回调。 |
同步钩子的使用
1. SyncHook
基础钩子,并不关心回调的内部具体实现,只是单纯地执行回调:
// SyncHook.js
const { SyncHook } = require('tapable')
const hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function(name, age) {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
hook.tap('fn2', function(name, age) {
console.log(`fn2 ---> ${name} is ${age} years old.`)
return name + 'fun2'
})
hook.tap('fn3', function(name, age) {
console.log(`fn3 ---> ${name} is ${age} years old.`)
})
hook.call('jack', 12)
fn1 ---> jack is 12 years old.
fn2 ---> jack is 12 years old.
fn3 ---> jack is 12 years old.
由此可见 BasicHook 的回调都是独立的,互不干扰。在注册事件回调时,配置对象有两个可以改变执行顺序的属性:
stage
stage
可以设置为一个数字,值越大执行顺序就越靠后:
// SyncHookStage.js
const { SyncHook } = require('tapable')
const hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function(name, age) {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
hook.tap({
name: 'fn2',
stage: 8,
}, function(name, age) {
console.log(`fn2 ---> ${name} is ${age} years old.`)
})
hook.tap({
name: 'fn3',
stage: 6,
}, function(name, age) {
console.log(`fn3 ---> ${name} is ${age} years old.`)
})
hook.tap('fn4', function(name, age) {
console.log(`fn4 ---> ${name} is ${age} years old.`)
})
hook.call('jack', 12)
fn1 ---> jack is 12 years old.
fn4 ---> jack is 12 years old.
fn3 ---> jack is 12 years old.
fn2 ---> jack is 12 years old.
before
顾名思义,before
就是配置其在某个任务之前执行,它的值是一个字符串,是其它的任务名:
// SyncHookBefore.js
const { SyncHook } = require('tapable')
const hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function(name, age) {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
hook.tap('fn2', function(name, age) {
console.log(`fn2 ---> ${name} is ${age} years old.`)
})
hook.tap({
name: 'fn3',
before: 'fn2'
}, function(name, age) {
console.log(`fn3 ---> ${name} is ${age} years old.`)
})
hook.call('jack', 12)
fn1 ---> jack is 12 years old.
fn3 ---> jack is 12 years old.
fn2 ---> jack is 12 years old.
2. SyncBailHook
保险类型的钩子,当上一个回调函数的返回值不为 undefined
时,后续注册的回调函数不再执行:
// SyncBailHook.js
const { SyncBailHook } = require('tapable')
const hook = new SyncBailHook(['name', 'age'])
hook.tap('fn1', (name, age) => {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
hook.tap('fn2', (name, age) => {
console.log(`fn2 ---> ${name} is ${age} years old.`)
return undefined
})
hook.tap('fn3', (name, age) => {
console.log(`fn3 ---> ${name} is ${age} years old.`)
return name + '3'
})
hook.tap('fn4', (name, age) => {
console.log(`fn4 ---> ${name} is ${age} years old.`)
})
hook.call('tom', 8)
fn1 ---> tom is 8 years old.
fn2 ---> tom is 8 years old.
fn3 ---> tom is 8 years old.
3. SyncWaterfallHook
瀑布流hook,上一个函数的返回结果不为undefined,那就将其作为下一个函数的第一个参数:
// SyncWaterfallHook.js
const { SyncWaterfallHook } = require('tapable')
const hook = new SyncWaterfallHook(['name', 'age'])
hook.tap('fn1', (name, age) => {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
hook.tap('fn2', (name, age) => {
console.log(`fn2 ---> ${name} is ${age} years old.`)
return 'jack'
})
hook.tap('fn3', (name, age) => {
console.log(`fn3 ---> ${name} is ${age} years old.`)
return 'lucy'
})
hook.tap('fn4', (name, age) => {
console.log(`fn4 ---> ${name} is ${age} years old.`)
})
hook.call('tom', 8)
fn1 ---> tom is 8 years old.
fn2 ---> tom is 8 years old.
fn3 ---> jack is 8 years old.
fn4 ---> lucy is 8 years old.
即使返回一个错误,也会将错误的message作为参数:
// SyncWaterfallHook.js
...
hook.tap('fn3', (name, age) => {
console.log(`fn3 ---> ${name} is ${age} years old.`)
return new Error('fn3 error')
})
...
fn4 ---> Error: fn3 error is 8 years old.
4. SyncLoopHook
Loop 类型的钩子类在当前执行的事件回调的返回值不是 undefined 时,会重新从第一个注册的事件回调处执行,直到当前执行的事件回调没有返回值才会执行后面的回调函数:
// SyncLoopHook.js
const { SyncLoopHook } = require('tapable')
const hook = new SyncLoopHook(['name', 'age'])
let count1 = 0
let count2 = 0
let count3 = 0
hook.tap('fn1', (name, age) => {
console.log(`fn1 ---> ${name} is ${age} years old.`)
if (++count1 === 1) {
count1 = 0
return undefined
}
return true
})
hook.tap('fn2', (name, age) => {
console.log(`fn2 ---> ${name} is ${age} years old.`)
if (++count2 === 2) {
count2 = 0
return undefined
}
return true
})
hook.tap('fn3', (name, age) => {
console.log(`fn3 ---> ${name} is ${age} years old.`)
})
hook.call('lucy', 10)
fn1 ---> lucy is 10 years old.
fn2 ---> lucy is 10 years old.
fn1 ---> lucy is 10 years old.
fn2 ---> lucy is 10 years old.
fn3 ---> lucy is 10 years old.
hook.tap 注册回调事件时会调用hook的 _tap
方法,经过一些内部处理,将回调填入 tabs
中:
[
{
type: "sync",
fn: (name, age) => {
console.log(`fn1 ---> ${name} is ${age} years old.`)
if (++count1 === 1) {
count1 = 0
return undefined
}
return true
},
name: "fn1",
},
{
type: "sync",
fn: (name, age) => {
console.log(`fn2 ---> ${name} is ${age} years old.`)
if (++count2 === 2) {
count2 = 0
return undefined
}
return true
},
name: "fn2",
},
{
type: "sync",
fn: (name, age) => {
console.log(`fn3 ---> ${name} is ${age} years old.`)
},
name: "fn3",
},
]
我们看一下 call
方法做了什么:
// Hook.js
const CALL_DELEGATE = function(...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
// <anonymous> (<eval>/VM46947590:3)
// this.call 调用堆栈
(function anonymous(name, age) {
"use strict";
var _context;
// tags 中的回调函数fn
var _x = this._x;
// 定义一个变量判断是否需要进行循环调用
var _loop;
do {
// 初始值设为false
_loop = false;
// 取出第一个回调函数执行
var _fn0 = _x[0];
// 接收第一个函数返回值
var _result0 = _fn0(name, age);
// 如果第一个函数的返回值不为 undefined,就设置循环调用为 true
// 继续调用第一个回调函数
if (_result0 !== undefined) {
_loop = true;
} else {
// 否则就取出第二个回调函数执行
var _fn1 = _x[1];
var _result1 = _fn1(name, age);
// 第二个函数的返回结果不为 undefined,如上
if (_result1 !== undefined) {
_loop = true;
} else {
var _fn2 = _x[2];
var _result2 = _fn2(name, age);
if (_result2 !== undefined) {
_loop = true;
} else {
if (!_loop) {
}
}
}
}
} while (_loop);
});
从上面执行栈中的代码可以看出,标识 _loop
取决于上一个回调函数的执行结果,为 true 时不再执行后续的回调函数,而是循环执行上面的回调函数,直到返回结果为 undefined 为止。
异步钩子使用
异步钩子按照执行回调的机制分为 并行 和 串行 两种,我们先看一下并行执行。对于异步钩子,添加事件监听的方式有三种:tap、tapAsync、tapPromise:
1. tap
// AsyncParallelHookTap.js
const { AsyncParallelHook } = require('tapable')
const hook = new AsyncParallelHook(['name'])
hook.tap('fn1', (name) => {
console.log(`fn1 ---> ${name}`)
console.log(Date.now())
})
hook.tap('fn2', (name) => {
console.log(`fn2 ---> ${name}`)
console.log(Date.now())
})
hook.callAsync('run', () => {
console.log('all done.')
})
fn1 ---> run
1629700481997
fn2 ---> run
1629700481997
all done.
并行的异步钩子几乎是同时执行的,当所有回调函数执行完毕后会触发执行完毕的回调函数,且所有回调函数之间互不干扰。
2. tabAsync
tabAsync
通过添加回调函数的方式来表示当前回调执行完毕:
// AsyncParallelHookTapAsync.js
const { AsyncParallelHook } = require('tapable')
const hook = new AsyncParallelHook(['name'])
console.time('time')
hook.tapAsync('fn1', (name, callback) => {
// console.log(callback.toString())
// function (_err0) {
// if(_err0) {
// if(_counter > 0) {
// _callback(_err0);
// _counter = 0;
// }
// } else {
// if(--_counter === 0) _done();
// }
// }
setTimeout(() => {
console.log(`fn1 ---> ${name}`)
callback()
}, 1000)
})
hook.tapAsync('fn2', (name, callback) => {
// console.log(callback.toString())
// function(_err1) {
// if(_err1) {
// if(_counter > 0) {
// _callback(_err1);
// _counter = 0;
// }
// } else {
// if(--_counter === 0) _done();
// }
// }
setTimeout(() => {
console.log(`fn2 ---> ${name}`)
callback()
}, 2000)
})
hook.callAsync('run', () => {
console.log('all done.')
console.timeEnd('time')
})
fn1 ---> run
fn2 ---> run
all done.
time: 2.010s
如上所示,我们打印了这个回调函数,可以看出,当我们调用回调函数不传递错误信息时,并且调用量(回调函数总数)递减为0时确认所有回调函数调用完毕。
一旦我们在某个回调中传递了错误信息,则立即调用hook的回调,并将调用量置为0,终止下面回调执行:
// AsyncParallelHookTapAsync.js
...
setTimeout(() => {
console.log(`fn1 ---> ${name}`)
callback('err')
}, 1000)
...
fn1 ---> run
all done.
time: 1.011s
当第一个回调传递错误信息时,立即终止执行,所有回调执行完毕。
3. tapPromise
tapPromise
注册的回调必须返回一个 Promise
,通过调用 resolve()
方法表示该回调函数执行完毕:
// AsyncParallelHookTapPromise.js
const { AsyncParallelHook } = require('tapable')
const hook = new AsyncParallelHook(['name'])
console.time('time')
hook.tapPromise('fn1', (name) => {
return new Promise(resolve => {
setTimeout(() => {
console.log(`fn1 ---> ${name}`)
resolve()
}, 1000)
})
})
hook.tapPromise('fn2', (name) => {
return new Promise(resolve => {
setTimeout(() => {
console.log(`fn2 ---> ${name}`)
resolve()
}, 2000)
})
})
hook.promise('run').then(() => {
console.log('all done.')
console.timeEnd('time')
})
fn1 ---> run
fn2 ---> run
all done.
time: 2.008s
tapPromise 注册的回调必须返回一个 Promise
,否则会报错,必须调用 resolve
方法标注该回调执行完毕,否则 hook 执行完毕的回调无法执行。如果某个回调调用了 reject
,hook 的回调也不会执行。
以上就是三种添加异步回调的方法。
AsyncParallelBailHook
异步并行熔断钩子,和同步的熔断钩子类似,当回调函数的return
、 callback
的实参或者 resolve
的值不为 undefined 时,终止所有回调函数的执行,并调用 hook 的回调。
以上存在三种回调函数,这里不能弄混淆了,一个是我们自己注册的回调函数,用来处理我们自定义的任务;一个是标注我们自定义的函数执行完毕,成功与否通过回调实参表示;最后一个是所有回调都成功执行后的最终回调,只要有一个回调函数执行异常,标注了错误信息,那么这个最终回调就无法执行。
以上注册的回调函数都是并行的(同时执行),而串行执行就是注册的回调函数依次顺序执行,下面举一个例子看一下特点:
// AsyncSeriesHookTapAsync.js
const { AsyncSeriesHook } = require('tapable')
const hook = new AsyncSeriesHook(['name'])
console.time('time')
hook.tapAsync('fn1', (name, callback) => {
setTimeout(() => {
console.log(`fn1 ---> ${name}`)
callback()
}, 1000)
})
hook.tapAsync('fn2', (name, callback) => {
setTimeout(() => {
console.log(`fn2 ---> ${name}`)
callback()
}, 2000)
})
hook.callAsync('run', () => {
console.log('all done.')
console.timeEnd('time')
})
fn1 ---> run
fn2 ---> run
all done.
time: 3.015s
当使用 tapAsync 注册回调函数时,因为 callback
函数的特点,不管你使用的是熔断钩子还是非熔断钩子,它都会立即调用hook的回调,唯一区别就是串行后续回调函数不再执行,并行因为是同时执行的原因,后面的回调会执行,只是在标注错误信息的那一刻立即调用hook的最终回调。
以下是并行和串行使用callback的调用栈对比,可以看出不同的地方:
// parallel hook VM
(function anonymous(name, _callback) {
"use strict";
var _context;
var _x = this._x;
do {
var _counter = 3;
var _done = function () {
_callback();
};
if (_counter <= 0) break;
var _fn0 = _x[0];
_fn0(name, function (_err0) {
if (_err0) {
if (_counter > 0) {
_callback(_err0);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
if (_counter <= 0) break;
var _fn1 = _x[1];
_fn1(name, function (_err1) {
if (_err1) {
if (_counter > 0) {
_callback(_err1);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
if (_counter <= 0) break;
var _fn2 = _x[2];
_fn2(name, function (_err2) {
if (_err2) {
if (_counter > 0) {
_callback(_err2);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
} while (false);
});
// series hook VM
(function anonymous(name, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next1() {
var _fn2 = _x[2];
_fn2(name, function (_err2) {
if (_err2) {
_callback(_err2);
} else {
_callback();
}
});
}
function _next0() {
var _fn1 = _x[1];
_fn1(name, function (_err1) {
if (_err1) {
_callback(_err1);
} else {
_next1();
}
});
}
var _fn0 = _x[0];
_fn0(name, function (_err0) {
if (_err0) {
_callback(_err0);
} else {
_next0();
}
});
});
SyncHook源码解析
调试代码:
// SyncHook.js
const { SyncHook } = require('tapable')
const hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function(name, age) {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
hook.tap('fn2', function(name, age) {
console.log(`fn2 ---> ${name} is ${age} years old.`)
})
hook.tap('fn3', function(name, age) {
console.log(`fn3 ---> ${name} is ${age} years old.`)
})
hook.call('jack', 12)
// SyncHookBefore.js
const { SyncHook } = require('tapable')
const hook = new SyncHook(['name', 'age'])
hook.tap('fn0', function(name, age) {
console.log(`fn0 ---> ${name} is ${age} years old.`)
})
hook.tap('fn1', function(name, age) {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
hook.tap('fn2', function(name, age) {
console.log(`fn2 ---> ${name} is ${age} years old.`)
})
hook.tap('fn3', function(name, age) {
console.log(`fn3 ---> ${name} is ${age} years old.`)
})
hook.tap({
name: 'fn4',
before: 'fn2'
}, function(name, age) {
console.log(`fn4 ---> ${name} is ${age} years old.`)
})
hook.call('jack', 12)
// SyncHookStage.js
const { SyncHook } = require('tapable')
const hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function(name, age) {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
hook.tap({
name: 'fn2',
stage: 8,
}, function(name, age) {
console.log(`fn2 ---> ${name} is ${age} years old.`)
})
hook.tap({
name: 'fn3',
stage: 6,
}, function(name, age) {
console.log(`fn3 ---> ${name} is ${age} years old.`)
})
hook.tap('fn4', function(name, age) {
console.log(`fn4 ---> ${name} is ${age} years old.`)
})
hook.call('jack', 12)
1. 实例化SyncHook对象
const hook = new SyncHook(['name', 'age'])
SyncHook源码:
// SyncHook.js
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
// 加载Hook类
const Hook = require("./Hook");
// 加载Hook生成执行代码的工厂类(也就是生成上文中存在于调用栈中的VM- - - -)
const HookCodeFactory = require("./HookCodeFactory");
// 同步hook代码生成工厂类继承基础的hook代码生成工厂类
class SyncHookCodeFactory extends HookCodeFactory {
/**
* 定义成员方法,调用父类的callTapsSeries组装执行代码块
* @param { 失败的回调,成功的回调,处理异常并重新抛出异常 } param0
* @returns code
*/
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
// 实例化code工厂类
const factory = new SyncHookCodeFactory();
// 重写Hook类的TAP_ASYNC方法,syncHook不能使用tapAsync调用
const TAP_ASYNC = () => {
throw new Error("tapAsync is not supported on a SyncHook");
};
// 重新Hook类的TAP_PROMISE方法,syncHook不能使用tapPromise调用
const TAP_PROMISE = () => {
throw new Error("tapPromise is not supported on a SyncHook");
};
/**
* 编译代码(生成可执行代码)
* @param { 参数选项 } options
* {
* taps: this.taps,
* interceptors: this.interceptors,
* args: this._args,
* type: type
* }
*/
const COMPILE = function(options) {
// this对应hook实例,它有一个_x属性(this._x = undefined;)
// 这一步的目的就是将taps中所有tap的fn取出来放到_x中
factory.setup(this, options);
// 这一步就是根据不同的type来创建执行函数
return factory.create(options);
};
/**
* SyncHook构造函数,new SyncHook时会调用它
* @param {回调函数携带的参数} args
* @param {该syncHook命名} name
* @returns syncHook实例
*/
function SyncHook(args = [], name = undefined) {
// 实例化一个基础的hook类
const hook = new Hook(args, name);
// 构造函数是SyncHook
hook.constructor = SyncHook;
// 重写hook的tapAsync、tapPromise、compile
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
// SyncHook的原型链指向null
SyncHook.prototype = null;
module.exports = SyncHook;
2. 实例化Hook,并调用hook实例的tap方法注册一个钩子函数
const hook = new Hook(args, name);
hook.tap('fn1', function(name, age) {
console.log(`fn1 ---> ${name} is ${age} years old.`)
})
Hook.js源码
// Hook.js
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const util = require("util");
// 新的版本已近弃用 Hook.context
const deprecateContext = util.deprecate(() => {},
"Hook.context is deprecated and will be removed");
// 异步钩子的三种调用方法,主要是为了标注调用方式,为了后面生成执行函数时可以通过 type 来进行判断
const CALL_DELEGATE = function (...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function (...args) {
this.callAsync = this._createCall("async");
return this.callAsync(...args);
};
const PROMISE_DELEGATE = function (...args) {
this.promise = this._createCall("promise");
return this.promise(...args);
};
class Hook {
constructor(args = [], name = undefined) {
// 接收注册的回调函数参数集合
this._args = args;
// 实例化的hook名称
this.name = name;
// 用来存放tap的集合,hook实例调用一次tap就往taps里面推送一条数据
this.taps = [];
// 拦截器集合
this.interceptors = [];
// 三种调用方法,_开头的是私有成员,仅供类内部使用
this._call = CALL_DELEGATE;
this.call = CALL_DELEGATE;
this._callAsync = CALL_ASYNC_DELEGATE;
this.callAsync = CALL_ASYNC_DELEGATE;
this._promise = PROMISE_DELEGATE;
this.promise = PROMISE_DELEGATE;
// _x就是注册的回调函数集合
this._x = undefined;
// 代码编译的函数
this.compile = this.compile;
// 创建异步钩子的三种方法
// tap可以创建同步钩子和异步钩子,tap创建的钩子只能使用call调用
this.tap = this.tap;
this.tapAsync = this.tapAsync;
this.tapPromise = this.tapPromise;
}
/**
* 编译函数,被子类重写,也就是上面SyncHook中的COMPILE方法
* @param {} options
*/
compile(options) {
throw new Error("Abstract: should be overridden");
}
/**
* 创建call函数
* @param {调用类型} type call callAsync promise
* @returns 编译后的可执行函数
*/
_createCall(type) {
// 内部调用重写的编译方法,也就是调用代码生成的工厂函数
// 最终返回一段可执行的代码,执行注册的所有回调
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type,
});
}
/**
* 注册钩子函数
* @param {类型} type
* @param {参数选项} options
* @param {待注册的回调函数} fn
* hook.tap('fn1', () => {}) --> fn1 就是 options () => {} 就是 fn
*/
// type Tap = TapOptions & {
// name: string;
// };
// type TapOptions = {
// before?: string;
// stage?: number;
// };
_tap(type, options, fn) {
if (typeof options === "string") {
// 如果这个参数选项是一个字符串,将其去除空格后作为钩子命名
options = {
name: options.trim(),
};
} else if (typeof options !== "object" || options === null) {
// 如果这个参数选项不是一个对象,或者是null,那么就抛出错误信息
throw new Error("Invalid tap options");
}
if (typeof options.name !== "string" || options.name === "") {
// 如果参数选项的name不是一个字符串类型,或者是一个空字符串,那么就抛出错误信息
throw new Error("Missing name for tap");
}
// 如果参数选项存在 content,那么就提示该方法已近弃用
if (typeof options.context !== "undefined") {
deprecateContext();
}
// 合并参数选项:{ type, fn, name }
options = Object.assign({ type, fn }, options);
// 参数选项经过拦截器处理
options = this._runRegisterInterceptors(options);
// 推入taps
this._insert(options);
}
// 三个注册钩子函数的方法
tap(options, fn) {
this._tap("sync", options, fn);
}
tapAsync(options, fn) {
this._tap("async", options, fn);
}
tapPromise(options, fn) {
this._tap("promise", options, fn);
}
/**
* 注册拦截器
* @param {参数选项} options
* @returns 经过拦截器处理后的参数选项
*/
_runRegisterInterceptors(options) {
// 遍历拦截器
for (const interceptor of this.interceptors) {
// 如果这个拦截器有注册方法,就去处理options
if (interceptor.register) {
const newOptions = interceptor.register(options);
// 如果register返回的新options不为空,替换原options
if (newOptions !== undefined) {
options = newOptions;
}
}
}
// 最终返回经过处理的options
return options;
}
/**
* 组合参数选项
* @param {参数选项} options
* @returns
*/
withOptions(options) {
const mergeOptions = (opt) =>
Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);
return {
name: this.name,
tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
intercept: (interceptor) => this.intercept(interceptor),
isUsed: () => this.isUsed(),
withOptions: (opt) => this.withOptions(mergeOptions(opt)),
};
}
/**
* 是否被使用过了,如果taps或者interceptors不为空,说明已经注册了钩子函数
* @returns
*/
isUsed() {
return this.taps.length > 0 || this.interceptors.length > 0;
}
/**
* 为钩子函数注册拦截器
* @param {拦截器} interceptor
*/
intercept(interceptor) {
// 重置编译器
this._resetCompilation();
// 添加拦截器
this.interceptors.push(Object.assign({}, interceptor));
// 如果这个拦截器有注册方法,那么就去处理注册的钩子函数
if (interceptor.register) {
for (let i = 0; i < this.taps.length; i++) {
this.taps[i] = interceptor.register(this.taps[i]);
}
}
}
/**
* 重置编译器,为下一次编译做准备
*/
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
/**
* 向taps中推入钩子函数
* @param {注册的钩子函数} item --> 如果未经过拦截器处理,那么就是 { type, name, fn }
*/
_insert(item) {
// 重置一下编译器
this._resetCompilation();
// 调用tap创建钩子函数时第一个参数,也就数options可以是一个字符串,也可以是一个对象
// 如果是字符串,就会将其去除首尾空格,作为钩子的命名
// 如果是一个对象,可以配置一些参数,其中就包括before和stage
// before表示在某个任务之前,类型限定为字符串
let before;
if (typeof item.before === "string") {
// Set(1) {fn2}
// ▼ size (get):ƒ size()
// :1
// ▼ [[Entries]]:Array(1)
// ▶ 0:"fn2"
// length:1
// ▶ __proto__:Object
// ▶ __proto__:Set
before = new Set([item.before]);
} else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
// stage表示执行阶段,值越大越靠后执行
// 限定为number类型
let stage = 0;
if (typeof item.stage === "number") {
stage = item.stage;
}
let i = this.taps.length;
while (i > 0) {
// i自减1
i--;
// 取出taps中的第i项
const x = this.taps[i];
// taps中插入一项,插入的是原taps中的第i项,也就是将第i项下沉一位
this.taps[i + 1] = x;
// 初始化xStage表示当前tap的stage,不存在就默认为0,优先执行的
const xStage = x.stage || 0;
// 如果存在before,就会去找before对应的那个钩子名称
if (before) {
// 如果找到了,就删除掉before,然后跳出本轮循环,进入下次循环
// 进入循环后会继续自减i,将找到的那个tap上面的一项拷贝一份下沉
// 此时before还是存在的,只是size为0,会继续向下执行代码
// 到i++,就是i自增1,此时的i就对应那个拷贝的一项,它此时正好在before对应tap上面
// break就会跳出循环,最终this.taps[i] = item;就会将那个拷贝的一项替换成带有before的tap
// 如果before对应的那项正好就是第0项,那么就直接跳出循环(循环条件i > 0不成立了),直接进行最终的赋值操作
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
// 如果没找到,就会跳出本轮循环,继续进入下轮循环,目的就是将before对应的那项之后的每一项都下沉一位,只到找到before对应的那个tap
// 找到后就会执行上面的代码,进行替换准备
if (before.size > 0) {
continue;
}
}
// 如果当前i项tap存在stage,且比item的stage还大,那么就跳出本轮循环
// 继续执行下轮循环,将上轮的比item stage大的那项下沉
// 直到当前i项的stage小于item的stage,说明当前i项的tap优先item执行
// 条件不成立,继续执行代码,i++后跳出循环
// 最终执行this.taps[i] = item;将下沉的那一项替换为item
if (xStage > stage) {
continue;
}
i++;
break;
}
// 当taps为空时,不会进入循环体,将item直接推入taps
this.taps[i] = item;
}
}
Object.setPrototypeOf(Hook.prototype, null);
module.exports = Hook;
3. 执行钩子
hook.call('jack', 12)
SyncHook
和 Hook
的源码都在上面两步,主要就是创建 hook 实例,调用实例的 tap 方法注册钩子函数。最终 call 的执行顺序是这样的:
- 调用 hook 实例的
call
方法,指向CALL_DELEGATE
CALL_DELEGATE
第一句this.call = this._createCall("sync");
调用 Hook 的私有成员方法__createCall
,它又会调用自己的compile
,compile
会被子类重写,也就是 SyncHook 中的COMPILE
COMPILE
第一句factory.setup(this, options);
调用 factory实例的 setup 方法挂载钩子回调函数至 hook 实例的_x
属性上instance._x = options.taps.map(t => t.fn);
COMPILE
第二句return factory.create(options);
就是调用 factory实例的 create 方法生成执行钩子回调的函数,并将生成的函数返回赋值给 hook 的 call 方法- 最终
CALL_DELEGATE
第二句return this.call(...args);
执行生成的执行函数,参数就是调用call
时传入的值
以下就是生成的执行代码,存在于调用栈中(内存中):
/**
* 执行上下文就是 hook 实例,this._x 能够访问到 hook 实例的 _x 属性,也就是钩子回调集合
* 参数就是...args
*/
(function anonymous(name, age) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(name, age);
var _fn1 = _x[1];
_fn1(name, age);
var _fn2 = _x[2];
_fn2(name, age);
});
HookCodeFactory 源码:
// HookCodeFactory.js
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
/**
* 生成执行钩子回调函数的程式
* 返回的是一个可执行函数,它的执行上下文是hook实例
* 可执行函数最终在 Hook.js CALL_DELEGATE 第二句执行
*/
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
this._args = undefined;
}
/**
* 创建执行函数
* @param { 参数选项 } options
* {
* taps: this.taps,
* interceptors: this.interceptors,
* args: this._args,
* type: type
* }
* @returns function 代码片段
*/
create(options) {
// 初始化配置选项
this.init(options);
// 声明一个函数
let fn;
// 根据不同的type生成不同的执行函数,这里是同步钩子,只看sync部分
switch (this.options.type) {
case "sync":
fn = new Function(
// 第一个参数是函数形参(通过args方法组装而成)
this.args(),
// 第二个参数是函数体
'"use strict";\n' +
// 函数头部内容,主要是取出钩子回调函数
this.header() +
// 拦截器处理
this.contentWithInterceptors({
onError: (err) => `throw ${err};\n`,
onResult: (result) => `return ${result};\n`,
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true,
})
);
break;
...
}
this.deinit();
return fn;
}
/**
* 绑定钩子回调函数
* @param {上下文hook} instance
* @param {参数选项} options
*/
setup(instance, options) {
instance._x = options.taps.map((t) => t.fn);
}
/**
* 初始化配置选项,缓存配置选项和调用参数
* @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
*/
init(options) {
this.options = options;
this._args = options.args.slice();
}
/**
* 重置配置选项
*/
deinit() {
this.options = undefined;
this._args = undefined;
}
contentWithInterceptors(options) {
if (this.options.interceptors.length > 0) {
// 拦截器处理后生成code
...
} else {
// 我们暂时不看拦截器,只生成代码,这里调用子类 syncHook 的 content 方法
// 内部调用父类 hoolCodeFactory 的 callTapsSeries 方法
// return this.callTapsSeries({
// onError: (i, err) => onError(err),
// onDone,
// rethrowIfPossible
// });
return this.content(options);
}
}
/**
* 拼装函数头部内容
* @returns 函数头部内容
*/
header() {
let code = "";
// hook.context 已经废弃了
if (this.needContext()) {
code += "var _context = {};\n";
} else {
code += "var _context;\n";
}
// 取出钩子回调函数
code += "var _x = this._x;\n";
// 取出拦截器
if (this.options.interceptors.length > 0) {
code += "var _taps = this.taps;\n";
code += "var _interceptors = this.interceptors;\n";
}
return code;
}
needContext() {
for (const tap of this.options.taps) if (tap.context) return true;
return false;
}
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
let code = "";
let hasTapCached = false;
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.tap) {
if (!hasTapCached) {
code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
hasTapCached = true;
}
code += `${this.getInterceptor(i)}.tap(${
interceptor.context ? "_context, " : ""
}_tap${tapIndex});\n`;
}
}
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
const tap = this.options.taps[tapIndex];
switch (tap.type) {
case "sync":
if (!rethrowIfPossible) {
code += `var _hasError${tapIndex} = false;\n`;
code += "try {\n";
}
if (onResult) {
code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined,
})});\n`;
} else {
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined,
})});\n`;
}
if (!rethrowIfPossible) {
code += "} catch(_err) {\n";
code += `_hasError${tapIndex} = true;\n`;
code += onError("_err");
code += "}\n";
code += `if(!_hasError${tapIndex}) {\n`;
}
if (onResult) {
code += onResult(`_result${tapIndex}`);
}
if (onDone) {
code += onDone();
}
if (!rethrowIfPossible) {
code += "}\n";
}
break;
...
}
return code;
}
/**
* 同步调用钩子回调函数
* @param {
* onError: 执行失败回调
* onResult: 执行结果回调
* resultReturns: 返回的执行结果
* onDone: 执行完毕回调
* doneReturns: 执行完毕后的返回结果
* rethrowIfPossible: 异常捕获
* } param0
* @returns
*/
callTapsSeries({
onError,
onResult,
resultReturns,
onDone,
doneReturns,
rethrowIfPossible,
}) {
if (this.options.taps.length === 0) return onDone();
const firstAsync = this.options.taps.findIndex((t) => t.type !== "sync");
const somethingReturns = resultReturns || doneReturns;
let code = "";
let current = onDone;
let unrollCounter = 0;
for (let j = this.options.taps.length - 1; j >= 0; j--) {
const i = j;
const unroll =
current !== onDone &&
(this.options.taps[i].type !== "sync" || unrollCounter++ > 20);
if (unroll) {
unrollCounter = 0;
code += `function _next${i}() {\n`;
code += current();
code += `}\n`;
current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
}
const done = current;
const doneBreak = (skipDone) => {
if (skipDone) return "";
return onDone();
};
const content = this.callTap(i, {
onError: (error) => onError(i, error, done, doneBreak),
onResult:
onResult &&
((result) => {
return onResult(i, result, done, doneBreak);
}),
onDone: !onResult && done,
rethrowIfPossible:
rethrowIfPossible && (firstAsync < 0 || i < firstAsync),
});
current = () => content;
}
code += current();
return code;
}
/**
* 组装参数
* @param {前置参数, 后置参数} param0
* @returns 拼装好的回调参数
*/
args({ before, after } = {}) {
let allArgs = this._args;
if (before) allArgs = [before].concat(allArgs);
if (after) allArgs = allArgs.concat(after);
if (allArgs.length === 0) {
return "";
} else {
return allArgs.join(", ");
}
}
getTapFn(idx) {
return `_x[${idx}]`;
}
getTap(idx) {
return `_taps[${idx}]`;
}
getInterceptor(idx) {
return `_interceptors[${idx}]`;
}
}
module.exports = HookCodeFactory;
以上代码删除了异步相关的代码片段。