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
。
参考
事件循环
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
。