再来聊一聊Promise
由于 JavaScript 采用的是单线程异步 IO 的模型,所以其代码编写方式也是多采用回调的方式。结果就是导致代码中充斥着大量的回调函数,而一旦进行多个有序的异步操作,就会造成代码回调嵌套很难看的问题。人们称之为“回调地狱”。
例如这样的代码
1 | function request(url, param, successFun, errorFun) { |
一旦需要在 request 得到 data 之后再利用该 data 发起新的请求,则必须在第一个回调函数里继续写一个跟外层一样的回调函数,最终会形成层层嵌套的回调函数:
1 | request( |
使用 Promise 可以部分解决这个问题,因为 Promise 将回调函数的传入,变成了在异步方法调用之后来传入(与异步方法的调用是链式的关系)。相当于:
1 | request("test1.html", "") |
而由于 Promise 的 then 可以返回新的 promise,所以可以分成多行来写
1 | var p = request() |
写起来没有嵌套的那么难看了。
当然,实际上他还是看起来不那么完美,但解决了前端长久存在的一个问题。 而在 ES6 之后,可以考虑使用 co 和 async 特性书写彻底的同步风格的代码。
本文我们先不谈 async,而是主要来聊一聊 Promise 的注意的地方
Promise 的状态
一个 Promise 对象有 3 种状态:
- pending:初始值,不是 fulfilled,也不是 rejected
- fulfilled:代表操作成功
- rejected:代表操作失败
Promise 既可以从 pending 转变为 fulfilled,也可以从 pending 转变为 rejected。promise 对象会最终变成两种状态中的一种, 即成功(resolve)或失败(reject)。 一旦状态改变,就「凝固」了,会一直保持这个状态,不会再发生变化
异步动作发起的时机
Promise 一般是做一件异步的事情,而这个异步动作会在 Promise 创建时 立即执行
。假如一个 request 发送请求的函数是返回一个 Promise,那么我们可以这样写:
1 | var p = request(); |
在 request 函数调用的时候,这个异步请求已经发送了。也可以说: 只要 Promise 创建,异步动作就立即执行。
对于我们开发者使用 Promise 来封装传统异步函数的时候,我们也要注意。在使用 Promise 的时候,异步动作会立即发起。对应到我们的代码:
1 | var p = new Promise(function (resolve, reject) { |
在上面的代码中,在你创建这个 Promise 对象的时候(调用 new 操作符),你传入的这个匿名函数就开始执行了。所以”开始一个异步动作”会优先打印出来。
总结一下:
new 一个 Promise 的时候,传入的传统异步操作函数会在创建 Promise 对象的过程中 立即执行
。 但 resolve 和 reject 却不会立即触发,一般会在异步操作完成的时候调用 resolve 或 reject,例如:
1 | let p = new Promise((resolve, reject) => { |
上面代码 ‘hi i begin async code ‘ 会最先打印出来, 接下来 ‘i after promise then’ 会打印,这两个打印过程遵循先写的先执行(因为是同步的)。 而 Promise 内部的 resolve 虽然被优先执行了,但是它是改变 Promise 的状态,状态改变后 then 要等到所有同步任务都完成后才触发。所以 then 回调会在接下来触发。 最后会打印 async finish
, 异步任务完成。 但是 resolve(2)不会再次改变 Promise 的状态了(因为 Promise 的状态只能改变一次),因此 resolve(2) 什么也不会发生。
一个 promise 能否 resolve 两次
前面已经说过了,Promise 发生状态改变时,会触发 Promise 通过 then 函数注册的 resolve 和 reject 回调函数。那么,如果同一个 Promise 却 resolve 了两次,会发生什么呢?
1 | let p = new Promise((resolve, reject) => { |
上面的代码 1 肯定是可以输出的,2 可以输出吗? 答案是不会,3 秒之后会输出’async finish’(说明不会阻止异步任务的执行和回调), 但 resolve 不会再有反应了,而且不会抛出异常。
由于 then 必然是在 Promise 状态变化时触发,因此我们有理由认为第二次 resolve 的时候 Promise 状态并没有发生变化。因此,说明 Promise 内部状态改变后就会凝固,要么是 fullfill 要么是 reject。不可以再多次 resolve,如果确实多次 resolve 了,则 Promise 不会发生任何反应(也不会抛出异常)
直接 resolve 的 Promise 会立即调用 then 吗?
这是个不错的问题。我们使用如下的代码进行了实验:
1 | var p = new Promise(function (resolve, reject) { |
这段代码是先创建了一个 Promise,根据上文讲解,2 肯定在 1 之后打印;因为创建 Promise 的匿名函数参数会同步立即执行。
但是匿名函数里面如果直接同步调用了 resolve,那么 resolve()函数是同步执行的吗?
这个例子其实也可以该写成这样:
1 | var p = Promise.resolve(3); // 直接返回一个即将变成resolve(3)状态的promise |
经过测试,我们得到的结果都是: 先打印 2
再打印 3
. 也就是说哪怕你同步去执行了 resolve 函数,实际上 then 注册的处理函数是会被异步触发的。 这是因为 Promise 规范规定了: 所有回调都是异步的.
想起之前我博客中分析的 JavaScript 任务队列和事件循环机制,可以大概猜测出 resolve 的原理:
实际上 resolve 调用后,会发送一个 microtask 的任务放入 microtask 的任务队列,等待当前 js 线程空闲后来执行。因此 js 同步任务执行完成之后,发现 microtask 任务队列中有一个 resolve 的回调需要执行,所以才打印了 3
多重链式调用
Promise 的真正强大之处在于它的多重链式调用,可以避免层层嵌套回调. 之所以可以链式调用,是因为通过 then 对 promise 添加 onFulfilled 和 onRejected 回调,返回的是一个新的 Promise 实例(不是原来那个 Promise 实例),且返回值将作为参数传入这个新 Promise 的 resolve 函数。
Promise 更多的面试之坑
可以参考这里关于 Promise:你可能不知道的 6 件事
1 | new Promise((resolve) => { |
已经 resolve 过的 Promise,过一段时间再添加 then 可以吗
1 | var p = new Promise((resolve) => { |
一个 Promise 可以添加多个 then 吗,也可以过一段时间再添加 then 吗
1 | var p = new Promise((resolve) => { |
1 | var p = new Promise((resolve) => { |
从表现上看,microtask 在调用 then 函数的时候,如果 Promise 还没有 resolve,则 Promise 会把这个回调任务记录下来等到 Promise resolve 的时候,把它们放入 microtask 任务队列;如果 then 函数调用时 Promise 已经 resolve 了,那么 Promise 会在此时直接把 then 注册的回调放入 microtask 任务队列。
refer
初探 promise
从 Promise 来看 JavaScript 中的 Event Loop、Tasks 和 Microtasks