tapable

1|0tapable

what: webpack 事件流机制的核心库。

why : 方便 webpack 运行的各生命周期中扩展功能。

how : 核心原理依赖于发布订阅模式,类似于 nodejs 的 events 库。

1|1基础使用

const { SyncHook } = require('tapable'); //① const hook = new SyncHook(['name']); //② hook.tap('register', (name) => { //③ console.log('has register ' + name); }); hook.tap('register2', (data) => { //④ console.log('has register2 ' + data); }); hook.call('event'); //⑤ // has register event // has register2 event
  1. tapable 库导出的是各种钩子的集合
  2. 创建钩子实例的时候传参必须为数组,数组内元素为注册函数的形参
  3. hook 分别有注册和响应的函数
  4. 注册的第一个参数名为了方便代码阅读和维护,对实际运行没有影响
  5. 响应函数的参数,作为注册函数的实参运行

1|2Hook 的种类

const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesLoopHook, AsyncSeriesWaterfallHook, } = require('tapable');

Hook 最主要可分为同步和异步钩子两大类,异步钩子还可细分为串行和并行两类。

  1. 同步:

    • SyncHook :

      是所有 hook 中最普通、功能最纯粹的,每个注册的函数都将按照注册的顺序执行。

      所有执行函数参数相同。

      call 函数无返回值。

    • SyncBailHook :

      带 Bail 的 hook 的特点是允许注册的函数停止后续函数的执行。

      const hook = new SyncBailHook(['name']); hook.tap('ev1', function(name) { console.log('ev1 ' + name); return '不往下执行了'; }); hook.tap('ev2', function(name) { console.log('ev2 ' + name); }); hook.call('call'); //返回值为 '不往下执行了' //ev1 call

      当前函数的返回值不为 undefined 则会阻止后续函数的执行。

      所有执行函数参数相同。

      call 函数返回值为最后一个执行函数的返回值。

    • SyncWaterfallHook :

      带 Waterfall 的 hook 的特点是当前执行函数的返回值作为下一个函数执行的第一个参数。

      const hook = new SyncWaterfallHook(['name', 'data']); hook.tap('ev1', function(name, data) { console.log('ev1', name, data); return 'ev1 的返回值'; }); hook.tap('ev2', function(name, data) { console.log('ev2', name, data); }); hook.call('call', 'other'); // 返回值为 'ev1 的返回值' // ev1 call other // ev2 ev1 的返回值 other

      所有注册函数依旧按照注册顺序执行。

      执行函数的参数数量相同,但第一个参数可能是上一个函数的返回值。

      call 函数返回的是最后一个有返回值函数的返回值。

    • SyncLoopHook :

      带 Loop 的 hook 的特点是当前执行函数返回值不为 undefined 时,当前函数将循环执行。

      const hook = new SyncLoopHook(['name']); let idx = 0; hook.tap('ev1', function(name) { console.log('ev1', name); idx++; return idx === 3 ? undefined : '再来一次'; }); hook.tap('ev2', function(name) { console.log('ev2', name); }); hook.call('call'); //ev1 call //ev1 call //ev1 call //ev2 call

      所有执行函数参数相同。

      call 函数无返回值。

      该 hook 并没有在 webpack 核心库使用,可根据业务需求做插件扩展。

  2. 异步 :

    带 Async 的 hook 的均为异步钩子。

    异步钩子增加了两种注册和执行函数。(异步钩子也可以使用同步注册和执行)

    tapAsync 注册的函数通过 callAsync 执行。

    tapAsync 注册的函数的参数最后增加一个回调函数,该回调函数的执行用来通知 hook 当前函数执行结束。
    tapAsync 参数不为 undefined/null 时,视为出现异常, callAsync的回调函数会将次异常作为参数立即执行,往后其余注册函数的回调将不影响最终回调函数的执行。
    callAsync 的最后一个参数也增加一个回调函数,当所有注册函数的回调执行完成后,该回调函数最后执行。

    hook.tapAsync('async', function(name, cb) { setTimeout(function() { cb(); }, 10); }); hook.callAsync('jw', function() { console.log('end'); });

    tapPromise 注册的函数通过 promise 执行。

    tapPromise 注册的函数相当于 一个 Promise 对象。

    promise 执行函数的效果相当于 Promise.all 的执行。

    关于注册函数出现 reject 的情况根据 与相同 hook 的 tapAsync 出现异常情况相同。

    hook.tapPromise('promise', function(name) { return new Promise((res, rej) => { setTimeout(() => { console.log('promise', name); res(); }, 200); }); }); hook.promise('promiseArgs').then(() => { console.log('promise done'); });
    • AsyncParallelHook :

      带 Parallel 的 hook 的均为异步并行钩子,其特点是各个注册函数之间的执行互不影响。

      const hook = new AsyncParallelHook(['name']); hook.tapAsync('react', function(name, cb) { setTimeout(function() { console.log('react', name); cb('finish right now'); }, 500); }); hook.tapAsync('node', function(name, cb) { setTimeout(function() { console.log('node', name); cb(); }, 500); }); hook.callAsync('jw', function() { console.log('end'); }); //react jw //end //node jw

      此处演示 tapAsync 回调执行异常的情况。
      即便出现异常也不影响后置函数的执行。

    • AsyncParallelBailHook :

      与 AsyncParallelHook 区别在于:
      AsyncParallelHook 的 callAsync 的回调函数的异常参数为第一个执行时出现异常的函数。

      AsyncParallelBailHook 的 callAsync 的回调函数的异常参数为出现异常的函数中注册顺序最靠前的函数。

    • AsyncSeriesHook :

      带 Series 的 hook 的均为异步串行钩子,其特点是前置注册函数执行完成之后,后置的注册函数才开始执行。

      当执行回调出现异常时,将阻断后置函数的执行,异常作为callAsync 的参数执行。

      const hook = new AsyncSeriesHook(['name']); hook.tapAsync('ev1', function(name, cb) { setTimeout(function() { console.log('ev1', name); cb('error'); }, 1500); }); hook.tapAsync('ev2', function(name, cb) { setTimeout(function() { console.log('ev2', name); }, 1000); }); hook.callAsync('call', function(err) { console.log('finish', err); }); //ev1 call //finish error
    • AsyncSeriesBailHook / AsyncSeriesWaterfallHook / AsyncSeriesLoopHook :结合以上关键字的特性可得特性。

总结:

关键字 含义
Sync 同步钩子,注册函数按注册顺序同步执行
tap/call 同步注册 /执行
Async 异步钩子,支持异步执行的注册函数
tapAsync/callAsync 异步,通过回调函数通知钩子执行结束,回调函数有传参视为抛出异常处理
tapPromise/promise 异步,按照 Promise 的规则运行
Parallel 异步并行,注册函数并行执行
Series 异步串行,注册函数串行执行
Bail 保险钩子,前置注册函数的有返回值时,后置注册函数将不执行
Waterfall 瀑布流钩子,前置注册函数的返回值将作为后置函数的参数
Loop 循环钩子,当注册函数有返回值时,将会再次执行
AsyncParallelBailHook 相对特殊,注册函数异步并行互不影响,但影响最终回调函数的执行参数

1|3源码分析

PS: webpack 中的 tapable 源码不在 master 上 ,需要切到分支 tapable-1 。

exports.__esModule = true; exports.Tapable = require('./Tapable'); exports.SyncHook = require('./SyncHook'); exports.SyncBailHook = require('./SyncBailHook'); exports.SyncWaterfallHook = require('./SyncWaterfallHook'); exports.SyncLoopHook = require('./SyncLoopHook'); exports.AsyncParallelHook = require('./AsyncParallelHook'); exports.AsyncParallelBailHook = require('./AsyncParallelBailHook'); exports.AsyncSeriesHook = require('./AsyncSeriesHook'); exports.AsyncSeriesBailHook = require('./AsyncSeriesBailHook'); exports.AsyncSeriesWaterfallHook = require('./AsyncSeriesWaterfallHook'); exports.HookMap = require('./HookMap'); exports.MultiHook = require('./MultiHook');

从入口文件可知, 引入的 tapable (index.js) 主要就是复杂导出各种钩子。

const Hook = require('./Hook'); const HookCodeFactory = require('./HookCodeFactory'); class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible, }); } } const factory = new SyncHookCodeFactory(); class SyncHook extends Hook { tapAsync() { throw new Error('tapAsync is not supported on a SyncHook'); } tapPromise() { throw new Error('tapPromise is not supported on a SyncHook'); } compile(options) { factory.setup(this, options); return factory.create(options); } } module.exports = SyncHook;
const Hook = require('./Hook'); const HookCodeFactory = require('./HookCodeFactory'); class AsyncSeriesLoopHookCodeFactory extends HookCodeFactory { content({ onError, onDone }) { return this.callTapsLooping({ onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true), onDone, }); } } const factory = new AsyncSeriesLoopHookCodeFactory(); class AsyncSeriesLoopHook extends Hook { compile(options) { factory.setup(this, options); return factory.create(options); } } Object.defineProperties(AsyncSeriesLoopHook.prototype, { _call: { value: undefined, configurable: true, writable: true }, }); module.exports = AsyncSeriesLoopHook;

每个钩子的结构大致如上,共同点:

  1. 当前钩子继承于 Hook 类。
  2. 通过重写 content 的实现,改变当前钩子的 compile 方法。
  3. 同步钩子重写 tapAsync/tapPromise 阻止执行。
  4. 异步钩子在其原型上添加(重写覆盖)了 _call属性。

解析一下 Hook 类的内部:

class Hook { constructor(args) { if (!Array.isArray(args)) args = []; this._args = args; this.taps = []; this.interceptors = []; this.call = this._call; this.promise = this._promise; this.callAsync = this._callAsync; this._x = undefined; } compile(options) { throw new Error('Abstract: should be overriden'); } _createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type, }); } tap(options, fn) { // .... 略去非核心代码 options = Object.assign({ type: 'sync', fn: fn }, options); this._insert(options); } tapAsync(options, fn) { // ....略去非核心代码 , 与 tap 方法区别就是一个类型的传参 options = Object.assign({ type: 'async', fn: fn }, options); this._insert(options); } tapPromise(options, fn) { // ....略去非核心代码 , 与 tap 方法区别就是一个类型的传参 options = Object.assign({ type: 'async', fn: fn }, options); this._insert(options); } _resetCompilation() { this.call = this._call; this.callAsync = this._callAsync; this.promise = this._promise; } _insert(item) { // ....略去非核心代码, 将注册事件与函数添加到 taps中 this.taps[i] = item; } // ....略去 hook 类中 拦截器相关方法 } function createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); }; } Object.defineProperties(Hook.prototype, { _call: { value: createCompileDelegate('call', 'sync'), //略去非核心代码.... }, _promise: { value: createCompileDelegate('promise', 'promise'), //略去非核心代码.... }, _callAsync: { value: createCompileDelegate('callAsync', 'async'), //略去非核心代码.... }, }); module.exports = Hook;

略去拦截器实现与注册的特殊配置相关代码,以简单的例子做演示跟踪。

  • 创建钩子跟踪:

    const pluginHook = new SyncHook(['arg1', 'arg2']);
    1. 原型上定义了 _call/_promise/_callAsync ,主要作用是为了防止命名冲突。
    2. 执行 constructor 构造函数,初始化私有属性。
  • 注册函数跟踪:

    instanceHook.tap('evt1',func1})
    1. 组织注册函数参数,增加到 taps 中(此处略去非核心功能)
    // Hook 的 tap 中 ,此函数中包含拦截器对参数的重整 this._insert({ type: 'sync', fn: func1, name: 'evt1' }); // Hook 的 _insert 中 ,此函数中包含特殊配置改变 taps 中的注册顺序 this._resetCompilation(); this.taps[0] = { type: 'sync', fn: func1, name: 'evt1' };
  • 执行注册跟踪:

    instanceHook.call('param1', 'param2');
    1. 构造函数执行时, this.call 指向了 this._call,即为原型上的 _call,也即为 createCompileDelegate('call', 'sync') 的返回值。
    2. createCompileDelegate 通过 _createCall 返回了一个新的 call
    3. _createCall 实际是将现有的钩子内部参数及由 compile 生成的函数。
    4. 上文提到各个钩子的 compile 是经过钩子类中重写了实现的, 而在 Hook 类相当于占位符。
    //instanceHook.call('param1', 'param2') 相当于 instanceHook.compile({ taps: [{ type: 'sync', fn: func1, name: 'evt1' }], args: ['arg1', 'arg2'], type: 'sync', })('param1', 'param2');

回看 SyncHook 类中,compile的相关实现

class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible, }); } } const factory = new SyncHookCodeFactory(); class SyncHook extends Hook { compile(options) { factory.setup(this, options); return factory.create(options); } }

为了探寻 compile 的具体实现,需要解析一下 HookCodeFactory 内部实现

class HookCodeFactory { constructor(config) { this.config = config; this.options = undefined; this._args = undefined; } create(options) { this.init(options); let fn; switch (this.options.type) { case 'sync': fn = new Function( this.args(), '"use strict";\n' + this.header() + this.content({ onError: (err) => `throw ${err};\n`, onResult: (result) => `return ${result};\n`, resultReturns: true, onDone: () => '', rethrowIfPossible: true, }) ); break; //省略...... } this.deinit(); return fn; } setup(instance, options) { instance._x = options.taps.map((t) => t.fn); } callTapsSeries({ onError, onResult, resultReturns, onDone, doneReturns, rethrowIfPossible, }) { //省略...... return code; } callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) { //省略...... return code; } //省略...... }

由此可看出 HookCodeFactory 是一个根据钩子的内部参数生成代码字符串,再及由 new Function() 的方式返回代码。

SyncHook 生成的 call 为例:

const SyncHook = require('./lib/SyncHook'); function func1() { console.log('注册函数1'); } function func2() { console.log('注册函数2'); } function func3() { console.log('注册函数3'); } const hook = new SyncHook(['arg1', 'arg2']); hook.tap('evt1', func1); hook.tap('evt2', func2); hook.tap('evt3', func3); hook.call('param1', 'param2');
function(arg1, arg2){ "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; _fn0(arg1, arg2); var _fn1 = _x[1]; _fn1(arg1, arg2); var _fn2 = _x[2]; _fn2(arg1, arg2); }

SyncWaterfallHook 生成的 call 为例:

const SyncWaterfallHook = require('./lib/SyncWaterfallHook'); function func1() { console.log('注册函数1'); return undefined; } function func2() { console.log('注册函数2'); return 1; } function func3() { console.log('注册函数3'); return undefined; } const hook = new SyncWaterfallHook(['arg1', 'arg2']); hook.tap('evt1', func1); hook.tap('evt2', func2); hook.tap('evt3', func3); hook.call('param1', 'param2');
function(arg1, arg2){ "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; var _result0 = _fn0(arg1, arg2); if(_result0 !== undefined) { arg1 = _result0; } var _fn1 = _x[1]; var _result1 = _fn1(arg1, arg2); if(_result1 !== undefined) { arg1 = _result1; } var _fn2 = _x[2]; var _result2 = _fn2(arg1, arg2); if(_result2 !== undefined) { arg1 = _result2; } return arg1; }

AsyncParallelBailHook 生成的 call 为例:

const AsyncParallelBailHook = require('./lib/AsyncParallelBailHook'); function func1(name, cb) { setTimeout(() => { cb(); }, 10); return undefined; } function func2(name, data, cb) { console.log('注册函数2'); setTimeout(() => { cb('this is error'); }, 10); return 1; } function func3(cb) { console.log('注册函数3'); cb(); return undefined; } const hook = new AsyncParallelBailHook(['arg1', 'arg2']); hook.tapAsync('evt1', func1); hook.tapAsync('evt2', func2); hook.tapAsync('evt3', func3); hook.callAsync('param1', 'param2', function(err) { if (err) { console.err('err'); } console.log('finish'); });
function(arg1, arg2, _callback){ "use strict"; var _context; var _x = this._x; var _results = new Array(3); var _checkDone = () => { for (var i = 0; i < _results.length; i++) { var item = _results[i]; if (item === undefined) return false; if (item.result !== undefined) { _callback(null, item.result); return true; } if (item.error) { _callback(item.error); return true; } } return false; }; do { var _counter = 3; var _done = () => { _callback(); }; if (_counter <= 0) break; var _fn0 = _x[0]; _fn0(arg1, arg2, (_err0, _result0) => { if (_err0) { if (_counter > 0) { if ( 0 < _results.length && ((_results.length = 1), (_results[0] = { error: _err0 }), _checkDone()) ) { _counter = 0; } else { if (--_counter === 0) _done(); } } } else { if (_counter > 0) { if ( 0 < _results.length && (_result0 !== undefined && (_results.length = 1), (_results[0] = { result: _result0 }), _checkDone()) ) { _counter = 0; } else { if (--_counter === 0) _done(); } } } }); if (_counter <= 0) break; if (1 >= _results.length) { if (--_counter === 0) _done(); } else { var _fn1 = _x[1]; _fn1(arg1, arg2, (_err1, _result1) => { if (_err1) { if (_counter > 0) { if ( 1 < _results.length && ((_results.length = 2), (_results[1] = { error: _err1 }), _checkDone()) ) { _counter = 0; } else { if (--_counter === 0) _done(); } } } else { if (_counter > 0) { if ( 1 < _results.length && (_result1 !== undefined && (_results.length = 2), (_results[1] = { result: _result1 }), _checkDone()) ) { _counter = 0; } else { if (--_counter === 0) _done(); } } } }); } if (_counter <= 0) break; if (2 >= _results.length) { if (--_counter === 0) _done(); } else { var _fn2 = _x[2]; _fn2(arg1, arg2, (_err2, _result2) => { if (_err2) { if (_counter > 0) { if ( 2 < _results.length && ((_results.length = 3), (_results[2] = { error: _err2 }), _checkDone()) ) { _counter = 0; } else { if (--_counter === 0) _done(); } } } else { if (_counter > 0) { if ( 2 < _results.length && (_result2 !== undefined && (_results.length = 3), (_results[2] = { result: _result2 }), _checkDone()) ) { _counter = 0; } else { if (--_counter === 0) _done(); } } } }); } } while (false); }

所有钩子都可以通过打印 create 打印出相应的当前 hook 执行的 call 函数。

本文主要目的为讲解 tapable 在 webpack 中的机制, 各个钩子生成代码的部分不一一进行解析。
源码中关键字解释:

  • _x: 注册函数的数组。
  • _context : 当前注册函数的执行上下文。 (bail/waterfall 等钩子的返回值需要判断或传递给后置函数,或有拦截器等情况)
  • interceptors: 拦截器, 高级用法。
    instanceHook.intercept({ context: true, tap: (context, ...args) => { //.... }, }); instanceHook.tap( { name: 'evtType', context: true, }, (context, ...args) => { //.... } );
  • before : 在参数中的特殊标识,指 _context 或 拦截器。
  • after : 在参数中的特殊标识,指钩子执行结束后的最终回调函数。

__EOF__

本文作者Odyssey
本文链接https://www.cnblogs.com/qingzhao/p/16516723.html
关于博主:I am a good person
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   --Odyssey--  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示