前言
这篇文章主要给大家分享一下,自己关于异步编程方面的一些见解,和实际开发项目过程中使用到的一些技术以及技巧
一、引入异步编程
对异步编程编程不太熟悉的小伙伴,可以通过下面这些简单的例子,先建立对异步编程的基本认知,首先作为一名前端程序员,联调接口是我们最熟悉不过的技能。下面这种代码,这样的回调函数,大家肯定是再熟悉不过。
ajax(url, (res) => {
console.log(res);
})
但是大家是否有想过,如果某一个业务场景比较复杂,需要一个类似链式调用的操作,那么,如果没猜错我相信大多数人的代码,可能会变成这样。
ajax(url, (res) => {
console.log(res);
// ...处理代码
ajax(url2, (res2) => {
console.log(res2);
// ...处理代码
ajax(url3, (res3) => {
console.log(res3);
// ...处理代码
// 若干个回调
})
})
})
如果有若干个,当然我相信,现实业务中这种情况肯定是不会出现的,我们只是为了说明这种写法这理论是不严谨,容易进入 回调地狱
回调地狱:
上面那个就是典型的回调地狱,那么肯定有人会问回调地狱会有哪些缺点呢,其实回调地狱不光只是缺点,也还是有些优点的,下面是个人的一些理解。
- 优点:解决了 同步阻塞 的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)
- 缺点:回调地狱;不能用 try catch 捕获错误;不能 return
二、常见处理异步编程的几种方式
1.Generator函数
- 介绍:ES6 新引入了 Generator 函数(生成器函数),可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。最大的特点就是 可以控制函数的执行。
- Generator和函数不同的是,generator由function *定义(注意多出的星号),并且,除了return语句,还可以用yield返回多次
利用 yield返回多次这个特性,我们就可以实现有同步的方式写出异步的代码
//菲波那切数列,简单介绍一下它,n=(n-1) + (n-2)
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
可能有人对[a, b] = [b, a+b],不太理解。其实[a, b] = [b, a+b]起到了var t = a + b;a = b;b = t;的作用,或者说是这段代码的精简版(省略了中间变量t的申请),实际上,它的运算顺序是从右到左的,先计算b和a+b,然后将b的值赋予a,再将a+b的值赋予b,以此来达到预期的计算结果。
next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”。返回的value就是yield的返回值,done表示这个generator是否已经执行结束了。如果done为true,则value就是return的返回值。
当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。最终我们可以将复杂的回调函数改成这样
try {
r1 = yield ajax('http://url-1', data1);
r2 = yield ajax('http://url-2', data2);
r3 = yield ajax('http://url-3', data3);
success(r3);
}
catch (err) {
handle(err);
}
2.Promise函数
这个函数我相信大家都很熟悉,在这里就不过多介绍
3.async/await
async 是一个通过 异步执行并隐式返回 Promise 作为结果的函数。
async function async1() {
return '测试';
}
console.log(async1());
执行这段代码,可以看到调用 async 声明的 async1 函数返回了一个 Promise 对象,状态是 resolved,返回结果如下所示:Promise {: “测试”}。和 Promise 的链式调用 then 中处理返回值一样。这就完美的解释了,MDN对async的定义,即 异步执行并隐式返回 Promise 作为结果的函数。
await 需要跟 async 搭配使用,结合下面这段代码来看看 await 到底是什么
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)
分析:
- 首先,执行 console.log(0) 这个语句,打印出来 0。
- 紧接着就是执行 foo 函数,由于 foo 函数是被 async 标记过的,所以当进入该函数的时候,JS 引擎会保存当前的调用栈等信息,然后执行 foo 函数中的 console.log(1) 语句,并打印出 1。
- 当执行到 await 100 时,会默认创建一个 Promise 对象,进入微任务队列, 然后 JS 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。
- 主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用 promise_.then 来监控 promise 状态的改变。
- 接下来继续执行父协程的流程,执行 console.log(3),并打印出来 3。
- 随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,微任务队列中有 resolve(100) 的任务等待执行,执行到这里的时候,会触发 promise_.then 中的回调函数
该回调函数被激活以后,会将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程。foo 协程激活之后,会把刚才的 value 值赋给了变量 a,然后 foo 协程继续执行后续语句
总结
- 早期的异步回调函数虽然解决了同步阻塞的问题,但是容易写出回调地狱。
- Generator 生成器最大的特点是可以控制函数的执行,也能勉强实现同步方式实现异步事件。
- 使用 Promise 能很好地解决回调地狱的问题,但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程。后期移交维护的成本比较大。
- async/await 可以算是异步编程的终极解决方案,它通过同步的方式写异步代码,可以把 await 看作是让出线程的标志,先去执行 async 函数外部的代码,等调用栈为空再回来调用 await 后面的代码。