Promise,async,await以及微任务的理解

120 阅读5分钟

Promise ,async ,await

在稀土掘金上看到一篇关于async,await,promise帖子.里面讲async,await以及Promise,一反我之前对事件循环的理解,文章中讲述这些会等待不同的时间周期.(我记得事件循环不是微任务FIFO嘛).于是进行了验证.也提出了一些自圆其说的假设.

文章也会按照帖子的顺序逐个解读,使用的环境是

  1. Tyepscript Playerground
  2. Chrome 自带的 console

主要集中在 async,await 以及 Promise形成的微任务中

前言

1

原帖中让尝试以下代码,试试就逝世!

async function async1 () {
    await new Promise((resolve, reject) => {
        resolve()
    })
    console.log('A')
}

async1()

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})
// B A C D

这个和async 函数里的Promise没有关系,和之前我的理解一致

  1. 首先执行async1函数,由于async1函数里有await,之后的代码相当于注册一个微任务.(B)
  2. 之后的Promise首先执行同步代码,之后注册两个微任务
  3. 宏任务结束,微任务队列要清空,且按照注册顺序执行(A C D)

2

async function async1 () {
    await async2()
    console.log('A')
}

async function async2 () {
    return new Promise((resolve, reject) => {
        resolve()
    })
}

async1()

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})
//  B C D A

这里我们先按下不表

async

1

async function testA () {
    return 1;
}

testA().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));
    
//  1 2 3

我们知道,对于一个async函数,会将他的返回值包裹成有then函数的Promise对象,而then是一个注册微任务的函数,所以这里依次注册 1,2,3 ,打印也是按照这个顺序.

2

async function testB () {
    return {
        then (cb) {
            cb();
        }
    };
}

testB().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));
// 2 1 3

就是这里,给我看蒙了,这返回的是啥一个自定义的thenable对象,于是我测试了一下

async function testB() {
    return {
        then(cb: any) {
            cb()
            console.log("4")
        }
    };
}

testB().then(() => console.log(1))

Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));
// 4 2 1 3

(这里typescript会显示报错,说 async不应该返回一个带有then函数的对象)

按照我开始的肤浅理解,这里的cb应该指的是我传的函数,那么应该是4和1同时出现.于是我仔细观察了一下这个cb函数.

async function testB() {
    return {
        then(cb: any) {
        	console.log(cb)       
        }
    };
}
testB().then(()=>{console.log(1)})
// function () { [native code] } 

您瞧怎么找,它打印了引擎中自己带的代码,这说明他执行的根本不是我们给他的,给他的应该这么打印

function testB() {
    return {
        then(cb: any) {
        	console.log(cb)       
        }
    };
}
testB().then(()=>{console.log(1)})
// () => { console.log(1); } 

(这个时候typescript也不报错了)

那么这个thencb函数到底是啥,我倾向于是注册微任务的函数,结构类似于下面

async function testB() {
    return {
        then(cb: any) {
            Promise.resolve().then(()=>{console.log("should be 1 position")})
            // cb()
            console.log(4)
        }
    };
}

testB().then(() => console.log(1))

Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));

// 4 2 "should be 1 position" 3

执行的时候发现你返回的对象有then函数,那么就把自己的then函数作为你的参数,让你去调用它.

那么上述的解释就是

  1. 我注册了一个函数(()=> console.log(1)),但是被发现里面有then方法,被改造了,执行的时候被替换成了下面的(下面是我认为的处理),它注册了一个注册微任务的微任务

    // 发现返回的有 then() ,假设这个函数叫 customThen()
    
    Promise.prototype.then=function(cb)=>{
    	const callback=function(){
    		Promise.prototype.then.bind(this)(cb)
    	}
    	customThen(callback)
    }
    
  2. 注册打印2的微任务.

  3. 宏任务执行结束,开始清空微任务

  4. 执行注册微任务的微任务,即注册打印1的微任务

  5. 打印2

  6. 上面打印2的微任务的任务结束,注册打印3的微任务

  7. 打印1

  8. 打印3

3

async function testC () {
    return new Promise((resolve, reject) => {
        resolve()
    })
}

testC().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));
    
//  2 3 1

其实async本身就会包装成一个Promise,所以我尝试了将async去掉

function testC () {
    return new Promise<void>((resolve, reject) => {
        resolve()
    })
}

testC().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));

// 1 2 3 

这里就和平常的一样了

那么为什么两者不一样呢,我猜测可能是Promise包装了多个,导致了跟上面customThenable一样的问题.而我记得对于Promise多个嵌套时,源码里有处理,直接获得最里面的非Promise元素.

所以第一个例子可以理解为

const promiseA = new Promise((resolve, reject) => {
    resolve(new Promise((resolve, reject) => {
        resolve(4)
    }))
})

promiseA.then(res => { console.log(1);console.log(res) })

Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));
// 2 3 1 4

Promise是嵌套注册的,但是最终结果是穿透的

await

这里补充一个我之前学习到的await的处理思路

async function test1(){
	console.log(1)
	await async2()
	console.log(2)
}
test1()

可以理解成

function test1(){
	console.log(1)
	new Promise((resolve,reject)=>{
        async2()
        resolve()
    }).then(()=>{
		console.log(2)
	})
}

test

下面来试试

async function async1 () {
    console.log('1')
    await async2()
    console.log('AAA')
}

async function async2 () {
    console.log('3')
    return new Promise((resolve, reject) => {
        resolve()
        console.log('4')
    })
}

console.log('5')

setTimeout(() => {
    console.log('6')
}, 0);

async1()

new Promise((resolve) => {
    console.log('7')
    resolve()
}).then(() => {
    console.log('8')
}).then(() => {
    console.log('9')
}).then(() => {
    console.log('10')
})
console.log('11')

// 5 1 3 4 7 11 8 9 AAA 10 6
  1. 先打印5

  2. 注册宏任务(打印6)

  3. 执行async1函数

  4. 打印1

  5. 执行async2函数

  6. 打印3

  7. 执行 async2Promise的同步代码,打印4

  8. 这里既是async 又返回Promise

  9. async1await函数后半变成注册微任务的代码,这里由于async2是一个async函数,且返回Promise,所以相当于

    async function async1 () {
        console.log('1')
        async2.then(()=>{
    		console.log("AAA")
    	})
    }
    
    function async2() {
        return new Promise<void>((resolve, reject) => {
            console.log(3)
            resolve(new Promise((resolve, reject) => {
                resolve()
                console.log(4)
            }))
        })
    }
    // 尝试后,输出结果一致
    
  10. 执行Promise里面的同步代码,打印7,并且注册微任务(打印8)

  11. 打印11,宏任务结束

  12. 剩下的就是微任务队列中的两个任务分别执行后又注册新的微任务,只不过async2注册的微任务一直没有输出,直到最后一个微任务打印"AAA"

  13. 所以打印 8 9 AAA 10

  14. 所有微任务执行结束,执行宏任务打印6

补充

如果想让"AAA"在10后面打印,也可以再加一层

async function async1 () {
    console.log('1')
    await async2()
    console.log('AAA')
}

async function async2 () {
    console.log('3')
    return new Promise((resolve, reject) => {
        resolve(new Promise((resolve,reject)=>{
        	resolve()
        }))
        console.log('4')
    })
}

console.log('5')

setTimeout(() => {
    console.log('6')
}, 0);

async1()

new Promise((resolve) => {
    console.log('7')
    resolve()
}).then(() => {
    console.log('8')
}).then(() => {
    console.log('9')
}).then(() => {
    console.log('10')
})
console.log('11')

// 5 1 3 4 7 11 8 9 10 AAA 6

总结

平时没事别瞎给async函数返回带then方法的对象,其他都还可以理解(微任务排队罢了)

所以原帖子说的也对,可看作是已经提炼出了简单直接的做法(毕竟一个async,一个Promise,最多出2层)

以上只是我粗浅的理解,可能会有偏差.(叠甲) (平时感觉能用到这知识的机会也不多..)