原文地址:regenerator
搜了一圈,關於 Generator 基本都是在講用法,但很少提及到其工作原理,也就是“協程”。但又因為這東西我們直接或間接的每天都在使用,於是準備專門寫一篇文章來講講這個
JS 回調史
一、Callback
- ES5 及更早時期,寫回調基本都是 callback,回調地獄就不説了,離它遠點
二、Promise
- Promise 通過鏈式調用,優化了回調的書寫方式,本質上也是回調。由其封裝出來的
Deferred也在各大開源庫能看到蹤影,如 qiankun - Promise 本身沒有什麼新穎的東西,但由 then 註冊的回調要在當前事件循環的微任務階段去執行這一點,意味着 Promise 只能由原生層面提供。用户層面的 polyfill 只能用宏任務完成,如 promise-polyfill
三、Generator
- Generator 是本文的主角,ES6 重磅推出的特性,可以理解成一個狀態機,裏面包含了各種狀態,使用 yield 來觸發下一步
- Generator 引入的“協程”概念,是傳統回調無法比擬的,這就意味着我們可以以同步的方式來書寫異步代碼,再配上自動執行,如 tj 大神的 co 庫 ,簡直美翻
- generator 對象同時實現了:
- 可迭代協議(Symbol.iterator):可通過 for...of 進行迭代,如內置對象 Array、String,它們都實現了這個協議
- 迭代器協議(next()):可調用其 next 方法獲取
{ value: any, done: boolean }來判斷狀態
四、async、await
- Generator、yield 的語法糖,精選了一些特性。反過來説就是舍掉了些功能(後文會講)
- 用 babel 編譯一段含 async、await 和 yield 的代碼,可知前者多了兩個函數
asyncGeneratorStep和_asyncToGenerator,其實它就是自動執行功能 - 原理很簡單:
- 獲取 Generator 對象,藉助 Promise 的微任務能力執行 next
- ret.value 返回的值就是 await 的值,封裝成 Promise 當做下次入參
- 判斷每次遞歸結果,直到返回 done 為 true
async function a() {}
function* b() {}
// babel 編譯後
function asyncGeneratorStep(gen, resolve, reject, _next, ...) {
// 調用 gen 的 next 或 throw 方法
var info = gen[key](arg);
var value = info.value;
if (info.done) {
resolve(value);
} else {
// 遞歸執行
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function () {
return new Promise(function (resolve, reject) {
// 獲取 generator 對象
var gen = fn.apply(self, arguments);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
// 初始化執行 next
_next(undefined);
});
};
}
generator Object、Generator、 GeneratorFunction
一、generator Object
- 由 Generator 執行後返回,帶有 next、return、throw 等原型方法,就是我們常操作的小夥伴
function* gen() {}
const gObj = gen();
gObj.next();
gObj.return();
二、Generator
- 可通過
function*語法來定義,它是 GeneratorFunction 的實例
Object.getPrototypeOf(gen).constructor // GeneratorFunction {prototype: Generator, ...}
- Generator 函數本身在用户代碼層面,意義不大,基本不會用到
三、GeneratorFunction
- 它是內置函數,但沒有直接掛到 window 上,但我們可以通過它的實例來獲取
const GeneratorFunction = Object.getPrototypeOf(gen).constructor;
- GeneratorFunction 和
Function是一個級別的,可以傳參來創建函數,如
const gen = new GeneratorFunction('a', 'yield a * 2');
const gObj = gen(10);
gObj.next().value // 20
Generator 的工作原理
正片開始,代碼示例:
let num = 0;
async function gen() {
num = num + (await wait(10));
await 123;
await foo();
return num;
}
function wait(num: number) {
return new Promise((resolve) => setTimeout(() => resolve(num), 600));
}
async function foo() {
await "literal";
}
await gen();
console.log("regenerator: res", num);
一、核心點
- Generator 的狀態是如何實現的,或者説 Generator 是如何執行到 yield 就停止的
- 多個 Generator 是如何協作的,即如何讓權給另一個 Generator,之後又讓權回來的
- 一個 Generator 是如何監聽另一個 Generator 的執行過程,即 yield* genFn()
二、Generator、GeneratorFunction 及其 prototype 的關係
如果你對原型鏈和繼承有所遺忘的話,建議先看下這篇 prototype&extends
class GeneratorFunction {}
// GeneratorFunction 的 prototype 很通用,單獨拎出來
class GeneratorFunctionPrototype {
static [Symbol.toStringTag] = "GeneratorFunction";
// 實現 iterator protocol
next(args) {}
return(args) {}
throw(args) {}
// 實現 iterable protocol
[Symbol.iterator]() {
return this;
}
}
// 相互引用
GeneratorFunctionPrototype.constructor = GeneratorFunction;
GeneratorFunction.prototype = GeneratorFunctionPrototype;
// 作用不大,設置 prototype 即可
class Generator {}
Generator.prototype = GeneratorFunctionPrototype.prototype;
二、Generator 的狀態
- 狀態機實現不難,通過一個 flag 記錄狀態,每次狀態運行後記錄下次的狀態,一定時機後再進入執行
- 狀態機是由用户層面代碼生成,裏面使用
switch case + context 記錄參數實現
function _callee$(_context) {
while (1) {
switch (_context.next) {
case 0:
// await wait(10)
_context.next = 3;
return wait(10);
case 3:
// await 123
_context.next = 7;
return 123;
case 7:
_context.next = 9;
// await foo()
return foo();
case "end":
return _context.stop();
}
}
}
- 可知每次 yield 對應着一個 switch case,每次都會 return,自然每次 yield 完後就“卡住了”
三、多個 Generator 協作
- 由 case return 可知 Generator 讓權,就是主動執行別的 Generator,並退出自己的狀態
- 同理 foo Generator 也是 switch case 這種結構,那它執行完是如何讓權回到並觸發父級狀態機繼續執行呢
- 我們來看 babel 是如何編譯 async 函數的。先拋開 mark 和 warp 函數,
_asyncToGenerator我們之前説了,就是自動執行,這其實和co(markFn)無異。另一方面你可以推斷出regeneratorRuntime.mark函數返回的其實就是 polyfill 的 Generator
function _foo() {
_foo = _asyncToGenerator(
regeneratorRuntime.mark(function _callee2() {
return regeneratorRuntime.wrap(function _callee2$(_context2) {
switch (_context2.next) {
case 0:
_context2.next = 2;
return "literal";
case "end":
return _context2.stop();
}
}, _callee2);
})
);
return _foo.apply(this, arguments);
}
- 所以 foo 執行 switch 完,經過處理後把
{ value: "literal", done: true }作為了 mark 函數的返回值,並交給 _asyncToGenerator 使用,它如何使用的呢,當然是promise.then(next) - 那協作呢?你別隻侷限於 foo 函數,父級 gen 函數不也是這樣!gen 函數這時在幹啥,當然是等待 foo resolve,然後 gen 返回
{ value: fooRetValue, done: false },繼續 next - 整理下:
- ① 父級 gen 函數執行到一個 case,將子 foo 函數的返回值作為本次結果,然後將自己卡住(其實就是在 co 層面等待子 promise resolve)
- ② foo 執行完後返回 done true,並結束自己的狀態生涯,再將自己 co 層面的 Promise resolve
- ③ gen 卡住的 Promise 收到了 foo 的結果,本次返回 done false,開啓下一輪 next,並重新通過 context.next 進入到對應 case 中
- 所以你可以看出,Generator 離開了 Promise 時成不了大器的,無論是原生實現還是 polyfill,主要原因還是之前説的,我們沒法在 js 層面干涉到 v8 的事件循環
四、mark、wrap、Context
- 你應該知道 mark 函數了:接收一個函數並把它改造成 Generator。怎麼做呢,繼承啊
function mark(genFn: () => void) {
return _inheritsLoose(genFn, GeneratorFunctionPrototype);
}
function _inheritsLoose(subClass, superClass) {
Object.setPrototypeOf(subClass, superClass);
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
return subClass;
}
- 每個 wrap 會創建一個 context 來管理狀態以及上下文參數,每次執行 case 時會先打個快照,防止 yield 完後參數更改
- mark 函數的 next、return、throw 最終調用是 wrap 的能力,因為實際是 wrap 在協調用户代碼(switch case)和 context 來決定接下來的走向,所以要完善下 GeneratorFunctionPrototype,讓其和 wrap 連接起來,自己只負責傳遞 type 和 args
type GeneratorMethod = "next" | "return" | "throw";
class GeneratorFunctionPrototype {
// set by wrap fn
private _invoke: (method: GeneratorMethod, args) => { value: any, done: boolean };
// 注意這是原型方法哦
next(args) {
return this._invoke("next", args);
}
return(args) {
return this._invoke("return", args);
}
throw(args) {
return this._invoke("throw", args);
}
}
- wrap 實現
function wrap(serviceFn) {
// 依然借用 GeneratorFunctionPrototype 的能力
const generator = new Generator();
const context = new Context();
let state = GenStateSuspendedStart;
// 實現 _invoke
generator._invoke = function invoke(method: GeneratorMethod, args) {
context.method = method;
context.args = args;
if (method === "next") {
// 記錄上下文參數
context.sent = args;
} else if (method === "throw") {
throw args
} else {
context.abrupt("return", args);
}
// 執行業務上的代碼
const value = serviceFn(context);
state = context.done ? GenStateCompleted : GenStateSuspendedYield;
return {
value,
done: context.done
};
};
return generator;
}
- Context 記錄當前運行狀態和上下文參數等,並提供結束、報錯、代理等方法
class Context {
next: number | string = 0;
sent: any = undefined;
method: GeneratorMethod = "next";
args: any = undefined;
done: boolean = false;
value: any = undefined;
stop() {
this.done = true;
return this.value;
}
abrupt(type: "return" | "throw", args) {
if (type === "return") {
this.value = args;
this.method = "return";
this.next = "end";
} else if (type === "throw") {
throw args;
}
}
}
五、yield* genFn()
最後一點,可能各位用得少,但缺了的話,Generator 是不完整的
- 之前挖了個坑,await、async 捨棄了的功能就是:一個 Generator 是監聽到另一個 Generator 的執行過程。事實上使用 await 我們並不能知道子函數經歷了多少個 await
async function a() {
const res = await b();
}
async function b() {
await 1;
await 'str';
return { data: 'lawler', msg: 'ok' };
}
- 那在 yield 層面,這個功能是如何實現的呢。實際上 yield* 是通過 delegateYield 方法接替了 foo,在 context 內部循環運行,使得這次 yield 在一個 case 中完成
function gen$(_context) {
switch (_context.next) {
case 0:
_context.next = 7;
return wait(10);
case 7:
// 傳遞 foo generator object 給 gen 的 context
return _context.delegateYield(foo(), "t2", 8);
case "end":
return _context.stop();
}
}
- wrap 裏面,循環執行
generator._invoke = function invoke(method, args) {
context.method = method;
// yield* genFn 時使用,循環返回 genFn 迭代的結果,直到 return
while (true) {
const delegate = context.delegate;
if (delegate) {
const delegateResult = maybeInvokeDelegate(delegate, context);
if (delegateResult) {
if (delegateResult === empty) continue;
// 傳出內部迭代結果 { value, done }
return delegateResult;
}
}
}
if (method === "next") {}
}
最後
- 本文只是簡單對 Generator 進行了實現,實際上
regenerator做的事情還很多,如 throw error、yield* gen() 時各種狀況的處理以及其他方便的 api,喜歡的自行 dive in 吧 - 通過本文對 Generator 工作原理的講解,讓我們對“協程”這個概念更加深刻的認識,這對於我們每天都要用的東西、調試的代碼都有“磨刀不誤砍柴工”的功效
- 源碼獲取:regenerator
- 碼字不易,喜歡的小夥伴,記得留下你的小 ❤️ 哦~
參考資料
- MDN Generator
- MDN Iteration protocols
- regenerator
- co