背景
期约(Promise)这个名字最早是由Daniel Friedman 和David Wise 在他们于1976年发表的论文“The Impact of Application Programming on Multiprocessing” 中提出来的。但直到十几年后,Barara Liskov 和Liuba Shrira 在1988 年发表了论文“Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems”,这个概念才真正确立下来。同一时期的计算机科学家还使用了“终局(eventual)”、“期许(future)”、“延迟(delay)”、“迟付(deferred)”等术语指代同样的概念。所有这些概念描述的都是一种异步程序执行的机制。
JS 利用事件轮询机制,可以模拟出多线程效果,也就是异步操作,而回调函数(callback)是事件轮询调用的目标方法。但是,通过回调函数处理异步事件有很多不确定性,并且容易陷入“回调地狱(callback hell,或者说毁灭金字塔,pyramid of doom)”。于是Promise 概念被提出,并且很多JavaScript 框架(比如JQuery)支持的异步API 都基于Promise 理念构建的。
Promise 是一个对象,用来传递异步操作的信息,它代表了某个未来时刻才知道结果的事件,并且这个事件提供统一API 接口,它将回调嵌套拆成链式调⽤。
回调函数本身是同步代码。
JavaScript 中的回调函数结构,默认是同步的结构,由于JavaScript 单线程异步模型的规则,如果想要编写异步的代码,必须使⽤回调嵌套的形式才能实现,所以回调函数结构不⼀定是异步代码,但是异步代码⼀定是回调函数结构。
异步流程需要回调函数
function test(fn){
fn()
}
console.log(1)
test(function(){
console.log(2)
})
console.log(3)
// 以上代码属于直接进⼊执⾏栈的程序,会按照正常程序解析的流程输出。
function test(fn){
setTimeout(fn, 0)
}
console.log(1)
test(function(){
console.log(2)
})
console.log(3)
// 1 => 3 => 2 因为在调⽤test 的时候settimeout 将fn 放到了异步任务队列挂起了,等待主程序执⾏完毕之后才会执⾏
如果有⼀个变量a 的值为0,想要1 秒之后设置他的值为1,并且在之后得到a 的新结果,这个逻辑中如果1 秒之后设置a 为1 采⽤的是setTimeout,在同步结构⾥能否实现?
// 尝试用阻塞的方式来获取异步代码
var a = 0
// 依然使⽤setTimeout设置1秒的延迟设置a的值
setTimeout(function(){
a = 1
}, 1000)
var d = new Date().getTime()
var d1 = new Date().getTime()
// 采⽤while 循环配合时间差来阻塞同步代码2秒
while(d1 - d < 2000){
d1 = new Date().getTime()
}
console.log(a)
本例的同步代码会在while 循环中阻塞2 秒,所以console.log(a) 这⾏代码会在2 秒之后才能获得执⾏资源,但是最终输出的结果仍然是0。
由于JS 单线程异步模型的规则:严格的同步在前异步靠后顺序。
本案例的同步代码虽然阻塞了2秒,已经超过了setTimeout 的等待时间, 但是setTimeout 中的宏任务到时间后,仅仅会被移动到任务队列中进⾏等待。在时间到达1 秒时,while 循环没有执⾏结束,所以函数执⾏栈会被继续占⽤,直到循环释放并输出a 之后,任务队列中的宏任务才能执⾏,所以这⾥就算setTimeout 时间到了,也必须等待同步代码执⾏完毕,那么输出a 的时候a=1 的事件仍然没有发⽣, 所以采⽤默认的上下结构永远拿不到异步回调中的结果。
// 只有在这个回调函数中才能获取到a 改造之后的结果
var a = 0
setTimeout(function(){
a = 1
}, 1000)
// 注册⼀个新的宏任务,让他在上⼀个宏任务后执⾏
setTimeout(function(){
console.log(a)
}, 2000)
⼀个Promise 对象包含两部分回调函数,第⼀部分是new Promise 时候传⼊的对象,这段回调函数是同步的;另一部分.then .catch .finally
中的回调函数是异步的。
Promises/A+ 规范
早期的期约机制在jQuery 和Dojo(Dojo is a modern TypeScript framework for building scalable enterprise web applications.) 中是以Deferred API 的形式出现的。
到了2010 年,CommonJS 项目实现的Promises/A 规范日益流行起来。Q 和Bluebird 等第三方JS 期约库也越来越得到社区认可,虽然这些库的实现多少有些不同。
为弥合现有实现之间的差异,2012 年Promises/A+ 组织fork 了CommonJS 的Promises/A 建议,并以相同的名字制定了Promises/A+ 规范。这个规范最终成为了ES6 规范实现的版本。
ES6 增加了对Promises/A+ 规范的完善支持,即Promise 类型。一经推出,Promise 就大受欢迎,成为了主导性的异步编程机制。所有现代浏览器都支持ES6 期约,很多其他浏览器API(如fetch() 和Battery Status API)也以期约为基础。
功能特性
pending/fulfilled/rejected
Promise 本身具备三种状态:
- pending:初始状态,也叫就绪状态,这是在Promise 对象定义初期的状态,这时Promise 仅仅做了初始化并注册了他对象上所有的任务。
- fulfilled:已完成,通常代表成功执⾏了某⼀个任务,当初始化函数中的resolve 执⾏时,Promise 的状态就变更为fulfilled,并且then 函数注册的回调函数会开始执⾏,resolve 中传递的参数会进⼊回调函数作为形参。
- rejected:已拒绝,通常代表执⾏了⼀次失败任务,或者流程中断,当调⽤reject 函数时,catch 注册的回调函数就会触发,并且reject 中传递的内容会变成回调函数的形参。
// 实例化⼀个Promise 对象
var p = new Promise(function(resolve, reject){
// 未决阶段的处理
// 通过调用resolve 函数将Promise 推向已决阶段的resolved 状态
// 通过调用reject 函数将Promise 推向已决阶段的rejected 状态
// resolve 和reject 均可以传递最多一个参数,表示推向状态的数据
// 立即执行
})
p.then(data => {
// 这是thenable 函数
}, err => {
// 这是catchable 函数
})
// 通过链式调⽤控制流程
p.then(function(){
console.log('then执⾏')
}).catch(function(){
console.log('catch执⾏')
}).finally(function(){
console.log('finally执⾏')
})
// 这段程序并没有任何输出
- 未决阶段的处理函数是同步的,会立即执行
- thenable 和catchable 函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,加入的队列是微队列
- p.then 可以只添加thenable 函数,p.catch 可以单独添加catchable 函数
- 在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向rejected,并会被catchable 捕获
- 一旦状态推向了已决阶段,无法再对状态做任何更改
- Promise并没有消除回调,只是让回调变得可控
function MyPromise(){
return this
}
MyPromise.prototype.then = function(){
console.log('触发了then')
return this
}
new MyPromise().then().then().then()
resolve/reject
ES6 规定,Promise
对象是一个构造函数,用来生成Promise
实例。Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
const promise = new Promise(function(resolve, reject) {
if (/*异步操作成功*/) {
resolve(value)
} else {
reject(error)
}
})
构造函数上的静态方法
Promise.resolve()
Promise.resolve()
实际上的结果可能是完成或拒绝。
Promise.resolve('foo') 等价于 => new Promise(resolve => resolve('foo'))
Promise.resolve()
方法的参数分成四种情况:
(1)参数是一个 Promise 实例
如果参数是 Promise 实例,那么Promise.resolve
将不做任何修改、原封不动地返回这个实例。
(2)参数是一个thenable
对象
thenable
对象指的是具有then
方法的对象。
Promise.resolve()
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then()
方法。
const thenable = {
then: function(resolve, reject) {
resolve(42)
}
}
const p1 = Promise.resolve(thenable)
p1.then(function(value) {
console.log(value)
})
thenable
对象的then()
方法执行后,对象p1
的状态就变为fulfilled
,从而立即执行最后那个then()
方法指定的回调函数,输出42。
(3)参数不是具有then()
方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then()
方法的对象,则Promise.resolve()
方法返回一个新的 Promise 对象,状态为fulfilled
。
const p = Promise.resolve('Hello')
p.then(function(s) {
console.log(s)
})
由于字符串Hello
不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是fulfilled
,所以回调函数会立即执行。Promise.resolve()
方法的参数,会同时传给回调函数。
(4)不带有任何参数
Promise.resolve()
方法允许调用时不带参数,直接返回一个fulfilled
状态的 Promise 对象。
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()
方法。
立即resolve()
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
setTimeout(function() {
console.log('three')
}, 0)
Promise.resolve().then(function() {
console.log('two')
})
console.log('one')
// one,立即执行
// two,在本轮“事件循环”结束时执行
// three,setTimeout(fn, 0) 在下一轮“事件循环”开始时执行
实现Promise.resolve
Promise.resolve = function(value) {
if (value instanceof Promise) {
return value
}
return new Promise(resolve => resolve(value))
}
Promise.reject()
Promise.reject(reason)
方法也会返回一个新的 Promise 实例,该实例的状态为rejected
。
// 生成一个Promise 对象的实例p,状态为rejected,回调函数会立即执行。
const p = Promise.reject('出错了')
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function(s) {
console.log(s)
})
// 出错了
Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。
Promise.reject('出错了')
.catch(e => {
console.log(e === '出错了')
})
// true
实现Promise.reject
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason))
}
懒惰的Promise
Promise 新建后就会立即执行。
如果希望以后再执行Promise,可以使用函数。函数是一种耗时的机制。只有当开发者明确地用()
来调用它们时,它们才会执行。让Promise 变得懒惰的最有效方法是将其包裹在一个函数中。
const createMyPromise = () => new Promise(resolve => {
// HTTP request
resolve(result)
})
原型对象上的方法(期约的实例方法)
期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据,处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码。
Promise.prototype.then()
Promise 实例具有then
方法,即then
方法是定义在原型对象Promise.prototype
上的。它的作用是为 Promise 实例添加状态改变时的回调函数。
// then 方法的第一个参数是fulfilled 状态的回调函数,第二个参数是rejected 状态的回调函数,都是可选的。
Promise.prototype.then(resolvedFunc, rejectedFunc)
传给then 的任何非函数类型的参数都会被静默忽略。
如果只提供onRejected 参数,那就要在onResolved 参数的位置上传入undefined。这样有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代。
then
方法返回的是一个新的Promise
实例。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL)
}).then(function(comments) {
console.log('resolved:', comments)
}, function(err) {
console.log('rejected:', err)
})
链式调用的基本形式
- 只要有then()并且触发了resolve,整个链条就会执⾏到结尾,这个过程中的第⼀个回调函数的参数是resolve 传⼊的值
- 后续每个函数都可以使⽤return 返回⼀个结果,如果没有返回结果的话下⼀个then 中回调函数的参数就是 undefined
- 返回结果如果是普通变量,那么这个值就是下⼀个then 中回调函数的参数
- 如果返回的是⼀个Promise 对象,那么这个Promise 对象resolve 的结果会变成下⼀次then 中回调的函数的参 数
- 如果then 中传⼊的不是函数或者未传值,Promise 链条并不会中断then 的链式调⽤,并且在这之前最后⼀次 的返回结果,会直接进⼊离它最近的正确的then 中的回调函数作为参数
Promise.prototype.catch()
Promise.prototype.catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
getJSON('/post/1.json').then(function(post) {
// ...
}).catch(function(error) {
// 处理getJSON 和前一个回调函数运行时发生的错误
console.log('发生错误!', error)
})
getJSON()
方法返回一个 Promise 对象,如果该对象状态变为resolved
,则会调用then()
方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected
,就会调用catch()
方法指定的回调函数,处理这个错误。
then()
方法指定的回调函数,如果运行中抛出错误,也会被catch()
方法捕获。
如果 Promise 状态已经变成resolved
,再抛出错误是无效的。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。错误总是会被下一个catch
语句捕获。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL)
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise 产生的错误
// 一个由getJSON()产生,两个由then()产生。它们之中任何一个抛出的错误,都会被最后一个catch()捕获。
})
一般来说,不要在then() 方法里面定义 Reject 状态的回调函数(即then 的第二个参数),总是使用catch 方法。
跟传统的try/catch
代码块不同的是,如果没有使用catch()
方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x 没有声明
resolve(x + 2)
})
}
someAsyncThing().then(function() {
console.log('everything is great')
})
setTimeout(() => {console.log(123)}, 2000)
someAsyncThing()
函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined
,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123
。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
手写实现catch
/**
* 本质就是then,只是少传了一个onFulfilled
* 所以仅处理失败的场景
* @param {*} onRejected
*/
Promise.prototype.catch = function(onRejected) {
return this.then(null, onRejected)
}
Promise.prototype.finally()
用于给期约添加onFinally 处理程序。这个方法主要用于添加清理代码。
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
finally
方法的回调函数不接受任何参数。
实现Promise.finally
Promise.prototype.finally = function(callback) {
return this.then(
value => Promise.resolve(callback()).then(() => value),
err => Promise.resolve(callback()).then(() => { throw err })
)
}
// 不管前面的 Promise 是fulfilled 还是rejected,都会执行回调函数callback。
finally
方法总是会返回原来的值。
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
非重入期约方法
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的。这个特性由JavaScript 运行时保证,被称为“非重入”(non-reentrancy)特性。
// 创建解决的期约
let p = Promise.resolve()
// 添加解决处理程序
// 直觉上,这个处理程序会等期约一解决就执行
p.then(() => console.log('onResolved handler'))
// 同步输出,证明then() 已经返回
console.log('then() returns')
// 实际的输出:
// then() returns
// onResolved handler
在一个解决期约上调用then() 会把onResolved 处理程序推进消息队列。但这个处理程序在当前线程上的同步代码执行完成前不会执行。因此,跟在then() 后面的同步代码一定先于处理程序执行。
非重入适用于onResolved/onRejected 处理程序、catch() 处理程序和finally() 处理程序。
邻近处理程序的执行顺序
如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行,无论是then()、catch() 还是finally() 添加的处理程序都是如此。
期约连锁与期约合成
期约连锁
每个期约实例的方法都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。
期约图
因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序。
由于期约的处理程序是添加到消息队列,然后才逐个执行,因此构成了层序遍历。
树只是期约图的一种形式。考虑到根节点不一定唯一,且多个期约也可以组合成一个期约,所以有向非循环图是体现期约连锁可能性的最准确表达。
Promise 类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all() 和Promise.race()。而合成后期约的行为取决于内部期约的行为。
Promise.all()
Promise.all() 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3])
该方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果不是,就会先调用Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。空的可迭代对象等价于promise.resolve()。
Promise.all() 方法的参数可以不是数组,但必须具有 Iterator 接口(可迭代对象),且返回的每个成员都是 Promise 实例。
- 只有p1、p2、p3 的状态都变成fulfilled,p 的状态才会变成
fulfilled
,此时p1、p2、p3 的返回值组成一个数组,传递给p 的回调函数。 - 只要p1、p2、p3 之中有一个被rejected,p 的状态就变成
rejected
,此时第一个被reject 的实例的返回值,会传递给p 的回调函数。
如果作为参数的 Promise 实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello')
})
.then(result => result)
.catch(e => e)
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了')
})
.then(result => result)
.catch(e => e)
Promise.all([p1, p2])
.then(result => console.log(result)) // ['hello', Error: 报错了...]
.catch(e => console.log(e))
// 如果p2 没有自己的catch 方法,就会调用Promise.all() 的catch 方法。
实现Promise.all
Promise._all = function(promises) {
return new Promise((resolve, reject) => {
if (promises === null || typeof promises[Symbol.iterator] !== 'function') {
throw new TypeError(`${promises} is not a iterable`)
}
promises = [...promises]
if (promises.length === 0) {
resolve([])
}
let count = 0
const values = []
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((res) => {
values[index] = res
if (++count === promises.length) resolve(values)
})
.catch(reject)
})
})
}
Promise.race()
Promise.race([promise对象,promise对象,...]).then(回调函数)
该静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个方法接收一个可迭代对象,返回一个新期约。
使用场景:
假设有⼀个播放视频的⻚⾯,通常流媒体播放为了保证⽤户可以获得较低的延迟,都会提供多个媒体数据源。我们希望⽤户在进⼊⽹⻚时,优先展示的是这些数据源中针对当前⽤户速度最快的那⼀个,这时便可以使⽤Promise.race()来让多个数据源进⾏竞赛,得到竞赛结果后,将延迟最低的数据源⽤于⽤户播放视频的默认数据源。
// promise.race()相当于将传⼊的所有任务进⾏了⼀个竞争,他们之间最先将状态变成fulfilled 的那⼀个任务就会直接的触发race的.then 函数并且将他的值返回,主要⽤于多个任务之间竞争时使⽤
let p1 = new Promise((resolve,reject) => {
setTimeout(() => {
resolve('第⼀个promise执⾏完毕')
},5000)
})
let p2 = new Promise((resolve,reject) => {
setTimeout(() => {
reject('第⼆个promise执⾏完毕')
},2000)
})
let p3 = new Promise(resolve => {
setTimeout(() => {
resolve('第三个promise执⾏完毕')
},3000)
})
Promise.race([p1,p3,p2]).then(res => {
console.log(res)
}).catch(function(err){
console.error(err)
})
拼多多:实现一个promiseTimeout 方法,该方法接受两个参数:第一个参数为promise,第二个参数为number 类型;该方法的作用:
- 若promise 在第二个参数给定的时间内处于pengding 状态,则返回一个rejected 的promise,其reason 为new Error(‘promise timeout’)
- 若promise 在第二个参数给定的时间内处于非pending 状态,则返回该promise
let timeout = time => new Promise((resolve, reject) => {
setTimeout(reject, time, new Error('promise timeout'))
})
let p = time => new Promise(resolve => {
setTimeout(resolve, time, 'xxxx')
})
let promiseTimeout = (promise, time) => {
return Promise.race([promise, timeout(time)])
}
console.log(promiseTimeout(p(5000), 1000))
实现Promise.race
Promise._race = function(promiseArr) {
if(typeof promiseArr[Symbol.iterator] !== 'function') {
throw(`传入的参数不是一个可迭代对象`)
}
return new Promise((resolve, reject) => {
for (const promise of promiseArr) {
Promise.resolve(promise).then(val => {
resolve(val)
}, err => {
reject(err)
})
}
})
}
Promise.allSettled()
ES2020 引入了Promise.allSettled()
方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做"Settled",包含了"fulfilled"和"rejected"两种情况。
Promise.allSettled()
方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled
还是rejected
),返回的 Promise 对象才会发生状态变更。
const promises = [ fetch('/api-1'), fetch('/api-2'), fetch('/api-3')]
await Promise.allSettled(promises)
removeLoadingIndicator()
数组promises 包含了三个请求,只有等到这三个请求都结束了(不管请求成功还是失败),removeLoadingIndicator 才会执行。
该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled
,不会变成rejected
。状态变成fulfilled
后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。
const resolved = Promise.resolve(42)
const rejected = Promise.reject(-1)
const allSettledPromise = Promise.allSettled([resolved, rejected])
allSettledPromise.then(function(results) {
console.log(results)
})
// [{status: 'fulfiled', value: 42}, {status: 'rejected', reason: -1}]
Promise.allSettled() 的返回值allSettledPromise,状态只可能变成fulfilled。它的回调函数接收到的参数是数组results。该数组的每个成员都是一个对象,对应传入Promise.allSettled() 的数组里面的两个 Promise 对象。
results 的每个成员是一个对象,对象的格式是固定的,对应异步操作的结果。
// 异步操作成功时
{status: 'fulfilled', value: value}
// 异步操作失败时
{status: 'rejected', reason: reason}
// 成员对象的status 属性的值只可能是字符串fulfilled 或字符串rejected,用来区分异步操作是成功还是失败。
// 如果是成功(fulfilled),对象会有value 属性,如果是失败(rejected),会有reason 属性,对应两种状态时前面异步操作的返回值。
实现Promise.allSettled
function allSettled(promise) {
if (promises.length === 0) return Promise.resolve([])
const _promises = promises.map(item => item instanceof Promise ? item : Promise.resolve(item))
return new Promise((resolve, reject) => {
const result = []
let unSettledPromiseCount = _promises.length
_promises.forEach((promise, index) => {
promise.then((value) => {
result[index] = {
status: 'fulfilled',
value
}
unSettledPromiseCount -= 1
// resolve after all are settled
if (unSettledPromiseCount === 0) {
resolve(result)
}
}, (reason) => {
result[index] = {
status: 'rejected',
reason
}
unSettledPromiseCount -= 1
// resolve after all are settled
if (unSettledPromiseCount === 0) {
resolve(result)
}
})
})
})
}
Promise.any()
ES2021 引入了Promise.any()
方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。
只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态。Promise.any()
不会因为某个 Promise 变成rejected
状态而结束,必须等到所有参数 Promise 变成rejected
状态才会结束。
Promise.any([
fetch('https://v8.dev/').then(() => 'home'),
fetch('https://v8.dev/blog').then(() => 'blog'),
fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {
// 只要有一个fetch() 请求成功
console.log(first)
}).catch((error) => {
// 所有三个fetch() 全部请求失败
console.log(error)
})
Promise.any()
抛出的错误,不是一个一般的 Error 错误对象,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被rejected
的操作所抛出的错误。
const resolved = Promise.resolve(42)
const rejected = Promise.reject(-1)
const alsoRejected = Promise.reject(Infinity)
Promise.any([resolved, rejected, alsoRejected]).then(function(result) {
console.log(result) // 42
})
Promise.any([rejected, alsoRejected]).catch(function(results) {
console.log(results) // [-1, Infinity]
})
实现Promise.any
Promise.any = function(promiseArr) {
let index = 0
return new Promise((resolve, reject) => {
if (promiseArr.length === 0) return
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
index++
if (index === promiseArr.length) {
reject(new AggregateError('All promises were rejected'))
}
})
})
})
}
期约扩展
ES6 期约实现是很可靠的,但它也有不足之处。如,很多第三方期约库实现中具备而ES 规范却未涉及的两个特性:期约取消和进度追踪。
期约取消
某些第三方库如Bluebird,就提供了这个特性。TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。
实际上,可以在现有实现基础上提供一种临时性的封装,以实现取消期约的功能。可以用到Kevin Smith 提到的“取消令牌(cancel token)”。生成的令牌实例提供了一个借口,利用这个借口可以取消期约;同时也提供了一个期约的实例,可以用来触发取消后的操作并求值取消状态。
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(resolve)
})
}
}
// 这个累包装了一个期约,把解决方法暴漏给了cancelFn 参数。这样,外部代码就可以向构造函数中传入一个函数,从而控制什么情况下可以取消期约。这里期约是令牌类的公共成员,因此可以给它添加处理程序以取消期约。
<button id='start'>Start</button>
<button id='cancel'>Cancel</button>
<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
setTimeout(console.log, 0, 'delay cancelled')
resolve()
})
})
}
}
const startButton = document.querySelector('#start')
const cancelButton = document.querySelector('#cancel')
function cancellableDelayedResolve(delay) {
setTimeout(console.log, 0, 'set delay')
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
setTimeout(console.log, 0, 'delay resolve')
resolve()
}, delay)
const cancelToken = new CancelToken((cancelCallback) => cancelButton.addEventListener('click', cancelCallback))
cancelToken.promise.then(() => clearTimeout(id))
})
}
startButton.addEventListener('click', () => cancellableDelayedResolve(1000))
</script>
进度追踪
执行中的期约可能会有不少离散的阶段,在最终解决之前必须依次经过。某些情况下,监控期约的执行进度会很有用。ES6 期约并不支持进度追踪,但是可以通过扩展实现。
如扩展Promise 类,为它添加notify() 方法。
class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = []
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status))
})
})
this.notifyHandlers = notifyHandlers
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler)
return this
}
}
ES6 不支持取消期约和进度通知,一个主要原因是这样会导致期约连锁和期约合成过度复杂化。比如在一个期约连锁中,如果某个被其他期约依赖的期约被取消了或者发出了通知,那么接下来应该发生什么完全说不清楚。
常见应用
异步加载图片
// 使用Promise 包装了一个图片加载的异步操作。如果加载成功,就调用resolve 方法,否则就调用reject 方法。
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image()
image.onload = function() {
resolve(image)
}
image.onerror = function() {
reject(new Error('Could not load image at' + url))
}
image.src = url
})
}
实现Ajax
// getJSON 是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise 对象。
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject) {
const handler = function() {
if (this.readyState !== 4) {
return
}
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
const client = new XMLHttpRequest()
client.open('GET', url)
client.onreadystatechange = handler
client.responseType = 'json'
client.setRequestHeader('Accept', 'application/json')
client.send()
})
return promise
}
getJSON('/posts.json').then(function(json) {
console.log('Contents': + json)
}, function(error) {
console.error('出错了', error)
})
const p1 = new Promise(function(resolve, reject) {})
const p2 = new Promise(function(resolve, reject) {
resolve(p1)
})
p1
和p2
都是 Promise 的实例,但是p2
的resolve
方法将p1
作为参数,即一个异步操作的结果是返回另一个异步操作。
注意,这时p1
的状态就会传递给p2
,p1
的状态决定了p2
的状态:
如果p1
的状态是pending
,那么p2
的回调函数就会等待p1
的状态改变;如果p1
的状态已经是resolved
或者rejected
,那么p2
的回调函数将会立刻执行。
const p1 = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function(resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
p1
是一个 Promise,3 秒之后变为rejected
。p2
的状态在 1 秒之后改变,resolve
方法返回的是p1
。由于p2
返回的是另一个 Promise,导致p2
自己的状态无效了,由p1
的状态决定p2
的状态。所以,后面的then
语句都变成针对后者(p1
)。又过了 2 秒,p1
变为rejected
,导致触发catch
方法指定的回调函数。
经典习题
执行顺序
setTimeout(function() {
console.log(1)
}, 0)
new Promise(function(resolve) {
console.log(2)
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve()
}
console.log(3)
}).then(function() {
console.log(4)
})
console.log(5)
// 2、3、5、4、1
// promise的回调是同步代码,所以会打印出2,for循环结束后调用了resolve,所以then的回调会被放入微任务队列,然后打印出3
const pro1 = new Promise((resolve, reject) => {
resolve(1)
})
const pro2 = pro1.then(result => result * 2) // 2
pro2.then(result => console.log(result), err => console.log(err))
const pro1 = new Promise((resolve, reject) => {
throw 1
})
const pro2 = pro1.then(result => {
return result * 2
}, err => err * 3) // 3
pro2.then(result => console.log(result * 2), err => console.log(err * 3)) // 6
const pro1 = new Promise((resolve, reject) => {
throw 1
})
const pro2 = pro1.then(result => {
return result * 2
}, err => {
throw err
})
pro2.then(result => console.log(result * 2), err => console.log(err * 3)) // 3
const pro1 = new Promise((resolve, reject) => {
throw 1
})
const pro2 = pro1.then(result => {
return result * 2
}, err => {
return err * 3 // 3
});
const pro3 = pro1.catch(err => {
throw err * 2 // 2
})
pro2.then(result => console.log(result * 2), err => console.log(err * 3)) // 6,调用的是result
pro3.then(result => console.log(result * 2), err => console.log(err * 3)) // 6,调用的是err
new Promise((resolve, reject) => {
console.log("外部promise")
resolve()
})
.then(() => {
console.log("外部第一个then")
return new Promise((resolve, reject) => {
console.log("内部promise")
resolve()
})
.then(() => {
console.log("内部第一个then")
})
.then(() => {
console.log("内部第二个then")
});
})
.then(() => {
console.log("外部第二个then")
})
// 外部第一个then 方法里面return 一个Promise,这个return 代表外部的第二个then 的执行需要等待return 之后的结果
// 输出
外部promise
外部第一个then
内部promise
内部第一个then
内部第二个then
外部第二个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")
})
.then(() => {
console.log("内部第二个then")
})
})
.then(() => {
console.log("外部第二个then")
})
// 内部的resolve 之后,当然是先执行内部的new Promise 的第一个then 的注册,这个 new Promise 执行完成,立即同步执行了后面的.then 的注册。
// 然而这个内部的第二个then 是需要第一个then 的的执行完成来决定的,而第一个then 的回调是没有执行,仅仅只是执行了同步的 .then 方法的注册,所以会进入等待状态。
// 这个时候,外部的第一个then 的同步操作已经完成了,然后开始注册外部的第二个then,此时外部的同步任务也都完成了。同步操作完成之后,那么开始执行微任务,内部的第一个then 是优先于外部的第二个then 的注册,所以会执行完内部的第一个then 之后,然后注册内部的第二个then,然后执行外部的第二个then,然后再执行内部的第二个then。
// 输出
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then
new Promise((resolve, reject) => {
console.log("外部promise")
resolve()
})
.then(() => {
console.log("外部第一个then")
let p = new Promise((resolve, reject) => {
console.log("内部promise")
resolve()
})
p.then(() => {
console.log("内部第一个then")
})
p.then(() => {
console.log("内部第二个then")
})
})
.then(() => {
console.log("外部第二个then")
})
// new Promise 之后的两个同步p.then 是两个执行代码语句,都是同步执行,自然是会同步注册完。
// 输出:
外部promise
外部第一个then
内部promise
内部第一个then
内部第二个then
外部第二个then
let p = new Promise((resolve, reject) => {
console.log("外部promise")
resolve()
})
p.then(() => {
console.log("外部第一个then")
new Promise((resolve, reject) => {
console.log("内部promise")
resolve()
})
.then(() => {
console.log("内部第一个then")
})
.then(() => {
console.log("内部第二个then")
})
})
p.then(() => {
console.log("外部第二个then")
})
// 输出
外部promise
外部第一个then
内部promise
外部第二个then
内部第一个then
内部第二个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")
})
.then(() => {
console.log("内部第二个then")
})
return new Promise((resolve, reject) => {
console.log("内部promise2")
resolve()
})
.then(() => {
console.log("内部第一个then2")
})
.then(() => {
console.log("内部第二个then2")
})
})
.then(() => {
console.log("外部第二个then")
})
// 外部的第二个then ,依赖于内部的return 的执行结果,所以会等待return 执行完成。
// 内部的第一段 new Promise 变成和内部的第二段 new Promise 的交替输出
// 输出
外部promise
外部第一个then
内部promise
内部promise2
内部第一个then
内部第一个then2
内部第二个then
内部第二个then2
外部第二个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')
})
.then(() => {
console.log('外部第五个then')
})
// 由于V8 底层机制, return new Promise(resolve) 会产生一个注册2个空任务的隐式异步链
// 输出
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
外部第三个then
外部第四个then
内部第二个then
外部第五个then
// <来源:B站>
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(5)
}).then(() => {
console.log(6)
})
// enque 表示微任务入队,task 表示微任务执行,log 是微任务执行时的console.log
enque:1
enque:2
=========
task:1
log:0
enque:3
task:2
log:1
enque:4
task:3
enque:5
task:4
log:2
enque:6
task:5
enque:7
task:6
log:3
enque:8
task:7
log:4
task:8
log:5
enque:9
task:9
log:6
串行执行多个promise
function delay(time) {
retun new Promise((resolve, reject) => {
console.log(`wait${time}s`)
setTimeout(() => {
console.log('execute')
resolve()
}, time * 1000)
})
}
const arr = [3, 4, 5]
// reduce
arr.reduce((s, v) => {
return s.then(() => delay(v))
}, Promise.resolve())
// async + 循环 + await
(
async function() {
for (const v of arr) {
await delay(v)
}
}
)()
// 普通循环
let p = Promise.resolve()
for (const i of arr) {
p = p.then(() => delay(i))
}
let i
let p = Promise.resolve()
while(i = arr.shift()) {
let s = i
p = p.then(() => delay(s))
}
// 递归
function dispatch(i, p = Promise.resolve()) {
if (!arr[i]) return Promise.resolve()
return p.then(() => dispatch(i + 1, delay(arr[i])))
}
dispatch(0)
// for await of
function createAsyncIterable(arr) {
return {
[Symbol.asyncIterator]() {
return {
i: 0,
next() {
if (this.i < arr.length) {
return delay(arr[this.i]).then(() => ({ value: this.i++, done: false }))
return Promise.resolve({ done: true })
}
}
}
}
}
}
(async function() {
for await (i of createAsyncIterable(arr)) {}
})()
// generator
function* gen() {
for (const v of arr) {
yield delay(v)
}
}
function run(gen) {
const g = gen()
function next(data) {
const result = g.next(data)
if (result.done) return result.value
result.value.then(function(data) {
next(data)
})
}
next()
}
run(gen)
手撕Promise
function myPromise(fn) {
this.promiseState = 'pending'
this.promiseResult = undefined
// 注册本Promise对象的resolve任务函数
this.thenCallback = undefined
// 注册本Promise对象的reject任务函数
this.catchCallback = undefined
var _this = this
var resolve = function(resolveValue) {
if (_this.promiseState === 'pending') {
// 先将状态和值改变
_this.promiseState = 'fulfilled'
_this.promiseResult = resolveValue
// 利用setTimeout模拟Promise对象的异步控制
// 虽然resolve是在then函数前执行的,但是该函数的回调一定是在Promise对象初始化完毕后执行的
// 所以我们的回调执行时,thenCallback就已经初始化完毕了
if (resolveValue instanceof myPromise) {
// 当传入的resolve的值的类型是Promise对象本身的时候
// 不需要使用异步控制,直接用他的then去处理下一步的流程
resolveValue.then(function(res) {
if (_this.thenCallback) {
// 此时调用then中注册的回调函数时应该传入该对象then之后的结果
_this.thenCallback(res)
}
})
} else {
setTimeout(function() {
if (_this.thenCallback) {
_this.thenCallback(resolveValue)
}
})
}
}
}
var reject = function(rejectValue) {
if (_this.promiseState === 'pending') {
// 先将reject的状态和值变更
_this.promiseState = 'rejected'
_this.promiseResult = rejectValue
setTimeout(function() {
// 当只有catchCallback的时候代表直接写的catch直接触发流程即可
if (_this.catchCallback) {
_this.catchCallback(rejectValue)
// 如果没有catchCallback但是存在thenCallback代表Promise对象直接使用了then
// 所以此时应该先让then执行来进行本次then函数的跳过,直到找到catch
} else if (_this.thenCallback) {
_this.thenCallback(rejectValue)
} else {
throw('no catch found')
}
})
}
}
if (fn) {
fn(resolve, reject)
} else {
throw('Promise resolver undefined is not a function')
}
}
// 由于then都是放在new Promise后编写的,所以他一定比实例化的回调函数执行晚
myPromise.prototype.then = function(callback) {
var _this = this
// 实现链式调用,需要return回去一个新的Promise对象
return new myPromise(function(resolve, reject) {
// 我们通常在then函数执行的时候优先在函数内部注册回调函数任务
// 等待resolve执行的时候通过注册异步的任务来在该回调后捕获他
_this.thenCallback = function(value) {
// 由于catch的链式调用比较复杂
// 所以可能在catch执行的时候也会触发thenCallback
// 所以在此需要判断当前promise的对象是不是已拒绝
if (_this.promiseState === 'rejected') {
// 如果是reject触发的thenCallback,直接调用下一个对象的reject
reject(value)
} else {
var res = callback(value)
// 判断,如果某一次then函数返回的是一个rejected的Promise对象
// 此时我们需要在这里直接注册它的catch并且在catch内部拿到对象的结果
// 然后通过下一个对象的reject链式的通知最近的catch执行
if (res instanceof myPromise && res.promiseState === 'rejected') {
res.catch(function(errValue) {
reject(errValue)
})
}
resolve(res)
}
}
})
}
myPromise.prototype.catch = function(callback) {
var _this = this
return new myPromise(function(resolve, reject) {
_this.catchCallback = function(value) {
var res = callback(value)
// 由于catch本次的对象是reject状态,但是如果继续调用默认触发的还是then函数
resolve(res)
}
})
}
myPromise.resolve = function(value) {
return new myPromise(function(resolve, reject) {
resolve(value)
})
}
myPromise.reject = function(value) {
return new myPromise(function(resolve, reject) {
reject(value)
})
}
myPromise.all = function(promiseArr) {
var resArr = []
return new myPromise(function(resolve, reject) {
// PromiseAll 的特点:必须等待promiseArr数组中所有的状态都变成fulfilled之后才能出发then
promiseArr.forEach((promiseItem, index) => {
promiseItem.then(function(res) {
resArr[index] = res
const success = promiseArr.every(item => {
return item.promiseState === 'fulfilled'
})
if (success) {
resolve(resArr)
}
}).catch(function(err) {
reject(err)
})
})
})
}
myPromise.race = function(promiseArr) {
var resArr = []
return new myPromise(function(resolve, reject) {
promiseArr.forEach(promiseItem => {
promiseItem.then(function(res) {
resolve(res)
}).catch(function(err) {
reject(err)
})
})
})
}
var p = new myPromise(function(resolve, reject) {
// 情况1
resolve('resolve')
// 情况2
resolve(new myPromise(function(resolve1, reject1) {
resolve1('promise对象最开始的值')
}))
})
p.then(function(res) {
console.log(res)
console.log('then触发')
return 123
}).then(function(res) {
console.log('第二个then触发')
console.log(res)
return new myPromise(function(resolve) {
resolve('第二个then的值')
})
}).then(function(res) {
console.log('第三个then触发')
console.log(res)
return 789
}).then(function(res) {
console.log('第四个then触发')
console.log(res)
})
console.log(p)
var q = new myPromise(function(resolve, reject) {
reject('reject的值')
})
q.then(function(res) {
console.log(res)
console.log('then执行')
}).then(function(res) {
console.log(res)
console.log('then执行')
}).then(function(res) {
console.log(res)
console.log('then执行')
}).catch(function(err) {
console.log(err)
console.log('catch执行')
})
var r = new myPromise(function(resolve, reject) {
resolve('第一步')
})
q.then(function(res) {
console.log(res)
console.log('第一个then执行')
return myPromise.reject('我中断了')
}).then(function(res) {
console.log(res)
console.log('第二个then执行')
return 3
}).then(function(res) {
console.log(res)
console.log('第三个then执行')
return 4
}).catch(function(err) {
console.log(err)
console.log('catch执行')
})
var s1 = new myPromise(function(resolve, reject) {
setTimeout(function() {
resolve('第一个')
}, 1000)
})
var s2 = new myPromise(function(resolve, reject) {
setTimeout(function() {
resolve('第二个')
}, 300)
})
var s3 = new myPromise(function(resolve, reject) {
setTimeout(function() {
resolve('第三个')
}, 2000)
})
myPromise.all([s1, s3, s2]).then(function(res) {
console.log(res)
}).catch(function(err) {
console.log(err)
})
Promise 的演进
Promise 将JavaScript ⼀个时代的弊病从此“解套”。这个解套虽然⽐较成功,但是如果直接使⽤then() 函数进⾏链式调⽤,代码量仍然是⾮常沉重的,想要开发⼀个⾮常复杂的异步流程,依然需要⼤量的链式调⽤进⾏⽀撑,开发者还是会变得⾮常的难受。
Generator函数
function* fnName(){
yield ***
yield ***
}
ES6 新引⼊了 Generator 函数,可以通过 yield 关键字,把函数的执⾏流挂起,为改变执⾏流程提供了可能,从⽽为异步编程提供解决⽅案:它提供了让函数可以进⾏分步执⾏的能⼒。
// 该函数和普通函数不同,在执⾏的时候函数并不会运⾏并且会返回⼀个分步执⾏对象
// 该对象存在next ⽅法⽤来让程序继续执⾏,当程序遇到yield 关键字的时候会停顿
// next 返回的对象中包含value 和done 两个属性,value 代表上⼀个yield 返回的结果
// done 代表程序是否执⾏完毕
function* test(){
var a = yield 1
console.log(a, 'a')
var b = yield 2
console.log(b, 'b')
var c = a+b
console.log(c, 'c')
}
// 获取分步执⾏对象
var generator = test()
// 输出
console.log(generator, 'generator')
// 步骤1 该程序从起点执⾏到第⼀个yield 关键字后,step1 的value 是yield 右侧的结果1
var step1 = generator.next()
console.log(step1, 'step1')
// 步骤2 该程序从var a开始执⾏到第2个yield 后,step2 的value 是yield 右侧的结果2
var step2 = generator.next()
console.log(step2, 'step2')
// 由于没有yield 该程序从var b开始执⾏到结束
var step3 = generator.next()
console.log(step3, 'step3')
查看结果发现a 和b 的值不⻅了,c 也是NaN。
虽然程序实现了分步执⾏,但是流程却出现了问题。这是因为在分步执⾏过程中,我们是可以在程序中对运⾏的结果进⾏⼈为⼲预的,也就是说yield 返回的结果和他左侧变量的值都是我们可以⼲预的。
// 代码改造
function* test(){
var a = yield 1
console.log(a)
var b = yield 2
console.log(b)
var c = a+b
console.log(c)
}
var generator = test()
console.log(generator, 'gen')
var step1 = generator.next()
console.log(step1, 's1')
var step2 = generator.next(step1.value)
console.log(step2, 's2')
var step3 = generator.next(step2.value)
console.log(step3, 's3')
也就是说next 函数执⾏的过程中是需要传递参数的。
当下⼀次next 执⾏的时候如果不传递参数,那么本次yield 左侧变量的值就变成了undefined,所以如果想让yield 左侧的变量有值就必须在next 中传⼊指定的结果。
Generator 能控制什么样的流程?
function* test(){
var a = yield 1
console.log(a, 'a')
var res = yield setTimeout(function(){
return 123
},1000)
console.log(res, 'res')
var res1 = yield new Promise(function(resolve){
setTimeout(function(){
resolve(456)
},1000)
})
console.log(res1, 'res1')
}
var generator = test()
console.log(generator, 'generator')
var step1 = generator.next()
console.log(step1, 'step1')
var step2 = generator.next()
console.log(step2, 'step2')
var step3 = generator.next()
console.log(step3, 'step3')
var step4 = generator.next()
console.log(step4, 'step4')
观察打印输出:
发现普通变量可以直接在value 中拿到,setTimeout 位置我们拿到的值和回调函数内部的值完全不⼀样,⽽Promise 对象可以拿到它本身。
展开查看Promise 对象:
发现Promise 对象中是可以获取到内部的结果的。
使用生成器
生成ID 序列
function* IdGenerator() {
let id = 0
while(true) {
yield ++id
}
}
const idIterator = IdGenerator()
const ninja1 = {
id: idIterator.next().value
}
// 标准函数中一般不应该写无限循环的代码。但在生成器中没问题。当生成器遇到了一个yield 语句,它就会一直挂起执行直到下次调用next 方法,所以只有每次调用一次next 方法,while 循环才会迭代一次并返下一个ID 值。
遍历DOM 树
function* DomTraversal(element) {
yield element
element = element.firstElementChild
while(element) {
yield* DomTraversal(element)
element = element.nextElementSibling
}
}
const subTree = document.getElementById('subTree')
for (let element of DomTraversal(subTree)) {
assert(element !== null, element.nodeName)
}
通过执行上下文追踪生成器函数
function* NinjaGenerator(action) {
yield 'Hattori' + action
return 'Yoshi' + action
}
const ninjaIterator = NinjaGenerator('skulk')
const result1 = ninjaIterator.next()
const result2 = ninjaIterator.next()
当调用NinjaGenerator 函数,控制流进入了生成器,会创建一个新的函数环境上下文NinjaGenerator(和相对应的词法字典并列),并将该上下文入栈。而生成器比较特殊,它不会执行任何函数代码。取而代之则生成一个新的迭代器再从中返回,通过在代码中用
ninjaIterator 可以来引用这个迭代器。由于迭代器是用来控制生成器的执行的,故迭代器中保存着一个在它创建位置处的执行上下文。
调用生成器的next 方法会重新激活执行上下文栈中与该生成器相对应的项,首先将该项入栈,然后从它上次退出的位置继续执行。
与标准函数不同,标准函数仅仅会被重复调用,每次调用都会创建一个新的执行环境上下文,而生成器的执行环境上下文则会暂时挂起并在将来恢复。
生成器函数得到的结果为Hattori skulk,然后执行中又遇到了yield 关键字,这表明了Hattori skulk 是该生成器的第一个中间值,所以需要挂起生成器的执行并返回该值。NinjaGenerator 上下文离开了调用栈,但由于ninjaIterator 还持有着对它的引用,故而它并未被销毁。现在生成器挂起了,又在非阻塞的情况下移动到了挂起让渡状态。程序在全局代码中恢复执行,并将生产出的值存入变量result1。
当遇到另一个迭代器调用时,又继续执行:
- 生成器处于挂起让渡状态
- 生成器的执行环境上下文从栈中弹出,但由于ninjaIterator 还持有着对它的引用,不会被销毁
通过ninjaIterator 激活NinjaGenerator 的上下文引用,将其入栈,在上次离开的位置继续执行,遇到return 语句,返回Yoshi skulk 并结束生成器的执行,随之生成器进入结束状态。
Generator - 将Promise 的异步流程同步化
通过递归调⽤的⽅式,来动态的去执⾏⼀个Generator 函数,以done 属性作为是否结束的依据,通过next 来推动函数执⾏,如果过程中遇到了Promise 对象就等待Promise 对象执⾏完毕再进⼊下⼀步(这⾥排除异常和对象reject 的情况)。
封装⼀个动态执⾏的函数如下:
/**
* fn:Generator函数对象
*/
function generatorFunctionRunner(fn){
// 定义分步对象
let generator = fn()
// 执⾏到第⼀个yield
let step = generator.next()
// 定义递归函数
function loop(stepArg,generator){
// 获取本次的yield右侧的结果
let value = stepArg.value
// 判断结果是不是Promise对象
if(value instanceof Promise){
// 如果是Promise对象就在then函数的回调中获取本次程序结果
// 并且等待回调执⾏的时候进⼊下⼀次递归
value.then(function(promiseValue){
if(stepArg.done == false){
loop(generator.next(promiseValue),generator)
}
})
}else{
// 判断程序没有执⾏完就将本次的结果传⼊下⼀步进⼊下⼀次递归
if(stepArg.done == false){
loop(generator.next(stepArg.value),generator)
}
}
}
//执⾏动态调⽤
loop(step,generator)
}
有了这个函数之后就可以将最初的三个setTimeout 转换成如下结构进⾏开发:
function* test(){
var res1 = yield new Promise(function(resolve){
setTimeout(function(){
resolve('第⼀秒运⾏')
},1000)
})
console.log(res1)
var res2 = yield new Promise(function(resolve){
setTimeout(function(){
resolve('第⼆秒运⾏')
},1000)
})
console.log(res2)
var res3 = yield new Promise(function(resolve){
setTimeout(function(){
resolve('第三秒运⾏')
},1000)
})
console.log(res3)
}
generatorFunctionRunner(test)
// 控制台:每间隔1秒钟就输出⼀次
第⼀秒运⾏
第⼆秒运⾏
第三秒运⾏
抛去generatorFunctionRunner 函数外,在Generator 函数中已经可以将Promise 的.then 回调成功的规避了,yield 修饰的Promise 对象在运⾏到当前⾏时,程序就会进⼊挂起状态直到Promise 对象变成完成状态,程序才会向下⼀⾏执⾏。
这样就通过Generator 函数对象成功的将Promise 对象同步化了。
这也是JavaScript 异步编程的⼀个过渡期,通过这个解决⽅案,只需要提前准备好⼯具函数那么编写异步流程可以很轻松的使⽤yield 关键字实现同步化。
async/await - 终极解决方案
经过了Generator 的过渡之后异步代码同步化的需求逐渐成为了主流需求,这个过程在ES7 版本中得到了提案,并在ES8 版本中进⾏了实现,提案中定义了全新的异步控制流程。
// 提案中定义的函数使⽤成对的修饰符
async function test(){
await ...
await ...
}
test()
Generator 慢慢淡出了业务开发者的舞台,不过Generator 函数成为了向下兼容过渡期版本浏览器的候补实现⽅式,虽然在现今的⼤部分项⽬业务中使⽤Generator 函数的场景⾮常的少,但是如果查看脚⼿架项⽬中通过babel 构建的JavaScript⽣产代码,还是能⼤量的发现Generator 的应⽤的,它的作⽤就是为了兼容不⽀持async 和await 的浏览器。
async function test(){
return 1
}
let res = test()
console.log(res)
Promise {<fulfilled>: 1}
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 1
async function test(){
console.log(3)
return 1
}
console.log(1)
test()
console.log(2)
// 结果:1,3,2
很惊喜是不是!按照Promise 对象的执⾏流程function 被async 修饰之后它本身应该变成异步函数,那么他应该在1和2输出完毕之后在输出3,但是结果却出⼈意料,这⼜⼀次打破了单线程异步模型的概念。
Promise 中的回调函数是⼀个极少数的既使⽤同步回调流程⼜使⽤了异步的回调流程的对象,所以在new Promise 时的function 是同步流程。
async function test(){
console.log(3)
var a = await 4
console.log(a)
return 1
}
console.log(1)
test()
console.log(2)
// 1,3,2,4
// 等价于
console.log(1)
new Promise(function(resolve){
console.log(3)
resolve(4)
}).then(function(a){
console.log(a)
})
console.log(2)
// 由于初始化的回调是同步的所以1,3,2 都是同步代码,⽽4 是在resolve 中传⼊的,then 代表异步回调所以4 应该最后输出。
await 的右侧和上⾯的代码,全部是同步代码区域相当于new Promise 的回调,await 的左侧和下⾯的代码,就变成了异步代码区域相当于then 的回调。
【参考资料】
《你不知道的JavaScript》中卷 第2部分 异步和性能
《JS 忍者秘籍》第2版 章节6