前言
Javascript是异步编程语言,所以避免不了回调函数的使用。前端开发中经常遇到下面这样的场景:
$.ajax({
url: '/***',
success: function (res) {
var xxId = res.id
// 下一个接口的参数需要这个ID
$.ajax({
url: '/***',
data: {
xxId: xxId, // 使⽤上⼀个请求结果作为参数调⽤下⼀个接⼝
},
success: function (res1) {
}
})
}
})
使用ajax请求接口的时候,想要保证多个接口按照指定顺序执行,就需要这样编写代码,需要处理过多的异步回调的时候,需要更多层的嵌套,就会形成回调地狱。这时候代码将很难维护。
在ES6中,提出了Promise对象,很好的解决了回调地狱的问题。Promise也是js中使用最广泛的处理异步操作的方法。
关于回调函数
被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数,称为回调函数。
function greeting(name) {
alert('Hello ' + name);
}
function processUserInput(callback) {
var name = prompt('请输入你的名字。');
callback(name);
}
processUserInput(greeting);
同步回调,直接执行。
var a = 0
setTimeout(function () {
a = 1
}, 1000)
// 异步回调
setTimeout(function () {
console.log(a)
}, 2000)
上述代码是异步回调。
JS中的回调函数结构,默认是同步的结构,由于JavaScript单线程异步模型的规则,如果想要编写异步的代码,必须使⽤回调嵌套的形式才能实现,所以回调函数结构不⼀定是异步代码,但是异步代码⼀定是回调函数结构。
Promise
Promise是一个对象,它代表了一个异步操作的最终完成或者失败。
实例化
// 实例化一个Promise对象
let p = new Promise(function (resolve, reject) {
resolve("成功!!!");
// reject("失败");
})
new Promise时,传入的回调函数是同步的。回调函数中调用了resolve(),表示操作成功完成,在链式调用时就会执行.then;如果调用reject(),表示操作失败,在链式调用时就会执行.catch。
链式调用:
p.then(function () {
console.log('then执⾏----', res)
}).catch(function () {
console.log('catch')
}).finally(function () {
console.log('finally执⾏')
})
.then .catch .finally的回调函数都是异步的。.finally无论在操作失败还是成功时都会被调用。
Promise 的三种状态
-
pending:初始状态,这是在Promise对象定义初期的状态,这时Promise仅仅做了初始化并注册了他对象上所有的任务。
-
fulfilled:已完成,通常代表成功执⾏了某⼀个任务,当初始化函数中的resolve执⾏时,Promise的状态就变更为fulfilled,并且then函数注册的回调函数会开始执⾏,resolve中传递的参数会进⼊回调函数作为形参。
-
rejected:已拒绝,通常代表执⾏了⼀次失败任务,或者流程中断,当调⽤reject函数时,catch注册的回调函数就会触发,并且reject中传递的内容会变成回调函数的形参。
三种状态的关系:
Promise中约定,当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中一种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏。
let p = new Promise(function (resolve, reject) {
resolve()
reject()
})
p.then(function (res) {
console.log('then执⾏')
}).catch(function () {
console.log('catch')
}).finally(function () {
console.log('finally执⾏')
})
上述代码执行顺序,then执行 -> finally执行。因为状态一旦变更就不会再改变,resolve() 执行后,状态变为了fulfilled不会再改变。
Promise对象
let p = new Promise(function (resolve, reject) {
resolve("promise对象...")
})
console.log(p)
执行这段代码控制台会输出:
- [[Prototype]] 代表Promise的原型对象
- [[PromiseState]] 代表Promise对象当前的状态,与上面说的三种状态对应
- [[PromiseResult]] 代表Promise对象的值,分别对应resolve或reject传⼊的结果
链式调用
运行下面的代码:
let p = new Promise(function (resolve, reject) {
resolve("我是resolve");
});
console.log(p);
p.then(function (res) {
console.log("1---", res);
})
.then(function (res) {
console.log("2---", res);
return "步骤2 return返回的值";
})
.then(function (res) {
console.log("3---", res);
return new Promise(function (resolve) {
resolve("return new Promise 返回的值");
});
})
.then(function (res) {
console.log("4---", res);
return "步骤4 return返回的值";
})
.then()
.then("不返回值")
.then(function (res) {
console.log("5---", res);
});
观察上面代码在控制台输出:
2022.html:52 1--- 我是resolve
2022.html:55 2--- undefined
2022.html:59 3--- 步骤2 return返回的值
2022.html:65 4--- return new Promise 返回的值
2022.html:71 5--- 步骤4 return返回的值
总结:
- 只要有then()并且触发了resolve,整个链条就会执⾏到结尾,这个过程中的第⼀个回调函数的参数是resolve 传⼊的值
- 后续的函数返回一个普通变量,则这个变量作为下一个then回调函数的参数
- 后续的函数返回一个Promise对象,则这个Promise的resolve的结果作为下一个then回调函数的参数
- 如果没有return 返回,则下一个then回调函数的参数为
undefined - 如果then中传⼊的不是函数或者未传值,Promise链条并不会中断then的链式调⽤,并且在这之前最后⼀次 的返回结果,会直接进⼊离它最近的正确的then中的回调函数作为参数
中断链式调用
有两种方式可以让then链式中断,如下代码:
p.then(function (res) {
console.log(res);
})
.then(function (res) {
//有两种⽅式中断Promise
// throw('throw抛出异常中断')
return Promise.reject("reject中断");
})
.then(function (res) {
console.log(res);
})
.then(function (res) {
console.log(res);
})
.catch(function (err) {
console.log(err);
});
可以使用throw或者return Promise.reject()中断then,从中断开始到catch中间的then都不会执行,最后触发catch函数,流程结束。
Promise相关Api
在说Promise的Api之前,先来解决一下前言中提到的ajax回调地狱的问题,该如何使用Promise解决回调地狱的问题呢?看下面代码:
let p = new Promise(function (resolve, reject) {
$.ajax({
url: "/user",
success: function (res1) {
resolve(res1.id);
},
});
});
// 得到id之后调用下一个接口
p.then(function(xxId){
$.ajax({
url: "/userinfo",
data: {
xxId: xxId, // 使⽤上⼀个请求结果作为参数调⽤下⼀个接⼝
},
success: function (res2) {},
});
})
这样就解决了本文前言中说的回调地狱的问题。下面来看Promise常见的Api:
Promise.all()
根据上面的描述,可以知道,我们可以通过Promise.then来控制异步流程按指定的顺序来执行。假设一个场景,a页面需要调用两个接口都成功返回数据后,再渲染。接口1耗时1s,接口2耗时0.6s,如果使用上面的链式调用就会耗费1.6s的时间才渲染出页面。这时就可使用Promise.all()。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("第⼀个promise执⾏完毕");
}, 1000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("第⼆个promise执⾏完毕");
}, 600);
});
let p = Promise.all([p1, p2])
.then((res) => {
console.log(res);
})
.catch(function (err) {
console.log(err);
});
上述代码在在1s后输出结果,也就是说Promise.all()会等待最慢的接口返回数据后统一处理。只有p1、p2的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2的返回值组成一个数组,传递给p的回调函数。
Promise.race()
race()和Promise.all() 方法一样用于将多个 Promise 实例,包装成一个新的 Promise 实例。
let p = Promise.race([p1, p2])
区别是只要p1、p2之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。
Generator函数
Promise功能很强大,但是如果直接使⽤then()函数进⾏链式调⽤,代码仍然是很臃肿的,想要开发⼀个⾮常复杂的异步流程,依然需要⼤量的链式调⽤进⾏⽀撑。如果能有一种方法更明确的让异步看起来像同步,这应该是很好的结果。
ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。它的存在提供了让函数可以分步执行的能力。
Generator 函数组成
Generator区别于普通函数的地方:
- function 后面,函数名之前有个 *
- 函数内部有 yield 表达式
function* test(){
yield 11;
yield 12;
yield "string";
}
执行机制
调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行。
let gen = test();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
执行代码会输出:
{value: 11, done: false}
{value: 12, done: false}
{value: 'string', done: false}
{value: undefined, done: true}
可以看到next方法会返回一个对象,可以使用next().value取到具体的值。执行完毕后,done会变为true。
异步流程同步化
function* test() {
yield new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("延迟3秒");
resolve();
}, 3000);
});
yield new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("延迟2秒");
resolve();
}, 2000);
});
}
上述代码,按照正常的逻辑来说,应该延迟2秒部分的代码先输出,延迟3秒 后输出。我们要把这段异步流程的代码转化成同步执行,从上到下的顺序执行,可以像下面这样处理:
gen.next().value.then(function () {
gen.next();
});
输出:
2022.html:55 延迟3秒
2022.html:61 延迟2秒
这样就改变了原来的异步执行的顺序,让延迟3秒先输出了。
还可以封装成一个工具函数,来处理更复杂的异步流程,下面是一个简易函数,没有考虑到异常等一些边界情况。
// 一个简易的工具函数
function toSync(gen) {
const item = gen.next();
if (item.done) {
return item.value;
}
const { value, done } = item;
if (value instanceof Promise) {
value.then((e) => toSync(gen));
} else {
toSync(gen);
}
}
使用函数:
toSync(test());
输出的结果和上面一样。
上面所说的内容是JavaScript异步编程的一个过渡期,下面即将说到的async和await是现在处理异步编程最主流的一个方法。
Aync & Await
JS的异步编程是一件比较麻烦的事,人们一直在寻找解决方案。从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。aysnc函数在es8版本中发布,被大多数人认为是异步编程的终极解决方案,也是现在使用最多的。
可以认为async 函数就是 Generator 函数的语法糖。
认识async函数
创建一个函数
async function test() {
return 1;
}
let res = test();
console.log(res);
查看控制台输出:
Promise {<fulfilled>: 1}
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 1
看控制台结果发现其实async修饰的函数,本身就是⼀个Promise对象。
await
- async 函数中可能会有
await表达式,async 函数执行时,如果遇到await就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。 await关键字仅在 async function 中有效。- 正常情况下,
await命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数。非Promise对象时直接返回相关值。
看下面的示例:
async function test() {
await new Promise((resolve)=>{
setTimeout(()=>{
console.log(4);
resolve();
},2000)
})
let a = await 2;
console.log(a);
}
console.log(1);
test();
console.log(3);
输出顺序 1,3,4,2。代码执行先输出 1,然后执行test(),test函数里面遇到两个await都暂停执行,所以第二个输出 3。然后按照上下顺序执行两个await,两秒后输出 4,紧接着输出2。
再看另一个例子:
async function test() {
var res1 = await new Promise(function (resolve) {
setTimeout(function () {
resolve("第⼀秒运⾏");
}, 1000);
});
console.log(res1);
var res2 = await new Promise(function (resolve) {
setTimeout(function () {
resolve("第⼆秒运⾏");
}, 1000);
});
console.log(res2);
var res3 = await new Promise(function (resolve) {
setTimeout(function () {
resolve("第三秒运⾏");
}, 1000);
});
console.log(res3);
}
test();
输出:
2022.html:52 第⼀秒运⾏
2022.html:58 第⼆秒运⾏
2022.html:64 第三秒运⾏
上面的代码每隔一秒输出一次。
通过上面的示例可以看到,通过async和await可以自动将代码同步化。让我们用更简洁的代码就能控制异步流程的运行,所以async也是使用频率最高的语法。