前端战五渣学JavaScript——Promise
我是要成为海贼王的男人
悟空已成神,鸣人已成影,待路飞成王之时,便是我青春结束时!
悟空陪布玛找寻龙珠,一路拳打比克、斩弗利萨,生个儿子战沙鲁,最后净化布欧,只因承诺要保护地球。鸣人“有话直说,说到做到,这就是我的忍道”,一句会把佐助带回来的承诺,断臂践行。路飞要凑齐10个船员,成为海贼王,我们相信路飞一定会成王,因为我们相信他的承诺。
我为什么说承诺呢,今天主题不是Promise
吗,因为⬇️
回调地狱 Callback Hell
如果看这篇文章的你是有过项目经验的,应该都遭遇过这惨绝人寰的“回调地狱”。“回调地狱”并不是JS或者编程语言中的一种形式,只是大家把这种编程中遇到的现象、问题预定俗称的调侃成“回调地狱”。因为只要陷进去,就很难出来。并且回调地狱在代码层级上会越陷越深,逻辑看着会非常会乱,如下代码⬇️
// 我们用setTimeout模拟线上发送请求等异步执行的函数
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
}, 1000)
}, 1000)
}, 1000);
这是三个回调函数嵌套,延迟一秒后输出1,再过一秒输出2,再过一秒输出3。当然现实项目中,每个函数里面处理的逻辑肯定不仅仅只是输入一个数字这么简单,当我们回调嵌套很多的时候,如果产品提出的一个需求我们需要更改执行顺序,这个时候我们会发现嵌套逻辑复杂到难以简单的更改顺序,严重的只能重新写这段的逻辑代码。并且回调函数让逻辑很不清晰。
后来就有人提出了Promise
概念,这个概念意在让异步代码变得非常干净和直观。
Promise 这就是我的忍道
这个概念并不是ES2015首创的,在ES2015标准发布之前,早已有
Promise/A
和Promise/A+
等概念的出现,ES2015中的Promise
标准便源自于Promise/A+
。Promise
最大的目的在于可以让异步函数变得竟然有序,就如我们需要在浏览器中访问一个JSON座位返回格式的第三方API,在数据下载完成后进行JSON解码,通过Promise
来包装异步流程可以使代码变得非常干净。———————摘自《实战ES2015》
上面最重要的一句就是可以让异步函数变得竟然有序,可能有人会说await
和async
也可以让异步函数同步执行,但是await
操作符本来就是用于等待一个Promise对象的。
我们先来看一下Promise
是怎么解决上面回调地狱这样的难题的⬇️
// 封装一层函数
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
// 按回调函数的逻辑执行
timeout().then(() => {
console.log(1);
return timeout()
}).then(() => {
console.log(2);
return timeout()
}).then(() => {
console.log(3);
});
我们按照回调函数的逻辑用Promise
重新写了一遍,执行结果一样,我们可以看出来,相比回调函数的层级深入,使用Promise
以后函数的层级明显减少了,逻辑清晰许多。
下面我们来从头开始认识Promise
Promise基础
想要给一个函数赋予Promise
的邓丽,就要先创建一个Promise
对象,并将其作为函数值返回。Promise
构造函数要求传入一个函数,并带有resovle
和reject
参数。一个成功回调函数,一个失败成功回调函数。下面是Promise
对象的三个状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
三个状态的转换关系是从pending -> fulfilled
或者pending -> rejected
,并且状态改变以后就不会再变了。pending -> fulfilled
以后会去执行传入Promise
对象的resovle
函数,对应的,pending -> rejected
以后会去执行传入Promise
对象的reject
函数。
.then()
那resovle
函数和reject
函数是怎么传进去的呢,当然就是之前说的.then()
,.then()
可以接收两个参数,.then(onFulfilled[, onRejected])
这是官方写法,其实就是.then(resovle, reject)
,第一个参数是成功回调,第二个参数就是失败回调。如下⬇️
function timeout(isSuccess) {
return new Promise((resolve, reject) => {
if (isSuccess) {
setTimeout(resolve, 1000)
} else {
reject()
}
})
}
timeout(true).then(() => {
console.log('成功')
}, () => {
console.log('失败')
});
timeout(false).then(() => {
console.log('成功')
}, () => {
console.log('失败')
});
我用if语句模拟一下成功和失败的场景,这就是.then()
的用法。
.catch()
刚才说了.then()
的第二个参数传进去的是一个失败回调的函数,但是Promise
还有一个.catch()
的方法,也是用来处理失败的,例子如下⬇️:
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
timeout().then(() => {
throw new Error('因为被凯多打败了,所以没当上海贼王')
}).catch((err) => {
console.log('失败原因:', err)
});
这时候也会输出错误信息。这时候你可能会问,那.then(resovle, reject)
的reject
和.catch(reject)
有什么区别呢,下面是个人见解
.then(resovle, reject)
的reject
和.catch(reject)
有什么区别
我个人认为,.then(resovle, reject)
的reject
按就近原则,只对最近的这个异步函数进行错误处理,但是对以后的或者之前的异步函数不做处理,而.catch(reject)
会捕获到全局所有链式上异步函数的错误。链式调用下面会讲到。总之就是.catch(reject)
管的范围要大一些。
链式调用
Promise
有一个对象链,并且这个对象链式呈流水线的模式进行作业,是因为在Promise
对象对自身的onFulfilled
和onRejected
相应器的处理中,会对其中返回的Promise
对象进行处理。其中内部会将这个心的Promise
对象加入到Promise
对象链中,并将其暴露出来,使其继续接受新的Promise
对象的加入。只有当Promise
对象链中的上一个Promise
对象进入成功或者失败阶段,下一个Promise
对象菜户被激活,这就形成了流水线的作业模式。
这就好比一开始使用Promise
改造回调地狱函数时候的样子⬇️
// 封装一层函数
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
// 按回调函数的逻辑执行
timeout().then(() => {
console.log(1);
return timeout()
}).then(() => {
console.log(2);
return timeout()
}).then(() => {
console.log(3);
});
可以一层一层的传一下去,这也是厉害的地方。当链式调用中用.catch()
捕获错误的时候是这样的⬇️
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
timeout()
.then(() => {
console.log(1);
return timeout(err)
})
.then(() => {
throw new Error('发生错误了')
return timeout(2)
})
.catch((err) => {
console.log('123',err)
})
.then(() => {
console.log(3);
});
这种情况,.catch()
紧跟在抛出错误的一步函数后面,会抛出错误,然后继续往下执行,但是如果.catch()
是在最后,结果就完全不一样了⬇️
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
timeout()
.then(() => {
console.log(1);
return timeout(err)
})
.then(() => {
throw new Error('发生错误了')
return timeout(2)
})
.then(() => {
console.log(3);
})
.catch((err) => {
console.log('123',err)
});
如果是这样,前面说了.catch()
会捕获全局错误,但是,.catch()
写在最后,抛出错误以后,函数会直接跳到.catch()
然后继续往下执行,就像下面代码⬇️
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
timeout()
.then(() => {
console.log(1);
return timeout()
})
.then(() => {
console.log(11);
throw new Error('发生错误了')
return timeout()
})
.then(() => {
return timeout(2)
})
.catch((err) => {
console.log('2',err)
})
.then(() => {
throw new Error('发生错误了2')
console.log(3);
})
.catch((err) => {
console.log('3',err)
});
上面这段代码就会直接跳过输出2的异步函数,直接走到第一个.catch()
,然后再往下执行。
Promise高级
Promise.all()
这个方法真的太实用了,比如你进入首页,需要同时请求各种分类,用户信息等等信息,咱们可能需要在所有的请求都回来以后再展示页面,因为我们不能确定每个请求都要多久才能请求回来,所以这个问题一度很难解决。现在有了Promise.all()
这个方法,真的太方便了,下面就是例子⬇️
// Promise.all()需要传入的就是一个数组,每一项就是每一个异步函数
function timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(resolve, delay * 1000)
})
}
Promise.all([
timeout(1),
timeout(3),
timeout(5),
]).then(() => {
console.log('都请求完毕了!')
});
上面代码会在最大延迟的5秒后然后在执行.then()
的方法,当然还有一个差不多的函数,往下看
Promise.race()
Promise.race()
会监听所有的Promise
对象,在等待其中的第一个进入完成状态的Promise
对象。一旦有第一个Promise
对象进入了完成状态,该方法返回的Promise
对象便会根据这第一个完成的Promise
对象的状态而改变,如下⬇️
function timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(resolve, delay * 1000)
})
}
Promise.race([
timeout(1),
timeout(3),
timeout(5),
]).then(() => {
console.log('有一个请求已经结束!')
});
上面代码在执行1秒后就会执行.then()
的方法,然后剩下的两个请求继续等待返回。
反正我也没遇到过什么使用场景,知道有这个方法就行了
只管把目标定在高峰,人家要笑就让他去笑!
写到后面有点太官方的感觉,但是又觉得很不好解释,只能堆例子来解释了,跟大佬的差距还是有一定的差距,这只是基于我现在的水平到目前为止对Promise
的理解。
一句承诺,就要努力去兑现。自己选择的路,跪着也要走完。
我是前端战五渣,一个前端界的小学生。