Promise 异步操作的九阳神功
Promise 异步操作的九阳神功
1. Promise中的值穿透
假如有下面这样的代码,结果会打印出什么呢?
Promise.resolve(1).then(2).then(console.log)答案是1,这是因为如果then的参数不是一个函数,就会把上一层传入的值直接传递给下一层 (类似直接 return this),这就是值穿透现象。
通过具体的代码实现,可以比较容易地理解:
successCallback =
typeof successCallback === 'function'
? successCallback
: (value) => value // 如果callback不是函数,则构造一个函数返回传入的值
failCallback =
typeof failCallback === 'function'
? failCallback
: (reason) => reason // 如果callback不是函数,则构造一个函数返回传入的值2. await的执行顺序
由于Node.js和Chrome浏览器环境的细微差异,异步运行结果可能会有些许不同。
2.1 理解await
在异步编程里面常见的知识点有两个,一个是微任务和宏任务的异步执行顺序,另一个就是以Promise为主的异步理解。其中个人觉得最容易被错误理解的不是Promise本身,而是await这个语法糖。众所周知,await本质上还是生成器generator,因此可以让函数在内部暂停。
async function fn1() {
console.log('fn1 start')
}
let r = fn1() // fn1 start
console.log(r) // Promise { undefined }如果没有await,在函数内部即使有异步操作也不会以异步的方式执行,await就好比generator中的yield关键字,我们下面用一个完整的例子来具体分析await。
async function fn1() {
console.log('fn1 start')
fn2()
console.log('fn1 end')
}
async function fn2() {
console.log('fn2 start')
await fn3()
console.log('fn2 end')
}
async function fn3() {
console.log('fn3 start')
return 1
}
fn1()
Promise.resolve('Promise').then((value) => console.log(value))
console.log('finally')当fn1当中不使用await时,执行顺序为:
fn1 start
fn2 start
fn3 start
fn1 end
finally
fn2 end
Promise我们可以注意到fn1 end和fn2 end的输出区别,在fn2中由于使用了await,后面的log语句进入了异步队列,因此等到主程序打印finally之后才打印fn2 end。
而fn1 end是直接打印的,这正是因为我们没有在fn1中使用await,所以fn2()后面的log是以同步的方式运行的。理解了这一点之后,我们可以很容易发现await其实就是Promise.resolve,两者在语义上是一致的,任何在await之后的代码都会被放到一个then里面执行。
值得注意的是,关于await具体的实现V8引擎也几经更改,可参考这个知乎回答
async function fn1() {
console.log('fn1 start')
await fn2()
console.log('fn1 end')
}
// 等价于(注意仅仅是语义上等价,await并不是用Promise.resolve来实现的!)
async function fn1() {
console.log('fn1 start')
Promise.resolve(fn2())
.then(console.log('fn1 end'))
}当我们把await和Promise.resolve联系起来之后,就会很容易理解一个面试题的常考点,就是await同一行的函数会被立即执行,之后的代码才会被放到then里面,所以fn2 start和fn3 start都是马上就打印出来了。
如果我们在fn2()前面加个await的话,打印顺序就不一样了:
fn1 start
fn2 start
fn3 start
finally
fn2 end
Promise
fn1 end可以发现fn1 end现在最后才会打印,甚至晚于fn2 end。这是也因为await同一行的函数会被立即执行,就好比Promise.resolve(fn2())里的fn2是马上执行的,其返回值就是resolve出去的对象一样。
当执行fn2时,fn2内部也有一个await,所以fn3被马上执行,而后面的打印函数被放入了then里面,所以这里fn2中先产生了Promise,率先进入异步队列。而当执行栈再次回到fn1之后,才把fn1后面的log放入then,要晚于fn2。
最后为什么fn1 end要晚于Promise打印?因为fn2由于内部有await还没有执行完,主线程回到fn1的时候await fn2()产生的Promise还没能resolve,所以并不会把后面的代码放入异步队列(具体会在第4节的Promise源码部分),然后主线程继续执行后面的同步代码,于是Promise的打打印先进入异步队列。
2.2 await和Promise.resolve的区别
上一节我们提到了await和Promise.resolve在语义上的相似之处,但是也强调了两者并不完全等同,最重要的区别是由于await本质上是生成器,所以可以真正做到暂停函数,而Promise.resolve则没有这个能力。
来看一个例子:
async function fn1 () {
console.log('fn1 start')
Promise.resolve(fn2()).then((value) => {
console.log(value)
console.log('fn1 end')
})
}
async function fn2 () {
console.log('fn2 start')
Promise.resolve(fn3()).then(() => {
console.log('fn2 end')
return 2
})
}
async function fn3 () {
console.log('fn3 start')
}
fn1()
Promise.resolve('Promise').then((value) => console.log(value))
console.log('finally')这个例子和2.1中的例题基本一样,区别在于我们不使用await而是用Promise.resolve代替,来看看结果:
fn1 start
fn2 start
fn3 start
finally
fn2 end
undefined
fn1 end
Promise这里可以发现顺序和2.1有所出入
- 首先fn1 end先于Promise打印,也就是说其实fn1并没有真正等待fn2执行完毕就已经把后面的then放入异步队列了。
- fn1中打印的fn2的返回值是
undefined,从侧面佐证了fn1没有真正等待fn2,否则返回值应该是2。
所以可以发现Promise.resolve并没有暂停代码执行的能力,只不过是把返回放到了异步队列里,对于接受返回值的调用者来说返回值仍然是同步代码决定的,而因为同步代码部分没有定义返回值,所以默认返回undefined,fn2就是这样的一个例子。
所以对于fn1中的Promise.resolve(fn2())而言,fn2的返回值仅仅取决于它的同步代码,Promise.resolve不会去等待fn2中的异步任务。
我们再把fn2用await改写一下,结果就会迥然不同:
async function fn2 () {
console.log('fn2 start')
// Promise.resolve(fn3()).then(() => {
// console.log('fn2 end')
// return 2
// })
await fn3()
return 2
}现在的结果是:
fn1 start
fn2 start
fn3 start
finally
Promise
2
fn1 endawait背后的生成器可以真正做到暂停代码。
我们知道async函数返回的是一个Promise,所以Promise.resolve(fn2())里面fn2本身返回了一个Promise,当这个Promise状态是fulfilled了才会把后续的then放入异步队列,而这时由于fn2中使用了await,fn2在运行完之前这个返回的Promise一直是pending状态,所以Promise.resolve(fn2())后面的then无法马上进入异步队列,只能等待fn2继续执行完await之后的所有代码。
这也是await强大的地方,因为它从真正意义上做到了等待异步执行,而不是仅仅把后续代码放入异步队列而已!
最后值得一提的是,Promise.resolve有一个特性,如果resolve的值本来就是一个Promise,就会直接返回这个Promise而不是新生成一个,不过具体的细节当然没这么简单,我们下面会进一步详细讨论。
3. 深入理解then
下面我们来看更复杂一些的例子
new Promise((resolve, reject) => {
console.log('外部promise')
resolve()
})
.then(() => {
console.log('外部第一个then')
new Promise((resolve, reject) => {
console.log('内部promise')
resolve()
})
.then(() => {
console.log('内部第一个then')
return Promise.resolve()
})
.then(() => {
console.log('内部第二个then')
})
})
.then(() => {
console.log('外部第二个then')
})
.then(() => {
console.log('外部第三个then')
})
.then(() => {
console.log('外部第四个then')
})
console.log('外部结束')我们知道then方法里面的回调函数会被放入微任务队列,当主线任务执行完毕后下一次事件轮询才会执行,大多数人在没有真正从源码层面理解的情况下,对then的认知就止步于此了,于是上面这道题目马上就会掉坑。
这道题目的正确答案(以chrome浏览器运行环境为准)
外部promise
外部结束
外部第一个then
内部promise
内部第一个then
外部第二个then
外部第三个then
外部第四个then
内部第二个then诡异的地方来了,内层的第二个then居然是最后才执行的!
我们来分析一下代码的运行过程:
第一个
Promise内部打印外部promise并直接调用resolve()(这里要注意的一点就是Promise构造时传入的函数是同步执行的),状态变成fulfilled,外层的第一个then被放入微任务队列,外层的其余then都要等待第一个then完成后才会被执行,所以不用考虑。然后主任务直接执行到剩下的最后一行,打印外部结束。- 此时微任务队列
[外部第一个then]
- 此时微任务队列
由于主任务没有代码要执行了,微任务队列的第一个任务进栈处理,外部第一个
then执行,内部又声明了一个Promise,并执行里面的函数,打印内部promise,然后这个Promise也直接被resolve了,内部的第一个then进入微任务队列,内部第二个then要等待内部第一个then执行结束,暂时不会进入微任务队列。此时外部第一个
then的同步代码部分就执行结束了,由于它没有定义返回值,相当于就是返回了一个undefined,这个值就会被作为这个Promise的返回值被resolve(这个部分在下面的源码部分我们详细来看),于是外部第二个then也被放入微任务队列。- 此时微任务队列
[内部第一个then,外部第二个then]
- 此时微任务队列
主任务清空,开始执行微任务队列,先取出内部第一个
then处理,它的回调函数直接返回了一个Promise.resolve(),也就是一个处于fulfilled状态的Promise。我们知道then本身返回的就是一个Promise,而then内部函数return的值就是resolve出去的结果,于是乎等于then要resolve的值也是一个Promise。接下来就是真正考察功力的时候了,我们需要思考的问题是:
Promise在resolve一个普通值和一个Promise的时候,有什么差别呢?这里面的差别非常大:
如果resolve的是普通值,直接就会注册后面的
then方法,也就是把then当中的回调函数放入微任务队列。而当resolve的对象也是一个
Promise的时候,并不会直接返回这个Promise,而是会返回一个ECMA-262标准里规定的NewPromiseResolveThenableJob微任务(也是一个Promise)。这个任务内部会去执行要返回的Promise的then方法,同时这个Job本身会被放入到微任务队列里。
这里的细节一下子多了很多,我们分别列举出来一个个看:
I. 如果
Promise1要返回一个Promise2(也可以理解成Promise2会resolvePromise1,即把Promise1的状态变成fulfilled),会生成一个NewPromiseResolveThenableJob放入微任务队列。II. 这个Job里会调用
Promise2.then(resolve, reject),要注意这里传入的resolve和reject是Promise1自己的!它们处理的是Promise1,也就是说Promise2.then()里面的执行结果会决定Promise1的状态。III. 等到这个Job被执行了,
Promise2.then(resolve, reject)被放入微任务。IV. 等到
Promise2.then(resolve, reject)的回调被执行了,Promise1终于状态改变,它自己的then才能开始被处理。V. 上面一共经过了3次事件轮询!所以当
then需要resolve一个Promise2的时候,后面的链式then并不会马上被放入微任务,第一次放入的是NewPromiseResolveThenableJob,第二次放入的是Promise2.then(),第三次才是链式的then。回到3我们刚刚讨论到的地方:
- 此时微任务队列
[外部第二个then,NewPromiseResolveThenableJob]
执行外部第二个
then,打印外部第二个then。这个then没有返回值也没有异步代码,所以直接返回undefined,和2里面我们讨论过的一样,外部第三个then顺利被放入微任务队列。- 此时微任务队列
[NewPromiseResolveThenableJob,外部第三个then]
- 此时微任务队列
执行
NewPromiseResolveThenableJob,把Promise.resolve().then()放入微任务队列。- 此时微任务队列
[外部第三个then,Promise.resolve().then()]
- 此时微任务队列
执行外部第三个
then,打印外部第三个then,把外部第四个then放入微任务队列。- 此时微任务队列
[Promise.resolve().then(resolve),外部第四个then]
- 此时微任务队列
执行
Promise.resolve().then(),没有任何返回值(undefined),但是会把内部第一个then返回的Promise状态变为fulfilled,相当于内部第一个then最后返回了undefined,这时把内部第二个then放入微任务队列。- 此时微任务队列
[外部第四个then,内部第二then]
- 此时微任务队列
后面就很好理解了,依次执行剩下的两个
then,分别打印外部第四个then和内部第二个then。
4. Promise源码理解
我们来看下模拟部分Promise源码的简单实现
then (successCallback, failCallback) {
// 判断传入的回调是不是函数
successCallback =
typeof successCallback === 'function' ? successCallback : (value) => value
failCallback =
typeof failCallback === 'function' ? failCallback : (reason) => reason
let promiseReturned = new MyPromise((resolve, reject) => {
// 判断状态,如果已经变化了就立即调用回调函数
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = successCallback(this.value)
// 检查x是否是也是一个MyPromise对象
resolvePromise(promiseReturned, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
} else if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = failCallback(this.reason)
// 检查x是否是也是一个MyPromise对象
resolvePromise(promiseReturned, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
} else {
// 等待,状态还没有变化
// 将成功和失败的回调函数储存起来
this.successCallback.push(() => {
setTimeout(() => {
try {
let x = successCallback(this.value)
// 检查x是否是也是一个MyPromise对象
resolvePromise(promiseReturned, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.failCallback.push(() => {
setTimeout(() => {
try {
let x = failCallback(this.reason)
// 检查x是否是也是一个MyPromise对象
resolvePromise(promiseReturned, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
})
return promiseReturned
}从上面的代码中可以明显的看到几个特点:
一是果然所有的then都会在内部声明并返回一个Promise: promiseReturned
二是then是如何把回调函数放入异步队列的(代码里使用了setTimeout来模拟),包括如果当前Promise还处于pending状态时,会把回调函数包裹在一个异步任务里再放入数组中保存。
三是回调函数的返回值就是Promise要resolve的值,也就是x,但是具体怎么resolve或者reject还要经过一个函数resolvePromise。
首先我们先看看resolve函数是怎么工作的(要注意这里的resolve方法不是Promise.resolve!后者是一个独立的静态方法,这里没有讨论):
resolve = (value) => {
if (this.status != PENDING) return
this.status = FULFILLED
this.value = value
// 判断成功回调是否存在,如果存在就调用
// this.successCallback && this.successCallback(this.value)
// 现在回调函数是一个数组
while (this.successCallback.length) {
let callback = this.successCallback.shift()
callback()
}
}当调用resolve的时候,不仅会改变Promise的状态,同时也会把数组中保存的回调任务按顺序全部执行了,reject函数的原理完全一样。
最后我们再来看看resolvePromise和resolve有什么不同:
function resolvePromise (promiseReturned, x, resolve, reject) {
//! 不允许返回自身 会造成无限then嵌套
if (promiseReturned === x) {
reject(new TypeError('Chaining cycle detected for promise'))
}
if (x instanceof MyPromise) {
// 是一个 MyPromise对象
// 这里模拟了ECMA标准里的NewPromiseResolveThenableJob(promiseToResolve, thenable, then)
setTimeout(() => {
x.then(resolve, reject)
}, 0)
} else {
// 普通值
resolve(x)
}
}可以看到,resolvePromise相当于包裹了resolve,只不过处理了返回值的各种情况,而且要注意其参数设计别有用意:
promiseReturned: 当前then要返回的Promise(还记得吗,所有的then默认都返回一个Promise)x:当前then内部回调函数的返回值,用于resolve当前这个promiseReturnedresolve:resolvePromise自己的resolve方法reject:resolvePromise自己的reject方法
当返回值是一个普通值的时候,直接调用resolve;而如果也是一个Promise,就会出现我们之前分析的那种情况,构造一个新的异步任务,在这个里任务里面去调用x的then方法,在这个then方法里再去调用传入的resolve和reject,也就是说x会决定promiseReturned的状态,但不是这一时刻同步决定的,而是下一次异步轮询。
最后还有一点是,由于resolve和reject函数被这么传来传去,所以为了保持它们内部的this指向,必须要通过箭头函数的方法来定义,否则还得各种使用闭包去记录原本的this。
5. 总结
最后我们再来稍微改造一下2里面的例子,把fn3改为return一个Promise:
async function fn1() {
console.log('fn1 start')
await fn2()
console.log('fn1 end')
}
async function fn2() {
console.log('fn2 start')
await fn3()
console.log('fn2 end')
}
async function fn3() {
console.log('fn3 start')
return Promise.resolve()
}
fn1()
Promise.resolve('Promise').then((value) => console.log(value))
console.log('finally')现在运行的结果是什么样的呢?我们现在已经知道了Promise内部如果resolve另一个Promise要花费三个微任务才能真正完成,所以最后的打印结果:
fn1 start
fn2 start
fn3 start
finally
Promise
fn2 end
fn1 end外部的Promise打印会最先进入异步队列,fn2和fn1变得更晚了。
经此一役,相信你对Promise的理解已经远超凡人,不会再有什么题目能轻易难道你了!
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!