Generator 協程工做原理

原文地址:regeneratorgit

搜了一圈,關於 Generator 基本都是在講用法,但不多說起到其工做原理,也就是「協程」。但又由於這東西咱們直接或間接的天天都在使用,因而準備專門寫一篇文章來說講這個github

JS 回調史

1、Callback

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

2、Promise

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

3、Generator

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

4、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

1、generator Object

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

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

2、Generator

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

3、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 的工做原理

正片開始,代碼示例:api

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、核心點

  1. Generator 的狀態是如何實現的,或者說 Generator 是如何執行到 yield 就中止的
  2. 多個 Generator 是如何協做的,即如何讓權給另外一個 Generator,以後又讓權回來的
  3. 一個 Generator 是如何監聽另外一個 Generator 的執行過程,即 yield* genFn()

2、Generator、GeneratorFunction 及其 prototype 的關係

若是你對原型鏈和繼承有所遺忘的話,建議先看下這篇 prototype&extendspromise

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;

2、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 完後就「卡住了」

3、多個 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 的事件循環

4、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;
    }
  }
}

5、yield* genFn()

最後一點,可能各位用得少,但缺了的話,Generator 是不完整的babel

  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. 碼字不易,喜歡的小夥伴,記得留下你的小 ❤️ 哦~

參考資料

相關文章
相關標籤/搜索