文章首發於:github.com/USTB-musion…
Tapable
對於Webpack有一句話Everything is a plugin,Webpack本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是Tapable。Tapable有點類似nodejs的events庫,核心原理也是依賴與釋出訂閱模式。webpack中最核心的負責編譯的Compiler和負責建立bundles的Compilation都是Tapable的例項。下面介紹一下tapable的用法和原理。以下例項的程式碼原文地址為github.com/USTB-musion…
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
複製程式碼
Tapable Hook概覽
Tapable提供了很多型別的hook,分為同步和非同步兩大類(非同步中又區分非同步並行和非同步序列),而根據事件執行的終止條件的不同,由衍生出 Bail/Waterfall/Loop 型別。下圖展示了每種型別的作用:
- BasicHook: 執行每一個,不關心函式的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。
- BailHook: 順序執行 Hook,遇到第一個結果 result !== undefined 則返回,不再繼續執行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
- WaterfallHook: 類似於 reduce,如果前一個 Hook 函式的結果 result !== undefined,則 result 會作為後一個 Hook 函式的第一個引數。既然是順序執行,那麼就只有 Sync 和 AsyncSeries 類中提供這個Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook
- LoopHook: 不停的迴圈執行 Hook,直到所有函式結果 result === undefined。同樣的,由於對序列性有依賴,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暫時沒看到具體使用 Case)
SyncHook的用法及實現
Sync為同步序列的執行關係,用法如下:
let { SyncHook } = require("tapable");
class Lesson {
constructor() {
this.hooks = {
arch: new SyncHook(["name"])
};
}
// 註冊監聽函式
tap() {
this.hooks.arch.tap("node", function(name) {
console.log("node", name);
});
this.hooks.arch.tap("react", function(name) {
console.log("react", name);
});
}
start() {
this.hooks.arch.call("musion");
}
}
let l = new Lesson();
// 註冊這兩個事件
l.tap();
// 啟動鉤子
l.start();
/**
* 列印出來的值為:
* node musion
* react musion
*/
複製程式碼
SyncHook是一個很典型的通過釋出訂閱方式實現的,實現方式如下:
// 鉤子是同步的
class SyncHook {
// args => ["name"]
constructor() {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
this.tasks.forEach(task => task(...args));
}
}
let hook = new SyncHook(["name"]);
hook.tap("react", function(name) {
console.log("react", name);
});
hook.tap("node", function(name) {
console.log("node", name);
});
hook.call("musion");
/**
* 列印出來的值為:
* node musion
* react musion
*/
複製程式碼
SyncBailHook的用法及實現
SyncBailHook為同步序列的執行關係,只要監聽函式中有一個函式的返回值不為 null,則跳過剩下所有的邏輯,用法如下:
let { SyncBailHook } = require("tapable");
class Lesson {
constructor() {
this.hooks = {
arch: new SyncBailHook(["name"])
};
}
// 註冊監聽函式
tap() {
this.hooks.arch.tap("node", function(name) {
console.log("node", name);
//return "stop";
return undefined;
});
this.hooks.arch.tap("react", function(name) {
console.log("react", name);
});
}
start() {
this.hooks.arch.call("musion");
}
}
let l = new Lesson();
// 註冊這兩個事件
l.tap();
// 啟動鉤子
l.start();
/**
* 列印出來的值為:
* node musion
* react musion
*/
複製程式碼
SyncBailHook的實現:
// 鉤子是同步的,bail -> 保險
class SyncBailHook {
// args => ["name"]
constructor() {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 當前函式的返回值
let ret;
// 當前要先執行第一個
let index = 0;
do {
ret = this.tasks[index++](...args);
} while (ret === undefined && index < this.tasks.length);
}
}
let hook = new SyncBailHook(["name"]);
hook.tap("react", function(name) {
console.log("react", name);
return "stop";
});
hook.tap("node", function(name) {
console.log("node", name);
});
hook.call("musion");
/**
* 列印出來的值為:
* node musion
* react musion
*/
複製程式碼
SyncWaterfallHook的用法及實現
SyncWaterfallHook為同步序列的執行關係,上一個監聽函式的返回值可以傳給下一個監聽函式,用法如下:
let { SyncWaterfallHook } = require("tapable");
// waterfall 瀑布 上面會影響下面的
class Lesson {
constructor() {
this.hooks = {
arch: new SyncWaterfallHook(["name"])
};
}
// 註冊監聽函式
tap() {
this.hooks.arch.tap("node", function(name) {
console.log("node", name);
return "node學得還不錯";
});
this.hooks.arch.tap("react", function(data) {
console.log("react", data);
});
}
start() {
this.hooks.arch.call("musion");
}
}
let l = new Lesson();
// 註冊這兩個事件
l.tap();
// 啟動鉤子
l.start();
/**
* 列印出來的值為:
* node musion
* react node學得還不錯
*/
複製程式碼
SyncWaterfallHook的實現:
// 鉤子是同步的
class SyncWaterfallHook {
// args => ["name"]
constructor() {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
let [first, ...others] = this.tasks;
let ret = first(...args);
others.reduce((a, b) => {
return b(a);
}, ret);
}
}
let hook = new SyncWaterfallHook(["name"]);
hook.tap("react", function(name) {
console.log("react", name);
return "react ok";
});
hook.tap("node", function(data) {
console.log("node", data);
return "node ok";
});
hook.tap("webpack", function(data) {
console.log("webpack", data);
});
hook.call("musion");
/**
* 列印出來的值為:
* react musion
* node react ok
* webpack node ok
*/
複製程式碼
SyncLoopHook的用法及實現
SyncLoopHook為同步迴圈的執行關係,當監聽函式被觸發的時候,如果該監聽函式返回true時則這個監聽函式會反覆執行,如果返回 undefined 則表示退出迴圈,用法如下:
let { SyncLoopHook } = require("tapable");
// 同步遇到某個不返回undefined的監聽函式會多次執行
class Lesson {
constructor() {
this.index = 0;
this.hooks = {
arch: new SyncLoopHook(["name"])
};
}
// 註冊監聽函式
tap() {
this.hooks.arch.tap("node", name => {
console.log("node", name);
return ++this.index === 3 ? undefined : "繼續學";
});
this.hooks.arch.tap("react", data => {
console.log("react", data);
});
}
start() {
this.hooks.arch.call("musion");
}
}
let l = new Lesson();
// 註冊這兩個事件
l.tap();
// 啟動鉤子
l.start();
/**
* 列印出來的值為:
* node musion
* node musion
* node musion
* react musion
*/
複製程式碼
SyncLoopHook的實現:
// 鉤子是同步的
class SyncLoopHook {
// args => ["name"]
constructor() {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
this.tasks.forEach(task => {
let ret;
do {
ret = task(...args);
} while (ret != undefined);
});
}
}
let hook = new SyncLoopHook(["name"]);
let total = 0;
hook.tap("react", function(name) {
console.log("react", name);
return ++total === 3 ? undefined : "繼續學";
});
hook.tap("node", function(data) {
console.log("node", data);
});
hook.tap("webpack", function(data) {
console.log("webpack", data);
});
hook.call("musion");
/**
* 列印出來的值為:
* react musion
* react musion
* react musion
* node musion
* webpack musion
*/
複製程式碼
AsyncParallelHook的用法及實現
AsyncParallelHook為非同步併發的執行關係,用法如下:
let { AsyncParallelHook } = require("tapable");
// 非同步的鉤子分為序列和並行
// 序列:第一個非同步執行完,才會執行第二個
// 並行:需要等待所有併發的非同步事件執行後再執行回撥方法
// 註冊方法: tap註冊 tapAsync註冊
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncParallelHook(["name"])
};
}
// 註冊監聽函式
tap() {
this.hooks.arch.tapAsync("node", (name, cb) => {
setTimeout(() => {
console.log("node", name);
cb();
}, 1000);
});
this.hooks.arch.tapAsync("react", (name, cb) => {
setTimeout(() => {
console.log("react", name);
cb();
}, 1000);
});
}
start() {
this.hooks.arch.callAsync("musion", function() {
console.log("end");
});
}
}
let l = new Lesson();
// 註冊這兩個事件
l.tap();
// 啟動鉤子
l.start();
/**
* 列印出來的值為:
* node musion
* react musion
* end
*/
複製程式碼
AsyncParallelHook的實現:
class SyncParralleHook {
constructor() {
this.tasks = [];
}
tapAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
// 拿出最終的函式
let finalCallBack = args.pop();
let index = 0;
// 類似Promise.all
let done = () => {
index++;
if (index === this.tasks.length) {
finalCallBack();
}
};
this.tasks.forEach(task => {
task(...args, done);
});
}
}
let hook = new SyncParralleHook(["name"]);
hook.tapAsync("react", function(name, cb) {
setTimeout(() => {
console.log("react", name);
cb();
}, 1000);
});
hook.tapAsync("node", function(name, cb) {
setTimeout(() => {
console.log("node", name);
cb();
}, 1000);
});
hook.callAsync("musion", function() {
console.log("end");
});
/**
* 列印出來的值為:
* react musion
* react musion
* react musion
* node musion
* webpack musion
*/
複製程式碼
AsyncSeriesHook的用法及實現
AsyncSeriesHook為非同步序列的執行關係,用法如下:
// AsyncSeriesHook 非同步序列
let { AsyncSeriesHook } = require("tapable");
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncSeriesHook(["name"])
};
}
// 註冊監聽函式
tap() {
this.hooks.arch.tapAsync("node", (name, cb) => {
setTimeout(() => {
console.log("node", name);
cb();
}, 4000);
});
this.hooks.arch.tapAsync("react", (name, cb) => {
setTimeout(() => {
console.log("react", name);
cb();
}, 1000);
});
}
start() {
this.hooks.arch.callAsync("musion", function() {
console.log("end");
});
}
}
let l = new Lesson();
// 註冊這兩個事件
l.tap();
// 啟動鉤子
l.start();
/**
* 列印出來的值為:
* node musion
* react musion
* end
*/
複製程式碼
AsyncSeriesHook的實現:
class SyncSeriesHook {
constructor() {
this.tasks = [];
}
tapAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
let finalCallback = args.pop();
let index = 0;
let next = () => {
if (this.tasks.length === index) return finalCallback();
let task = this.tasks[index++];
task(...args, next);
};
next();
}
}
複製程式碼
AsyncSeriesWaterfallHook的用法及實現
AsyncSeriesWaterfallHook為非同步序列的執行關係,上一個監聽函式的中的callback(err, data)的第二個引數,可以作為下一個監聽函式的引數,用法如下:
class SyncSeriesWaterfallHook {
constructor() {
this.tasks = [];
}
tapAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
let finalCallback = args.pop();
let index = 0;
let next = (err, data) => {
let task = this.tasks[index];
if (!task) return finalCallback();
// 執行的是第一個
if (index === 0) {
task(...args, next);
} else {
task(data, next);
}
index++;
};
next();
}
}
let hook = new SyncSeriesWaterfallHook(["name"]);
hook.tapAsync("react", function(name, cb) {
setTimeout(() => {
console.log("react", name);
cb(null, "musion");
}, 3000);
});
hook.tapAsync("node", function(name, cb) {
setTimeout(() => {
console.log("node", name);
cb(null);
}, 1000);
});
hook.callAsync("musion", function() {
console.log("end");
});
/**
* 列印出來的值為:
* node musion
* end
*/
複製程式碼
AsyncSeriesWaterfallHook的實現:
class SyncSeriesWaterfallHook {
constructor() {
this.tasks = [];
}
tapAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
let finalCallback = args.pop();
let index = 0;
let next = (err, data) => {
let task = this.tasks[index];
if (!task) return finalCallback();
// 執行的是第一個
if (index === 0) {
task(...args, next);
} else {
task(data, next);
}
index++;
};
next();
}
}
複製程式碼