Skip to content

关于async await的执行顺序

Posted on:September 12, 2020

首先给出如下代码:

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

async1();

new Promise(resolve => {
  console.log(1);
  resolve();
})
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(4);
  });

其输出顺序为:async1 start, async2, 1, 2, 3, async1 end, 4

问题主要涉及以下 4 点:

  1. Promise 的链式 then() 是怎样执行的
  2. async 函数的返回值
  3. await 做了什么
  4. PromiseResolveThenableJob:浏览器对 new Promise(resolve => resolve(thenable)) 的处理

Promise 的链式 then 调用是怎样执行的

new Promise(r => {
  r();
})
  .then(() => console.log(1))
  .then(() => console.log(2))
  .then(() => console.log(3));

new Promise(r => {
  r();
})
  .then(() => console.log(4))
  .then(() => console.log(5))
  .then(() => console.log(6));

/*
  输出:
  1
  4
  2
  5
  3
  6
  */

总结一下就是:

  1. Promise.prototype.then()  会隐式返回一个新 Promise
  2. 如果 Promise 的状态是 pending,那么  then  会在该 Promise 上注册一个回调,当其状态发生变化时(resolve 或 reject 函数执行时),对应的回调将作为一个微任务被推入微任务队列
  3. 如果 Promise 的状态已经是 fulfilled 或 rejected,那么  then()  会立即创建一个微任务,将传入的对应的回调推入微任务队列

上面的例子中,第一个 then 回调函数是在 pending 状态注册的,但是是在 r()函数执行之后才被推入到微任务队列里的,而第二个 then 函数是在第一个 then 函数执行之后,此时返回的新的 Promise 以及变为 fulfilled 状态了,就会立即创建一个微任务

async/await 可视为 Promise 的语法糖,同样基于微任务实现;本题主要纠结的点在于 await 到底做了什么导致 async1 end 晚于 promise2 输出。问题的关键在于其执行过程中的微任务数量

解释 async 关键字做了什么:

async2 等价转换:

function async2() {
  console.log("async2");
  return Promise.resolve();
}

对于 await v:

将 async1 等价转换:

function async1() {
  console.log("async1 start");
  return new Promise(resolve => resolve(async2())).then(() => {
    console.log("async1 end");
  });
}

return new Promise(resolve => resolve(async2())) 中,Promise resolve 的是 async2(),而 async2() 返回了一个状态为 <resolved>: undefined 的 Promise,Promise 是一个 thenable 对象。

对于 PromiseResolveThenableJob 对象,MDN 上是这样解释的:

let thenable = {
  then(resolve, reject) {
    //注册微任务in thenable
    console.log("in thenable");
    resolve(100); //执行的时候注册微任务thenable ok并将其推入微任务队列
  },
};

new Promise(resolve => {
  console.log("in p0"); // 宏任务1
  resolve(thenable);
}).then(() => {
  console.log("thenable ok");
});

new Promise(resolve => {
  console.log("in p1"); // 宏任务2
  resolve(); //注册微任务1
})
  .then(() => {
    console.log("1");
  })
  .then(() => {
    console.log("2");
  })
  .then(() => {
    console.log("3");
  })
  .then(() => {
    console.log("4");
  });

/*
in p0
in p1
in thenable
1
thenable ok
2
3
4
*/

总结一下就是:

thenable 对象里的 then 函数会被当成微任务,导致原来的 Promise 后的 then 回调被延迟推入到微任务队列中(示例中是在 thenable 函数执行 resolve 后,”thenable ok”才被放入微任务里),所以当第一次微任务队列被清空后(输出 1),此时才输出“thenabel ok”

如果在 Promise 中 resolve 一个 Promise 实例呢?

由于 Promise 实例是一个对象,其原型上有 then 方法,所以这也是一个 thenable 对象。同样的,浏览器会创建一个 PromiseResolveThenableJob 去处理这个 Promise 实例,这是一个微任务。在 PromiseResolveThenableJob 执行中,执行了 Promise.prototype.then,而这时 Promise 如果已经是 resolved 状态 ,then 的执行会再一次创建了一个微任务。

最终结果就是:额外创建了两个 Job,表现上就是后续代码被推迟了 2 个时序

为什么这里被延迟了 2 个时序而不是 1 个呢?原因是:1. 这个 resolve 的 Promise 实例会创建一个 thenable 对象,这是一个微任务。2. 在这个 thenable 执行过程中,这个 Promise 实例会调用 then 方法并且已经是 resolved 状态(以保证后续的外层的 Promise 的 then 方法能够正常执行下去),这又会创建一个微任务。综上所以是延迟了 2 个时序

async 函数处理返回值的问题,它会像  Promise.prototype.then 一样,会对返回值的类型进行辨识。

结论

async 函数在抛出返回值时,会根据返回值类型开启不同数目的微任务

await 右值类型区别:

new Promise(resolve => {
  resolve(thenable);
});

//转换:
new Promise(resolve => {
  Promise.resolve().then(() => {
    thenable.then(resolve);
  });
});

//new Promise(resolve => resolve(async2())):
new Promise(resolve => {
  Promise.resolve().then(() => {
    async2().then(resolve);
  });
});

//最终转换:
function async1() {
  console.log("async1 start");
  const p = async2(); //返回一个Promise.resolve()
  return new Promise(resolve => {
    Promise.resolve().then(() => {
      //注册了一个微任务
      p.then(resolve); //又注册了一个微任务
    });
  }).then(() => {
    console.log("async1 end"); //再注册了一个微任务(前面注册的两个微任务导致这个微任务延后2个时序输出)
  });
}

function async2() {
  console.log("async2");
  return Promise.resolve();
}

async1();

new Promise(resolve => {
  //宏任务
  console.log(1);
  resolve();
})
  .then(() => {
    //注册微任务consoel.log(2)
    console.log(2);
  })
  .then(() => {
    //注册微任务consoel.log(3)
    console.log(3);
  })
  .then(() => {
    ////注册微任务consoel.log(4)
    console.log(4);
  });

/*
async1 start
async2
1
2
3
async1 end
4
*/

补充

  1. Promise.resolve(v) 不等于 new Promise(r => r(v)),因为如果 v 是一个 Promise 对象,前者会直接返回 v,而后者需要经过一系列的处理(主要是 PromiseResolveThenableJob)

  2. 宏任务的优先级是高于微任务的,而原题中的 setTimeout 所创建的宏任务可视为第二个宏任务,第一个宏任务是这段程序本身