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

异步

JS 是单线程的,对于耗时任务如果按照顺序执行,就会导致浏览器假死卡住。所以需要异步来处理耗时任务,当任务完成后才去处理。

同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务:不进入主线程,而进入任务队列中的任务,主线程完成一个事件循环空闲后,会从任务队列中读取新的任务进入主线程执行。

事件循环(Event Loop):只有执行栈中的所有同步任务都执行完毕,系统才会读取任务队列,看看里面的异步任务哪些可以执行,然后那些对应的异步任务,结束等待状态,进入执行栈,开始执行。

为什么JS要设计成单线程呢?

异步的解决方案

回调函数

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

1
2
3
4
5
6
7
8
9
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在英语里的意思就是“产出”)。

1
2
3
4
5
6
7
8
9
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特权。

常见的宏任务:setIntervalsetTimeOut。微任务:Promise

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

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

举个栗子

1
2
3
4
5
6
7
8
9
10
11
console.log(1);
setTimeout(function(){
console.log(2);
Promise.resolve(1).then(function(){
console.log('promise')
})
})
setTimeout(function(){
console.log(3);
})

输出结果:

1
2
3
4
1
2
promise
3

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

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

参考