它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
为什么需要引入 Promise
异步编程模型

Web 应用的异步编程模型
上图展示的是一个标准的异步编程模型,页面主线程发起了一个耗时的任务,并将任务交给另外一个进程去处理,这时页面主线程会继续执行消息队列中的任务。等该进程处理完这个任务后,会将该任务添加到渲染进程的消息队列中,并排队等待循环系统的处理。排队结束之后,循环系统会取出消息队列中的任务进行处理,并触发相关的回调操作。
Web 页面的单线程架构决定了异步回调,而异步回调影响到了我们的编码方式,
异步回调
// 执行状态function onResolve(response){console.log(response) }function onReject(error){console.log(error) }let xhr = new XMLHttpRequest()xhr.ontimeout = function(e) { onReject(e)}xhr.onerror = function(e) { onReject(e) }xhr.onreadystatechange = function () { onResolve(xhr.response) }// 设置请求类型,请求 URL,是否同步信息let URL = 'https://time.geekbang.com'xhr.open('Get', URL, true);// 设置参数xhr.timeout = 3000 // 设置 xhr 请求的超时时间xhr.responseType = "text" // 设置响应返回的数据格式xhr.setRequestHeader("X_TEST","time.geekbang")// 发出请求xhr.send();
一个普通的 AJAX 请求有5个回调,这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的直觉,这就是异步回调影响到我们的编码方式。
封装异步代码,让处理流程变得线性

封装请求过程
从图中你可以看到,我们将 XMLHttpRequest 请求过程的代码封装起来了,重点关注输入数据和输出结果。
那我们就按照这个思路来改造代码。首先,我们把输入的 HTTP 请求信息全部保存到一个 request 的结构中,包括请求地址、请求头、请求方式、引用地址、同步请求还是异步请求、安全设置等信息。request 结构如下所示:
//makeRequest 用来构造 request 对象function makeRequest(request_url) {let request = {method: 'Get',url: request_url,headers: '',body: '',credentials: false,sync: true,responseType: 'text',referrer: ''}return request}复制代码
然后就可以封装请求过程了,这里我们将所有的请求细节封装进 XFetch 函数,XFetch 代码如下所示:
//[in] request,请求信息,请求头,延时值,返回类型等//[out] resolve, 执行成功,回调该函数//[out] reject 执行失败,回调该函数function XFetch(request, resolve, reject) {let xhr = new XMLHttpRequest()xhr.ontimeout = function (e) { reject(e) }xhr.onerror = function (e) { reject(e) }xhr.onreadystatechange = function () {if (xhr.status = 200)resolve(xhr.response)}xhr.open(request.method, URL, request.sync);xhr.timeout = request.timeout;xhr.responseType = request.responseType;// 补充其他请求信息//...xhr.send();}复制代码
这个 XFetch 函数需要一个 request 作为输入,然后还需要两个回调函数 resolve 和 reject,当请求成功时回调 resolve 函数,当请求出现问题时回调 reject 函数。
有了这些后,我们就可以来实现业务代码了,具体的实现方式如下所示:
XFetch(makeRequest('https://time.geekbang.org'),function resolve(data) {console.log(data)}, function reject(e) {console.log(e)})
新的问题:回调地狱
上面的示例代码已经比较符合人的线性思维了,在一些简单的场景下运行效果也是非常好的,不过一旦接触到稍微复杂点的项目时,你就会发现,如果嵌套了太多的回调函数就很容易使得自己陷入了回调地狱。
XFetch(makeRequest('https://time.geekbang.org/?category'),function resolve(response) {console.log(response)XFetch(makeRequest('https://time.geekbang.org/column'),function resolve(response) {console.log(response)XFetch(makeRequest('https://time.geekbang.org')function resolve(response) {console.log(response)}, function reject(e) {console.log(e)})}, function reject(e) {console.log(e)})}, function reject(e) {console.log(e)})
这段代码之所以看上去很乱,归结其原因有两点:
- 第一是嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。
- 第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的错误处理的方式,明显增加了代码的混乱程度。
原因分析出来后,那么问题的解决思路就很清晰了:
- 第一是消灭嵌套调用;
- 第二是合并多个任务的错误处理。
使用 promise 重构
消灭嵌套调用
Promise 通过回调函数延迟绑定和回调函数返回值穿透的技术,解决了循环嵌套。
接下来,我们再利用 XFetch 来构造请求流程,代码如下:function XFetch(request) {function executor(resolve, reject) {let xhr = new XMLHttpRequest()xhr.open('GET', request.url, true)xhr.ontimeout = function (e) { reject(e) }xhr.onerror = function (e) { reject(e) }xhr.onreadystatechange = function () {if (this.readyState === 4) {if (this.status === 200) {resolve(this.responseText, this)} else {let error = {code: this.status,response: this.response}reject(error, this)}}}xhr.send()}return new Promise(executor)}
var x1 = XFetch(makeRequest('https://time.geekbang.org/?category'))var x2 = x1.then(value => {console.log(value)return XFetch(makeRequest('https://www.geekbang.org/column'))})var x3 = x2.then(value => {console.log(value)return XFetch(makeRequest('https://time.geekbang.org'))})x3.catch(error => {console.log(error)})
首先,Promise 实现了回调函数的延时绑定。回调函数的延时绑定在代码上体现就是先创建 Promise 对象 x1,通过 Promise 的构造函数 executor 来执行业务逻辑;创建好 Promise 对象 x1 之后,再使用 x1.then 来设置回调函数。
在执行 executor 里面的 resolve函数的时候回调函数还没有绑定,所以称为延时绑定,所以 resolve 必定是异步的回调
其次,需要将回调函数 onResolve 的返回值穿透到最外层。因为我们会根据 onResolve 函数的传入值来决定创建什么类型的 Promise 任务,创建好的 Promise 对象需要返回到最外层,这样就可以摆脱嵌套循环了。你可以先看下面的代码:
回调函数返回值穿透到最外层
合并多个任务的错误处理
function executor(resolve, reject) {let rand = Math.random();console.log(1)console.log(rand)if (rand > 0.5)resolve()elsereject()}var p0 = new Promise(executor);var p1 = p0.then((value) => {console.log("succeed-1")return new Promise(executor)})var p3 = p1.then((value) => {console.log("succeed-2")return new Promise(executor)})var p4 = p3.then((value) => {console.log("succeed-3")return new Promise(executor)})p4.catch((error) => {console.log("error")})console.log(2)
这段代码有四个 Promise 对象:p0~p4。无论哪个对象里面抛出异常,都可以通过最后一个对象 p4.catch 来捕获异常,通过这种方式可以将所有 Promise 对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题。
为什么 promise 要引入微任务
模拟 promise 内部解构
function Bromise(executor) {var onResolve_ = nullvar onReject_ = null// 模拟实现 resolve 和 then,暂不支持 rejcetthis.then = function (onResolve, onReject) {onResolve_ = onResolve};function resolve(value) {//setTimeout(()=>{onResolve_(value)// },0)}executor(resolve, null);}
function executor(resolve, reject) {resolve(100)}// 将 Promise 改成我们自己的 Bromsielet demo = new Bromise(executor)function onResolve(value){console.log(value)}demo.then(onResolve)Uncaught TypeError: onResolve_ is not a functionat resolve (<anonymous>:10:13)at executor (<anonymous>:17:5)at new Bromise (<anonymous>:13:5)at <anonymous>:19:12
执行 executor 的时候反向 onResolve 还是 null
修改 onResolve 延时执行
function resolve(value) {setTimeout(()=>{onResolve_(value)},0)}
不过使用定时器的效率并不是太高,好在我们有微任务,所以 Promise 又把这个定时器改造成了微任务了,这样既可以让 onResolve_ 延时被调用,又提升了代码的执行效率。
promise 缺点
Promise也有一些缺点。
- 最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆
then,原来的语义变得很不清楚,代码不能很好地表示执行流程。 - 首先,无法取消
Promise,一旦新建它就会立即执行,无法中途取消。 - 其次,如果不设置回调函数,
Promise内部抛出的错误,不会反应到外部。 - 第三,当处于
pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。浏览器用 async/await 解决
promise 状态
promise 有四种状态,其中三个核心状态为Pending,Fulfilled以及Rejected,分别表示该Promise挂起,完成以及拒绝,还有一种初始状态,表示还未执行。
promise 特点
Promise对象有以下两个特点。
- 对象的状态不受外界影响。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是
Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected,<br />这时就称为 resolved(已决议)。如果改变已经发生了,resolve 可以进入 fulfilled 或者 rejected 状态promise 对象
每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
function laodScript(src){// pending,undefinedreturn new Promise((resolve, reject) => {let script = document.createElement('script')script.src = srcscript.onload = () => resolve(src)// fulfilled,resultscript.onerror = (err) => reject(err)// rejecteddocument.head.append(script)})}loadScript('./1.js').then(loadScript('./2.js')).then(loadScript('./3.js'))
优点:回调函数变成了链式写法,平行的方式代替一层一层
程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。
Promise 原型上 then,catch finally 方法
function timeout(ms) {return new Promise((resolve, reject) => {setTimeout(resolve, ms, 'done');});}timeout(100).then((value) => {console.log(value);});
resolve函数
resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例
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的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变。
调用resolve或reject并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {resolve(1);console.log(2);}).then(r => {console.log(r);});// 2// 1
最好在它们前面加上return语句
new Promise((resolve, reject) => {return resolve(1);// 后面的语句不会执行console.log(2);})
promise 实例方法
- resolved状态的 promise 会回调后面的第一个.then
- reject 状态的 promise会回调后面的第一个.catch
- 热和一个 rejected 状态且后面没有 catch 的 promise,都会造成浏览器、node 环境的全局错误
执行 then 和 catch 会返回一个新 Promise,该 promise 最终状态根据 then 和 catch 的回调函数的执行结果决定
- throw,该 promise 是 rejiected
- return,该 promise 是 resolved 状态
return 了一个 promise,该 promise 会和回调函数 return 的 promise 状态保持一致
Promise.prototype.then
语法
promise.then(onFulfilled,onRejected)
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)如果第一个回调函数 return 不是 promise 数据
第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
- 如果第一个回调函数 return 是 promise 数据后一个回调函数,就会等待该
Promise对象的状态发生变化,才会被调用。 - 如果then 里面没有 return,返回新的 promise 实例
Promise.prototype.catch
语法
Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。如果在 then 中指定了 reject 回调函数,catch 不会接收。catch()方法返回的还是一个 Promise 对象。
如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。如果错误被捕获了,不会传到后面去
优点
用 catch比 then 中第二个参数的好处是,可以捕获 then 方法执行中的错误
抛出错误
reject()方法的作用,等同于抛出错误。
// 写法一const promise = new Promise(function(resolve, reject) {try {throw new Error('test');} catch(e) {reject(e);}});promise.catch(function(error) {console.log(error);});// 写法二const promise = new Promise(function(resolve, reject) {reject(new Error('test'));});promise.catch(function(error) {console.log(error);});// 写法三const promise = new Promise(function(resolve, reject) {throw new Error('test');});promise.catch(function(error) {console.log(error);});
如果 Promise 状态已经变成resolved,再抛出错误是无效的。
const promise = new Promise(function(resolve, reject) {resolve('ok');throw new Error('test');});promise.then(function(value) { console.log(value) }).catch(function(error) { console.log(error) });// ok
上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
catch 冒泡
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1.json').then(function(post) {return getJSON(post.commentURL);}).then(function(comments) {// some code}).catch(function(error) {// 处理前面三个Promise产生的错误});
上面代码中,一共有三个 Promise 对象:一个由getJSON()产生,两个由then()产生。它们之中任何一个抛出的错误,都会被最后一个catch()捕获。
和 try/catch 不同点
跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即错误会被吞掉。
Promise 内部的错误不会影响到 Promise 外部的代码,promise 外部的脚本继续执行。
如果再 catch 中出错只能依靠下一个 catch 去捕获
Promise.prototype.finally
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例。
promise.finally(() => {// 语句});// 等同于promise.then(result => {// 语句return result;},error => {// 语句throw error;});Promise.prototype.finally = function (callback) {let P = this.constructor;return this.then(value => P.resolve(callback()).then(() => value),reason => P.resolve(callback()).then(() => { throw reason; }));};Promise.prototype.done = function (onFulfilled, onRejected) {this.then(onFulfilled, onRejected).catch(function (reason) {// 抛出一个全局错误setTimeout(() => { throw reason; }, 0);});};
Promise.resolve(2).then((data) => { return data }).then(data => {console.log(data)});// 2Promise.resolve(2).finally(() => {}).then(data => {console.log(data)});// 2
串行-链式
// 4是没有,1,2,3都有loadScript('./4.js').then(() => {loadScript('./2.js')}, (err) => {console.log(err)}).then(() => {loadScript('./3.js')}, (err) => {console.log(err)})// 3loadScript('./1.js').then(() => {loadScript('./2.js')}, (err) => {console.log(err)}).then(() => {loadScript('./3.js')}, (err) => {console.log(err)})// 1,2,3loadScript('./1.js').then(() => {loadScript('./4.js')}, (err) => {console.log(err)}).then(() => {loadScript('./3.js')}, (err) => {console.log(err)})// 1 3loadScript('./1.js').then(() => {return loadScript('./4.js')}, (err) => {console.log(err)}).then(() => {loadScript('./3.js')}, (err) => {console.log(err)})// 1
promise 静态方法
function test (bool) {if (bool) {return new Promise((resolve, reject) => { resolve(30)})} else {// 42 数字不行要 promisereturn Promise.resolve(42)// return Promise.reject(new Error('ss'))}}test.then()
多个错误一起捕获
loadScript('./1.js').then(() => {return loadScript('./4.js')}).then(() => {loadScript('./3.js')}).catch((err) => {console.log(err)})
用 reject 去捕获,不要用 throw new Error
并行 Promise.all()
从Promise.all([ .. ])返回的主promise在且仅在所有的成员promise都完成后才会完 成。如果这些promise中有任何一个被拒绝的话,主Promise.all([ .. ])promise就会立 即被拒绝,并丢弃来自其他所有 promise 的全部结果。
const p1 = Promise.resolve(1)const p2 = Promise.resolve(2)const p3 = Promise.resolve(3)Promise.all([p1,p2,p3]).then(value => {console.log(value)})// [1, 2, 3]
注意,如果作为参数的 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)).catch(e => console.log(e));// 会进 then 方法// ["hello", Error: 报错了]
如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。
竞争 Promise.race()
与Promise.all([ .. ])类似,一旦有任何一个Promise决议为完成,Promise.race([ .. ]) 就会完成;一旦有任何一个 Promise 决议为拒绝,它就会拒绝。
可以用来防止回调未被调用
场景:主 cdn,备用 cdn,有一个加载了就行
并行,串行都不太符合
const p1 = () => {return new Promise((resolve, reject) => {setTimeout(() => {resolve(1)}, 1000)})}const p2 = () => {return new Promise((resolve, reject) => {resolve(2)}).then(() => {return 2})}// question: 都会请求?Promise.race([p1(),p2()]).then((value) => {console.log(value) // 2})
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。
const p = Promise.race([fetch('/resource-that-may-take-a-while'),new Promise(function (resolve, reject) {setTimeout(() => reject(new Error('request timeout')), 5000)})]);p.then(console.log).catch(console.error);
Promise.allSettled()
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。注意和 all 的区别,
const promises = [fetch('/api-1'),fetch('/api-2'),fetch('/api-3'),];await Promise.allSettled(promises);removeLoadingIndicator();
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 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: 'fulfilled', value: 42 },// { status: 'rejected', reason: -1 }// ]
有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];const results = await Promise.allSettled(promises);// 过滤出成功的请求const successfulPromises = results.filter(p => p.status === 'fulfilled');// 过滤出失败的请求,并输出原因const errors = results.filter(p => p.status === 'rejected').map(p => p.reason);
Promise.resolve()
有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。
等价于
Promise.resolve('foo')// 等价于new Promise(resolve => resolve('foo'))
- 参数是一个 Promise 实例如果参数是 Promise 实例,那么
Promise.resolve将不做任何修改、原封不动地返回这个实例。 - 参数是一个
thenable对象
上面代码将 jQuery 生成的const jsPromise = Promise.resolve($.ajax('/whatever.json'));
deferred对象,转为一个新的 Promise 对象。
let thenable = {then: function(resolve, reject) {resolve(42);}};let p1 = Promise.resolve(thenable);p1.then(function(value) {console.log(value); // 42});
- 参数不是具有
then方法的对象,或根本就不是对象Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。 - 不带有任何参数
Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。 ```javascript setTimeout(function () { console.log(‘three’); }, 0);
Promise.resolve().then(function () { console.log(‘two’); });
console.log(‘one’);
// one // two // three
<a name="R9ZzW"></a>## Promise.reject()`Promise.reject(reason)`方法也会返回一个新的 Promise 实例,该实例的状态为`rejected`。```javascriptconst p = Promise.reject('出错了');// 等同于const p = new Promise((resolve, reject) => reject('出错了'))
注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。
const thenable = {then(resolve, reject) {reject('出错了');}};Promise.reject(thenable).catch(e => {console.log(e === thenable)})// true
Promise.try() stage-1
同步任务变成异步任务执行
const f = () => console.log('now');Promise.resolve().then(f);console.log('next');// next// now
让同步函数同步执行,异步函数异步执行。
- async ```javascript // 同步 const f = () => console.log(‘now’); (async () => f())(); console.log(‘next’); // now // next
// 异步 const f = () => { setTimeout(() => { console.log(‘async’) }) } (async () => f())().then((data) => { console.log(data)}).catch(_ => {}); console.log(‘next’); // next async
- promise.try```javascriptconst f = () => console.log('now');Promise.try(f);console.log('next');
