图解 Google V8 # 19 :异步编程(二):V8 是如何实现 async/await 的?

简介: 图解 Google V8 # 19 :异步编程(二):V8 是如何实现 async/await 的?

说明

图解 Google V8 学习笔记



前端异步编程的方案史

2aff468da81640bca75b32d82ff095c4.png


1、什么是回调地狱

如果在代码中过多地使用异步回调函数,会将整个代码逻辑打乱,从而让代码变得难以理解,这就是回调地狱问题。

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

上面的代码一个异步请求套着一个异步请求,一个异步请求依赖于另一个的执行结果,使用回调的方式相互嵌套。


这会导致代码很丑陋,不方便后期维护。



2、使用 Promise 解决回调地狱问题


使用 Promise 可以解决回调地狱中编码不线性的问题。

const fs = require("fs")
const p = new Promise((resolve, reject) => {
  fs.readFile("./src/kaimo555.txt", (err, data) => {
    resolve(data)
  })
})
p.then(value => {
  return new Promise((resolve, reject) => {
    fs.readFile("./src/kaimo666.txt", (err, data) => {
      resolve([value, data])
    })
  })
}).then(value => {
  return new Promise((resolve, reject) => {
    fs.readFile("./src/kaimo777.txt", (err, data) => {
      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
 }



可行的方案就是执行到异步请求的时候,暂停当前函数,等异步请求返回了结果,再恢复该函数。

大致模型图:关键就是实现函数暂停执行和函数恢复执行


661471838d254ca4bace3664b230d9a1.png


生成器函数


生成器就是为了实现暂停函数和恢复函数而设计的,生成器函数是一个带星号函数,配合 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 协程的父协程。


  • 一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。
  • 协程不是被操作系统内核所管理,而完全是由程序所控制,不会像线程切换那样消耗资源。

上面例子的协程执行流程图大致如下:


a173889ff14b4331a620adac9237c30d.png

协程和 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 框架


fbea38beb5f744f1b02f7d412c45ef16.png


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, reject) {
    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, reject) {
    fn.call(ctx, function (err, res) {
      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, key) {
    // 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 函数


5383ea6266e04887988137bbc2fc1cb5.png


V8 是如何处理 await 后面的内容?


await 可以等待两种类型的表达式:


   任何普通表达式


   一个 Promise 对象的表达式


如果 await 等待的是一个 Promise 对象,它就会暂停执行生成器函数,直到 Promise 对象的状态变成 resolve,才会恢复执行,然后得到 resolve 的值,作为 await 表达式的运算结果。



目录
相关文章
|
Web App开发 缓存 JavaScript
图解 Google V8 # 13:字节码(一):V8为什么又重新引入字节码?
图解 Google V8 # 13:字节码(一):V8为什么又重新引入字节码?
223 0
图解 Google V8 # 13:字节码(一):V8为什么又重新引入字节码?
|
缓存 JavaScript 前端开发
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
279 0
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
|
Web App开发 JavaScript 前端开发
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
105 0
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
|
算法 JavaScript Java
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
92 0
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
|
消息中间件 前端开发 JavaScript
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
413 0
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
|
消息中间件 程序员 Android开发
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
102 0
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
|
存储 缓存 索引
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
133 0
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
|
JavaScript 前端开发 编译器
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
132 0
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
|
JavaScript 前端开发 Java
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?
219 0
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?
|
自然语言处理 JavaScript 前端开发
图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?
图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?
114 0
图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?