异步

概述

异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。

在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。

简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

在前端编程中(甚至后端有时也是这样),我们在处理一些简短、快速的操作时,例如计算 1 + 1 的结果,往往在主线程中就可以完成。主线程作为一个线程,不能够同时接受多方面的请求。所以,当一个事件没有结束时,界面将无法处理其他请求。

现在有一个按钮,如果我们设置它的 onclick 事件为一个死循环,那么当这个按钮按下,整个网页将失去响应。

为了避免这种情况的发生,我们常常用子线程来完成一些可能消耗时间足够长以至于被用户察觉的事情,比如读取一个大文件或者发出一个网络请求。因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。但是子线程有一个局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。

异步的演变

首先假设要渲染一个页面,只能异步的串行请求A,B,C,然后才能拿到页面的数据并请求页面 针对于不同的异步编程方式,我们会得到如下的代码:

回调函数

回调函数就是一个函数,它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。

function request(v, fun) {
    console.log("当前输入字符串:", v);
    fun();
}
request("A", function () {
    console.log("回调执行:B", );
    request("B", function () {
        console.log("回调执行:C", );
        request("C", function () {
            console.log("回调结束", );
        })
    })
})

回调函数的嵌套是愈发深入的。在不断的回调中,request('A')回调函数中的其他逻辑会影响到request('B'),request('C')中的逻辑,同理,request('B')中的其他逻辑也会影响到request('C')。

在这个例子中,request('A')调用request('B'),request('B')调用request('C'),request('C')执行完毕返回,request('B')执行完毕返回,request('A')执行完毕返回。

我们很快会对先后顺序产生混乱,从而很难直观的分析出异步回调的结果。这就被称为回调地狱。

为了解决这种情况,ES6新增了Promise对象。

Promise

function request(v) {
    return new Promise((resolve) => {
        console.log("当前输入字符串:", v);
        resolve();
    });
}
request("A").then(() => {
    console.log("链式执行:B", );
    return request("B")
}).then(() => {
    console.log("链式执行:C", );
    return request("C")
}).then(() => {
    console.log("链式结束", );
})

Promise对象用then函数来指定回调。所以,之前在回调函数的例子可以改为上文中的模样。可以看到,Promise并没有消除回调地狱,但是却通过then链将代码逻辑变得更加清晰了。

在这个例子中,request('A')调用request('B'),request('B')调用request('C'),request('C')执行完毕返回。现在,request('A')中的内容只能通过显示声明的data来影响到request('C')——如果没有显示的在回调中声明,则影响不了request('C'),换言之,每段回调被近乎独立的分割了。

但是Promise本身还是有一堆的then,还是不能让我们像写同步代码一样写异步的代码,因此JS又引入了Generator。

Generator

function request(v) {
    return new Promise((resolve) => {
        setTimeout(function () {
            console.log("当前输入字符串:", v);
            resolve();
        }, 500)
    });
}
function* gen() {
    yield request("A")
    console.log("执行:B");
    yield request("B")
    console.log("执行:C", );
    yield request("C");
}
const b = gen();
b.next();
b.next();
b.next();

Generator是协程在ES6上的实现,协程是指一个线程上不同函数间执行权可以相互切换。如本例,先执行gen(),然后在遇到yield时暂停,执行权交给request('A'),等到调用了next()方法,再将执行权还给gen()。

通过协程,JS就实现了用同步的方式写异步的代码,但是Generator的使用要配合执行器,并且在执行以上代码我们会发现代码执行顺序并不符合预期,这自然是麻烦的,于是就有了Async/Await。

Async/Await

function request(v) {
    return new Promise((resolve) => {
        setTimeout(function () {
            console.log("当前输入字符串:", v);
            resolve();
        }, 500)
    });
}
async function gen() {
    await request("A")
    console.log("执行:B", );
    await request("B")
    console.log("执行:C", );
    await request("C")
    console.log("执行结束", );
}
gen();

如果比较代码的话,这里的代码只是把代码中* => async,yield变为await,然后省略了next()。

除此以外,Async函数比Generator函数有更好的延展性——yield接的是Promise函数/Thunk函数,但await还可以包括普通函数。对于普通函数,await表达式的运算结果就是它等到的东西。否则若await等到的是一个Promise函数,await就会协程到这个Promise函数上,直到它resolve或者reject,然后再协程回主函数上。当然,Async函数也比Generator函数更加易读和易理解。

总结

本文阐述了从回调函数到Async/Await的演变历史。Async函数作为换一个终极解决方案,尽管在并行异步处理上还要借助Promise.all(),但其他方面已经足够完美。

本文参考:

https://www.runoob.com/js/js-async.html

https://segmentfault.com/a/1190000018038645

https://www.jianshu.com/p/c1b8b89c4905

最后更新于