這段時間沒更新文章,把時間全耗在了 tabable 核心原理分析上,如今終於把 tabable 搞懂了,經過本文分享給你們。最後我發現了一個很是容易掌握 tabable 的方法。javascript
通過這幾天的學習,我把 tapable 總結成一句話「經過一種機制,監聽特定的事件」。拿最經典的一個例子音頻播放器來講,核心播放服務只有一個,而須要監聽播放事件的對象有不少,好比 mini播放器,主播放器,這兩個播放器都要監聽音頻播放進度、播放開始、播放暫停事件。下圖中的兩個播放器都要監聽播放進度:html
遇到這種需求,大多數人的作法是採用「發佈訂閱模式」,使用 tapable 也能解決相似這種事件監聽的業務,可是它更靈活,能作更多的業務,好比 hook 函數數據之間傳遞,異步 hook,hook 流處理。這是在我第一次瞭解到 asyncHook,syncHook,syncBailHook、syncWaterfallHook 這種編程思想。固然 tapable 終究服務於 webpack,不少場景都是爲 webpack 的設計而考慮的。前端
tapable 的核心類有以下幾個,主要分爲同步 hook 和異步 hook:java
能夠經過 3 種方式來添加「事件監聽函數」,也就說若是想監聽某個事件,直接經過下面這幾個方法來添加便可:webpack
options 能夠是對象或字符串,fn 爲同步回調函數。可用於全部類型的 hook,包含 sync 類型和 async 類型。web
hook.tap('SuyanSyncHook', (name, age) => {
console.log(`syncHook name: ${name}, age: ${age}`);
});
複製代碼
經過 call 方法來觸發事件:syncHook.call('suyan', 20);
編程
監聽函數最後一個參數爲一個回調函數,這個函數不能用於 sync 類型的 hook。數組
asyncHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout(() => {
console.log(`source3: ${source}`);
callback();
}, 2000);
});
複製代碼
經過 callAsync 來觸發事件:promise
asyncSeriesHook.callAsync('關注公衆號素燕', ret => {
console.log(`ret = ${ret}`);
});
複製代碼
須要返回一個 promise 對象;不能用於 sync 類型的 hook。網絡
asynchook.tapPromise('SuyanasyncParallelHook', (source) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`source2: ${source}`);
resolve(`${source},素燕`);
}, 1000);
});
});
複製代碼
經過 promise 來觸發事件:
asyncHook.promise('關注公衆號素燕').then(ret => {
console.log(`ret = ${ret}`);
});
複製代碼
咱們下面來介紹各類 Hook 的做用,總共有兩類 Hook,第一類是同步 Hook。
同步 Hook,hook 事件之間互不干擾。下面是使用 SyncHook 的例子,向 syncHook 中添加了兩個回調函數,當調用 call 函數的時候,先前經過 tap 添加的回調函數將被執行:
const SyncHook = require('./lib/SyncHook');
// 建立一個 SyncHook,後面跟一個參數列表
const syncHook = new SyncHook(['name', 'age']);
// 添加一個 hook 事件
syncHook.tap('SuyanSyncHook', (name, age) => {
console.log(`syncHook name: ${name}, age: ${age}`);
});
// 添加一個 hook 事件
syncHook.tap('SuyanSyncHook', (name, age) => {
console.log(`1syncHook name: ${name}, age: ${age}`);
});
// 調用 hook
syncHook.call('suyan', 20);
複製代碼
這是咋麼作到的呢?咱們經過源碼來分析下 tapable 的核心原理,理解了其中一個原理,其它的 Hook 能夠用一樣的方式來理解。
全部的 hook 都繼承自 Hook 這個類,在我看來它主要作了 3 件事:
提供調用函數 call、promise、callAsync; 保存函數執行時的上下文,好比函數參數、監聽者; 攔截器處理; 咱們看一下它的構造函數 constructor
constructor(args) {
// 一個數組,用來保 hook 的參數
if (!Array.isArray(args)) args = [];
// 參數
this._args = args;
// 監聽器,或者是訂閱者
this.taps = [];
// 攔截器
this.interceptors = [];
// 這幾個函數最終其實指向的是 Object 的原型
// 普通調用
this.call = this._call;
// promise 調用
this.promise = this._promise;
// 異步調用
this.callAsync = this._callAsync;
// 全部的回調函數
this._x = undefined;
}
複製代碼
經過 Hook 收集的信息,而後經過 HookCodeFactory 來生成函數,好比上面的 SyncHook 代碼,最終生成的代碼以下:
"use strict";
var _context;
// _x 保存了全部的回調函數,也就是 tap 時的函數
// syncHook.tap('SuyanSyncHook', FN);
var _x = this._x;
// 執行第一個回調函數
var _fn0 = _x[0];
_fn0(name, age);
// 執行第二個回調函數
var _fn1 = _x[1];
_fn1(name, age);
複製代碼
tapable 的核心就是動態生成函數 Function。在 JavaScript 中能夠直接定義函數,也能夠經過 new Function 來生成一個函數,下面的函數是等價的:
// 直接定義函數
function sum(a, b) {
return a + b;
}
// 經過 new Function 生成函數
const sum = new Function(['a', 'b'], 'return a + b;');
複製代碼
這就是 Hook 的本質,能夠經過最終生成的代碼理解每個 Hook 的做用。
總之,tapable 的核心原理是收集函數要執行時的所有信息,根據這些信息 taps、intercepts、args、type ,經過 new Function 生成最終要調用的函數,當須要通知監聽者時,直接執行生成的函數。
能夠通 Hook 的名字理解它做用,好比 SyncHook,它很「純」,最基本的 Hook。而 SyncBailHook、SyncWaterfallHook、SyncLoopHook 這 3 個 Hook 添加了修飾符,它們與監聽函數的返回值有關。
Bail(熔斷),帶有這個詞的 Hook 表示只要有一個 Hook 的監聽函數返回不爲 undefined,監聽就會截止。因爲第二個監聽函數返回了「學習 webpack」,第三個監聽函數將不會被執行。
const SyncBailHook = require('./lib/SyncBailHook');
// 一個接一個的 hook,只要有一個返回 undefined 的就截止
const syncBailHook = new SyncBailHook(['source']);
syncBailHook.tap('SuyansyncBailHook', (source) => {
console.log(`source1: ${source}`);
});
syncBailHook.tap('SuyansyncBailHook', source => {
console.log(`source2: ${source}`);
return '學習 webpack';
});
syncBailHook.tap('SuyansyncBailHook', source => {
console.log(`source3: ${source}`);
});
let ret = syncBailHook.call('關注公衆號素燕,');
console.log(`ret = ${ret}`);
複製代碼
咱們在看下編譯後的代碼:
"use strict";
var _context;
var _x = this._x;
console.log('this._x = : ', this._x);
var _fn0 = _x[0];
var _result0 = _fn0(source);
if (_result0 !== undefined) {
return _result0;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(source);
if (_result1 !== undefined) {
return _result1;
} else {
var _fn2 = _x[2];
var _result2 = _fn2(source);
if (_result2 !== undefined) {
return _result2;
} else {
}
}
}
複製代碼
waterfall(瀑布流),監聽函數的返回值會傳遞給下一個監聽函數,就如同水流同樣,源源不斷。每個監聽函數的返回值會傳遞到下一個回調函數,下面這個 demo 最終的返回值爲「關注公衆號素燕,和素燕一塊兒學習 webpack」:
const SyncWaterfallHook = require('./lib/SyncWaterfallHook');
// 一個接一個的 hook,上一個函數的返回值是下一個函數的參數
const syncWaterfallHook = new SyncWaterfallHook(['source']);
syncWaterfallHook.tap('SuyanSyncWaterfallHook', (source) => {
console.log(`source1: ${source}`);
return `${source}和`;
});
syncWaterfallHook.tap('SuyanSyncWaterfallHook', source => {
console.log(`source2: ${source}`);
return `${source}素燕`;;
});
syncWaterfallHook.tap('SuyanSyncWaterfallHook', source => {
console.log(`source3: ${source}`);
return `${source}一塊兒學習 webpack`;;
});
let ret = syncWaterfallHook.call('關注公衆號素燕,');
console.log(`ret = ${ret}`);
複製代碼
最終編譯的代碼以下:
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(source);
if(_result0 !== undefined) {
source = _result0;
}
var _fn1 = _x[1];
var _result1 = _fn1(source);
if(_result1 !== undefined) {
source = _result1;
}
var _fn2 = _x[2];
var _result2 = _fn2(source);
if(_result2 !== undefined) {
source = _result2;
}
return source;
複製代碼
loop(循環),只要返回值不爲「假值」,監聽函數將一直被調用。不詳細講解,可自行學習。
第二類是異步的 Hook,這種 Hook 通常用在比較耗時的操做,好比網絡請求。
對於異步 hook,可使用 tapAsync 和 tapPromise 添加異步函數。監聽函數的最後一個參數爲 callback,調用 callback 時,若是帶有 error 參數,整個監聽函數鏈將會中斷,好比 callback('err'),當遇到這種 callback 時,下一個監聽函數將不會被執行。舉個例子:
const AsyncSeriesHook = require('./lib/AsyncSeriesHook');
// 異步串行,就是一個一個來
const asyncSeriesHook = new AsyncSeriesHook(['source']);
asyncSeriesHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout(() => {
console.log(`source1: ${source}`);
callback();
}, 3000);
});
asyncSeriesHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout(() => {
console.log(`source2: ${source}`);
callback();
}, 1000);
});
asyncSeriesHook.callAsync('關注公衆號素燕', ret => {
console.log(`ret = ${ret}`);
});
複製代碼
經過最終編譯後的代碼能夠清晰看到整個 hook 作的事情:
"use strict";
var _context;
var _x = this._x;
// 下一個要執行的函數
function _next0() {
var _fn1 = _x[1];
_fn1(source, _err1 => {
if (_err1) {
_callback(_err1);
} else {
_callback();
}
});
}
// 第一個要執行的函數
var _fn0 = _x[0];
_fn0(source, _err0 => {
if (_err0) {
_callback(_err0);
} else {
_next0();
}
});
複製代碼
這個 Hook 和 SyncBailHook 大同小異,只不過回調函數爲異步函數,回調函數 callback 接收 2 個參數,第一個爲 error,第二個爲 result,當遇到 error 和 result 都不爲空時,整個監聽函數鏈將會中斷。看下面的例子:
const AsyncSeriesBailHook = require('./lib/AsyncSeriesBailHook');
// 異步串行,就是一個一個來
const asyncSeriesBailHook = new AsyncSeriesBailHook(['source']);
asyncSeriesBailHook.tapAsync('SuyanasyncSeriesBailHook', (source, callback) => {
setTimeout(() => {
console.log(`source1: ${source}`);
callback();
}, 3000);
});
asyncSeriesBailHook.tapAsync('SuyanasyncSeriesBailHook', (source, callback) => {
setTimeout(() => {
console.log(`source2: ${source}`);
callback();
}, 1000);
});
asyncSeriesBailHook.callAsync('關注公衆號素燕', ret => {
console.log(`ret = ${ret}`);
});
複製代碼
最終編譯的代碼以下,與 AsyncSeriesHook 很類似,只是在 callback 中多了一個 result 參數:
"use strict";
var _context;
var _x = this._x;
console.log('this._x = : ', this._x);
function _next0() {
var _fn1 = _x[1];
_fn1(source, (_err1, _result1) => {
if (_err1) {
_callback(_err1);
} else {
if (_result1 !== undefined) {
_callback(null, _result1);
;
} else {
_callback();
}
}
});
}
var _fn0 = _x[0];
_fn0(source, (_err0, _result0) => {
if (_err0) {
_callback(_err0);
} else {
if (_result0 !== undefined) {
_callback(null, _result0);
;
} else {
_next0();
}
}
});
複製代碼
這個和 SyncWaterfallHook 相似,上一個回調結果會傳遞到下一個監聽函數中。
異步循環 Hook,只要回調函數不返回 undefined,循環將一直持續執行,知道回調函數的值爲 undefined。
異步並行 hook,也就是說全部的回調函數同時進行。
異步並行熔斷鉤子。
tapable 對於初學者來講並不友好,學習起來有必定的學習成本。總的來講,tapable 主要分紅同步 hook 和異步 hook,每類 hook 下又分爲了 bail(當函數有任何返回值時,接下來的 hook 回調函數將終止)、waterfall(瀑布流,函數返回值將向下一個回調函數傳遞)、loop(循環,只要函數返回值不爲 undefined,將一直循環)。若是想完全理解各類類型的 hook,能夠經過分析最終生成的函數代碼來理解,理解起來很是容易。
這節內容屬於理論知識,下一節內容咱們結合 compiler 來加深對 tapable 的理解。