盐焗乳鸽还要香锅

V1

2022/11/18阅读:25主题:默认主题

源码系列之Tapable--第一弹

源码系列之Tapable--第一弹SyncHook


Tapable是一个灵活多变的 Hook 体系。Webpack的健壮、扩展性极强的插件架构也离不开Tapable体系。

其实Hook的本质就是围绕着 「订阅/发布」 模式叠加各种特化逻辑,适配 Webpack 体系下复杂的事件源-处理器之间交互需求,比如:

  • 有些场景需要支持将前一个处理器的结果传入下一个回调处理器;
  • 有些场景需要支持异步并行调用这些回调处理器。

我相信通过阅读Tapable源码,可以让大家对Hook体系有一个更加深刻的认识,理解Webpack的插件架构,同时对订阅发布模式能有更加灵活的运用~

SyncHook

首先来看一下最简单的SyncHook。顾名思义,是一个同步的Hooks,可以依次执行注册好的回调函数。让我们一起看一下在Tapable中是如何实现SyncHook的吧~

const { SyncHook } = require("../index.js");

// 1. 创建钩子实例
const sleep = new SyncHook();

// 2. 调用订阅接口注册回调
sleep.tap("test", () => {
  console.log("callback A");
});

// 3. 调用发布接口触发回调
sleep.call();

1. 初始化:创建钩子实例

当我们去调用new SyncHook()的时候,会进入到SyncHook的构造函数:

function SyncHook(args = [], name = undefined{
 const hook = new Hook(args, name);
 hook.constructor = SyncHook;
 ...
 hook.compile = COMPILE;
 return hook;
}

这里有一个关键的基类Hook,这里做了一系列变量以及方法的初始化:

class Hook {
 constructor(args = [], name = undefined) {
  this._args = args;
  this.name = name;
  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;
  this._x = undefined;

  this.compile = this.compile;
  this.tap = this.tap;
  this.tapAsync = this.tapAsync;
  this.tapPromise = this.tapPromise;
 }
...

自此为止整个SyncHook初始化完毕了。这里有一个compile方法我们先留在后面去说。

2. 注册回调

然后接下来就是去使用tap方法去注册事件的回调函数,这里需要注意的是,tap同步方法,我们继续跟踪其运行轨迹:

// Hook.js
// 此时:options是事件名称,fn是回调函数
tap(options, fn) {
  this._tap("sync", options, fn);
}
...

_tap(type, options, fn) {
  if (typeof options === "string") {
   options = {
    name: options.trim()
   };
  } 
    ...
  options = Object.assign({ type, fn }, options);
  options = this._runRegisterInterceptors(options);
  this._insert(options);
 }

我们可以看到一系列对options的转换如下:

这里的runRegisterInterceptors会遍历this.interceptors,将options传入每一个拦截器,经过处理后返回一个新的options。由于这里我们并没有注册拦截器因此直接跳过。

接下来将会运行至_insert方法中去。这里有的小伙伴对之前初始化的时候为什么要将相同的函数赋值给call_call,在这里就得到答案了:其实就是拷贝了一份,用于还原。:

// Hook.js
_resetCompilation() {
  this.call = this._call;
  this.callAsync = this._callAsync;
  this.promise = this._promise;
 }

 _insert(item) {
  // 还原call方法
  this._resetCompilation();
  ...
  this.taps[i] = item; // 最后将之前的options插入到taps数组中去
 }
}

3. 触发Hook

sleep.call()执行时,触发Hook.call()CALL_DELEGATE

const CALL_DELEGATE = function(...args{
 this.call = this._createCall("sync");
 return this.call(...args);
};
...
// Hook
 _createCall(type) {
  // type = 'sync'
  return this.compile({
   taps: this.taps,
   interceptors: this.interceptors,
   args: this._args,
   typetype
  });
 }

还记得我们在初始化的时候提到过的complie函数嘛,complie函数由子类构造函数(这里是SyncHook)实现。

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

...
// SyncHook.js
const COMPILE = function(options{
 factory.setup(this, options);
 return factory.create(options);
};

setup 将taps数组内重映射成了Fn[],并将其赋值到SyncHook._x

这里的factory是库内部封装的一个类,主要是用于生成最终的complie函数,即最终执行的call函数,这里先不作多讲解。

// 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.contentWithInterceptors({
       onError: err => `throw ${err};\n`,
       onResult: result => `return ${result};\n`,
       resultReturns: true,
       onDone: () => "",
       rethrowIfPossible: true
      })
    );
    break;
   ......
  this.deinit();
  return fn;
 }
/**
  * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
  */

 init(options) {
  this.options = options;
  this._args = options.args.slice();
 }

这里最终create方法其实是用于创建compile方法,中间的创建过程有点繁杂就不展开了,上述例子最终创建的函数如下:

(function anonymous(
{
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0();
})

可以看到其实就是去执行注册好的回调函数。至此整个SyncHook的使用流程源码解读完毕。

  • new SyncHook(..args)传入参数名称列表
  • 通过tap方法注册事件并存储对应的回调函数
  • 通过call=complie方法依次执行注册好的回调函数
    • 这里要注意的就是,_insert方法内部还支持更复杂的逻辑,包括执行顺序的控制等「具体可以看本文最下方」

其实简而言之可以写成下述代码:

function syncCall({
  const callbacks = [fn1, fn2, fn3];
  for (let i = 0; i < callbacks.length; i++) {
    const cb = callbacks[i];
    cb();
  }
}

需要注意的是,complie方法是不关注返回值的!而且要想传入参数,可以先从new SyncHook(['a'])传入一个形参a,它被用于HookCodeFactory.create方法中的this.args(),并且生成的最终complie函数长这样:

(function anonymous(s
{
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(s); // 这就是回调函数中的参数
})

回调函数中的参数又是会被透穿进来即如果sleep.call(1),则变量s就是1

额外:_insert函数的处理

先把源码贴出来:

_insert(item) {
  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;
  }
  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;
 }

这里很明显的可以看出,我们需要分析的两个重要参数就是stagebefore

我们可以简单来的分析一下:

  • 首先记录下before:Set<tapItem.before>以及tapItem.stage的信息
  • 开始遍历taps数组,从后往前。
    • 首先复制一份尾元素并push进taps中,如果当前的tapItembefore或者stage信息,则直接替换掉之前复制的尾元素。
    • 如果有before属性,则移动i指针到前一个元素,即continue,直至匹配到before所指的name,然后插入在它之前
    • 如果有stage属性,它会对比当前指针指向的元素的stage以及tapItem的stage大小,如果大,则继续向前遍历,直至找到一个比tapItem小的,则插入到其之后(i++)。

这里官网其实没有关于stage属性是描述,但是我们可以从源码中去得出该属性的作用。

我是盐焗乳鸽还要香锅,喜欢我的文章欢迎关注噢,github链接https://github.com/1360151219,博客链接是strk2.cn,掘金账号https://juejin.cn/user/1812428713376845

分类:

前端

标签:

JavaScript

作者介绍

盐焗乳鸽还要香锅
V1