JavaScript的异步编程之Promise

Promise

一种更优的异步编程统一 方法,如果直接使用传统的回调函数去完成复杂操作就会形成回调深渊

 // 回调深渊
$.get('/url1'() => {
$.get('/url2'() => {
$.get('/url3'() => {
$.get('/url4'() => {
$.get('/url5'() => {
// 大概就是这样子的
})
})
})
})
})

CommonJS
社区提出了
Promise
规范,在
ES2015
中被标准化,成为语言规范。当等待状态改编程成功或者失败之后就再也不能再被改变了,成功的时候触发
onFulfilled
回调,失败的时候触发
onRejected
回调

image-20221028232857794

Promise 简单使用

new Promise
传入一个回调函数,这个回调函数两个参数,第一个把
Promise
改成为成功的状态,第二个参数把
Promise
改变成失败的状态,捕获成功和异常可以使用
.then
.catch
方法,这两个方法返回的也是一个
Promise
对象

 // 演示
const promsie = new Promise((resolve, reject) => {
reject(1)
})

promsie.then((value) => {
console.log(value)
}, (err) => {
// end 执行完之后才会执行这个
console.log(err)
})

// end 会先执行
console.log('end')

不管

Promise
中有没有异步操作,then方法中的回调函数依然会进入回调队列中排队,会等同步代码执行完之后才会执行

Promise
写一个请求函数

 function ajax (url) {
return new Promise((resove, reject) => {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
// 新方法可以直接接受一个j对象
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resove(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

ajax('/json1.json').then(ret => {
console.log(ret)
}).catch(err => {
console.log(err)
})

如果需要多个连续的请求可以使用链式调用

 ajax('/json1.json').then(ret => {
return ajax('/json2.json')
}).then(ret => {
return ajax('/json3.json')
}).then(ret => {
return ajax('/json4.json')
})

这种链式调用是不是很熟悉,在

jqeury
中也有链式调用,
jquery
中是返回了本身这个对象所以可以实现链式调用,那么在
Promise
中是不是这样呢

 let promsie1 = ajax('/json1.json')

let promise2 = promsie1.then(ret => {
console.log(ret)
}).catch(err => {
console.log(err)
})

console.log(promsie1 === promise2) // false

let a = $("body").attr('class', 'body')
let b = a.prop('disabled', true)
console.log(a === b) // true

经过测试发现,

Promise
返回的是一个全新的
Promise
对象,返回全新的
Promise
对象的目的就是为了实现
Promise
的链条,每个
.then
方法负责不同的任务,互不干扰,如果不断的链式调用
then
方法,这里的每个
then
方法都在为上一个
then
方法返回的
Promise
对象去添加状态明确后的回调,这些
Promise
会依次执行,而且我们可以在
then
方法中去手动返回一个
Promise
回调。如果
then
方法中的回调函数返回了值,则会给下一个
then
方法的回调函数传递这个返回的值,如果没有返回那么默认返回的就是
undefined
总结一下就是

  • Promise
    对象的
    then
    方法会返回一个全新的
    Promise
    对象
  • 后面的
    then
    方法就是在为上一个
    then
    返回的
    Promise
    注册回调
  • 前面的
    then
    方法中的回调函数的返回值回作为后面
    then
    方法回调的参数
  • 如果回调中返回的是
    Promise
    , 那后面的
    then
    方法的回调会等待他的结束

捕获异常

onRejected
回调会在
Promise
执行异常或者抛出的异常时触发, 捕获异常有两种方式,第一种,
then(成功处理的回调函数, 异常处理的回调函数)
then
方法中传递两个回调函数,第二种用
.catch
方法去捕获异常,
catch
方法其实就是
then
方法的别名,相当于
then
方法第一个参数传
undefined

 // then(成功处理的回调函数, 异常处理的回调函数)
ajax('/json1.json').then(ret => {
console.log(err)
}, err => {
console.log(err)
})


// catch
ajax('/json1.json').then(ret => {
console.log(err)
}).catch(err => {
console.log(err)
})
// catch
ajax('/json1.json').then(ret => {
console.log(err)
}).then(undefined,err => {
console.log(err)
})

这两种方式还是有很大的差异,

catch
其实是在给上一个
then
返回的
Promise
捕获异常,但是如果是同一个链条下的
Promise
的错误会向下传递直到有
catch
方法捕获,而
then
方法传递两个回调函数的捕获异常的方式只会捕获谁上一个
Promise
的错误

 ajax('/json1.json').then(ret => {
console.log(ret)
}).then(undefined, err => {
console.log(err)
}).then(ret => {
console.log(ret)
}).then(ret => {
console.log(ret)
})

// catch 捕获异常
ajax('/json1.json').then(ret => {
console.log(ret)
}).catch(err => {
// 这里能捕获之前的所有Promise的异常
})

// 传递then 第二个参数捕获异常
ajax('/json1.json').then(ret => {
console.log(ret)
}).then(undefined, err => {
console.log(err)
throw new Error('故意的异常')
}, (err) => {
// 这里能捕获故意的错误
}).then(ret => {
console.log(ret)
}).then(ret => {
console.log(ret)
}).catch(err => {
// 这个时候已经捕获不到异常了,因为上一个故意的异常已经被捕获了,根据then方法会返回一个Promise所以捕获异常之后会返回一个成功的Promise
})

还可以全局捕获异常, 这种全局方式捕获异常是不推荐使用的,应该在代码块中明确的去捕获对应的异常

 // 浏览器环境中
window.addEventListener('unhandledrejection', event => {
console.log(event.reason, event.promise)
// reason 失败原因,
// promise 失败的Promise
event.preventDefault()
}, false)

// nodejs中
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise)
// reason 失败原因,
// promise 失败的Promise
})

如果需要无论成功和错误都需要执行则可以用

finally
来实现

 ajax('/json1.json')
.then(ret => {
console.log('成功执行这个')
}).catch(err => {
console.log("失败执行这个")
})
.finally(function() {
console.log("成功和失败都会执行这个")
});

Promise 静态方法

Promise.resolve

快速的一个值转化为一个

Promise
对象, 这种方式和
new Promise
返回一个值是等价的

 Promise.resolve({
data: "hahah"
})

new Promise((resolve) => {
resolve({
data: "hahah"
})
})

如果传入的是一个

Promise
对象会原封不动的把这个对象返回

 function ajax (url) {
return new Promise((resove, reject) => {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
// 新方法可以直接接受一个j对象
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resove(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

let promise1 = ajax('/url')

let promise2 = Promise.resolve(promise1)

console.log(promise1 === promise2) // true

如果传入的是一个对象,并且这个对象也有一个跟

Promise
一样的
then
方法,也就是说这个方也也可以接收到
onFulfilled, onRejected
两个回调,并且可以调用回调传递参数,这种有
then
方法的对象实现了一个
thenable
的接口,支持这种对象的原因是因为原生
Promise
还没有被普及之前,很多时候都是第三方的库实现的
Promise

 Promise.resolve({
then (onFulfilled, onRejected) {
onFulfilled('123')
}
}).then(ret => {
console.log(ret) // 123
})

Promise.reject

快速创建一个一定是失败的

Promise
对象,这个方法的参数就是
Promise
失败的原因

 Promise.reject("嘿嘿,这就是错误的理由").catch(err => {
console.log(err) // 嘿嘿,这就是错误的理由
})

Promise.all

接收一个数组,这些元素都是一个

Promise
对象,这个方法会返回一个全新的
Promise
对象,当内部所有
Promise
的都完成之后
Promise.all
返回的
Promise
对象才会完成。这个时候
Promise.all
返回的
Promise
对象拿到的结果是一个数组,这个数组中包含了每一个
Promise
返回的结果。值得注意的是只有数组中的所有
Promise
都成功了结束了,
Promise.all
返回的
Promise
对象才会成功结束。如果数组中有一个
Promise
失败的结束了,那么
Promise.all
返回的
Promise
对象也会以失败的结束

 Promise.all([
ajax('/url1'),
ajax('/url2'),
ajax('/url3'),
ajax('/url4'),
]).then(values => {
console.log(values)
}).catch(err => {
console.log(err)
})

Promise.race

Promise.all
方法一样也是接收一个数组,这些元素都是一个
Promise
对象,这个方法会返回一个全新的
Promise
对象,但是与
Promise.all
方法不同的是
Promise.all
是等待所有任务的结束而结束,
Promise.race
只会等待第一个结束的任务而结束

 const request = ajax('/api/???')
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject('timeout'), 5000);
})

Promise.race([
request,
timeout
]).then(ret => {
console.log(ret)
}).catch(err => {
console.log(err)
})

上面代码中,如果接口在5秒之前接口返回了,那么我们可以正常的得到返回结果,如果5秒还没有返回,那么请求就没有办法把结果返回回来了,因为

timeout
这个
Promise
会在5秒后以失败的方式结束,而
Promise.race
就是以第一个结束的
Promise
而结束

Promise.allSettled

Promise.all、Promise.race
方法一样也是接收一个数组,这些元素都是一个
Promise
对象,这个方法会返回一个全新的
Promise
对象,与他们不同的是无论这些
Promise
执行是成功还是失败都是等这些
Promise
都完成了之后才会完成,当有多个彼此不依赖的异步任务成功完成时,或者总是想知道每个
promise
的结果时,通常使用它

 const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises).
then((results) => results.forEach((result) => console.log(result.status)));

// > "fulfilled"
// > "rejected"

Promise.any

Promise.race
方法一样也是接收一个数组,这些元素都是一个
Promise
对象,这个方法会返回一个全新的
Promise
对象,不同的是只要有一个
Promise
执行是成功的就算成功,只有全部都失败了才会失败。这个全新的
Promise
onFulfilled
的回调函数的参数为第一个成功完成的
Promise
所传递的数据

 const alwaysError = new Promise((resolve, reject) => {
reject("失败就失败下一个成功");
});

const two = new Promise((resolve, reject) => {
setTimeout(resolve, 30, "我是第二个完成的Promise");
});

const three = new Promise((resolve, reject) => {
setTimeout(resolve, 70, "我是第三个个完成的Promise");
});

const one = new Promise((resolve, reject) => {
setTimeout(resolve, 10, "我是最先完成的Promise");
});

Promise.any([two, three, alwaysError, one]).then((value) => {
console.log(value); // 我是最先完成的Promise
// 这个value是最先完成的Promise传递的值也就是=>我是最先完成的Promise
})

Promise 执行时序问题

宏任务,微任务

测试执行顺序

 console.log('global start')

Promise.resolve().then(ret => {
console.log('promise')
})

console.log('global end')
// outlog
// 1. global start
// 2. global end
// 3. promise

链式调用多个执行看执行顺序

 console.log('global start')

Promise.resolve().then(ret => {
console.log('promise1')
}).then(ret => {
console.log('promise2')
}).then(ret => {
console.log('promise3')
})

console.log('global end')

// outlog
// 1. global start
// 2. global end
// 3. promise1
// 4. promise2
// 5. promise3

加入

setTimeout

 console.log('global start')

setTimeout(() => {
console.log('settimeout')
}, 0);

Promise.resolve().then(ret => {
console.log('promise1')
}).then(ret => {
console.log('promise2')
}).then(ret => {
console.log('promise3')
})

console.log('global end')
// 1. global start
// 2. global end
// 3. promise1
// 4. promise2
// 5. promise3
// 6. settimeout

没想到吧,

Promise
的异步时序执行优点特殊。举个例子、假如我们去银行ATM办理存款,办完之后突然想起要转一笔账,这时候肯定会直接办理转账业务,不会到后面重新排队再转账。这个例子中我们排队就像在
javascipt
中的等待执行的任务一样,我们队伍中的每一个人都对应着回调回列中的一个任务、。回调队列中任务称之为
宏任务
,而宏任务执行过程中可以临时加上一些额外需求,这些额外的需求可以选择作为一个新的宏任务进行到队列中排队。上面的
setTimeout
就会作为宏任务再次到回调队列中排队,也可以跟我们刚的例子一样作为当前任务的
微任务
直接在当前任务结束之后立即执行。
Promise
的回调会作为微任务执行,会在本轮调用的末尾去执行,所以说上面代码会先打印
promise1,promise2,promise3
在打印
settimeout

微任务
是在后来才被引入到
js
中的,他的目的是为了提高整体的响应能力,目前的绝大多数异步调用都是作为宏任务执行。
Promise、MutationObserver
nodejs
中的
process.nextTick
会作为微任务在本轮调用的末尾执行

更多内容微信公众号搜索

充饥的泡饭

小程序搜一搜
开水泡饭的博客

标签: Javascript

添加新评论