webpack核心库 - tapable的设计思路与核心源码解析
前言
大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.
我的宗旨就是 万物皆可手写
最近也是在继续研究webpack,暂时不知道写点啥,那我们就是来炒一炒冷饭, 扒一扒tapable中的设计思路和关键逻辑
新手创作不易,有问题欢迎指出和轻喷,谢谢
照例附上本人手写的tapable库, 简化代码逻辑,注释齐全,适合学习
lzy19926/lzy-tapable: 手写tapable库 (github.com)
tapable简介
一个最简单的SyncHook的使用方式 它可以将回调函数tap到hook里, call的之后依次执行内部所有的回调函数
// 新建一个hook,传入compiler作为参数
const hook = new SyncHook(['compiler'])
// hook中添加一个回调
hook.tap('001', (compiler, callback) => {
console.log('----1',compiler);
});
// hook中添加一个回调
hook.tap('002', (compiler, callback) => {
console.log('----2',compiler);
});
// 执行hooks, 传入compiler,依次顺序执行所有回调
const conpiler = {info:"这是compiler"}
hook.call(conpiler);
// 打印结果
// ----1,{info:"这是compiler"}
// ----2,{info:"这是compiler"}
tapable库的主体一共有两个,这里我们拿SyncHook举例
- Hook
- HookCodeFactory
- 其他Hook
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
// 创建SyncHookCodeFactory, 用于给SyncHook提供compile方法,同时实现content方法
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
// 实现TAP_ASYNC方法
const TAP_ASYNC = () => {
throw new Error("tapAsync is not supported on a SyncHook");
};
// 实现TAP_PROMISE方法
const TAP_PROMISE = () => {
throw new Error("tapPromise is not supported on a SyncHook");
};
// 实现COMPILE方法(通过创建的SyncHookCodeFactory)
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
// ES5的方式实现SyncHook
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
SyncHook.prototype = null;
module.exports = SyncHook;
怎么理解这个代码呢? 画个图它就长这样
- Hook是各种SyncHook的基类,提供了各种方法
- HookCodeFactory用于构造SyncHook对应的SyncHookCodeFactory,用于提供SyncHook中最重要的Compile方法
Hook的分类
一般来说,hook可以分为AsyncHook和SyncHook两大类,一个仅支持同步处理函数,一个支持异步
又可以分为子类:Loop,Waterfall,Serise,Bail...
所以我们有这么多Hook
这些Hook的处理方式各不一样,比如最简单的syncHook就是 顺序执行
- waterfall指: 下一个回调接收上一个回调的参数作为返回值
- Bail指: 如果回调中断了,则直接退出
- Series: 在上一个函数中需要手动调用callback,触发下一个回调
- 其他
Compile方法会动态生成代码字符串并转化为函数,从而实现这些不同Hook的调用逻辑
Compile方法,js实现抽象类
这个方法是干什么的呢?点进去我们会发现,这是一个抽象方法,需要在外部实现这个方法 (这也是JS实现抽象方法的方式)
class Hook {
...
compile(options) {
throw new Error("Abstract: should be overridden");
}
}
而恰巧,在构造SyncHook的时候我们通过CodeFactory实现了这个方法
// 实现COMPILE方法(通过创建的SyncHookCodeFactory)
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
// 实现SyncHook
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.compile = COMPILE;
return hook;
}
可以发现,COMPILE方法实际执行的逻辑是factory.create,让我们进去看看
create(options) {
this.init(options)
let fn;
switch (type) {
case "sync":
// 使用new Function,通过字符串构造一个函数
fn = new Function(
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;
case "async":...
break;
case "promise":...
break;
}
this.deInit()
return fn
}
其实这个方法主要做了一件事,就是动态生成一个函数,按照Hook的要求依次执行推入其中的回调函数 用伪代码解释这个过程
// 每次tap,都会往Hook中推入一个回调,在HOOK中作为一个tap保存起来
// _x保存了所有推入的回调
const _x = [fn1,fn2,fn3]
在我们call调用一个hook时, 会通过create构造出一个函数 (实际调用栈: _createCall->compile—>factory.create—>content—>callTaps)
注意这个content方法也是在创建Factory时实现的抽象类
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries();
}
}
通过create构造出的函数大概长这样
// VM46975430
(function anonymous(compiler
) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(compiler); // 执行回调0
var _fn1 = _x[1];
_fn1(compiler); // 执行回调1
var _fn2 = _x[2];
_fn2(compiler); // 执行回调2
})
可以看到,这就是SyncHook的执行流程,然后我们执行这个函数即可
这样做的优点:
- 通过字符串构造代码,更加灵活,可以方便的传入参数,插入回调,Error处理
- 不同的Hook执行顺序和逻辑不一样,通过字符串构建更方便定义.
现在大家明白,为什么创建Hook的时候传参是一个字符串了吧
// 新建一个hook,传入compiler作为参数
const hook = new SyncHook(['compiler'])
总结
- tap时会收集传入的回调,等待hook.call的时候按hook顺序执行
- Hooks根据回调执行时机分为两大类:
sync
,async
- Hooks根据回调执行顺序分为四大类:
普通执行
,Waterfall执行
,Bail中断执行
,Series手动触发执行
- 通过抽象出contnet方法和CodeFactory基类,于Hook基类继承组合成不同的Hook类
- content方法,通过字符串构造出不同的函数,实现各Hook的执行逻辑
照例附上本人手写的tapable库, 简化代码逻辑,注释齐全,适合学习