揭开 Webpack 工作流程面纱:Tapable 模块

news/2024/7/19 14:54:57 标签: javascript, java, js, 面试, python
js_content">

百度T8老大哥带来的技术分享。

近日在做一个新项目用到了 Tapable 来处理异步流程,这里翻出了之前写的 Webpack 小册内容,修改了本篇文章,将使用中遇见的思考进行了补充。

Webpack 工程相当庞大,但是 Webpack 本质上是一种事件流机制,通过事件流将各种插件串联起来,最终完成 webpack 的全流程,而实现事件流机制的核心是今天要讲的 Tapable 模块。Webpack 负责编译的 Compiler 和创建 Bundle 的 Compilation 都是继承自 Tapable。所以在讲 Webpack 工作流程之前,我们需要先掌握 Tapable。

事件监听和发射器

我们都知道 Node.js 特点提到事件驱动,这是因为 Node.js 本身利用 JavaScript 的语言特点实现了自定义的事件回调,Node.js 内部一个事件发射器 EventEmitter,通过这个类,可以进行事件监听与发射,这个也是 Node.js 的核心模块,很多 Node.js 内部模块都是继承自它,或者引用了它。

js">const EventEmitter = require('events').EventEmitter;
const event = new EventEmitter();
event.on('event_name', (arg) => {
  console.log('event_name fire', arg);
});
setTimeout(function () {
  event.emit('event_name', 'hello world');
}, 1000);

上面代码就是事件发射器的用法。

webpack 核心库 Tapable 的原理和 EventEmitter 类似,但是功能更强大,包括多种类型,通过事件的注册和监听,触发 webpack 生命周期中的函数方法,在 Webpack 中,tapable 都是放到对象的 hooks 上,所以我们叫他们钩子。翻阅 webpack 的源码时,会发现很多类似下面的代码:

js">// webpack 4.29.6
// lib/compiler
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']),
      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([]),


      environment: new SyncHook([]),
      afterEnvironment: new SyncHook([]),
      afterPlugins: new SyncHook(['compiler']),
      entryOption: new SyncBailHook(['context', 'entry']),
    };
  }
}

这些代码就是一个类或者函数完整生命周期需要 **「走过的路」**,所有 Webpack 代码虽然代码量很大,但是从 hook 找生命周期事件点,然后通过 hook 名称,基本就可以猜出大概流程。

Tapable 中 Hook 的类型

在 Tapable 的文档中显示了,Tapable 分为以下类型:

js">// tapable 1.1.1
const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
} = require('tapable');

Hook 类型可以分为同步(Sync)和异步(Async),异步又分为并行和串行。

e706e6a424a52b8172c03cf80be8b7f9.png

根据使用方式来分,又可以分为 BasicWaterfalBail 和 Loop 四类,每类 Hook 都有自己的使用要点:

类型使用要点
Basic基础类型,不关心监听函数的返回值,不根据返回值做事情
Bail保险式,只要监听函数中有返回值(不为 undefined),则跳过之后的监听函数
Waterfal瀑布式,上一步的返回值继续交给下一步处理和使用
Loop循环类型,如果该监听函数返回 true 则这个监听函数会反复执行,如果返回 undefined 则退出循环

Basic 类型 Hook

基础类型包括 SyncHookAsyncParallelHook 和 AsyncSeriesHook,这类 Hook 不关心函数的返回值,会一直执行到底。下面以 SyncHook 为例来说明下:

js">const {SyncHook} = require('tapable');
// 所有的构造函数都接收一个可选的参数,这个参数是一个参数名的字符串数组
// 1. 这里array的字符串随便填写,但是array的长度必须与实际要接受参数个数保持一致;
// 2. 如果回调不接受参数,可以传入空数组。
// 后面类型都是这个规则,不再做过多说明
const hook = new SyncHook(['name']);


// 添加监听
hook.tap('1', (arg0, arg1) => {
  // tap 的第一个参数是用来标识`call`传入的参数
  // 因为new的时候只的array长度为1
  // 所以这里只得到了`call`传入的第一个参数,即Webpack
  // arg1 为 undefined
  console.log(arg0, arg1, 1);
  return '1';
});
hook.tap('2', (arg0) => {
  console.log(arg0, 2);
});
hook.tap('3', (arg0) => {
  console.log(arg0, 3);
});


// 传入参数,触发监听的函数回调
// 这里传入两个参数,但是实际回调函数只得到一个
hook.call('Webpack', 'Tapable');
// 执行结果:
/*
Webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数
Webpack 2
Webpack 3
*/

通过上面的代码可以得出结论:

  1. 在实例化 SyncHook 传入的数组参数实际是只用了长度,跟实际内容没有关系

  2. 执行 call 时,入参个数跟实例化时数组长度相关

  3. 回调栈是按照「先入先出」顺序执行的(这里叫回调队列更合适,队列是先入先出)

  4. 功能跟 EventEmitter 类似

详细的流程图如下:

d5ca120a4c678523ae276b11d08d5c0d.png

再来看下 AsyncSeriesHook 的示例:

js">const {AsyncSeriesHook} = require('tapable');


const hook = new AsyncSeriesHook(['name']);
hook.tapAsync('one', (name, cb) => {
  console.log('one', name);
  setTimeout(() => {
    console.log('one timeout');
    cb();
  }, 100);
});
hook.tapAsync('two', (name, cb) => {
  console.log('two', name);
  cb();
});
hook.callAsync('asyncHook', (endArgs) => {
  console.log('end');
  console.log('endArgs', endArgs);
});
// 执行结果:
/*
one asyncHook
one timeout
two asyncHook
end
endArgs undefined
*/

如果对代码 setTimeout 部分进行修改:

js">setTimeout(() => {
  console.log('one timeout');
  cb(1); // <- 回调
}, 100);
// 执行结果
/*
one asyncHook
one timeout
end
err 1
*/

说明在 AsyncSeriesHook 流程中只要任何地方回调了 cb 传入参数,则直接跳过后续流程,直接进入 callAsync 的回调,从而结束流程。

Bail 类型 Hook

Bail 类型的 Hook 包括:SyncBailHookAsyncSeriesBailHookAsyncParallelBailHookBail 类型的 Hook 也是按回调栈顺序一次执行回调,但是如果其中一个回调函数返回结果 result !== undefined 则退出回调栈调。代码示例如下:。

js">const {SyncBailHook} = require('tapable');
const hook = new SyncBailHook(['name']);
hook.tap('1', (name) => {
  console.log(name, 1);
});
hook.tap('2', (name) => {
  console.log(name, 2);
  return 'stop';
});
hook.tap('3', (name) => {
  console.log(name, 3);
});
hook.call('hello');


/* output
hello 1
hello 2
 */

通过上面的代码可以得出结论:

  1. BailHook 中的回调是顺序执行的

  2. 调用 call 传入的参数会被每个回调函数都获取

  3. 当回调函数返回非undefined 才会停止回调栈的调用

详细的流程图如下:

08cdd1c9ee8398d02cc617d891b4c8d4.png

SyncBailHook 类似 Array.find,找到(或者发生)一件事情就停止执行;AsyncParallelBailHook 类似 Promise.race 这里竞速场景,只要有一个回调解决了一个问题,全部都解决了。

Waterfall 类型 Hook

Waterfall 类型 Hook 包括 SyncWaterfallHookAsyncSeriesWaterfallHook。类似 Array.reduce 效果,如果上一个回调函数的结果 result !== undefined,则会被作为下一个回调函数的第一个参数。代码示例如下:

js">const {SyncWaterfallHook} = require('tapable');
const hook = new SyncWaterfallHook(['arg0', 'arg1']);
hook.tap('1', (arg0, arg1) => {
  console.log(arg0, arg1, 1);
  return 1;
});
hook.tap('2', (arg0, arg1) => {
  console.log(arg0, arg1, 2);
  return 2;
});
hook.tap('3', (arg0, arg1) => {
  // 这里 arg0 = 2
  console.log(arg0, arg1, 3);
  // 等同于 return undefined
});
hook.tap('4', (arg0, arg1) => {
  // 这里 arg0 = 2 还是2
  console.log(arg0, arg1, 4);
});
hook.call('Webpack', 'Tapable');
/* console log output
Webpack Tapable 1
1 'Tapable' 2
2 'Tapable' 3
2 'Tapable' 4 */

通过上面的代码可以得出结论:

  1. WaterfallHook 的回调函数接受的参数来自于上一个函数结果

  2. 调用 call 传入的第一个参数会被上一个函数的非undefined 结果给替换

  3. 当回调函数返回非undefined 不会停止回调栈的调用

详细的流程图如下:

69c5bf48e7f54fb907e31d6162b9e786.png

Loop 类型 Hook

这类 Hook 只有一个 SyncLoopHook(虽然 Tapable 1.1.1 版本中存在 AsyncSeriesLoopHook,但是并没有将它 export 出来),LoopHook 执行特点是不停的循环执行回调函数,直到所有函数结果 result === undefined。为了更加直观的展现 LoopHook 的执行过程,我对示例代码做了一下丰富:

js">const {SyncLoopHook} = require('tapable');
const hook = new SyncLoopHook(['name']);
let callbackCalledCount1 = 0;
let callbackCalledCount2 = 0;
let callbackCalledCount3 = 0;
let intent = 0;
hook.tap('callback 1', (arg) => {
  callbackCalledCount1++;
  if (callbackCalledCount1 === 2) {
    callbackCalledCount1 = 0;
    intent -= 4;
    intentLog('');
    return;
  } else {
    intentLog('');
    intent += 4;
    return 'callback-1';
  }
});


hook.tap('callback 2', (arg) => {
  callbackCalledCount2++;
  if (callbackCalledCount2 === 2) {
    callbackCalledCount2 = 0;
    intent -= 4;
    intentLog('');
    return;
  } else {
    intentLog('');
    intent += 4;
    return 'callback-2';
  }
});


hook.tap('callback 3', (arg) => {
  callbackCalledCount3++;
  if (callbackCalledCount3 === 2) {
    callbackCalledCount3 = 0;
    intent -= 4;
    intentLog('');
    return;
  } else {
    intentLog('');
    intent += 4;
    return 'callback-3';
  }
});


hook.call('args');


function intentLog(...text) {
  console.log(new Array(intent).join(' '), ...text);
}
/* output
 <callback-1>
 </callback-1>
 <callback-2>
    <callback-1>
    </callback-1>
 </callback-2>
 <callback-3>
    <callback-1>
    </callback-1>
    <callback-2>
        <callback-1>
        </callback-1>
    </callback-2>
 </callback-3>
 */

通过上面的代码可以得出结论:

  1. LoopHook 中的回调返回 undefined(没有 return 其实就是 undefined)才会跳出循环

  2. 所说的循环,起点是第一个回调栈的函数

详细的流程图如下:

61c6a5f28b0df97404c757dfb4d7a345.png

Tapable 在 Webpack 中的应用

了解了 Tapable 的类型和基本使用方法,我们可能产生疑惑,这些类型在 Webpack 中是怎么被应用的,又是为什么要舍近求远的写个专门的库来实现异步,EventEmitter 和 Promise 不香吗?下面我们来看下 Webpack 内部的应用。

在 Webpack 中用的最多的是 AsyncSeriesHook,我们摘录了 Webpack Compiler 一段代码:

js">// 为了好理解,对代码进行必要的精简
// 定义hooks
this.hooks = Object.freeze({
  beforeRun: new AsyncSeriesHook(['compiler']),
  run: new AsyncSeriesHook(['compiler']),
  done: new AsyncSeriesHook(['stats']),
  afterDone: new SyncHook(['stats']),
  failed: new SyncHook(['error']),
});


const finalCallback = (err, stats) => {
  // ...
  if (err) {
    this.hooks.failed.call(err);
  }
  if (callback !== undefined) callback(err, stats);
  // 触发 afterDone
  this.hooks.afterDone.call(stats);
};
const onCompiled = (err, compilation) => {
  if (err) return finalCallback(err);


  if (this.hooks.shouldEmit.call(compilation) === false) {
    // ...
    // 生成stats实例
    const stats = new Stats(compilation);
    this.hooks.done.callAsync(stats, (err) => {
      if (err) return finalCallback(err);
      return finalCallback(null, stats);
    });
    return;
  }
  // ...
  this.hooks.done.callAsync(stats, (err) => {
    if (err) return finalCallback(err);
    this.cache.storeBuildDependencies(compilation.buildDependencies, (err) => {
      if (err) return finalCallback(err);
      return finalCallback(null, stats);
    });
  });
};
const run = () => {
  this.hooks.beforeRun.callAsync(this, (err) => {
    if (err) return finalCallback(err);


    this.hooks.run.callAsync(this, (err) => {
      if (err) return finalCallback(err);


      this.readRecords((err) => {
        if (err) return finalCallback(err);


        this.compile(onCompiled);
      });
    });
  });
};
// ==开始执行==
run();

在上面精简的代码中:

  1. 执行是从 run 开始,依次调用 Tapable 的 Hooks;

  2. 用的最多的是 AsyncSeriesHook 这个

  3. 我们看到整个代码出现最多的是 if (err) return finalCallback(err);

Webpack 内部的 Tapable 的使用,实际是实现了一个异步操作流程,因为使用了 AsyncSeriesHook,所以整个流程都是串行的异步,并且任何函数中遇见错误,回调 Callback 传回错误(finalCallback(err)),则流程立即停止。

这里实际是实现了一个符合 Node.js 的错误优先原则异步流程,这种用法和实现,保证了异步的同时,也很好的控制了流程中遇见的错误,即当错误在任何环节发生的时候,就可以调用回调函数直接结束整个流程。看到这里不得不说这种设计真的很赞。

Node.js 的错误优先原则:Node.js 核心 API 暴露的大多数异步方法都遵循称为错误优先回调的惯用模式。使用这种模式,回调函数作为参数传给方法。当操作完成或出现错误时,回调函数将使用 Error 对象(如果有)作为第一个参数传入。如果没有出现错误,则第一个参数将作为 null 传入。

Tapable 的原理解析

Tapable 的执行流程可以分为四步:

  1. 使用 tap* 对事件进行注册绑定,根据类型不同,提供三种绑定的方式:taptapPromisetapAsync,其中 tapPromisetapAsync 为异步类 Hook 的绑定方法

  2. 使用 call* 对事件进行触发,根据类型不同,也提供了三种触发的方式:callpromisecallAsync

  3. 生成对应类型的代码片段(要执行的代码实际是拼字符串拼出来的)

  4. 生成第三步生成的代码片段

下面以 SyncHook 源码为例,分析下整个流程。先来看下 lib/SyncHook.js 主要代码:

js">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);
  }
}

首先所有的 Hook 都是继承自 Hook 类,针对同步 Hook 的事件绑定,如 SyncHookSyncBailHookSyncLoopHookSyncWaterfallHook, 会在子类中覆写基类 Hook 中 tapAsync 和 tapPromise 方法,这样做可以防止使用者在同步 Hook 中误用异步方法。

下面我按照执行流程的四个步骤来分析下源码,看一下一个完整的流程中,都是调用了什么方法和怎么实现的。

绑定事件

SyncHook 中绑定事件是下面的代码。

js">hook.tap('evt1', (arg0) => {
  console.log(arg0, 2);
});
hook.tap('evt2', (arg0) => {
  console.log(arg0, 3);
});

下面我们来看下 tap 的实现,因为 SyncHook 是继承子 Hook,所以我们找到 lib/Hook.js 中 tap 的实现代码:

js">tap(options, fn) {
    // 实际调用了_tap
    this._tap("sync", options, fn);
}
_tap(type, options, fn) {
    // 这里主要进行了一些参数的类型判断
    if (typeof options === "string") {
        options = {
            name: options
        };
    } else if (typeof options !== "object" || options === null) {
        throw new Error("Invalid tap options");
    }
    if (typeof options.name !== "string" || options.name === "") {
        throw new Error("Missing name for tap");
    }
    if (typeof options.context !== "undefined") {
        deprecateContext();
    }
    options = Object.assign({ type, fn }, options);
    // 这里是注册了Interceptors(拦截器)
    options = this._runRegisterInterceptors(options);
    // 参数处理完之后,调用了_insert,这是关键代码
    this._insert(options);
}

通过查阅 Hook.tap 和 Hook._tap 的代码,发现主要是做一些参数处理的工作,而主要的实现是在 Hook._insert 实现的:

js">// tapable/lib/Hook.js
_insert(item) {
    this._resetCompilation();
    let before;
    if (typeof item.before === "string") {
        before = new Set([item.before]);
    } else if (Array.isArray(item.before)) {
        before = new Set(item.before);
    }
    let stage = 0;
    if (typeof item.stage === "number") {
        stage = item.stage;
    }
    // 这里根据 stage 对事件进行一个优先级排序
    let i = this.taps.length;
    while (i > 0) {
        i--;
        const x = this.taps[i];
        this.taps[i + 1] = x;
        const xStage = x.stage || 0;
        if (before) {
            if (before.has(x.name)) {
                before.delete(x.name);
                continue;
            }
            if (before.size > 0) {
                continue;
            }
        }
        if (xStage > stage) {
            continue;
        }
        i++;
        break;
    }
    // 这是找到了回调栈
    this.taps[i] = item;
}

_insert 的代码主要目的是将传入的事件推入 this.taps 数组,等同于:

js">hook.tap('event', callback);
// → 即
this.taps.push({
  type: 'sync',
  name: 'event',
  fn: callback,
});

在基类 lib/Hook.js 的 constructor 中,可以找到一些变量初始化的代码:

js">class Hook {
  constructor(args = []) {
    // 这里存入初始化的参数
    this._args = args;
    // 这里就是回调栈用到的数组
    this.taps = [];
    // 拦截器数组
    this.interceptors = [];
    this.call = this._call;
    this.promise = this._promise;
    this.callAsync = this._callAsync;
    // 这个比较重要,后面拼代码会用
    this._x = undefined;
  }
}

这样绑定回调函数就完成了,下面看下触发回调的时候发生了什么。

事件触发

在事件触发,我们使用同 syncHook 的 call 方法触发一个事件:

js">hook.call(1, 2);

这里的 call 方法,实际是通过 Object.defineProperties 添加到 Hook.prototype 上面的:

js">// tapable/lib/Hook.js
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'),
    configurable: true,
    writable: true,
  },
  _promise: {
    value: createCompileDelegate('promise', 'promise'),
    configurable: true,
    writable: true,
  },
  _callAsync: {
    value: createCompileDelegate('callAsync', 'async'),
    configurable: true,
    writable: true,
  },
});

在上面的代码中,Hook.prototype 通过对象定义属性方法 Object.defineProperties 定义了三个属性方法:_call_promise_callAsync,这三个属性的 value 都是通过 createCompileDelegate 返回的一个名为 lazyCompileHook 的函数,从名字上面来猜测是「懒编译」,当我们真正调用 call 方法的时候,才会编译出真正的 call 函数。

call 函数编译的用到的是_createCall 方法,这个是在 Hook 类定义的时候就定义的方法,_createCall 实际最终调用了 compile 方法,而通过 Hook.js 代码来看,compile 是个需要子类重写实现的方法:

js">// tapable/lib/Hook.js
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
    });
}

所以,在 Hook 中绕了一圈,我们又回到了 SyncHook 的类,我们再看下 SyncHook 的代码:

js">// lib/SyncHook.js
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);
  }
}

SyncHook 的 compile 来自是 HookCodeFactory 的子类 SyncHookCodeFactory。在 lib/HookCodeFactory.js 找到 setup 方法:

js">// lib/HookCodeFactory
setup(instance, options) {
    instance._x = options.taps.map(t => t.fn);
}

这里的 instance 实际就是 SyncHook 的实例,而_x 就是我们之前绑定事件时候最后的_x

最后 factory.create(options) 调用了 HookCodeFactory 的 create 方法,这个方法就是实际拼接可执行 JavaScript 代码片段的,具体看下实现:

js">// lib/HookCodeFactory.js
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;
        case "async":
            fn = new Function(
                this.args({
                    after: "_callback"
                }),
                '"use strict";\n' +
                    this.header() +
                    this.content({
                        onError: err => `_callback(${err});\n`,
                        onResult: result => `_callback(null, ${result});\n`,
                        onDone: () => "_callback();\n"
                    })
            );
            break;
        case "promise":
            let errorHelperUsed = false;
            const content = this.content({
                onError: err => {
                    errorHelperUsed = true;
                    return `_error(${err});\n`;
                },
                onResult: result => `_resolve(${result});\n`,
                onDone: () => "_resolve();\n"
            });
            let code = "";
            code += '"use strict";\n';
            code += "return new Promise((_resolve, _reject) => {\n";
            if (errorHelperUsed) {
                code += "var _sync = true;\n";
                code += "function _error(_err) {\n";
                code += "if(_sync)\n";
                code += "_resolve(Promise.resolve().then(() => { throw _err; }));\n";
                code += "else\n";
                code += "_reject(_err);\n";
                code += "};\n";
            }
            code += this.header();
            code += content;
            if (errorHelperUsed) {
                code += "_sync = false;\n";
            }
            code += "});\n";
            fn = new Function(this.args(), code);
            break;
    }
    this.deinit();
    return fn;
}

上面 create 代码中的重要参数是 type,而 type 是由 Hook 类在 createCompileDelegate("call", "sync") 的时候传入进去的,所以调用 call 方法,实际 type 为 sync,在 create 中会进入到 case 'sync' 的分支,在 switch 中用到最重要的 content 实际是在 class SyncHookCodeFactory extends HookCodeFactory 的时候定义的。这里我们就不继续追踪代码生成的逻辑实现了,我们可以直接在最后将 fn 的源码 console.log 出来:console.log(fn.toString()),大致可以得到下面的代码:

js">// 调用 call 的代码
const hook = new SyncHook(['argName0', 'argName1']);
hook.tap('evtName', (arg0) => {
  console.log(arg0, 1);
});
hook.call('Webpack', 'Tapable');
// 最终得到的源码是:
function anonymous(argName0, argName1) {
  'use strict';
  var _context;
  var _x = this._x;
  var _fn0 = _x[0];
  _fn0(argName0, argName1);
}

上面的_fn0 实际就是我们 tap 绑定的回调函数,argName0 和 argsName1 就是我们实例化 SyncHook 传入的形参,而我们实际只是在 tap 的回调中用了 arg0 一个参数,所以输出的结果是 Webpack 1

总结

Tapable 是 Webpack 的核心模块,Webpack 的所有工作流程都是通过 Tapable 来实现的。Tapable 本质上是提供了多种类型的事件绑定机制,根据不同的流程特点可以选择不同类型的 Hook 来使用。Tapable 的核心实现在绑定事件阶段跟我们平时的自定义 JavaScript 事件绑定(例如 EventEmitter)没有太大区别,但是在事件触发执行的时候,会临时生成可以执行的函数代码片段。通过这种实现方式,Tapable 实现了强大的事件流程控制能力,也增加了如 waterfall/parallel 系列方法,实现了异步 / 并行等事件流的控制能力。


http://www.niftyadmin.cn/n/1781217.html

相关文章

使用Jquery的Ajax实现无刷新更新,修改,删除页面

本文将向大家讲述一下最近工作的一些总结&#xff0c;主要包括了以下内容&#xff0c;注册界面以及详细信息界面的编辑。主要是介绍了AJAX技术&#xff0c;因为我觉得其他方面没什么好介绍的。首先是跟大家说一下Ajax的优点&#xff0c;假如你删除了一个页面的内容&#xff0c;…

GlusterFS集群文件系统

GlusterFS集群文件系统1.GlusterFS概述GlusterFS是Scale-Out存储解决方案Gluster的核心&#xff0c;它是一个开源的分布式文件系统&#xff0c;具有强大的横向扩展能力&#xff0c;通过扩展能够支持数PB存储容量和处理数千客户端。GlusterFS借助TCP/IP或InfiniBand RDMA网络将物…

年度读书总结:宏观经济学系列

2021 年对于我来说震撼的是四本书&#xff1a;《宏观经济学家 25 讲》《激荡三十年》《十次危机》《结构性改革》。开始的时候&#xff0c;年初朋友推荐了徐高的《宏观经济学 25 讲》B 站视频&#xff0c;从此对新中国建立这段时间的经济学史很感兴趣&#xff0c;于是后来跟人聊…

5、条件、循环和其他语句

赋值魔法包括&#xff1a; 序列解包&#xff1a;将多个值的序列解开&#xff0c;然后放在变量的序列中。 >>> x,y,z1,2,3 >>> x,yy,x >>> x,y,z (2, 1, 3) 链式赋值&#xff1a; >>> xy1 >>> x,y (1, 1) 增量赋值 >>> x…

〖Linux〗noip免费域名申请,及更新域名的API

1. 登录 http://www.noip.com2. 选择 Hosts/Redirects -- Add A Host3. 填写 期望的域名即可(如下图) 4. 更新域名的API: wget -q -O - --http-userusername --http-passwordpasswd "https://dynupdate.no-ip.com/nic/update?hostnameyour_domain&myipyour_ip" …

掌握这些方法论后我工资涨了一倍!

得益于B站Leader的言传身教&#xff0c;方法论这个词算是植入基因了&#xff0c;那么我们一直在说的方法论到底是什么呢&#xff1f;这里直接上百科解释&#xff1a;方法论&#xff0c;就是关于人们认识世界、改造世界的方法的理论。它是人们用什么样的方式、方法来观察事物和处…

goclipse 修改输出编译命令,显示完整的错误信息

2019独角兽企业重金招聘Python工程师标准>>> goclipse来写golang遇到一个问题, 编译总是报错, 但一看出错文件一对不是那个错. 肯定是goclipse的问题, 但又看不到编译go代码的完整命令, 没有完整的错误信息. 方法, 修改goclipse显示完整的命令和错误信息: svn ch…

hibernate开发步骤

2019独角兽企业重金招聘Python工程师标准>>> 1、创将项目 2、导入hibernate依赖的jar包 3、创建domain对象 public class Customer implements Serializable{ private Integer id;private String name;private Integer age; private String des; //省略set和get方…