一个事件循环引发的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
*/
微调 – 声明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
*/
async和await的语法糖使得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,那yield和await有什么区别…
看看有没有第三方插件,有两个有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在两个函数都有出现对于
_async,Promise.resolve将thenable对象递归解析为Promise对象,然后通过另一个then将值递归解析为非thenable类型,赋值给返回的Promise对象。对于
_await,Promise.resolve是将传入的参数封装为Promise对象,再为其添加传入的回调函数我们重点分析
_async的Promise.resolve(value).then(resolve, reject)这一行代码:首先
value是一个thenable对象,即有一个then的函数属性。我们跟着代码进入Promise.resolve:
- 如果
value是Promise就直接返回- 如果不是则返回一个新的
Promise对象result,生成的过程如下:
- 如果
value不是thenable对象,直接将result的状态转为最终态,并赋值- 如果
value是thenable对象,则将一个回调函数推入微任务队列
- 这个回调函数是为了
递归解析value,直到获取最终的一个非thenable类型,并赋值- 具体执行顺序(按源码行号):
- L181,调用
value自身的then函数,传入result的resolve、reject- L68,我们按
resolve被调用来处理,判断value类型
- L74,如果是
Promise对象,调用Promise的then,传入result的resolve、reject- L82,如果是其他
thenable对象,调用thenable对象的then,传入result的resolve、reject- 自此进入
递归解析过程,直到获取最终的一个非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,得到value为Promise {<resolved>: "testing..."}value是thenable对象,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");
});
};
}