博客 / 詳情

返回

Generator 協程工作原理

原文地址:regenerator

搜了一圈,關於 Generator 基本都是在講用法,但很少提及到其工作原理,也就是“協程”。但又因為這東西我們直接或間接的每天都在使用,於是準備專門寫一篇文章來講講這個

JS 回調史

一、Callback

  1. ES5 及更早時期,寫回調基本都是 callback,回調地獄就不説了,離它遠點

二、Promise

  1. Promise 通過鏈式調用,優化了回調的書寫方式,本質上也是回調。由其封裝出來的 Deferred 也在各大開源庫能看到蹤影,如 qiankun
  2. Promise 本身沒有什麼新穎的東西,但由 then 註冊的回調要在當前事件循環的微任務階段去執行這一點,意味着 Promise 只能由原生層面提供。用户層面的 polyfill 只能用宏任務完成,如 promise-polyfill

三、Generator

  1. Generator 是本文的主角,ES6 重磅推出的特性,可以理解成一個狀態機,裏面包含了各種狀態,使用 yield 來觸發下一步
  2. Generator 引入的“協程”概念,是傳統回調無法比擬的,這就意味着我們可以以同步的方式來書寫異步代碼,再配上自動執行,如 tj 大神的 co 庫 ,簡直美翻
  3. generator 對象同時實現了:
  • 可迭代協議(Symbol.iterator):可通過 for...of 進行迭代,如內置對象 Array、String,它們都實現了這個協議
  • 迭代器協議(next()):可調用其 next 方法獲取 { value: any, done: boolean } 來判斷狀態

四、async、await

  1. Generator、yield 的語法糖,精選了一些特性。反過來説就是舍掉了些功能(後文會講)
  2. 用 babel 編譯一段含 async、await 和 yield 的代碼,可知前者多了兩個函數 asyncGeneratorStep_asyncToGenerator,其實它就是自動執行功能
  3. 原理很簡單:
  • 獲取 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

  1. 由 Generator 執行後返回,帶有 next、return、throw 等原型方法,就是我們常操作的小夥伴
function* gen() {}
const gObj = gen();

gObj.next();
gObj.return();

二、Generator

  1. 可通過 function* 語法來定義,它是 GeneratorFunction 的實例
Object.getPrototypeOf(gen).constructor // GeneratorFunction {prototype: Generator, ...}
  1. Generator 函數本身在用户代碼層面,意義不大,基本不會用到

三、GeneratorFunction

  1. 它是內置函數,但沒有直接掛到 window 上,但我們可以通過它的實例來獲取
const GeneratorFunction = Object.getPrototypeOf(gen).constructor;
  1. 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);

一、核心點

  1. Generator 的狀態是如何實現的,或者説 Generator 是如何執行到 yield 就停止的
  2. 多個 Generator 是如何協作的,即如何讓權給另一個 Generator,之後又讓權回來的
  3. 一個 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 的狀態

  1. 狀態機實現不難,通過一個 flag 記錄狀態,每次狀態運行後記錄下次的狀態,一定時機後再進入執行
  2. 狀態機是由用户層面代碼生成,裏面使用 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();
    }
  }
}
  1. 可知每次 yield 對應着一個 switch case,每次都會 return,自然每次 yield 完後就“卡住了”

三、多個 Generator 協作

  1. 由 case return 可知 Generator 讓權,就是主動執行別的 Generator,並退出自己的狀態
  2. 同理 foo Generator 也是 switch case 這種結構,那它執行完是如何讓權回到並觸發父級狀態機繼續執行呢
  3. 我們來看 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);
}
  1. 所以 foo 執行 switch 完,經過處理後把 { value: "literal", done: true } 作為了 mark 函數的返回值,並交給 _asyncToGenerator 使用,它如何使用的呢,當然是 promise.then(next)
  2. 那協作呢?你別隻侷限於 foo 函數,父級 gen 函數不也是這樣!gen 函數這時在幹啥,當然是等待 foo resolve,然後 gen 返回 { value: fooRetValue, done: false },繼續 next
  3. 整理下:
  • ① 父級 gen 函數執行到一個 case,將子 foo 函數的返回值作為本次結果,然後將自己卡住(其實就是在 co 層面等待子 promise resolve)
  • ② foo 執行完後返回 done true,並結束自己的狀態生涯,再將自己 co 層面的 Promise resolve
  • ③ gen 卡住的 Promise 收到了 foo 的結果,本次返回 done false,開啓下一輪 next,並重新通過 context.next 進入到對應 case 中
  1. 所以你可以看出,Generator 離開了 Promise 時成不了大器的,無論是原生實現還是 polyfill,主要原因還是之前説的,我們沒法在 js 層面干涉到 v8 的事件循環

四、mark、wrap、Context

  1. 你應該知道 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;
}
  1. 每個 wrap 會創建一個 context 來管理狀態以及上下文參數,每次執行 case 時會先打個快照,防止 yield 完後參數更改
  2. 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);
  }
}
  1. 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;
}
  1. 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 是不完整的

  1. 之前挖了個坑,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' };
}
  1. 那在 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();
  }
}
  1. 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") {}
}

最後

  1. 本文只是簡單對 Generator 進行了實現,實際上 regenerator 做的事情還很多,如 throw error、yield* gen() 時各種狀況的處理以及其他方便的 api,喜歡的自行 dive in 吧
  2. 通過本文對 Generator 工作原理的講解,讓我們對“協程”這個概念更加深刻的認識,這對於我們每天都要用的東西、調試的代碼都有“磨刀不誤砍柴工”的功效
  3. 源碼獲取:regenerator
  4. 碼字不易,喜歡的小夥伴,記得留下你的小 ❤️ 哦~

參考資料

  • MDN Generator
  • MDN Iteration protocols
  • regenerator
  • co
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.