天天看點

圖解 Google V8 # 19 :異步程式設計(二):V8 是如何實作 async/await 的?

說明

圖解 Google V8 學習筆記

前端異步程式設計的方案史

圖解 Google V8 # 19 :異步程式設計(二):V8 是如何實作 async/await 的?

1、什麼是回調地獄?

如果在代碼中過多地使用異步回調函數,會将整個代碼邏輯打亂,進而讓代碼變得難以了解,這就是回調地獄問題。

var fs = require('fs')

fs.readFile('./src/kaimo555.txt', 'utf-8', function(err,) {
    if (err) {
        throw err
    }
    console.log(data)
    fs.readFile('./src/kaimo666.txt', 'utf-8', function(err,) {
        if (err) {
            throw err
        }
        console.log(data)
        fs.readFile('./src/kaimo777.txt', 'utf-8', function(err,) {
            if (err) {
                throw err
            }
            console.log(data)
        })
    })
})      

上面的代碼一個異步請求套着一個異步請求,一個異步請求依賴于另一個的執行結果,使用回調的方式互相嵌套。

這會導緻代碼很醜陋,不友善後期維護。

2、使用 Promise 解決回調地獄問題

使用 Promise 可以解決回調地獄中編碼不線性的問題。

const fs = require("fs")
 
const p = new Promise((resolve,) => {
  fs.readFile("./src/kaimo555.txt", (err,) => {
    resolve(data)
  })
})
p.then(value => {
  return new Promise((resolve,) => {
    fs.readFile("./src/kaimo666.txt", (err,) => {
      resolve([value, data])
    })
  })
}).then(value => {
  return new Promise((resolve,) => {
    fs.readFile("./src/kaimo777.txt", (err,) => {
      value.push(data)
      resolve(value)
    })
  })
}).then(value => {
  let str = value.join("\n")
  console.log(str)
})      

3、使用 Generator 函數實作更加線性化邏輯

雖然使用 Promise 可以解決回調地獄中編碼不線性的問題,但這種方式充滿了 Promise 的 ​

​then()​

​ 方法,如果處理流程比較複雜的話,那麼整段代碼将充斥着大量的 then,異步邏輯之間依然被 then 方法打斷了,是以這種方式的語義化不明顯,代碼不能很好地表示執行流程。

那麼怎麼才能像編寫同步代碼的方式來編寫異步代碼?

例子:

function getResult(){
   let id = getUserID(); // 異步請求
   let name = getUserName(id); // 異步請求
   return name
 }      

可行的方案就是執行到異步請求的時候,暫停目前函數,等異步請求傳回了結果,再恢複該函數。

大緻模型圖:關鍵就是實作函數暫停執行和函數恢複執行。

圖解 Google V8 # 19 :異步程式設計(二):V8 是如何實作 async/await 的?

生成器函數

生成器就是為了實作暫停函數和恢複函數而設計的,生成器函數是一個帶星号函數,配合 yield 就可以實作函數的暫停和恢複。恢複生成器的執行,可以使用 next 方法。

例子:

function* getResult() {
  yield 'getUserID'
  yield 'getUserName'
  return 'name'
}

let result = getResult()

console.log(result.next().value)
console.log(result.next().value)
console.log(result.next().value)      

V8 是怎麼實作生成器函數的暫停執行和恢複執行?

協程

協程是一種比線程更加輕量級的存在。 如果從 A 協程啟動 B 協程,我們就把 A 協程稱為 B 協程的父協程。

  • 一個線程上可以存在多個協程,但是線上程上同時隻能執行一個協程。
  • 協程不是被作業系統核心所管理,而完全是由程式所控制,不會像線程切換那樣消耗資源。

上面例子的協程執行流程圖大緻如下:

協程和 Promise 互相配合執行的大緻流程:

function* getResult() {
    let id_res = yield fetch(id_url);
    let id_text = yield id_res.text();
    let new_name_url = name_url + "?id=" + id_text;
    let name_res = yield fetch(new_name_url);
    let name_text = yield name_res.text();
}


let result = getResult()
result.next().value.then((response) => {
    return result.next(response).value
}).then((response) => {
    return result.next(response).value
}).then((response) => {
    return result.next(response).value
}).then((response) => {
    return result.next(response).value      

執行器

把執行生成器的代碼封裝成一個函數,這個函數驅動了生成器函數繼續往下執行,我們把這個執行生成器代碼的函數稱為執行器。

可以參考著名的 ​​co 架構​​

function* getResult() {
    let id_res = yield fetch(id_url);
    let id_text = yield id_res.text();
    let new_name_url = name_url + "?id=" + id_text;
    let name_res = yield fetch(new_name_url);
    let name_text = yield name_res.text();
}

co(getResult())      

co 源碼實作原理:其實就是通過不斷的調用 generator 函數的 ​

​next()​

​ 函數,來達到自動執行 generator 函數的效果(類似 async、await 函數的自動自行)。

/**
 * slice() reference.
 */

var slice = Array.prototype.slice;

/**
 * Expose `co`.
 */

module.exports = co['default'] = co.co = co;

/**
 * Wrap the given generator `fn` into a
 * function that returns a promise.
 * This is a separate function so that
 * every `co()` call doesn't create a new,
 * unnecessary closure.
 *
 * @param {GeneratorFunction} fn
 * @return {Function}
 * @api public
 */

co.wrap = function (fn) {
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};

/**
 * Execute the generator function or a generator
 * and return a promise.
 *
 * @param {Function} fn
 * @return {Promise}
 * @api public
 */

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1);

  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve,) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();

    /**
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
      return null;
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

/**
 * Convert a `yield`ed value into a promise.
 *
 * @param {Mixed} obj
 * @return {Promise}
 * @api private
 */

function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}

/**
 * Convert a thunk to a promise.
 *
 * @param {Function}
 * @return {Promise}
 * @api private
 */

function thunkToPromise(fn) {
  var ctx = this;
  return new Promise(function (resolve,) {
    fn.call(ctx, function (err,) {
      if (err) return reject(err);
      if (arguments.length > 2) res = slice.call(arguments, 1);
      resolve(res);
    });
  });
}

/**
 * Convert an array of "yieldables" to a promise.
 * Uses `Promise.all()` internally.
 *
 * @param {Array} obj
 * @return {Promise}
 * @api private
 */

function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}

/**
 * Convert an object of "yieldables" to a promise.
 * Uses `Promise.all()` internally.
 *
 * @param {Object} obj
 * @return {Promise}
 * @api private
 */

function objectToPromise(obj){
  var results = new obj.constructor();
  var keys = Object.keys(obj);
  var promises = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]);
    if (promise && isPromise(promise)) defer(promise, key);
    else results[key] = obj[key];
  }
  return Promise.all(promises).then(function () {
    return results;
  });

  function defer(promise,) {
    // predefine the key in the result
    results[key] = undefined;
    promises.push(promise.then(function (res) {
      results[key] = res;
    }));
  }
}

/**
 * Check if `obj` is a promise.
 *
 * @param {Object} obj
 * @return {Boolean}
 * @api private
 */

function isPromise(obj) {
  return 'function' == typeof obj.then;
}

/**
 * Check if `obj` is a generator.
 *
 * @param {Mixed} obj
 * @return {Boolean}
 * @api private
 */

function isGenerator(obj) {
  return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}

/**
 * Check if `obj` is a generator function.
 *
 * @param {Mixed} obj
 * @return {Boolean}
 * @api private
 */
 
function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
  return isGenerator(constructor.prototype);
}

/**
 * Check for plain object.
 *
 * @param {Mixed} val
 * @return {Boolean}
 * @api private
 */

function isObject(val) {
  return Object == val.constructor;
}      

4、async/await:異步程式設計的“終極”方案

生成器依然需要使用額外的 co 函數來驅動生成器函數的執行,基于這個原因,ES7 引入了 ​

​async/await​

​,這是 JavaScript 異步程式設計的一個重大改進,它改進了生成器的缺點,提供了在不阻塞主線程的情況下使用同步代碼實作異步通路資源的能力。

  • ​async/await​

    ​ 不是 generator promise 的文法糖,而是從設計到開發都是一套完整的體系,隻不過使用了協程和 promise
  • ​async/await​

    ​ 支援 try catch 也是引擎的底層實作的
async function getResult() {
    try {
        let id_res = await fetch(id_url);
        let id_text = await id_res.text();
        let new_name_url = name_url+"?id="+id_text;
        let name_res = await fetch(new_name_url);
        let name_text = await name_res.text();
    } catch (err) {
        console.error(err)
    }
}
getResult()      

async

async 是一個通過異步執行并隐式傳回 Promise 作為結果的函數。

​​MDN:async 函數​​

圖解 Google V8 # 19 :異步程式設計(二):V8 是如何實作 async/await 的?

V8 是如何處理 await 後面的内容?

  • 任何普通表達式
  • 一個 Promise 對象的表達式

拓展資料

  • ​​《學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理》​​
  • ​​MDN:async 函數​​

繼續閱讀