上一篇文章 我们了解了微任务是如何工作的,今天主要来学习一下微任务的一个实现 — Promise,BOM和DOM中大多数的API都是建立在Promise上, 可以说Promise已经成为现代前端的一把利刃
我们先从JavaScript引入Promise的动机谈起,了解一下为什么引入promise,它又能解决什么问题呢?
异步编程:代码逻辑不连贯
页面的事件是通过循环系统来维护的,页面中的任务都是执行在主线程之上的,而那些比较耗时的任务,比如下载网络文件,获取用户设备信息等,这些任务都不会放在主线程,而是放在主线程之外的进程或线程来完成,这样会提高效率。
上图是一个 异步回调 的示意图,页面主线程发起一个耗时的任务,并将任务交给其他进程来处理,页面主线程会继续执行消息队列中的任务,等该进程处理完这个任务后,会将该任务添加到渲染进程的消息队列中,等待循环系统来处理。
web页面的单线程架构决定了异步回调,异步回调又影响了我们的编程方式。假设有一个XMLHTTPRequest任务:
//执行状态
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();
这段传统的五步请求方法并没有什么问题,可以正常获取结果,但是回调函数太多,造成逻辑不连贯,那么该如何解决这个问题呢?
封装异步代码,让处理流程变得线性
有没有这样一种方式,可以让我们只关注输入和输出,而减少中间过程的代码?
从图中可以看出:我们把输入之后的步骤完全封装起来,只在乎输入和输出,不管中间的XMLHttpRequest过程。
我们把输入的请求信息全部保存在一个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函数中,
//[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(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)
})
接下来分析一下具体实现:
1、 在实现XFetch的时候我们引入了Promise,调用XFetch就会返回一个promise对象
2、构建promise的时候,传入一个excutor回调函数,主要业务流程都放在这个函数里面
3、如果excutor里运行成功了,就会执行then里的resolve方法,如果失败,就会reject
4、在excutor函数中调用resolve函数,会触发then里的回调函数,如果reject,则会调用catch方法
Promise主要通过下面两步解决了嵌套回调的问题:
首先,Promise实现了回调函数的延时绑定,具体实现就是先创建Promise对象x1,通过Promise的构造函数excutor来执行业务代码逻辑,然后使用x1.then来设置回调函数,示例代码如下:
//创建Promise对象x1,并在executor函数中执行业务逻辑
function executor(resolve, reject){
resolve(100)
}
let x1 = new Promise(executor)
//x1延迟绑定回调函数onResolve
function onResolve(value){
console.log(value)
}
x1.then(onResolve)
其次,需要将回调函数onResolve的返回值穿透到最外层
们会根据 onResolve 函数的传入值来决定创建什么类型的 Promise 任务,创建好的 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 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止
Promise 与微任务
看下面代码:
function executor(resolve, reject) {
resolve(100)
}
let demo = new Promise(executor)
function onResolve(value){
console.log(value)
}
demo.then(onResolve)
对于上面这段代码,我们需要重点关注下它的执行顺序:
1、首先执行 new Promise 时,Promise 的构造函数会被执行
2、 Promise 的构造函数会调用 Promise 的参数 executor 函数。然后在 executor 中执行了 resolve,resolve的执行会引起then函数里onResolve函数的执行
口说无凭,接下来看一下具体实现,通过模拟一个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);
}
观察上面这段代码,我们实现了自己的构造函数、resolve、then 方法。接下来我们使用 Bromise 来实现我们的业务代码,实现后的代码如下所示:
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
之所以出现这个错误,是由于 Bromise 的延迟绑定导致的,在调用到 onResolve_ 函数的时候,Bromise.then 还没有执行,所以执行上述代码的时候,当然会报“onResolve_ is not a function“的错误了。
也正是因为此,我们要改造 Bromise 中的 resolve 方法,让 resolve 延迟调用 onResolve_。
要让 resolve 中的 onResolve_ 函数延后执行,可以在 resolve 函数里面加上一个定时器,让其延时执行 onResolve_ 函数,你可以参考下面改造后的代码:
function resolve(value) {
setTimeout(()=>{
onResolve_(value)
},0)
}
上面采用了定时器来推迟 onResolve 的执行,不过使用定时器的效率并不是太高,好在我们有微任务,所以 Promise 又把这个定时器改造成了微任务了,这样既可以让 onResolve_ 延时被调用,又提升了代码的执行效率。这就是 Promise 中使用微任务的原由了。