JavaScript是??典型的异步编程脚本语?,在编程过程中会?量的出现异步代码的编写,在JS的整个发展历程中,对异步编程的处理?式经历了很多个时代,其中最典型也是现今使?最?泛的时代,就是Promise对象处理异步编程的时代。
异步编程是相对同步编程来说的,开发项目时,开发者总是希望,程序的执行顺利能按照编程的顺序从上至下执行,这样符合人的思维易于理解,但是现实开发中,一般都事与愿违,相信每个开发者或多或少遇到过,程序执行到某些复杂的、耗时的任务时,往往要开启异步任务去执行,这样就不会阻塞当前任务,让当前任务继续执行,当异步任务执行完后,再将异步任务执行完的结果传给当前任务使用。所以异步编程主要为提高程序整体的执行效率。
那么,Promise对象到底是什么呢?
长期以来,异步操作一直是JavaScript语言的痛点。在该语言的早期版本中,异步操作仅支持定义回调函数以指示异步操作已完成。异步行为的执行是一个常见的问题,通常可以通过一个充满嵌套回调函数的代码片段来解决,该代码片段通常称为“回调地狱”。
在过去的编程中JavaScript的主要异步处理?式,是采?回调函数的?式来进?处理。在前端开发过程中使?最多的异步流程就是AJAX请求,当系统中要求某个??的n个接?保证有序调?的情况下就会出现下?的情况。看如下代码:
//获取类型数据
$.ajax({
url: '/***',
success: function(res) {
const xId = res.id;
//获取该类型的数据集合,必须等待回调执?才能进?下?步
$.ajax({
url: '/***',
data: {
//使?上?个请求结果作为参数调?下?个接?
xxId: xId,
},
success: function(res1) {
const xxId = res1.id;
//得到指定类型集合
$.ajax({
url:'/***',
data:{
//使?上?个请求结果作为参数调?下?个接?
xxxId: xxId,
},
success:function(res2){
//得到指定类型集合
...
}
})
}
})
}
})
看如上代码,这三个任务必须按先后顺序执?,并且每一个请求执行前都要先拿到上?个请求运?的结果,那么我们不得不将代码编写为以上案例代码。该写法主要是为了保证代码的严格顺序要求,这样就避免不了?量的逻辑在回调函数中不停的进?嵌套,这也是我们经常听说的“回调地狱”。这种情况在很多?的代码中都出现过,如果流程复杂化,在?络请求中继续夹杂其他的异步流程,那么这样的代码就会变得难以维护了。
再举一个我们在开发业务中常见的例子——省市区三级联动。
// 假如接口和参数如下
省级接口:http://localhost:8080/province
市级接口:http://localhost:8080/city(参数:provinceId)
区级接口:http://localhost:8080/area(参数:cityId)
先请求省级接口,获取某个省的编号,在使用省的编号,请求市级接口,获取某个市的编号,最后使用市的编号,请求区县级接口,获取区县的名称。在没有Promise之前,就会出现如下代码:
// 1.请求省级接口,获取省的编号
axios.get('http://localhost:3000/province').then(res => {
for (let item of res.data) {
if (item.provinceId === 'xxx') {
// 2.使用省的编号,请求市级接口, 获取市的编号
axios.get(`http://localhost:3000/city?provinceId=${item.provinceId}`).then(res => {
for (let item of res.data) {
if (item.cityId === 'xxx') {
// 3.使用长沙市的编号,请求区级接口,获取岳麓区的名称
axios.get(`http://localhost:3000/area?cityId=${item.cityId}`).then(res => {
for (let item of res.data) {
if (item.countyId == 'xxx') {
console.log(item.countyName )
}
}
})
}
}
})
}
}
})
其它的例子诸如Node中的原始fs模块,操作?件系统等场景就不再一一列举了。由此,之所以在ECMA提案中出现Promise解决?案,就是因为此类代码导致了JS在开发过程中遇到的实际问题:回调地狱。当然解决回调地狱还有其他?案,本篇文章核?介绍Promise流程控制对象,因为它是解决回调地狱?常好的?案。
在介绍Promise前,我们先了解一个重要的概念——回调函数!JavaScript语?中,有?个特殊的函数叫做回调函数。回调函数的特点是把函数作为变量看待,由于JavaScript变量可以作为函数的形参并且函数可以通过声明变量的?式匿名创建,所以我们可以在定义函数时将?个函数的参数当作函数来执?,进?在调?时在参数的位置编写?个执?函数。
//把fn当作函数对象那么就可以在test函数中使?()执?他
function test(fn){
fn()
}
//那么运?test的时候fn也会随着执?,所以test中传?的匿名函数就会运?
test(function(){
...
})
上?的代码结构,就是JavaScript中典型的回调函数结构。按照我们在事件循环中介绍的JavaScript函数运?机制,会发现其实回调函数本身是同步代码,这是?个需要重点理解的知识点。
通常在编写JavaScript代码时,使?的回调嵌套的形式?多是异步函数,所以?些开发者可能会下意识的认为,凡是回调形式的函数都是异步流程。其实并不是这样的,真正的解释是:JavaScript中的回调函数结构,默认是同步的结构,由于JavaScript单线程异步模型的规则,如果想要编写异步的代码,必须使?回调嵌套的形式才能实现,所以回调函数结构不?定是异步代码,但是异步代码?定是回调函数结构。
那么为什么异步流程都需要回调函数?
请看如下代码,想一想输出的顺序是什么?
function test(fn) {
fn()
}
console.log(1)
test(function() {
console.log(2)
})
console.log(3)
很显然,这段代码的输出顺序应该是1、2、3,因为它属于直接进?执?栈的程序,会按照正常程序解析的流程输出。
上面的代码我们变换成如下代码,它的输出的顺序又是什么呢?
function test(fn) {
setTimeout(fn, 0)
}
console.log(1)
test(function() {
console.log(2)
})
console.log(3)
这段代码会输出1、3、2,因为在调?test的时候settimeout将fn放到了异步任务队列挂起了,等待主程序执?完毕之后才会执?。
再思考?个问题,如果我们有?个变量a的值为1,想要1秒之后设置他的值为2,并且我们想要在之后得到a的新结果,这个逻辑中如果1秒之后设置a为2采?的是setTimeout,那么我们在同步结构?能否实现?
let a = 1;
setTimeout(() => {
a = 2
}, 1000)
console.log(a)
上述代码块输出的结果?定是1,根据JavaScript单线程异步模型的知识,可以得知,当前的代码块中setTimeout的回调函数是?个宏任务,会在本次的同步代码执?完毕后执?,所以声明a=1和输出a的值这两?代码会优先执?,这时对a设置为2的事件还没有发?,所以输出的结果就?定为1。
那么,上述的问题怎么才能实现呢?接下来对代码做如下改造,我们试图使?阻塞的?式来获取异步代码的结果。看如下代码:
let a = 1;
//依然使?setTimeout设置1秒的延迟设置a的值
setTimeout(function(){
a = 2
},1000)
let d = new Date().getTime()
let d1 = new Date().getTime()
//采?while循环配合时间差来阻塞同步代码2秒
while(d1-d<2000){
d1 = new Date().getTime()
}
console.log(a)
上面的同步代码会在while循环中阻塞2秒,所以console.log(a)这?代码会在2秒之后才能获得执?资源,但是最终输出的结果仍然是1。这是为什么呢?这?仍然可以通过JavaScript的运?模型来进?理解,由于单线程异步模型的规则是严格的同步在前异步靠后顺序,本案例的同步代码虽然阻塞了2秒,已经超过了setTimeout的等待时间,但是setTimeout中的宏任务到时间后,仅仅会被从?作线程移动到任务队列中进?等待。在时间到达1秒时,while循环没有执?结束,所以函数执?栈会被继续占?,直到循环释放并输出a之后,任务队列中的宏任务才能执?,所以这?就算setTimeout时间到了,也必须等待同步代码执?完毕,那么输出a的时候a=2的事件仍然没有发?,所以我们采?默认的上下结构永远拿不到异步回调中的结果,这也是为什么异步流程都是回调函数的原因。
据此我们可以知道想要真正的在2秒后获取a的新结果的代码结构是这样的:
//只有在这个回调函数中才能获取到a改造之后的结果
let a = 1;
setTimeout(function() {
a = 2;
}, 1000)
//注册一个新的宏任务,让它在上一个宏任务后执
setTimeout(function() {
console.log(a)
}, 2000)
到这?也就?概明?了回调函数的意义以及使?场景了。接下来就详细介绍Promise是什么以及使?Promise如何解决异步控制问题,而且Promise它是?个及特殊的存在,Promise中既包含同步的回调函数,?包含异步的回调函数。
从上?的案例介绍得知Promise的作?是解决“回调地狱”,它的解决?式是将回调嵌套拆成链式调?,这样便可以按照上下顺序来进?异步代码的流程控制。那么Promise是什么以及如何实现这个能?的呢?
MDN上是这样解释的:Promise是一个对象,它代表了一个异步操作的最终完成或者失败。本质上Promise是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要一开始把回调函数作为参数传入这个函数了。
1、构造 Promise
Promise对象是?个JavaScript对象,在?持ES6语法的运?环境中作为全局对象提供,初始化?式如下:
new Promise(function (resolve, reject) {
// 要做的事情...
});
我们先对Promise做?个简单的介绍:Promise对象的主要?途是通过链式调?的结构,将原本回调嵌套的异步处理流程,转化成“对象.then().then()…”的链式结构,这样虽然仍离不开回调函数,但是将原本的回调嵌套结构,转化成了连续调?的结构,这样就可以在阅读上编程上下左右结构的异步执?流程了。
看如下代码:
setTimeout(function(){
//第?秒后执?的逻辑
console.log('第?秒之后发?的事情')
setTimeout(function(){
//第?秒后执?的逻辑
console.log('第?秒之后发?的事情')
setTimeout(function(){
//第三秒后执?的逻辑
console.log('第三秒之后发?的事情')
},1000)
},1000)
},1000)
上?的代码,分3秒每间隔1秒运?1个任务,这三个任务必须按时间顺序执?,并且每个下?秒触发前都要先拿到上?秒运?的结果。现在用 Promise 来实现同样的功能。
//使?Promise拆解的setTimeout流程控制
const p = new Promise(function(resolve){
setTimeout(function(){
resolve()
},1000)
})
p.then(function(){
//第?秒后执?的逻辑
console.log('第?秒之后发?的事情')
return new Promise(function(resolve){
setTimeout(function(){
resolve()
},1000)
})
}).then(function(){
//第?秒后执?的逻辑
console.log('第?秒之后发?的事情')
return new Promise(function(resolve){
setTimeout(function(){
resolve()
},1000)
})
}).then(function(){
//第三秒后执?的逻辑
console.log('第三秒之后发?的事情')
})
通过如上代码我们发现使?了Promise后的代码,将原来的3个setTimeout的回调嵌套,拆解成了三次then包裹的回调函数,按照上下顺序进?编写。这样我们从视觉上就可以按照?类的从上到下从左到右的线性思维来阅读代码,这样很容易能查看这段代码的执?流程,代价是代码的编写量增加了接近1倍。
2、Promise 的构造函数
Pomise对象相当于?个未知状态的对象,它的定义就是声明?个等待未来结果的对象,在结果发?之前他?直是初始状态,在结果发?之后他会变成其中?种?标状态,Promise在英?中是绝对保证的意思,所以在编程中Promise对象是?个?常严谨的对象,?定会按照约定执?,除使?不当外,不会出现任务灵异问题。
那么Promise本身具备三种状态:
pending:初始状态,也叫就绪状态,这是在Promise对象定义初期的状态,这时Promise仅仅做了初始化并注册了他对象上所有的任务。
fulfilled:已完成,通常代表成功执?了某?个任务,当初始化函数中的resolve执?时,Promise的状态就变更为fulfilled,并且then函数注册的回调函数会开始执?,resolve中传递的参数会进?回调函数作为形参。
rejected:已拒绝,通常代表执?了?次失败任务,或者流程中断,当调?reject函数时,catch注册的回调函数就会触发,并且reject中传递的内容会变成回调函数的形参。
三种状态之间的关系:
Promise中约定,当对象创建之后同?个Promise对象只能从pending状态变更为fulfilled或rejected中的其中?种,并且状态?旦变更就不会再改变,此时Promise对象的流程执?完成并且finally函数执?。
经过了上?的代码我们可以分析?下Promise的运?流程和结构,?先从运?流程上我们发现了new Promise中的回调函数确实是在同步任务中执?的,其次是如果这个回调函数内部没有执?resolve或者reject,那么p对象的后?的回调函数内部都不会有输出,?运?resolve函数之后.then和.finally就会执?,运?了reject之后.catch和.finally就会执?。
根据上?的分析,结合下?的代码实例来加深一下对Promise规则的了解,分析该对象的运?结果。
//实例化?个Promise对象
const p = new Promise(function(resolve,reject){
})
//通过链式调?控制流程
p.then(function(){
console.log('then执?')
}).catch(function(){
console.log('catch执?')
}).finally(function(){
console.log('finally执?')
})
上?的Promise对象结构,?个Promise对象包含两部分回调函数,第?部分是new Promise时候传?的对象,这段回调函数是同步的,?.then/.catch/.finally中的回调函数是异步的,这?要记好。实际上,在控制台内会发现这段程序并没有任何输出,我们继续往下看。
console.log('起步')
const p = new Promise(function(resolve,reject){
console.log('调?resolve')
resolve('执?了resolve')
})
p.then(function(res){
console.log(res)
console.log('then执?')
}).catch(function(){
console.log('catch执?')
}).finally(function(){
console.log('finally执?')
})
console.log('结束')
上面这段程序运??下会发现输出顺序为: 起步->调?resolve->结束->执?了resolve->then执?->finally执?。
接着我们再看下面这段代码:
console.log('起步')
const p = new Promise(function(resolve,reject){
console.log('调?reject')
reject('执?了reject')
})
p.then(function(res){
console.log(res)
console.log('then执?')
}).catch(function(res){
console.log(res)
console.log('catch执?')
}).finally(function(){
console.log('finally执?')
})
console.log('结束')
上面这段程序运??下会发现输出顺序为: 起步->调?reject->结束->执?了reject->catch执?->finally执?。
欲知后文如何,且听下回分解!!!
分析了Promise的对象结构和状态后,我们了解了Promise的异步回调部分如何执?,取决于我们在初始化函数中的操作,并且初始化函数中?旦调?了resolve后?再执?reject也不会影响then执?,catch也不会执?,反之同理。?在初始化回调函数中,如果不执?任何操作,那么promise的状态就仍然是pending,所有注册的回调函数都不会执?。
写在最后:
一行代码,可能会创造出下一个让人惊叹的产品;一个创新,可能会开启一个全新的科技时代;一份初心,可能会影响到无数人的生活;无论是在大公司工作,还是在小团队奋斗;无论是资深的程序员,还是刚刚入行的新手;每个人的代码,都有力量改变世界。
创作不易,喜欢的老铁们加个关注,点个赞,后面会不定期更新干货和技术相关的资讯,速速收藏,谢谢!你们的一个小小举动就是对小编的认可,更是创作的动力。