它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
为什么需要引入 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()
else
reject()
}
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_ = null
var onReject_ = null
// 模拟实现 resolve 和 then,暂不支持 rejcet
this.then = function (onResolve, onReject) {
onResolve_ = onResolve
};
function resolve(value) {
//setTimeout(()=>{
onResolve_(value)
// },0)
}
executor(resolve, null);
}
function executor(resolve, reject) {
resolve(100)
}
// 将 Promise 改成我们自己的 Bromsie
let demo = new Bromise(executor)
function onResolve(value){
console.log(value)
}
demo.then(onResolve)
Uncaught TypeError: onResolve_ is not a function
at 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,undefined
return new Promise((resolve, reject) => {
let script = document.createElement('script')
script.src = src
script.onload = () => resolve(src)// fulfilled,result
script.onerror = (err) => reject(err)// rejected
document.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)
});
// 2
Promise.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)
})
// 3
loadScript('./1.js')
.then(() => {
loadScript('./2.js')
}, (err) => {
console.log(err)
})
.then(() => {
loadScript('./3.js')
}, (err) => {
console.log(err)
})
// 1,2,3
loadScript('./1.js')
.then(() => {
loadScript('./4.js')
}, (err) => {
console.log(err)
})
.then(() => {
loadScript('./3.js')
}, (err) => {
console.log(err)
})
// 1 3
loadScript('./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 数字不行要 promise
return 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`。
```javascript
const 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
```javascript
const f = () => console.log('now');
Promise.try(f);
console.log('next');