天天看點

async/await 源碼實作

async/await 是es7帶來的新文法糖,可以将内部的異步方法同步處理,看一下下面的例子

async/await 應用場景

如果你有一個這樣的場景,b依賴于a,c依賴于b,那麼我們隻能通過promise then的方式實作。這樣的的可讀性就會變得很差,而且不利于流程控制,比如我想在某個條件下隻走到 b 就不往下執行 c 了,這種時候就變得不是很好控制!

Promise.resolve(a)
  .then(b => {
    // do something
  })
  .then(c => {
    // do something
  })
  ...
  ...
  .catch(x => {
    console.error(x)
  })           

複制

async/await實作上述代碼,可讀性跟流程控制都變的很友善,但是異常捕獲隻能通過try/catch來實作

async () => {
  try {
    const resA = await Promise.resolve(a);
    // do something
    const resB = await Promise.resolve(b);
    // do something
  } catch(e) {
    console.log(e)
  }
}           

複制

生成器(generator)

在想知道 async/await 實作原理之前,我們要首先了解生成器(generator),其實async/await看起來,像極了generator(生成器),隻是生成器它不能自動疊代,隻能手動觸發。舉個?
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

let g = gen();

g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.next() // { value: 3, done: true }           

複制

  • g.next() 傳回的是一個對象 { value: 1, done: false }
  • value 每調一次就會執行下一個 yield,value 就是 yield 的一個值,比如第一次就是 1,第二次就是 2
  • done done 的值為 true 則說明下面已經沒有可疊代項了
  • yield 你可以認為是一個 return,會阻斷下面代碼的執行,并且會将後面的數值傳回回去

生成器自疊代

我們可以試想一下,如果生成器能夠自動執行所有的疊代任務的話,是否執行下次疊代由 Promise 來決定,那麼我們就可以實作 async/await 了?

為什麼必須是 Promise?

因為 Promise 用于表示一個異步操作的最終完成 (或失敗), 及其結果值。最适合用來判斷上一個動作的狀态。

如何實作自疊代?

思路:

  1. 通過遞歸調用生成器對象 next 函數。
function _asyncToGenerator(fn) {
  return function() {
    var self = this,
      args = arguments;
    // 将傳回值promise化
    return new Promise(function(resolve, reject) {
      // 擷取疊代器執行個體
      var gen = fn.apply(self, args);
      // 執行下一步
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      // 抛出異常
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      // 第一次觸發
      _next(undefined);
    });
  };
}           

複制

  1. 上次 Promise 執行完成後,立即執行下一步,疊代器狀态 done = true 時結束
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    // 疊代器完成,将傳回值(return)儲存起來
    resolve(value);
  } else {
    // -- 這行代碼就是精髓 --
    // 将所有值promise化
    // 比如 yield 1
    // const a = Promise.resolve(1) a 是一個 promise
    // const b = Promise.resolve(a) b 是一個 promise
    // 可以做到統一 promise 輸出
    // 當 promise 執行完之後再執行下一步
    // 遞歸調用 next 函數,直到 done == true
    Promise.resolve(value).then(_next, _throw);
  }
}           

複制

  1. 接下來測試一下我們寫的生成器
const asyncFunc = _asyncToGenerator(function* () {
  console.log(1);
  yield new Promise(resolve => {
    setTimeout(() => {
      resolve();
      console.log('sleep 1s');
    }, 1000);
  });
  console.log(2);
  const a = yield Promise.resolve('a');
  console.log(3);
  const b = yield Promise.resolve('b');
  const c = yield Promise.resolve('c');
  return [a, b, c];
})

asyncFunc().then(res => {
  console.log(res)
});

// 運作結果
// 1
// sleep 1s
// 2
// 3
// ["a", "b", "c"]           

複制

  1. 與使用 async/await 對比
const func = async () => {
  console.log(1)
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve()
      console.log('sleep 1s')
    }, 1000)
  })
  console.log(2)
  const a = await Promise.resolve('a')
  console.log(3)
  const b = await Promise.resolve('b')
  const c = await Promise.resolve('c')
  return [a, b, c]
}

func().then(res => {
  console.log(res)
})

// 運作結果
// 1
// sleep 1s
// 2
// 3
// ["a", "b", "c"]           

複制

可以看出,我們的代碼與 async/await 的輸出完全一緻,最後再通過 babel等工具 做一些詞法轉換就可以了

生成器實作

雖然我們已經完成了對 async/await 的實作,但是作為一個好奇貓,我們還想知道 generator 到底怎麼實作的?為什麼能夠阻斷我們代碼的執行,下次調用的時候再走下一個 yield。這好像很難用 js 代碼去解釋!

還是使用我們剛才的?,我們看一下 babel 是怎麼實作生成器的?

看完這段代碼之後,一定會打破你的認知!原來代碼還可以這樣寫!

// 這是我們的異步生成器
var asyncFunc = _asyncToGenerator(
// regeneratorRuntime 這個對象是 疊代器的運作時,mark函數 将所有的變量儲存在它作用域下
regeneratorRuntime.mark(function _callee() {
  var a, d, b, c;
  // wrap 是對下面代碼片段的一個包裹函數,每執行一次疊代就會調用一次 _callee$
  // _context.next, 執行完本次疊代後将指針指到下一個疊代
  return regeneratorRuntime.wrap(function _callee$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          // --------- ⬇⬇ 這是第一個代碼片段 ⬇⬇ -----------
          console.log(1);
          _context.next = 3;
          return new Promise(function (resolve) {
            setTimeout(function () {
              resolve();
              console.log('sleep 1s');
            }, 1000);
          });
          // --------- ⬆⬆ 這是第一個代碼片段 ⬆⬆ -----------
        case 3:
          // --------- ⬇⬇ 這是第二個代碼片段 ⬇⬇ -----------
          console.log(2);
          _context.next = 9;
          return Promise.resolve('a');
          // --------- ⬆⬆ 這是第二個代碼片段 ⬆⬆ -----------
          // ...
          // ... 下面以此類推每一個 yield 會被放進一個 case,作為一個代碼片段,
          // ... 每次執行完就return,并且将 _context.next 指向下一個
          // ... 等待下次調用
        case 9:
          d = _context.sent;
          console.log(3);
          _context.next = 13;
          return Promise.resolve('b');

        case 13:
          b = _context.sent;
          _context.next = 16;
          return Promise.resolve('c');

        case 16:
          c = _context.sent;
          return _context.abrupt("return", [a, b, c, d]);

        case 18:
        case "end":
          // 最後執行 stop 結束
          return _context.stop();
      }
    }
  }, _callee);
}));

asyncFunc().then(function (res) {
  console.log(res);
});           

複制

如此巧妙的構思,讓我對代碼有了新的認識。通過詞法解析将代碼分割成多個片段,用現有的文法實作未來的功能,實在是很巧妙。

體會

通過這次的源碼學習,讓我打破了原本的思維模式,通過源碼更加了解到了 promise 的妙用,以及詞法的巧妙轉換,都讓我感觸頗深。