likes
comments
collection
share

理解Webpack不得不提的Tapable

作者站长头像
站长
· 阅读数 77
  • Tapable是一款提供了各种事件钩子领域模型的工具,为webpack提供生命周期管理能力。

Tapable介绍

钩子类型

Tapable提供十种类型事件钩子管理方案,可以按同步和异步场景做区分:

  1. 同步场景:回调函数函数是同步执行
  2. 异步场景:回调函数是异步执行

理解Webpack不得不提的Tapable

钩子运行机制

无论是同步还是异步,钩子一共有四种运行机制,接下来以同步钩子为例子讲解这四种钩子运行机制:

  1. 普通钩子(SyncHook)

普通钩子类型是按照注册顺序执行钩子函数,并不关心返回值如何:

理解Webpack不得不提的Tapable

const syncHook = new SyncHook();

syncHook.tap('A', () => {
  console.log('call A');
});
syncHook.tap('B', () => {
  console.log('call B');
});
syncHook.tap('C', () => {
  console.log('call C');
});

syncHook.call();

// call A
// call B
// call C
  1. 保险钩子(SyncBailHook)

保险钩子是在同步钩子的基础上新增判断钩子返回值能力,一旦返回值不为undefined则会跳过后面的钩子函数执行

理解Webpack不得不提的Tapable

const syncBailHook = new SyncBailHook();

syncBailHook.tap('A', () => {
  console.log('call A');
});
syncBailHook.tap('B', () => {
  console.log('call B');
  
  return false;
});
syncBailHook.tap('C', () => {
  console.log('call C');
});

syncBailHook.call();

// call A
// call B
  1. 循环钩子(SyncLoopHook)

循环钩子机制有点类似于保险钩子,但是有一点不同:当钩子返回内容不为undefined时,会重新开始钩子执行而非中断执行:

理解Webpack不得不提的Tapable

const syncLoopHook = new SyncLoopHook();

let hookBCallCount = 0

syncLoopHook.tap('A', () => {
  console.log('call A');
  
  return count + 1;
});
syncLoopHook.tap('B', () => {
  console.log('call B');
  hookBCallCount++;
  
  if (hookBCallCount === 1) {
    return false;
  }
});
syncLoopHook.tap('C', () => {
  console.log('call C');
  
  return count + 1;
});

const result = syncLoopHook.call();

// call A
// call B
// call A
// call B
// call C
  1. 瀑布流钩子(SyncWaterfallHook)

瀑布钩子是在同步钩子的基础上将前面钩子的返回值作为下一个钩子执行结果:

理解Webpack不得不提的Tapable

const syncWaterfallHook = new SyncWaterfallHook(['count']);

syncWaterfallHook.tap('A', (count) => {
  console.log('call A', count);
  
  return count + 1;
});
syncWaterfallHook.tap('B', (count) => {
  console.log('call B', count);
  
  return count + 1;
});
syncWaterfallHook.tap('C', (count) => {
  console.log('call C', count);
  
  return count + 1;
});

const result = syncWaterfallHook.call(1);

console.log('result: ', result);

// call A 1
// call B 2
// call C 3
// result 4

拦截器机制

Tapable可以为钩子注册、调用时注册拦截器,在注册钩子或者调用时先执行拦截器内容,类似于Axios的拦截器能力。

interface Interceptor {
  call: (...args) => void;
  tap: (...args) => void;
  loop: (...args) => void;
  register: (tap: Tap) => Tap | undefined;
}

// 下面是官方的一个拦截器例子,
myCar.hooks.calculateRoutes.intercept({
  call: (source, target, routesList) => {
    console.log("Starting to calculate routes");
  },
  register: (tapInfo) => {
    // tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
    console.log(`${tapInfo.name} is doing its job`);
    return tapInfo; // may return a new tapInfo object
  }
})

Tapable原理讲解

将Tapable内容进行拆分可以分为Hook管理和Hook代码生成两个模块:

  1. Hook管理负责管理钩子和拦截器注册
  2. Hook代码生成负责生成钩子运行代码

api介绍

名称介绍
tap注册同步回调钩子函数
tapAsync注册异步回调钩子函数
call同步触发回调钩子
callAsync以Async形式异步调用
callPromise以Promise形式异步调用

以SyncHook为切入点讲解Tapable原理,首先进入src/SyncHook.js文件,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;

很明显SyncHook继承于Hook类,本函数代码相对比较简单,所以需要切入Hook.js文件中查看Hook的实现。

Hook管理

在进入到Hook类讲解之前,先聊聊Hook的本质是什么?Hook本质就是一个订阅发布模型,将事件的生产者和消费者解耦开来,所以最简单的Hook应当有订阅和发布两个操作。

接下来进入到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;
 }
}

可以关注到三部分内容:

  1. 属性设置:

    1. class Hook {
        constructor(args = [], name = undefined) {
          // 外部透传进来,设置钩子函数的参数
          this._args = args;
          // 钩子的名称
          this.name = name;
          // 订阅事件
          this.taps = [];
          // 拦截器
          this.interceptors = [];
       }
      }
      
    2.   其中taps存储者发布订阅模型中的订阅事件,而interceptors则是拦截器,拦截器能力。
  2. 接下来是发布方法的初始化

    1. const CALL_DELEGATE = function(...args) {
       this.call = this._createCall("sync");
       return  this.call(...args);
      };
      const CALL_ASYNC_DELEGATE = function(...args) {
       this.callAsync = this._createCall("async");
       return  this.callAsync(...args);
      };
      const PROMISE_DELEGATE = function(...args) {
       this.promise = this._createCall("promise");
       return  this.promise(...args);
      };
      
      class Hook {
        constructor(args = [], name = undefined) {
          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; 
        }
        
        // 通过HookCodeFacotory生成代码
        _createCall(type) {
          return  this.compile({
            taps: this.taps,
            interceptors: this.interceptors,
            args: this._args,
            type: type
          });
        }
      }
      

这里的逻辑很清晰,在第一次触发钩子执行函数时进行相关钩子运行时代码生成及执行,这里的compile可以理解为执行事件触发,具体详情在后面会提及。

  1. 第三部分也许会把很多人看懵,为什么有这个操作?

    1. class Hook {
        constructor(args = [], name = undefined) {
          this.compile = this.compile;
          this.tap = this.tap;
          this.tapAsync = this.tapAsync;
          this.tapPromise = this.tapPromise;
       }
      }
      
    2.   我思考了一下得出一个答案:将原型链上的方法赋予本实例,避免因修改原型链指向而导致方法丢失

Taps

tap区分tap、tapAsync、tapPromise:

  1. tap:回调是同步按顺序执行
  2. tapAsync:传入callback参数,由callback决定下一个tap的执行时机
  3. tapPromise:返回Promise对象决定下一个tap的执行时机

讲解taps的添加流程:

class Hook {
  _tap(type, options, fn) {
    // ...数据处理
    // 1. tap注册时需要执行拦截器的register方法
    options = this._runRegisterInterceptors(options);
    // 2. tap按照优先级别插入
    this._insert(options);
  }
  
  // 运行钩子函数
  _runRegisterInterceptors(options) {
    for (const interceptor of  this.interceptors) {
      if (interceptor.register) {
        const newOptions = interceptor.register(options);
        if (newOptions !== undefined) {
         options = newOptions;
        }
      }
    }
    return options;
  }
  
  // 按照优先级别排序
  _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;
    }
    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;
  }
}
  1. Interceptor.register:拦截器可在注册钩子时进行修改钩子meta数据的操作

  2. insert:tap的排序优先级别:

    1. before:高优先级别,表示本tap一定在某个具名tap之前
    2. stage:数值越高越靠后,排序优先级别比before要低

Interceptor

interceptor是tap拦截器,允许注册tap的生命周期钩子函数,其数据结构如下:

interface Interceptor {
  // 注册前
  register: (tap: Tap) => Tap || void;
  // 调用前
  call: (...args: any[]) => void;
  // 产生错误
  error:(e: Error) => void;
  // 完成后
  done: () => void;
  // 获得结果
  result: (v: any) => void;
  // 名称
  name: string;
}

Hook可以通过intercept注册钩子函数的拦截器,允许在tap的生命周期回调中做事情

小结

Hook是所有类型Hook的基础类,提供顶层的tap、interceptor管理能力。


Hook管理讲解中还遗留个问题,compile从哪来?我们回到SyncHook方法:


class SyncHookCodeFactory extends HookCodeFactory {
 content({ onError, onDone, rethrowIfPossible }) {
  return  this.callTapsSeries({
   onError: (i, err) => onError(err),
   onDone,
   rethrowIfPossible
  });
 }
}

const factory = new SyncHookCodeFactory();

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

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;
}

compile是由HookCodeFactory实例提供,所以我们需要进入到HookCodeFactory类讲解:

HookCodeFactory

Tapable比较新奇的点是Hook执行代码是在运行时生成,而非运行前已经存在。以SyncBailHook为例子,在第一次执行call时会生成运行时代码,如下面的SyncBailHook例子,左侧为调用代码,右侧为生成的代码,其中:

  1. this._x为tap注册的回调函数
  2. this.interceptors是注册的拦截器
  3. 左侧为调用代码,右侧为生成运行时代码
const syncBailHook = new SyncBailHook();

syncBailHook.tap('A', () => {
 console.log('call A');
});
syncBailHook.tap('B', () => {
 console.log('call B');

 return  false;
});
syncBailHook.tap('C', () => {
 console.log('call C');
});

syncBailHook.intercept({
 register(tap) {
  console.log('tap', tap);
  return tap;
 },
 call() {
  console.log('syncBailHook call');
 },
 error(e) {
  console.error('error', e);
 },
 done() {
  console.log('syncBailHook done');
 },
 name: 'SyncBailHook',
 result() {
  console.log('syncBailHook result');
 }
})

syncBailHook.call();
(function  anonymous() {
 "use strict";
 var _context;
 var _x = this._x;
 var _taps = this.taps;
 // 1. 注册的拦截器
 var _interceptors = this.interceptors;
 // 2. 注册的call方法
_interceptors[0].call();
 var _fn0 = _x[0];
 var _result0 = _fn0();
 if (_result0 !== undefined) {
  // 3. 结束执行的结果
_interceptors[0].result(_result0);
  return _result0;
  
 } else {
  var _fn1 = _x[1];
  var _result1 = _fn1();
  if (_result1 !== undefined) {
   // 3. 结束执行的结果
_interceptors[0].result(_result1);
   return _result1;
   
  } else {
   var _fn2 = _x[2];
   var _result2 = _fn2();
   if (_result2 !== undefined) {
    // 3. 结束执行的结果
_interceptors[0].result(_result2);
    return _result2;
    
   } else {
    // 4. 运行结束钩子
_interceptors[0].done();
   }
  }
 }
})

右侧代码是由HookCodeFactory在运行时根据Hook配置在第一次执行hook的call/callAsync/callPromise方法时动态生成,是由create方法所产生,所以我们聚焦于该方法:

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

create方法区分不同的hook类型生成不同形式的Hook运行时代码,总体上看运行时代码可以分成两部分:

  1. 头部代码:存储taps、interceptors、context存储能力,可以看header方法,这里不展开讲;
  2. 内容:由interceptor执行代码和content组成,其中content是由具体钩子实现:
contentWithInterceptors(options) {
  // 存在拦截器
 if (this.options.interceptors.length > 0) {
  const onError = options.onError;
  const onResult = options.onResult;
  const onDone = options.onDone;
  let code = "";
  for (let i = 0; i < this.options.interceptors.length; i++) {
   const interceptor = this.options.interceptors[i];
   if (interceptor.call) {
    code += `${this.getInterceptor(i)}.call(${this.args({
     before: interceptor.context ? "_context" : undefined
    })});\n`;
   }
  }
  code += this.content(
   Object.assign(options, {
    onError:
     onError &&
     (err => {
      let code = "";
      for (let i = 0; i < this.options.interceptors.length; i++) {
       const interceptor = this.options.interceptors[i];
       if (interceptor.error) {
        code += `${this.getInterceptor(i)}.error(${err});\n`;
       }
      }
      code += onError(err);
      return code;
     }),
    onResult:
     onResult &&
     (result => {
      let code = "";
      for (let i = 0; i < this.options.interceptors.length; i++) {
       const interceptor = this.options.interceptors[i];
       if (interceptor.result) {
        code += `${this.getInterceptor(i)}.result(${result});\n`;
       }
      }
      code += onResult(result);
      return code;
     }),
    onDone:
     onDone &&
     (() => {
      let code = "";
      for (let i = 0; i < this.options.interceptors.length; i++) {
       const interceptor = this.options.interceptors[i];
       if (interceptor.done) {
        code += `${this.getInterceptor(i)}.done();\n`;
       }
      }
      code += onDone();
      return code;
     })
   })
  );
  return code;
 } else {
  // 不存在拦截器
  return  this.content(options);
 }
}

上述代码中content方法会因不同的hook有所不同,所以由Hook具体实现,除此之外,HookCodeFactory还提供串行、并行、循环钩子代码生成api,Hook实现类通过执行特性调用这些api实现content方法,以此生成符合预期的运行时代码,下面是SyncHook代码生成逻辑:

class SyncHookCodeFactory extends HookCodeFactory {
  content({ onError, onDone, rethrowIfPossible }) {
   // 1. SyncHook是串行执行,所以调用了串行Tap生成方法
   return  this.callTapsSeries({
    onError: (i, err) => onError(err),
    onDone,
    rethrowIfPossible
   });
  }
}

const factory = new SyncHookCodeFactory();

HookCodeFactory代码生成流程大体如上。


看到SyncHook源码后,可以看到其他类型的钩子都是同样的实现:

  1. 订阅发布管理能力通过继承于Hook类实现;
  2. 运行时代码生成通过竭诚与HookCodeFactory实现
转载自:https://juejin.cn/post/7231821116896821308
评论
请登录