JavaScript进阶笔记(七):异步任务和事件循环

JS
是单线程的,对于耗时任务如果按照顺序执行,就会导致浏览器假死卡住。所以需要异步来处理耗时任务,当任务完成后才去处理。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务:不进入主线程,而进入任务队列中的任务,主线程完成一个事件循环空闲后,会从任务队列中读取新的任务进入主线程执行。
事件循环(Event Loop):只有执行栈中的所有同步任务都执行完毕,系统才会读取任务队列,看看里面的异步任务哪些可以执行,然后那些对应的异步任务,结束等待状态,进入执行栈,开始执行。
为什么JS要设计成单线程呢?

异步的解决方案

回调函数

早期常用的异步操作方式,有个致命的缺点,极容易写出回调地狱。

ajax(url, ()=>{
    // xxx
    ajax(url,()=>{
        // xxx
        ajax(url, () => {
            // xxx
        })
    })
})

不利于代码阅读和维护,毕竟代码是用来读的顺便在机器上运行。不能使用 try-catch
不会异常。

事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合”(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

发布订阅

我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称”观察者模式”(observer pattern)。

Promise

ES6给我们提供了一个原生的构造函数 Promise
,用于异步操作可以将异步对象和回调函数脱离开来,通过 .then
方法在这个异步操作上绑定回调函数, Promise
可以让我们通过链式调用的方法去解决回调嵌套的问题,而且由于 promise.all
这样的方法存在,可以让同时执行多个操作变得简单。

Promise
中存在三种状态: pending
(进行中)、 fulfilled
(已成功)和 rejected
(已失败)。一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。

关于Promise具体用法可以参考阮老师书中的 《ES6入门-Promise》
章。

生成器Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

function *  hello () {
    yield 'hello'
    yield 'world'
    return 'ending'
}
const hl = hello()
hl.next() // {value: "hello", done: false}
h1.next() // {value: "world", done: false}
h1.next() // {value: "ending", done: true}

必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

具体可以参考阮老师 《ES6入门-Generator》
章。

async/await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数是什么?一句话,它就是 Generator 函数的语法糖。

Generator
使用太过复杂,通过 async/await
就比较简单了。

async/await
对生成器进行改进,内置了执行器不需要在调用 next
方法。更好的语义,返回值是 Promise

参考 《ES6入门-async函数》

参考

事件循环

Javascript
是单线程的,为了在处理异步任务的时候不会发生阻塞,提出了事件循环的解决方案。从宏观上来说,主线程在处理任务时,不会等待异步任务直到返回结果,而是将异步任务挂起,继续执行其他的任务。当异步任务返回结果不会立即处理而是加入到 事件队列
中。当主线程空闲时,读取事件队列中的任务,以此循环往复就形成事件循环。

事件队列

在事件循环中分为两种任务类型:宏任务(macro task) 和 微任务(micro task)。虽然都是异步任务但是两者的优先级不同,微任务属于人民币玩家拥有VIP特权。

常见的宏任务: setInterval
setTimeOut
。微任务: Promise

两种不同的任务对应着有两种不同的任务队列:宏任务队列 和 微任务队列。在事件循环中,异步任务的返回结果会根据不同的类型,放入不同的任务队列中。当主线程空闲时,会优先查看微任务队列,如果有任务依次执行任务直到微任务队列为空。然后去读取宏任务队列中的宏任务……依次循环,直到所有任务都完成。

注意:由于微任务队列优先级高,所以同一事件循环中微任务优先执行。

举个栗子

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise')
    })
})
setTimeout(function(){
    console.log(3);
})

输出结果:

1
2
promise
3

setTimeout
是宏任务,两个都被 Push 到宏任务队列中。而 Promise
是微任务,被 Push 到微任务队列中。当执行完第一个 setTimeout
会去读取微任务队列执行输出。然后在去执行下一个 setTimeout

Node中事件循环不同于浏览器。

参考