全方位的,零死角的,分析tapable源码

上一遍博文中,我们谈到了tapable的用法,现在我们来深入一下tap究竟是怎么运行的, 怎么处理,控制 tap 进去的钩子函数,拦截器又是怎么运行的.

俺们先从同步函数说起,再扩展到异步.

tap

这里有一个例子

let SyncHook = require('./lib/SyncHook.js')

let h1 = new SyncHook(['options']);

h1.tap('A', function (arg) {
  console.log('A',arg);
  return 'b'; // 除非你在拦截器上的 register 上调用这个函数,不然这个返回值你拿不到.
})

h1.tap('B', function () {
  console.log('b')
})
h1.tap('C', function () {
  console.log('c')
})
h1.tap('D', function () {
  console.log('d')
})

h1.intercept({
  call: (...args) => {
    console.log(...args, '-------------intercept call');
  },
  //
  register: (tap) => {
  console.log(tap, '------------------intercept register');

    return tap;
  },
  loop: (...args) => {
    console.log(...args, '-------------intercept loop')
  },
  tap: (tap) => {
    console.log(tap, '-------------------intercept tap')

  }
})
h1.call(6);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

new SyncHook(['synchook'])

首先先创建一个同步钩子对象,那这一步会干什么呢?

这一步会先执行超类Hook的初始化工作

// 初始化
constructor(args) {
  // 参数必须是数组
  if (!Array.isArray(args)) args = [];
  // 把数组参数赋值给 _args 内部属性, new 的时候传进来的一系列参数.
  this._args = args;
  // 绑定taps,应该是事件
  this.taps = [];
  // 拦截器数组
  this.interceptors = [];
  // 暴露出去用于调用同步钩子的函数
  this.call = this._call;
  // 暴露出去的用于调用异步promise函数
  this.promise = this._promise;
  // 暴露出去的用于调用异步钩子函数
  this.callAsync = this._callAsync;
  // 用于生存调用函数的时候,保存钩子数组的变量,现在暂时先不管.
  this._x = undefined;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

第二步 .tap()

现在我们来看看调用了tap() 方法后发生了什么

tap(options, fn) {
  // 下面是一些参数的限制,第一个参数必须是字符串或者是带name属性的对象,
  // 用于标明钩子,并把钩子和名字都整合到 options 对象里面
  if (typeof options === "string") options = { name: options };
  if (typeof options !== "object" || options === null)
    throw new Error(
      "Invalid arguments to tap(options: Object, fn: function)"
    );
  options = Object.assign({ type: "sync", fn: fn }, options);
  if (typeof options.name !== "string" || options.name === "")
    throw new Error("Missing name for tap");
  // 注册拦截器
  options = this._runRegisterInterceptors(options);
  // 插入钩子
  this._insert(options);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 现在我们来看看如何注册拦截器
_runRegisterInterceptors(options) {
  // 现在这个参数应该是这个样子的{fn: function..., type: sync,name: 'A' }
// 遍历拦截器,有就应用,没有就把配置返还回去
for (const interceptor of this.interceptors) {
  if (interceptor.register) {
    // 把选项传入拦截器注册,从这里可以看出,拦截器的register 可以返回一个新的options选项,并且替换掉原来的options选项,也就是说可以在执行了一次register之后 改变你当初 tap 进去的方法
    const newOptions = interceptor.register(options);
    if (newOptions !== undefined) options = newOptions;
  }
}
return options;
}
1
2
3
4
5
6
7
8
9
10
11
12

注意: 这里执行的register拦截器是有顺序问题的, 这个执行在tap()里面,也就是说,你这个拦截器要在调用tap(),之前就调用 intercept()添加的.

那拦截器是怎么添加进去的呢,来看下intercept()

intercept(interceptor) {
  // 重置所有的 调用 方法,在教程中我们提到了 编译出来的调用方法依赖的其中一点就是 拦截器. 所有每添加一个拦截器都要重置一次调用方法,在下一次编译的时候,重新生成.
  this._resetCompilation();
  // 保存拦截器 而且是复制一份,保留原本的引用
  this.interceptors.push(Object.assign({}, interceptor));
  // 运行所有的拦截器的register函数并且把 taps[i],(tap对象) 传进去.
  // 在intercept 的时候也会遍历执行一次当前所有的taps,把他们作为参数调用拦截器的register,并且把返回的 tap对象(tap对象就是指 tap函数里面把fn和name这些信息整合起来的那个对象) 替换了原来的 tap对象,所以register最好返回一个tap, 在例子中我返回了原来的tap, 但是其实最好返回一个全新的tap
  if (interceptor.register) {
    for (let i = 0; i < this.taps.length; i++)
      this.taps[i] = interceptor.register(this.taps[i]);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

注意: 也就是在调用tap() 之后再传入的拦截器,会在传入的时候就为每一个tap 调用register方法

  • 现在我们来看看_insert
_insert(item) {
  // 重置资源,因为每一个插件都会有一个新的Compilation
  this._resetCompilation();
  // 顺序标记, 这里联合 __test__ 包里的Hook.js一起使用
  // 看源码不懂,可以看他的测试代码,就知道他写的是什么目的.
  // 从测试代码可以看到,这个 {before}是插件的名字.
  let before;
  // before 可以是单个字符串插件名称,也可以是一个字符串数组插件.
  if (typeof item.before === "string") {
    before = new Set([item.before]);
  }
  else if (Array.isArray(item.before)) {
    before = new Set(item.before);
  }
  // 阶段
  // 从测试代码可以知道这个也是一个控制顺序的属性,值越小,执行得就越在前面
  // 而且优先级低于 before
  let stage = 0;
  if (typeof item.stage === "number") stage = item.stage;
  let i = this.taps.length;
  // 遍历所有`tap`了的函数,然后根据 stage 和 before 进行重新排序.
  // 假设现在tap了 两个钩子  A B  `B` 的配置是  {name: 'B', before: 'A'}
  while (i > 0) {// i = 1, taps = [A]
    i--;// i = 0 首先-- 是因为要从最后一个开始
    const x = this.taps[i];// x = A
    this.taps[i + 1] = x;// i = 0, taps[1] = A  i+1 把当前元素往后移位,把位置让出来
    const xStage = x.stage || 0;// xStage = 0
    if (before) {// 如果有这个属性就会进入这个判断
      if (before.has(x.name)) {// 如果before 有x.name 就会把这个插件名称从before这个列表里删除,代表这个钩子位置已经在当前的钩子之前
        before.delete(x.name);
        continue;// 如果before还有元素,继续循环,执行上面的操作
      }
      if (before.size > 0) {
        continue;// 如果before还有元素,那就一直循环,直到第一位.
      }
    }
    if (xStage > stage) {// 如果stage比当前钩子的stage大,继续往前挪
      continue;
    }
    i++;
    break;
  }
  this.taps[i] = item;// 把挪出来的位置插入传进来的钩子
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

这其实就是一个排序算法, 根据before, stage 的值来排序,也就是说你可以这样tap进来一个插件

h1.tap({
  name: 'B',
  before: 'A'
  }, () => {
    console.log('i am B')
  })
1
2
3
4
5
6

发布订阅模式

发布订阅模式是一个在前后端都盛行的一个模式,前端的promise,事件,等等都基于发布订阅模式,其实tapable 也是一种发布订阅模式,上面的tap 只是订阅了钩子函数,我们还需要发布他,接下来我们谈谈h1.call(),跟紧了,这里面才是重点.

我们可以在初始化中看到this.call = this._call,那我们来看一下 this._call() 是个啥

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
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

结果很明显,这个函数是由createCompileDelegate(),这个函数返回的,依赖于,函数的名字以及钩子的类型.

createCompileDelegate(name, type)

function createCompileDelegate(name, type) {
  return function lazyCompileHook(...args) {
    // 子类调用时,this默认绑定到子类
    // (不明白的可以了解js this指向,一个函数的this指向调用他的对象,没有就是全局,除非使用call apply bind 等改变指向)
    // 在我们的例子中,这个 this 是 SyncHook
    this[name] = this._createCall(type);
    // 用args 去调用Call
    return this[name](...args);
  };
}
1
2
3
4
5
6
7
8
9
10

在上面的注释上可以加到,他通过闭包保存了nametype的值,在我们这个例子中,这里就是this.call = this._createCall('sync');然后把我们外部调用call(666) 时 传入的参数给到他编译生成的方法中.

注意,在我们这个例子当中我在call的时候并没有传入参数.

这时候这个call方法的重点就在_createCall方法里面了.

_createCall()

_createCall(type) {

  // 传递一个整合了各个依赖条件的对象给子类的compile方法
  return this.compile({
    taps: this.taps,
    interceptors: this.interceptors,
    args: this._args,
    type: type
  });
}

1
2
3
4
5
6
7
8
9
10
11

从一开始,我们就在Hook.js上分析,我们来看看Hook上的compile

compile(options) {
  throw new Error("Abstract: should be overriden");
}
1
2
3

清晰明了,这个方法一定要子类复写,不然报错,上面的_createCompileDelegate的注释也写得很清楚,在当前的上下文中,this指向的是,子类,在我们这个例子中就是SyncHook

来看看SyncHook 的compile

compile(options) {
  // 现在options 是由Hook里面 传到这里的
  // options
  // {
  //  taps: this.taps, tap对象数组
  //  interceptors: this.interceptors, 拦截器数组
  //  args: this._args,
  //  type: type
  // }
  // 对应回教程中的编译出来的调用函数依赖于的那几项看看,是不是这些,钩子的个数,new SyncHook(['arg'])的参数个数,拦截器的个数,钩子的类型.
  factory.setup(this, options);

  return factory.create(options);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

好吧 现在来看看setup, 咦? factory 怎么来的,原来

const factory = new SyncHookCodeFactory();

是new 出来的,现在来看看SyncHookCodeFactory 的父类 HookCodeFactory

constructor(config) {

  // 这个config作用暂定.因为我看了这个文件,没看到有引用的地方,
  // 应该是其他子类有引用到
  this.config = config;
  // 这两个不难懂, 往下看就知道了
  this.options = undefined;
  this._args = undefined;
}
1
2
3
4
5
6
7
8
9

现在可以来看一下setup了

setup(instance, options) {
  // 这里的instance 是syncHook 实例, 其实就是把tap进来的钩子数组给到钩子的_x属性里.
  instance._x = options.taps.map(t => t.fn);
}
1
2
3
4

OK, 到create了

这个create有点长, 看仔细了,我们现在分析同步的部分.

create(options) {
  // 初始化参数,保存options到本对象this.options,保存new Hook(["options"]) 传入的参数到 this._args
  this.init(options);
  let fn;
  // 动态构建钩子,这里是抽象层,分同步, 异步, promise
  switch (this.options.type) {
    // 先看同步
    case "sync":
      // 动态返回一个钩子函数
      fn = new Function(
        // 生成函数的参数,no before no after 返回参数字符串 xxx,xxx 在
        // 注意这里this.args返回的是一个字符串,
        // 在这个例子中是options
        this.args(),
        '"use strict";\n' +
          this.header() +
          this.content({
            onError: err => `throw ${err};\n`,
            onResult: result => `return ${result};\n`,
            onDone: () => "",
            rethrowIfPossible: true
          })
      );
      break;
    case "async":
      fn = new Function(
        this.args({
          after: "_callback"
        }),
        '"use strict";\n' +
          this.header() +
          // 这个 content 调用的是子类类的 content 函数,
          // 参数由子类传,实际返回的是 this.callTapsSeries() 返回的类容
          this.content({
            onError: err => `_callback(${err});\n`,
            onResult: result => `_callback(null, ${result});\n`,
            onDone: () => "_callback();\n"
          })
      );
      break;
    case "promise":
      let code = "";
      code += '"use strict";\n';
      code += "return new Promise((_resolve, _reject) => {\n";
      code += "var _sync = true;\n";
      code += this.header();
      code += this.content({
        onError: err => {
          let code = "";
          code += "if(_sync)\n";
          code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
          code += "else\n";
          code += `_reject(${err});\n`;
          return code;
        },
        onResult: result => `_resolve(${result});\n`,
        onDone: () => "_resolve();\n"
      });
      code += "_sync = false;\n";
      code += "});\n";
      fn = new Function(this.args(), code);
      break;
  }
  // 把刚才init赋的值初始化为undefined
  // this.options = undefined;
  // this._args = undefined;
  this.deinit();

  return fn;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

到了这个方法,一切我们都一目了然了(看content的参数), 在我们的例子中他是通过动态的生成一个call方法,根据的条件有,钩子是否有context 属性(这个是根据header的代码才能知道), 钩子的个数, 钩子的类型,钩子的参数,钩子的拦截器个数.

注意,这上面有关于 fn这个变量的函数,返回的都是字符串,不是函数不是方法,是返回可以转化成代码执行的字符串,思维要转变过来.

现在我们来看看header()

header() {
  let code = "";
  // this.needContext() 判断taps[i] 是否 有context 属性, 任意一个tap有 都会返回 true
  if (this.needContext()) {
    // 如果有context 属性, 那_context这个变量就是一个空的对象.
    code += "var _context = {};\n";
  } else {
    // 否则 就是undefined
    code += "var _context;\n";
  }
  // 在setup()中 把所有tap对象的钩子 都给到了 instance ,这里的this 就是setup 中的instance _x 就是钩子对象数组
  code += "var _x = this._x;\n";
  // 如果有拦截器,在我们的例子中,就有一个拦截器
  if (this.options.interceptors.length > 0) {
    // 保存taps 数组到_taps变量, 保存拦截器数组 到变量_interceptors
    code += "var _taps = this.taps;\n";
    code += "var _interceptors = this.interceptors;\n";
  }
  // 如果没有拦截器, 这里也不会执行.一个拦截器只会生成一次call
  // 在我们的例子中,就有一个拦截器,就有call
  for (let i = 0; i < this.options.interceptors.length; i++) {
    const interceptor = this.options.interceptors[i];
    if (interceptor.call) {
      // getInterceptor 返回的 是字符串 是 `_interceptors[i]`
      // 后面的before 因为我们的拦截器没有context 所以返回的是undefined 所以后面没有跟一个空对象
      code += `${this.getInterceptor(i)}.call(${this.args({
        before: interceptor.context ? "_context" : undefined
      })});\n`;
    }
  }
  return code;
  // 注意 header 返回的不是代码,是可以转化成代码的字符串(这个时候并没有执行).
  /**
    * 此时call函数应该为:
    * "use strict";
    * function (options) {
    *   var _context;
    *   var _x = this._x;
    *   var _taps = this.taps;
    *   var _interterceptors = this.interceptors;
    * // 我们只有一个拦截器所以下面的只会生成一个
    *   _interceptors[0].call(options);
    *}
    */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

现在到我们的this.content()了,仔细一看,this.content()方法并不在HookCodeFactory上,很明显这个content是由子类来实现的,往回看看这个create是由谁调用的?没错,是SuncHookCodeFactory的石料理,我们来看看SyncHook.js上的SyncHookCodeFactory实现的content

在看这个content实现之前,先来回顾一下父类的create()给他传了什么参数.

this.content({
  onError: err => `throw ${err};\n`,
  onResult: result => `return ${result};\n`,
  onDone: () => "",
  rethrowIfPossible: true
})
1
2
3
4
5
6

注意了,这上面不是抛出错误,不是返回值. 这里面的回调执行了以后返回的是一个字符串,不要搞混了代码与可以转化成代码的字符串.

content({ onError, onResult, onDone, rethrowIfPossible }) {
  return this.callTapsSeries({
    // 可以在这改变onError 但是这里的 i 并没有用到,这是什么操作...
    // 注意这里并没有传入onResult
    onError: (i, err) => onError(err),
    onDone,
    // 这个默认为true
    rethrowIfPossible
  });
}
1
2
3
4
5
6
7
8
9
10

这个函数返回什么取决于this.callTapSeries(), 那接下来我们来看看这个函数(这层层嵌套,其实也是有可斟酌的地方.看源码不仅要看实现,代码的组织也是很重要的编码能力)

刚才函数的头部已经出来了,头部做了初始化的操作,与生成执行拦截器代码.content很明显,要开始生成执行我们的tap对象的代码了(如果不然,我们的tap进来的函数在哪里执行呢? 滑稽:).

callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
  // 如果 taps 钩子处理完毕,执行onDone,或者一个tap都没有 onDone() 返回的是一个字符串.看上面的回顾就知道了.
  if (this.options.taps.length === 0) return onDone();
  // 如果由异步钩子,把第一个异步钩子的下标,如果没有这个返回的是-1
  const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
  // 定义一个函数 接受一个 number 类型的参数, i 应该是taps的index
  // 从这个函数的命名来看,这个函数应该会递归的执行
  // 我们先开最后的return语句,发现第一个传进来的参数是0
  const next = i => {
    // 如果 大于等于钩子函数数组长度, 返回并执行onDone回调,就是tap对象都处理完了
    // 跳出递归的条件
    if (i >= this.options.taps.length) {
      return onDone();
    }
    // 这个方法就是递归的关键,看见没,逐渐往上遍历
    // 注意这里只是定义了方法,并没有执行
    const done = () => next(i + 1);
    // 传入一个值 如果是false 就执行onDone true 返回一个 ""
    // 字面意思,是否跳过done 应该是增加一个跳出递归的条件
    const doneBreak = skipDone => {
      if (skipDone) return "";
      return onDone();
    };
    // 这里就是处理单个taps对象的关键,传入一个下标,和一系列回调.
    return this.callTap(i, {
      // 调用的onError 是 (i, err) => onError(err) , 后面这个onError(err)是 () => `throw ${err}`
      // 目前 i done doneBreak 都没有用到
      onError: error => onError(i, error, done, doneBreak),
      // 这里onResult 同步钩子的情况下在外部是没有传进来的,刚才也提到了
      // 这里onResult是 undefined
      onResult:
        onResult &&
        (result => {
          return onResult(i, result, done, doneBreak);
        }),
      // 没有onResult 一定要有一个onDone 所以这里就是一个默认的完成回调
      // 这里的done 执行的是next(i+1), 也就是迭代的处理完所有的taps
      onDone:
        !onResult &&
        (() => {return done();}),
      // rethrowIfPossible 默认是 true 也就是返回后面的
      // 因为没有异步函数 firstAsync = -1.
      // 所以返回的是 -1 < 0,也就是true, 这个可以判断当前的是否是异步的tap对象
      //  这里挺妙的 如果是 false 那么当前的钩子类型就不是sync,可能是promise或者是async
      // 具体作用要看callTaps()如何使用这个.
      rethrowIfPossible:
        rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
    });
  };
  
  return next(0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

参数搞明白了,现在,我们可以进入callTap()

callTap挺长的,因为他也分了3种类型分别处理,想create()一样.

/** tapIndex 下标
  * onError:() => onError(i,err,done,skipdone) ,
  * onReslt: undefined
  * onDone: () => {return: done()} //开启递归的钥匙
  * rethrowIfPossible: false 说明当前的钩子不是sync的.
  */
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
  let code = "";
  // hasTapCached 是否有tap的缓存, 这个要看看他是怎么做的缓存了
  let hasTapCached = false;
  // 这里还是拦截器的用法,如果有就执行拦截器的tap函数
  for (let i = 0; i < this.options.interceptors.length; i++) {
    const interceptor = this.options.interceptors[i];
    if (interceptor.tap) {
      if (!hasTapCached) {
        // 这里getTap返回的是 _taps[0] _taps[1]... 的字符串
        // 这里生成的代码就是 `var _tap0 = _taps[0]`
        // 注意: _taps 变量我们在 header 那里已经生成了
        code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
        // 可以看到这个变量的作用就是,如果有多个拦截器.这里也只会执行一次.
        // 注意这句获取_taps 对象的下标用的是tapIndex,在一次循环中,这个tapIndex不会变
        // 就是说如果这里执行多次,就会生成多个重复代码,不稳定,也影响性能.
        // 但是你又要判断拦截器有没有tap才可以执行,或许有更好的写法
        // 如果你能想到,那么你就是webpack的贡献者了.不过这样写,似乎也没什么不好.
        hasTapCached = true;
      }
      // 这里很明显跟上面的getTap 一样 返回的都是字符串
      // 我就直接把这里的code 分析出来了,注意 这里还是在循坏中.
      // code += _interceptor[0].tap(_tap0);
      // 由于我们的拦截器没有context,所以没传_context进来.
      // 可以看到这里是调用拦截器的tap方法然后传入tap0对象的地方
      code += `${this.getInterceptor(i)}.tap(${
        interceptor.context ? "_context, " : ""
      }_tap${tapIndex});\n`;
    }
  }
  // 跑出了循坏
  // 这里的getTapFn 返回的也是字符串 `_x[0]`
  // callTap用到的这些全部在header() 那里生成了,忘记的回头看一下.
  // 这里的code就是: var _fn0 = _x[0]
  code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
  const tap = this.options.taps[tapIndex];
  // 开始处理tap 对象
  switch (tap.type) {
    case "sync":
      // 全是同步的时候, 这里不执行, 如果有异步函数,那么恭喜,有可能会报错.所以他加了个 try...catch
      if (!rethrowIfPossible) {
        code += `var _hasError${tapIndex} = false;\n`;
        code += "try {\n";
      }
      // 前面分析了 同步的时候 onResult 是 undefined
      // 我们也分析一下如果走这里会怎样
      // var _result0 = _fn0(option)
      // 可以看到是调用tap 进来的钩子并且接收参数
      if (onResult) {
        code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
          before: tap.context ? "_context" : undefined
        })});\n`;
      } else {
        // 所以会走这里
        // _fn0(options) 额... 我日 有就接受一下结果
        code += `_fn${tapIndex}(${this.args({
          before: tap.context ? "_context" : undefined
        })});\n`;
      }
      // 把 catch 补上,在这个例子中没有
      if (!rethrowIfPossible) {
        code += "} catch(_err) {\n";
        code += `_hasError${tapIndex} = true;\n`;
        code += onError("_err");
        code += "}\n";
        code += `if(!_hasError${tapIndex}) {\n`;
      }
      // 有onResult 就把结果给传递出去. 目前没有
      if (onResult) {
        code += onResult(`_result${tapIndex}`);
      }
      // 有onDone() 就调用他开始递归,还记得上面的next(i+1) 吗?
      if (onDone) {
        code += onDone();
      }
      // 这里是不上上面的if的大括号,在这个例子中没有,所以这里也不执行
      if (!rethrowIfPossible) {
        code += "}\n";
      }
      // 同步情况下, 这里最终的代码就是
      // var _tap0 = _taps[0];
      // _interceptors[0].tap(_tap0);
      // var _fn0 = _x[0];
      // _fn0(options);
      // 可以看到,这里会递归下去
      // 因为我们tap了4个钩子
      // 所以这里会从复4次
      // 最终长这样
      // var _tap0 = _taps[0];
      // _interceptors[0].tap(_tap0);
      // var _fn0 = _x[0];
      // _fn0(options);
      // var _tap1 = _taps[1];
      // _interceptors[1].tap(_tap1);
      // var _fn1 = _x[1];
      // _fn1(options);
      // ......
      break;
    case "async":
      let cbCode = "";
      if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
      else cbCode += `_err${tapIndex} => {\n`;
      cbCode += `if(_err${tapIndex}) {\n`;
      cbCode += onError(`_err${tapIndex}`);
      cbCode += "} else {\n";
      if (onResult) {
        cbCode += onResult(`_result${tapIndex}`);
      }
      if (onDone) {
        cbCode += onDone();
      }
      cbCode += "}\n";
      cbCode += "}";
      code += `_fn${tapIndex}(${this.args({
        before: tap.context ? "_context" : undefined,
        after: cbCode
      })});\n`;
      break;
    case "promise":
      code += `var _hasResult${tapIndex} = false;\n`;
      code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
        before: tap.context ? "_context" : undefined
      })});\n`;
      code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
      code += `  throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;
      code += `_promise${tapIndex}.then(_result${tapIndex} => {\n`;
      code += `_hasResult${tapIndex} = true;\n`;
      if (onResult) {
        code += onResult(`_result${tapIndex}`);
      }
      if (onDone) {
        code += onDone();
      }
      code += `}, _err${tapIndex} => {\n`;
      code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
      code += onError(`_err${tapIndex}`);
      code += "});\n";
      break;
  }
  return code;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

好了, 到了这里 我们可以把compile 出来的call 方法输出出来了

"use strict";
function (options) {
  var _context;
  var _x = this._x;
  var _taps = this.taps;
  var _interterceptors = this.interceptors;
// 我们只有一个拦截器所以下面的只会生成一个
  _interceptors[0].call(options);

  var _tap0 = _taps[0];
  _interceptors[0].tap(_tap0);
  var _fn0 = _x[0];
  _fn0(options);
  var _tap1 = _taps[1];
  _interceptors[1].tap(_tap1);
  var _fn1 = _x[1];
  _fn1(options);
  var _tap2 = _taps[2];
  _interceptors[2].tap(_tap2);
  var _fn2 = _x[2];
  _fn2(options);
  var _tap3 = _taps[3];
  _interceptors[3].tap(_tap3);
  var _fn3 = _x[3];
  _fn3(options);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

到了这里可以知道,我们的例子中h1.call()其实调用的就是这个方法.到此我们可以说是知道了这个库的百分之80了.

不知道大家有没有发现,这个生成的函数的参数列表是从哪里来的呢?往回翻到create()方法里面调用的this.args()你就会看见,没错就是this._args. 这个东西在哪里初始化呢? 翻一下就知道,这是在Hook.js这个类里面初始化的,也就是说你h1 = new xxxHook(['options']) 的时候传入的数组有几个值,那么你h1.call({name: 'haha'}) 就能传几个值.看教程的时候他说,这里传入的是一个参数名字的字符串列表,那时候我就纳闷,什么鬼,我传入的不是值吗,怎么就变成了参数名称,现在完全掌握....

好了,最简单的SyncHook 已经搞掂,但是一看tapable内部核心使用的钩子却不是他,而是SyncBailHook,在教程中我们已经知道,bail是只要有一个钩子执行完了,并且返回一个值,那么其他的钩子就不执行.我们来看看他是怎么实现的.

从刚才我们弄明白的synchook,我们知道了他的套路,其实生成的函数的header()都是一样的,这次我们直接来看看bailhook实现的content()方法,

content({ onError, onResult, onDone, rethrowIfPossible }) {
  return this.callTapsSeries({
    onError: (i, err) => onError(err),
  // 看回callTapsSeries 就知道这里传入的next 是 done
    onResult: (i, result, next) =>
      `if(${result} !== undefined) {\n${onResult(
        result
      )};\n} else {\n${next()}}\n`,
    onDone,
    rethrowIfPossible
  });
}
1
2
3
4
5
6
7
8
9
10
11
12

看出来了哪里不一样吗? 是的bailhookcallTapsSeries传了onResult属性,我们来看看他这个onResult是啥黑科技

父类传的onResult默认是 (result) => 'return ${result}',那么他这里返回的就是:


// 下面返回的是字符串,
if (xxx !== undefined) {
  // 这里说明,只要有返回值(因为不返回默认是undefined),就会立即return;
  return result;
} else {
  // next(); 这里返回的是一个字符串(因为要生成字符串代码)
  // 我在上面的注释中提到了 next 是 done 就是那个开启递归的门
  // 所以如果tap 一直没返回值, 这里就会一直 if...else.. 的嵌套下去
  
}
1
2
3
4
5
6
7
8
9
10
11

回头想想,我们刚刚是不是分析了capTap(),如果我们传了onResult 会怎样? 如果你还记得就知道,如果有传了onResult这个回调,他就会接收这个返回值.并且会调用这个回调把result传出去.

而且还要注意的是,onDonecallTap()的时候是处理过的,我在贴出来一次.

onDone:!onResult && (() => {return done();})
1

也就是说如果我传了onResult 那么这个onDone就是一个false.

所以递归的门现在从synconDone,变到syncBailonResult

好,现在带着这些变化去看this.capTap(),你就能推出现在这个 call 函数会变成这样.

"use strict";
function (options) {
  var _context;
  var _x = this._x;
  var _taps = this.taps;
  var _interterceptors = this.interceptors;
// 我们只有一个拦截器所以下面的只会生成一个
  _interceptors[0].call(options);

  var _tap0 = _taps[0];
  _interceptors[0].tap(_tap0);
  var _fn0 = _x[0];
  var _result0 = _fn0(options);

  if (_result0 !== undefined) {
    // 这里说明,只要有返回值(因为不返回默认是undefined),就会立即return;
    return _result0
  } else {
    var _tap1 = _taps[1];
    _interceptors[1].tap(_tap1);
    var _fn1 = _x[1];
    var _result1 = _fn1(options);
    if (_result1 !== undefined) {
      return _result1
    } else {
      var _tap2 = _taps[2];
      _interceptors[2].tap(_tap2);
      var _fn2 = _x[2];
      var _result2 = _fn2(options);
      if (_result2 !== undefined) {
        return _result2
      } else {
        var _tap3 = _taps[3];
        _interceptors[3].tap(_tap3);
        var _fn3 = _x[3];
        _fn3(options);
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

到如今,tapable库 已经删除了 tapable文件,只留下了钩子文件.但不影响功能,webpack 里的compile compilation 等一众重要插件,都是基于 tapable中的这些钩子.

所以我解析这个tapable也没有意义了.

到此,关于tapable的大部分我都解剖了一遍,还有其他类型的hook 如果你们愿意,相信你们去研究一下,也能够游刃有余.

那个,写得有些随性,可能会让你们觉得模糊,但是...我写作新手,尽力了,不懂就在那个评论区问我.我看到会回复的.共勉.

后记

本来以为会很难,但是越往下深入的时候发现,大神之所以成为大神,不是他的代码写得牛,是他的思维牛,没有看不懂的代码,只有跟不上的思路,要看懂他如何把call 函数组织出来不难,难的是,他居然能想到这样来生成函数,还可以考虑到,拦截器钩子,和context 属性,以及他的 onResult onDone 回调的判断,架构的设计,等等,一步接一步.先膜拜吧...

最近更新时间: 2/8/2019, 5:13:15 PM