在实际工作中遇到的异步很多,大概就是以下几类:
- ajax网络请求
- 事件触发
setTimeout/setInterval函数
1.以ajax请求为例
在ajax的请求中将探索出promise的前世今生。首先来看下jquery1.5之前的ajax请求写法
$.ajax({
url: '地址',
data: {name: 'tom'},
success: function(data){
console.log(data)
},
error: function(error){
console.log(error)
}
})
熟悉的配方,熟悉的味道。success的回调中我们处理请求返回的数据。在此试想下,如果请求完成后success中要处理复杂的逻辑呢?比如再次发起请求。难道真要写成类似这样
$.ajax({
url: '地址',
success: function(data) {
$.ajax({
url: '地址',
success: function(data) {
console.log(data)
}
})
}
})
简直不能忍。但1.5版本之后的jquery你可以这么写
var ajax = $.ajax('data.json')
ajax.done(function () {
console.log('success 1')
})
.fail(function () {
console.log('error')
})
.done(function () {
console.log('success 2')
})
console.log(ajax) // 返回一个 deferred 对象
采用done和fail的链式写法,当然也可以采用then()的写法,是不是突然发现和promise有些类似。但是ajax是如何做到可以链式操作的呢?Deferred对象的封装!
第一步:写个传统的异步操作
var wait = function(ms){
setTimeout(function(){
console.log('a')
},ms)
}
wait(100) // 100ms后会打印出 a
第二步:对这个异步操作进行更复杂的封装,让其返回Deferred对象(这里先不讨论Deferred对象如何封装的),因为Deferred对象可以then,then可以进行链式操作。
var waitHandle = function () {
var dtd = $.Deferred()
var wait = function (dtd) {
setTimeout(() => {
console.log('执行完成')
dtd.resolve()
},1000)
return dtd
}
return wait(dtd)
}
var w = waitHandle()
w.then(() => {
console.log('ok1')
}).then(() => {
console.log('ok2')
})
如此封装之后,最大的变化在于我们可以用同步的思维和代码去处理异步的操作,回调地狱将不复存在。原来的callback方式就像套娃,处理异步的方式是代码层层的嵌套。但是现在来看,虽然callback的本质没有变化,但是callback的方式确像是链条,通过 .then 可以不断的链接。
下面正式介绍一下es6的promise:
2.Promised 的探索
Promise对象是es6提供的一个原生对象,是处理异步操作的一种解决方案。
promise的状态:
promise有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败);三种状态不受外界干扰,并且状态一旦发生变化,无法逆转;状态只能由 pending -> fufilled 或者 pending -> rejected 没有第三种情况。
基本情况了解后,设想一个开发中常用的场景 还以ajax请求接口为例子,promise如何封装?
let httpPromise = function () {
return new Promise((resolve,reject) => {
$.ajax({
url: 'url',
data: {name: 'tom'},
success: function(res){
console.log(res)
resolve(res)
}
})
})
}
httpPromise().then(res => {
// 进行请求结果的处理
console.log(res)
})
** 这里只是做一个案例而已,实际应用中 axios jquery 等http请求都可以用then进行处理,不必封装
到目前为止,promise的优势好像并没有发挥出来。再设想一个更困难的应用场景: 有3个api接口 A_api, B_api, C_api,现在需求是要拿着A_api接口的返回结果作为参数去请求B_api,要拿这B_api接口的返回结果作为参数去请求C_api,看用promise如何来封装?
let httpPromise = function (url,data) {
return new Promise((resolve,reject) => {
axios({url,data}).then(res => {
resolve(res)
})
})
}
httpPromise('A_api',{}).then(res => {
console.log(res) // A_api的接口的返回值
return httpPromise('B_api',{data: res}) // a接口返回值作为参数传入
}).then(res => {
console.log(res) // B_api接口的返回值
return httpPromise('C_api',{data: res}) // a接口返回值作为参数传入
}).then(res => {
console.log(res) // c接口的返回值
})
可以看到在代码的写法上已经没有了层层嵌套的callback,而是采用了更加直观的.then的链条方式。 在此还要介绍一个重要问题,就是整个promise和then的链条中参数的传递问题:
promise中的参数传递问题
- 同一个promise中的then可以进行链式调用。
promise中resolve(res)res会作为参数传入紧跟着这个promise的then中。 - then中参数传递,上一个
then中通过return resres会作为参数传入下一个then中 - then中参数传递,上一个
then中return promise时,下一个then方法就是基于上个then中的`promise
有了此参数的传递规则,上面那个案例的理解就更加清晰了。当然promise的功能远不止于此,下面介绍下promise中常用的几个api:
Promise.prototype.catch()
promise中错误捕获:
let promise = function () {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve('a')
},1000)
})
}
promise.then(res => {
console.log('res:',res)
}).catch(err => {
console.log('err:',err)
})
catch可以捕获的错误包括:
- 封装的promise内部状态由于错误触发reject
- 执行then中的回调函数出现的错误
Promise.all()
:用于将多个 Promise 实例,包装成一个新的 Promise 实例。试想如下场景: 有aApi,bApi,cApi三个接口,cApi接口的请求参数就是aApi和bApi接口的返回结果的组合,我们如何以最快的时间完成这个任务。
let http = function () {
return new Promise((resolve,reject) => {
axios({url}).then(res => {
resolve(res)
})
})
}
Promise.all([http('aApi'),http('bApi')]).then(res => {
console.log(res) // 数组类型,接受了两个接口返回值
return http('cApi',res)
}).then(res => {
console.log(res) // cApi接口的返回值
})
- promise.all()方法接受一个数组作为参数,数组的每个元素都是一个promise的实例
- promise.all数组参数中,每个promise实例的状态都变为 fulfilled 时,整个大的实例的状态才变为 fulfilled
- promise.all数组参数中,当有一个promise的实例状态变为 rejected 时,整个大的实例的状态就会变为 rejected
Promise.race()
:也是用于将多个 Promise 实例,包装成一个新的 Promise 实例。 与promise.all的区别就是当接受的数组实例参数中,有一个实例的状态发生变化,就会触发整个实例的状态变化。 *** 目前在业务中还没遇到过类似的业务逻辑,以后遇到再进行补充。