Webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器,是對前端項目實現自動化和優化必不可少的工具,Webpack 的 loader
(加載器)和 plugin
(插件)是由 Webpack 開發者和社區開發者共同貢獻的,而目前又沒有比較系統的開發文檔,想寫加載器和插件必需要懂 Webpack 的原理,即看懂 Webpack 的源碼,tapable
則是 Webpack 依賴的核心庫,能夠說不懂 tapable
就看不懂 Webpack 源碼,因此本篇會對 tapable
提供的類進行解析和模擬。前端
Webpack 本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是 tapable
,Webpack 中最核心的,負責編譯的 Compiler
和負責建立 bundles
的 Compilation
都是 tapable
構造函數的實例。數組
打開 Webpack 4.0
的源碼中必定會看到下面這些以 Sync
、Async
開頭,以 Hook
結尾的方法,這些都是 tapable
核心庫的類,爲咱們提供不一樣的事件流執行機制,咱們稱爲 「鉤子」。promise
// 引入 tapable 以下
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
複製代碼
複製代碼
SyncHook
爲串行同步執行,不關心事件處理函數的返回值,在觸發事件以後,會按照事件註冊的前後順序執行全部的事件處理函數。bash
// SyncHook 鉤子的使用
const { SyncHook } = require("tapable");
// 建立實例
let syncHook = new SyncHook(["name", "age"]);
// 註冊事件
syncHook.tap("1", (name, age) => console.log("1", name, age));
syncHook.tap("2", (name, age) => console.log("2", name, age));
syncHook.tap("3", (name, age) => console.log("3", name, age));
// 觸發事件,讓監聽函數執行
syncHook.call("panda", 18);
// 1 panda 18
// 2 panda 18
// 3 panda 18
複製代碼
複製代碼
在 tapable
解構的 SyncHook
是一個類,註冊事件需先建立實例,建立實例時支持傳入一個數組,數組內存儲事件觸發時傳入的參數,實例的 tap
方法用於註冊事件,支持傳入兩個參數,第一個參數爲事件名稱,在 Webpack 中通常用於存儲事件對應的插件名稱(名字隨意,只是起到註釋做用), 第二個參數爲事件處理函數,函數參數爲執行 call
方法觸發事件時所傳入的參數的形參。併發
// 模擬 SyncHook 類
class SyncHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 也可在參數不足時拋出異常
if (args.length < this.args.length) throw new Error("參數不足");
// 傳入參數嚴格對應建立實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 依次執行事件處理函數
this.tasks.forEach(task => task(...args));
}
}
複製代碼
複製代碼
tasks
數組用於存儲事件處理函數,call
方法調用時傳入參數超過建立 SyncHook
實例傳入的數組長度時,多餘參數可處理爲 undefined
,也可在參數不足時拋出異常,不靈活,後面的例子中就再也不這樣寫了。異步
SyncBailHook
一樣爲串行同步執行,若是事件處理函數執行時有一個返回值不爲空(即返回值爲 undefined
),則跳過剩下未執行的事件處理函數(如類的名字,意義在於保險)。async
// SyncBailHook 鉤子的使用
const { SyncBailHook } = require("tapable");
// 建立實例
let syncBailHook = new SyncBailHook(["name", "age"]);
// 註冊事件
syncBailHook.tap("1", (name, age) => console.log("1", name, age));
syncBailHook.tap("2", (name, age) => {
console.log("2", name, age);
return "2";
});
syncBailHook.tap("3", (name, age) => console.log("3", name, age));
// 觸發事件,讓監聽函數執行
syncBailHook.call("panda", 18);
// 1 panda 18
// 2 panda 18
複製代碼
複製代碼
經過上面的用法能夠看出,SyncHook
和 SyncBailHook
在邏輯上只是 call
方法不一樣,致使事件的執行機制不一樣,對於後面其餘的 「鉤子」,也是 call
的區別,接下來實現 SyncBailHook
類。函數
// 模擬 SyncBailHook 類
class SyncBailHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 傳入參數嚴格對應建立實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 依次執行事件處理函數,若是返回值不爲空,則中止向下執行
let i = 0, ret;
do {
ret = this.tasks[i++](...args);
} while (!ret);
}
}
複製代碼
複製代碼
在上面代碼的 call
方法中,咱們設置返回值爲 ret
,第一次執行後沒有返回值則繼續循環執行,若是有返回值則當即中止循環,即實現 「保險」 的功能。工具
SyncWaterfallHook
爲串行同步執行,上一個事件處理函數的返回值做爲參數傳遞給下一個事件處理函數,依次類推,正因如此,只有第一個事件處理函數的參數能夠經過 call
傳遞,而 call
的返回值爲最後一個事件處理函數的返回值。oop
// SyncWaterfallHook 鉤子的使用
const { SyncWaterfallHook } = require("tapable");
// 建立實例
let syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);
// 註冊事件
syncWaterfallHook.tap("1", (name, age) => {
console.log("1", name, age);
return "1";
});
syncWaterfallHook.tap("2", data => {
console.log("2", data);
return "2";
});
syncWaterfallHook.tap("3", data => {
console.log("3", data);
return "3"
});
// 觸發事件,讓監聽函數執行
let ret = syncWaterfallHook.call("panda", 18);
console.log("call", ret);
// 1 panda 18
// 2 1
// 3 2
// call 3
複製代碼
複製代碼
SyncWaterfallHook
名稱中含有 「瀑布」,經過上面代碼能夠看出 「瀑布」 形象生動的描繪了事件處理函數執行的特色,與 SyncHook
和 SyncBailHook
的區別就在於事件處理函數返回結果的流動性,接下來看一下 SyncWaterfallHook
類的實現。
// 模擬 SyncWaterfallHook 類
class SyncWaterfallHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 傳入參數嚴格對應建立實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 依次執行事件處理函數,事件處理函數的返回值做爲下一個事件處理函數的參數
let [first, ...others] = this.tasks;
return reduce((ret, task) => task(ret), first(...args));
}
}
複製代碼
複製代碼
上面代碼中 call
的邏輯是將存儲事件處理函數的 tasks
拆成兩部分,分別爲第一個事件處理函數,和存儲其他事件處理函數的數組,使用 reduce
進行歸併,將第一個事件處理函數執行後的返回值做爲歸併的初始值,依次調用其他事件處理函數並傳遞上一次歸併的返回值。
SyncLoopHook
爲串行同步執行,事件處理函數返回 true
表示繼續循環,即循環執行當前事件處理函數,返回 undefined
表示結束循環,SyncLoopHook
與 SyncBailHook
的循環不一樣,SyncBailHook
只決定是否繼續向下執行後面的事件處理函數,而 SyncLoopHook
的循環是指循環執行每個事件處理函數,直到返回 undefined
爲止,纔會繼續向下執行其餘事件處理函數,執行機制同理。
// SyncLoopHook 鉤子的使用
const { SyncLoopHook } = require("tapable");
// 建立實例
let syncLoopHook = new SyncLoopHook(["name", "age"]);
// 定義輔助變量
let total1 = 0;
let total2 = 0;
// 註冊事件
syncLoopHook.tap("1", (name, age) => {
console.log("1", name, age, total1);
return total1++ < 2 ? true : undefined;
});
syncLoopHook.tap("2", (name, age) => {
console.log("2", name, age, total2);
return total2++ < 2 ? true : undefined;
});
syncLoopHook.tap("3", (name, age) => console.log("3", name, age));
// 觸發事件,讓監聽函數執行
syncLoopHook.call("panda", 18);
// 1 panda 18 0
// 1 panda 18 1
// 1 panda 18 2
// 2 panda 18 0
// 2 panda 18 1
// 2 panda 18 2
// 3 panda 18
複製代碼
複製代碼
SyncLoopHook
的執行機制,但有一點須要注意,返回值必須嚴格是 true
纔會觸發循環,屢次執行當前事件處理函數,必須嚴格返回 undefined
,纔會結束循環,去執行後面的事件處理函數,若是事件處理函數的返回值不是 true
也不是 undefined
,則會死循環。
在瞭解 SyncLoopHook
的執行機制之後,咱們接下來看看 SyncLoopHook
的 call
方法是如何實現的。
// 模擬 SyncLoopHook 類
class SyncLoopHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 傳入參數嚴格對應建立實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 依次執行事件處理函數,若是返回值爲 true,則繼續執行當前事件處理函數
// 直到返回 undefined,則繼續向下執行其餘事件處理函數
this.tasks.forEach(task => {
let ret;
do {
ret = this.task(...args);
} while (ret === true || !(ret === undefined));
});
}
}
複製代碼
複製代碼
在上面代碼中能夠看到 SyncLoopHook
類 call
方法的實現更像是 SyncHook
和 SyncBailHook
的 call
方法的結合版,外層循環整個 tasks
事件處理函數隊列,內層經過返回值進行循環,控制每個事件處理函數的執行次數。
tab
註冊。
Async
類型可使用 tap
、tapSync
和 tapPromise
註冊不一樣類型的插件 「鉤子」,分別經過 call
、callAsync
和 promise
方法調用,咱們下面會針對 AsyncParallelHook
和 AsyncSeriesHook
的 async
和 promise
兩種方式分別介紹和模擬。
AsyncParallelHook
爲異步並行執行,經過 tapAsync
註冊的事件,經過 callAsync
觸發,經過 tapPromise
註冊的事件,經過 promise
觸發(返回值能夠調用 then
方法)。
callAsync
的最後一個參數爲回調函數,在全部事件處理函數執行完畢後執行。
// AsyncParallelHook 鉤子:tapAsync/callAsync 的使用
const { AsyncParallelHook } = require("tapable");
// 建立實例
let asyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 註冊事件
console.time("time");
asyncParallelHook.tapAsync("1", (name, age, done) => {
settimeout(() => {
console.log("1", name, age, new Date());
done();
}, 1000);
});
asyncParallelHook.tapAsync("2", (name, age, done) => {
settimeout(() => {
console.log("2", name, age, new Date());
done();
}, 2000);
});
asyncParallelHook.tapAsync("3", (name, age, done) => {
settimeout(() => {
console.log("3", name, age, new Date());
done();
console.timeEnd("time");
}, 3000);
});
// 觸發事件,讓監聽函數執行
asyncParallelHook.callAsync("panda", 18, () => {
console.log("complete");
});
// 1 panda 18 2018-08-07T10:38:32.675Z
// 2 panda 18 2018-08-07T10:38:33.674Z
// 3 panda 18 2018-08-07T10:38:34.674Z
// complete
// time: 3005.060ms
複製代碼
複製代碼
3s
,而三個事件處理函數執行完成總共用時接近 3s
,因此三個事件處理函數是幾乎同時執行的,不需等待。
全部 tabAsync
註冊的事件處理函數最後一個參數都爲一個回調函數 done
,每一個事件處理函數在異步代碼執行完畢後調用 done
函數,則能夠保證 callAsync
會在全部異步函數都執行完畢後執行,接下來看一看 callAsync
是如何實現的。
// 模擬 AsyncParallelHook 類:tapAsync/callAsync
class AsyncParallelHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tabAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
// 先取出最後傳入的回調函數
let finalCallback = args.pop();
// 傳入參數嚴格對應建立實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 定義一個 i 變量和 done 函數,每次執行檢測 i 值和隊列長度,決定是否執行 callAsync 的回調函數
let i = 0;
let done = () => {
if (++i === this.tasks.length) {
finalCallback();
}
};
// 依次執行事件處理函數
this.tasks.forEach(task => task(...args, done));
}
}
複製代碼
複製代碼
在 callAsync
中,將最後一個參數(全部事件處理函數執行完畢後執行的回調)取出,並定義 done
函數,經過比較 i
和存儲事件處理函數的數組 tasks
的 length
來肯定回調是否執行,循環執行每個事件處理函數並將 done
做爲最後一個參數傳入,因此每一個事件處理函數內部的異步操做完成時,執行 done
就是爲了檢測是否是該執行 callAsync
的回調,當全部事件處理函數均執行完畢知足 done
函數內部 i
和 length
相等的條件時,則調用 callAsync
的回調。
要使用 tapPromise
註冊事件,對事件處理函數有一個要求,必須返回一個 Promise 實例,而 promise
方法也返回一個 Promise 實例,callAsync
的回調函數在 promise
方法中用 then
的方式代替。
// AsyncParallelHook 鉤子:tapPromise/promise 的使用
const { AsyncParallelHook } = require("tapable");
// 建立實例
let asyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 註冊事件
console.time("time");
asyncParallelHook.tapPromise("1", (name, age) => {
return new Promise((resolve, reject) => {
settimeout(() => {
console.log("1", name, age, new Date());
resolve("1");
}, 1000);
});
});
asyncParallelHook.tapPromise("2", (name, age) => {
return new Promise((resolve, reject) => {
settimeout(() => {
console.log("2", name, age, new Date());
resolve("2");
}, 2000);
});
});
asyncParallelHook.tapPromise("3", (name, age) => {
return new Promise((resolve, reject) => {
settimeout(() => {
console.log("3", name, age, new Date());
resolve("3");
console.timeEnd("time");
}, 3000);
});
});
// 觸發事件,讓監聽函數執行
asyncParallelHook.promise("panda", 18).then(ret => {
console.log(ret);
});
// 1 panda 18 2018-08-07T12:17:21.741Z
// 2 panda 18 2018-08-07T12:17:22.736Z
// 3 panda 18 2018-08-07T12:17:23.739Z
// time: 3006.542ms
// [ '1', '2', '3' ]
複製代碼
複製代碼
上面每個 tapPromise
註冊事件的事件處理函數都返回一個 Promise 實例,並將返回值傳入 resolve
方法,調用 promise
方法觸發事件時,若是全部事件處理函數返回的 Promise 實例結果都成功,會將結果存儲在數組中,並做爲參數傳遞給 promise
的 then
方法中成功的回調,若是有一個失敗就是將失敗的結果返回做爲參數傳遞給失敗的回調。
// 模擬 AsyncParallelHook 類 tapPromise/promise
class AsyncParallelHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tapPromise(name, task) {
this.tasks.push(task);
}
promise(...args) {
// 傳入參數嚴格對應建立實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 將全部事件處理函數轉換成 Promise 實例,併發執行全部的 Promise
return Promise.all(this.tasks.map(task => task(...args)));
}
}
複製代碼
複製代碼
其實根據上面對於 tapPromise
和 promise
使用的描述就能夠猜到,promise
方法的邏輯是經過 Promise.all
來實現的。
AsyncSeriesHook
爲異步串行執行,與 AsyncParallelHook
相同,經過 tapAsync
註冊的事件,經過 callAsync
觸發,經過 tapPromise
註冊的事件,經過 promise
觸發,能夠調用 then
方法。
與 AsyncParallelHook
的 callAsync
方法相似,AsyncSeriesHook
的 callAsync
方法也是經過傳入回調函數的方式,在全部事件處理函數執行完畢後執行 callAsync
的回調函數。
// AsyncSeriesHook 鉤子:tapAsync/callAsync 的使用
const { AsyncSeriesHook } = require("tapable");
// 建立實例
let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 註冊事件
console.time("time");
asyncSeriesHook.tapAsync("1", (name, age, next) => {
settimeout(() => {
console.log("1", name, age, new Date());
next();
}, 1000);
});
asyncSeriesHook.tapAsync("2", (name, age, next) => {
settimeout(() => {
console.log("2", name, age, new Date());
next();
}, 2000);
});
asyncSeriesHook.tapAsync("3", (name, age, next) => {
settimeout(() => {
console.log("3", name, age, new Date());
next();
console.timeEnd("time");
}, 3000);
});
// 觸發事件,讓監聽函數執行
asyncSeriesHook.callAsync("panda", 18, () => {
console.log("complete");
});
// 1 panda 18 2018-08-07T14:40:52.896Z
// 2 panda 18 2018-08-07T14:40:54.901Z
// 3 panda 18 2018-08-07T14:40:57.901Z
// complete
// time: 6008.790ms
複製代碼
複製代碼
1s
、2s
和 3s
,而三個事件處理函數執行完總共用時接近 6s
,因此三個事件處理函數執行是須要排隊的,必須一個一個執行,當前事件處理函數執行完才能執行下一個。
AsyncSeriesHook
類的 tabAsync
方法註冊的事件處理函數參數中的 next
能夠與 AsyncParallelHook
類中 tabAsync
方法參數的 done
進行類比,同爲回調函數,不一樣點在於 AsyncSeriesHook
與 AsyncParallelHook
的 callAsync
方法的 「並行」 和 「串行」 的實現方式。
// 模擬 AsyncSeriesHook 類:tapAsync/callAsync
class AsyncSeriesHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tabAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
// 先取出最後傳入的回調函數
let finalCallback = args.pop();
// 傳入參數嚴格對應建立實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 定義一個 i 變量和 next 函數,每次取出一個事件處理函數執行,並維護 i 的值
// 直到全部事件處理函數都執行完,調用 callAsync 的回調
// 若是事件處理函數中沒有調用 next,則沒法繼續
let i = 0;
let next = () => {
let task = this.tasks[i++];
task ? task(...args, next) : finalCallback();
};
next();
}
}
複製代碼
複製代碼
AsyncParallelHook
是經過循環依次執行了全部的事件處理函數,done
方法只爲了檢測是否已經知足條件執行 callAsync
的回調,若是中間某個事件處理函數沒有調用 done
,只是不會調用 callAsync
的回調,可是全部的事件處理函數都執行了。
而 AsyncSeriesHook
的 next
執行機制更像 Express
和 Koa
中的中間件,在註冊事件的回調中若是不調用 next
,則在觸發事件時會在沒有調用 next
的事件處理函數的位置 「卡死」,即不會繼續執行後面的事件處理函數,只有都調用 next
才能繼續,而最後一個事件處理函數中調用 next
決定是否調用 callAsync
的回調。
與 AsyncParallelHook
相似,tapPromise
註冊事件的事件處理函數須要返回一個 Promise 實例,promise
方法最後也返回一個 Promise 實例。
// AsyncSeriesHook 鉤子:tapPromise/promise 的使用
const { AsyncSeriesHook } = require("tapable");
// 建立實例
let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 註冊事件
console.time("time");
asyncSeriesHook.tapPromise("1", (name, age) => {
return new Promise((resolve, reject) => {
settimeout(() => {
console.log("1", name, age, new Date());
resolve("1");
}, 1000);
});
});
asyncSeriesHook.tapPromise("2", (name, age) => {
return new Promise((resolve, reject) => {
settimeout(() => {
console.log("2", name, age, new Date());
resolve("2");
}, 2000);
});
});
asyncParallelHook.tapPromise("3", (name, age) => {
return new Promise((resolve, reject) => {
settimeout(() => {
console.log("3", name, age, new Date());
resolve("3");
console.timeEnd("time");
}, 3000);
});
});
// 觸發事件,讓監聽函數執行
asyncSeriesHook.promise("panda", 18).then(ret => {
console.log(ret);
});
// 1 panda 18 2018-08-07T14:45:52.896Z
// 2 panda 18 2018-08-07T14:45:54.901Z
// 3 panda 18 2018-08-07T14:45:57.901Z
// time: 6014.291ms
// [ '1', '2', '3' ]
複製代碼
複製代碼
分析上面的執行過程,全部的事件處理函數都返回了 Promise 的實例,若是想實現 「串行」,則須要讓每個返回的 Promise 實例都調用 then
,並在 then
中執行下一個事件處理函數,這樣就保證了只有上一個事件處理函數執行完後纔會執行下一個。
// 模擬 AsyncSeriesHook 類 tapPromise/promise
class AsyncSeriesHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tapPromise(name, task) {
this.tasks.push(task);
}
promise(...args) {
// 傳入參數嚴格對應建立實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 將每一個事件處理函數執行並調用返回 Promise 實例的 then 方法
// 讓下一個事件處理函數在 then 方法成功的回調中執行
let [first, ...others] = this.tasks;
return others.reduce((promise, task) => {
return promise.then(() => task(...args));
}, first(...args));
}
}
複製代碼
複製代碼
上面代碼中的 「串行」 是使用 reduce
歸併來實現的,首先將存儲全部事件處理函數的數組 tasks
解構成兩部分,第一個事件處理函數和存儲其餘事件處理函數的數組 others
,對 others
進行歸併,將第一個事件處理函數執行後返回的 Promise 實例做爲歸併的初始值,這樣在歸併的過程當中上一個值始終是上一個事件處理函數返回的 Promise 實例,能夠直接調用 then
方法,並在 then
的回調中執行下一個事件處理函數,直到歸併完成,將 reduce
最後返回的 Promise 實例做爲 promise
方法的返回值,則實現 promise
方法執行後繼續調用 then
來實現後續邏輯。
在上面 Async
異步類型的 「鉤子中」,咱們只着重介紹了 「串行」 和 「並行」(AsyncParallelHook
和 AsyncSeriesHook
)以及回調和 Promise 的兩種註冊和觸發事件的方式,還有一些其餘的具備必定特色的異步 「鉤子」 咱們並無進行分析,由於他們的機制與同步對應的 「鉤子」 很是的類似。
AsyncParallelBailHook
和 AsyncSeriesBailHook
分別爲異步 「並行」 和 「串行」 執行的 「鉤子」,返回值不爲 undefined
,即有返回值,則當即中止向下執行其餘事件處理函數,實現邏輯可結合 AsyncParallelHook
、AsyncSeriesHook
和 SyncBailHook
。
AsyncSeriesWaterfallHook
爲異步 「串行」 執行的 「鉤子」,上一個事件處理函數的返回值做爲參數傳遞給下一個事件處理函數,實現邏輯可結合 AsyncSeriesHook
和 SyncWaterfallHook
。
在 tapable
源碼中,註冊事件的方法 tab
、tapSync
、tapPromise
和觸發事件的方法 call
、callAsync
、promise
都是經過 compile
方法快速編譯出來的,咱們本文中這些方法的實現只是遵守了 tapable
庫這些 「鉤子」 的事件處理機制進行了模擬,以方便咱們瞭解 tapable
,爲學習 Webpack 原理作了一個鋪墊,在 Webpack 中,這些 「鉤子」 的真正做用就是將經過配置文件讀取的插件與插件、加載器與加載器之間進行鏈接,「並行」 或 「串行」 執行,相信在咱們對 tapable
中這些 「鉤子」 的事件機制有所瞭解以後,再從新學習 Webpack 的源碼應該會有所頭緒。