17

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

 4 years ago
source link: https://www.tuicool.com/articles/aQzuQrM
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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特权。

常见的宏任务: setIntervalsetTimeOut 。微任务: 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中事件循环不同于浏览器。

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK