一个事件循环引发的Async原理探究

起因

最近在复习前端事件循环机制,在无意间修改了下代码,返回了不一样的结果。代码简化如下:

function testSometing() {
	return "testing...";
}

async function test() {
  console.log("test start...");
  const v1 = await testSometing();
  console.log(v1);
  console.log("test end...");
}

test();
console.log("suspend!");

new Promise((resolve) => { 	
  resolve("promise");
}).then(val => console.log(val));

/* 输出
test start...
suspend!
testing...
test end...
promise
*/

微调 – 声明async

现在的输出结果完全正常,是常见的事件循环,具体流程不再赘述,下面来微调一下代码:

// 只是函数声明微调为async
async function testSometing() {
	return "testing...";
}

async function test() {
  console.log("test start...");
  const v1 = await testSometing();
  console.log(v1);
  console.log("test end...");
}

test();
console.log("suspend!");

new Promise((resolve) => { 	
  resolve("promise");
}).then(val => console.log(val));

/* 输出
test start...
suspend!
testing...
test end...
promise
*/

asyncawait的语法糖使得Promise链式调用转为同步的写法。

我们常常await一个函数,这里的执行顺序需要注意:

  • 执行到这个语句会先执行await后面的函数,获得一个返回值,await会将其"修饰"为一个Promise对象,再"中断跳出"
  • 执行完后面代码后,再返回函数内,等待Promise状态转为最终态,再次执行函数内代码

async函数会返回一个Promise对象,所以我们常常await一个async函数的返回值。

综上所述,这个微调合情合理,且无影响。但是当我们再来微调下:

微调 – 显式返回Promise

async function testSometing() {
	// 只是函数返回值显式声明为Promise
	return Promise.resolve("testing...");
}

async function test() {
  console.log("test start...");
  const v1 = await testSometing();
  console.log(v1);
  console.log("test end...");
}

test();
console.log("suspend!");

new Promise((resolve) => { 	
  resolve("promise");
}).then(val => console.log(val));

/* 输出
test start...
suspend!
promise
testing...
test end...
*/

细心的童鞋已经发现了,让我们再来看下输出队列,'promise'的输出提前了。

async不是会返回Promise对象吗?现在我们只是显式声明呀,为什么顺序会发生变化?

定位问题

我们再简化一下代码:

// 隐式返回Promise
async function testSometing() {
	return "testing...";
}
testSometing()

/* 输出
Promise {<resolved>: "testing..."}
*/



// 显式声明Promise
async function testSometing() {
	return Promise.resolve("testing...");
}
testSometing()

/* 输出
Promise {<resolved>: "testing..."}
*/

诶,为什么又一致了?难道是await的原因?

await是一个黑盒,直接打断点调试,发现底层有很多事件循环的源码。

尝试编译,但babel官方async编译插件只支持转为generator。emmm,那yieldawait有什么区别…

看看有没有第三方插件,有两个有bug,第三次输出结果和前两个一样,不符合ES6标准(→_→)

那只能去找些polyfill看看,让我们修改一下代码:

// 隐式返回Promise
function testSometing() {
	return "testing...";
}

_async(function test() {
  console.log("test start...");
  _await(_async(testSometing))(val => {
    console.log(val);
    console.log("test end...")
  })
})

console.log("suspend!")

new Promise((resolve) => { 	
  resolve("promise");
}).then(val => console.log(val));

/* 输出
test start...
suspend!
testing...
test end...
promise
*/

上面我们曾提到了await的执行顺序,但还有些细节需要我们深究:

  • await"中断跳出"实现,只是将函数内下面的代码全部封装到Promise回调中,函数内没有代码执行,自然跳出函数,执行后面代码
  • await是怎么"修饰"返回值为Promise的?await内部通过Promise.then来实现(见下文);
  • 接下来只需要等待Promise转为最终态,执行后面回调即可;多个await就是多个嵌套的回调函数。

我们也看到了上面的输出和之前的隐式输出一致,那显式呢?

// 显式声明Promise
function testSometing() {
	return Promise.resolve("testing...");
}

_async(function test() {
  console.log("test start...");
  _await(_async(testSometing))(val => {
    console.log(val);
    console.log("test end...")
  })
})

console.log("suspend!")

new Promise((resolve) => {
  resolve("promise");
}).then(val => console.log(val));

/* 输出
test start...
suspend!
promise
testing...
test end...
*/

输出结果一致!

但根据代码,显式和隐式的await似乎并无不同,反而是传入async的函数返回值存在差异,看来问题似乎出现在async内部。

polyfill源码分析

我们下面贴下源码,深入分析:

// 接受一个函数参数,根据情况执行,并将返回值封装为一个Promise对象
const _async = (func) => {
  const p = new Promise((resolve, reject) => {
    // 捕获同步错误
    try {
      // 返回值
      const value = func()
      // 检查返回值是否是 对象 | 函数,它们可能是thenable对象
      if (
        (
          (typeof value === 'object' && value !== null) ||
          typeof value === 'function'
        ) &&
        typeof value.then === 'function'
      ) {
        /* 
         * 如果是thenable对象
         * 将其解析封装为Promise对象,并递归调用then函数
         * 最终解析为非thenable值返回
         */
        Promise.resolve(value).then(resolve, reject)
      } else {
        // 如果不是直接将其状态转为最终态
        resolve(value)
      }
    } catch (error) {
      reject(error)
    }
  })
  // 返回Promise对象
  return p
}

/* 
 * 接受一个任意参数,返回一个高阶函数
 * 这个高阶函数接收两个参数,分别代表await后,应该执行的正常回调函数和发生错误的回调函数
 */
const _await = (arg) => (onResolved, onRejected) => {
  /* 
   * 将参数解析为Promise对象,再为其添加回调函数
   * 如果有onRejected回调函数参数
   * 先通过catch解析期间可能产生的错误
   * 再执行onResolved回调函数
   */
  const innerPromise = onRejected ?
    Promise.resolve(arg)
      .catch(onRejected)
      .then(onResolved, onRejected) :
    Promise.resolve(arg)
      .then(onResolved, onRejected)
  return innerPromise
}

实现原理大家可以跟着上面的源码和注释走一遍,注意一些实现细节:

  • 为什么会多次调用Promise.resolve
  • _async是怎么解析thenable对象的

注意Promise的原型实现有多个规范,虽然现在的标准是Promises/A+,但ES6的实现和A+仍有出入。

上面源码中Promise.resolve在两个函数都有出现

对于_asyncPromise.resolvethenable对象递归解析Promise对象,然后通过另一个then将值递归解析非thenable类型,赋值给返回的Promise对象。

对于_awaitPromise.resolve是将传入的参数封装为Promise对象,再为其添加传入的回调函数

我们重点分析_asyncPromise.resolve(value).then(resolve, reject)这一行代码:


首先value是一个thenable对象,即有一个then的函数属性。

我们跟着代码进入Promise.resolve

  • 如果valuePromise就直接返回
  • 如果不是则返回一个新的Promise对象result,生成的过程如下:
    • 如果value不是thenable对象,直接将result的状态转为最终态,并赋值
    • 如果valuethenable对象,则将一个回调函数推入微任务队列
      • 这个回调函数是为了递归解析value,直到获取最终的一个非thenable类型,并赋值
      • 具体执行顺序(按源码行号):
        • L181,调用value自身的then函数,传入resultresolvereject
        • L68,我们按resolve被调用来处理,判断value类型
          • L74,如果是Promise对象,调用Promisethen,传入resultresolvereject
          • L82,如果是其他thenable对象,调用thenable对象的then,传入resultresolvereject
        • 自此进入递归解析过程,直到获取最终的一个非thenable类型
        • L89,赋值,result的状态转为最终态

终于Promise.resolve执行完成,返回一个新的Promise对象。

但这个对象的值不一定是非thenable类型,因为Promise.resolve没有对value是Promise做解析

而这个解析过程通过再次调用then来完成,解析完成后,赋值_async的返回值对象中。

综上所述,Promise.resolve的作用就是可以将所有参数类型封装为Promise;在遇到thenable对象(非Promise)时会调用resolve做递归处理,直到解析到一个非thenable类型

then(resolve, reject)的作用有两个,一方面调用resolve做递归处理,另一方面将解析到的非thenable类型通过resolve赋值

解决问题

终于,我们明白了_async的实现原理,下面我们再分析之前的问题就很简单了,分析下显式调用流程:

  • _async修饰test函数:
    • 输出'test start...'
    • 调用_await
      • _async修饰testSometing,得到valuePromise {<resolved>: "testing..."}
      • valuethenable对象,Promise.resolve封装会直接返回
      • value.then(resolve, reject)会被放到微任务队列[1]
      • 返回值Promise {<pending>: undefined}
      • Promise.resolve封装会直接返回,再将then传入的回调函数放入新Promise的队列中
    • 无返回值
  • 输出'suspend!'
  • 实例化Promise,状态转为最终态,并赋值,then回调放入微任务队列[1, 2]
  • 清空微任务队列,执行1会把新Promise的状态转为最终态,并将回调放入微任务队列[2, 3]
  • 继续清空微任务队列,输出'promise''testing...''test end...'

而隐式调用流程中因为value非thenable类型,所以会直接返回给_await,从而将_await的回调提前放入微任务队列。

其他思路

其实有一个错误不知道大家有没有发现,在我们第一次定位问题的时候,我们简化了代码,只输出async的两种情况,但结果却似乎完全一致。

其实这是因为我调试失误的原因( ̄ε(# ̄),我没有打断点,而是直接在调试台查看最终输出。最终异步队列全部清空,结果肯定是一致的。

然后我们认为是await的原因,所以直接抛弃了babel转译,其实将async编译为generator也可以解决这个问题( ̄▽ ̄)",编译代码如下:

// 让我们再次简化一下源代码:
async function testSometing() {
	return Promise.resolve("testing...");
}

// 编译后:
let testSometing = (() => {
  var _ref = _asyncToGenerator(function* () {
    return Promise.resolve("testing...");
  });

  return function testSometing() {
    return _ref.apply(this, arguments);
  };
})();

function _asyncToGenerator(fn) {
  return function () {
  	// fn执行完后,生成gen指针引用
    var gen = fn.apply(this, arguments);
    return new Promise(function (resolve, reject) {
      function step(key, arg) {
        try {
          /*
           * 调用next方法后
           * done变为true
           * value即返回的Promise {<resolved>: "testing..."}
           */
          var info = gen[key](arg);
          var value = info.value;
        } catch (error) {
          reject(error);
          return;
        }
        // 进入if,执行传入的resolve
        if (info.done) {
          /*
           * 进入resolve源码
           * 由于value是Promise,所以会将value.then推入微任务队列
           * 然后去执行其他代码,比如实例化一个Promise
           * 然后清空微任务队列,执行value.then
           * 此时才会执行resolve的赋值语句,将其他回调函数推入微任务队列
           */
          resolve(value);
        } else {
          return Promise.resolve(value).then(function (value) {
            step("next", value);
          }, function (err) {
            step("throw", err);
          });
        }
      }
      return step("next");
    });
  };
}