4

Javascript的事件循环机制

 2 years ago
source link: https://www.fly63.com/article/detial/12089
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.
neoserver,ios ssh client

单线程的Javascript

JavaScript是一种单线程语言,它主要用来与用户互动,以及操作dom。多线程需要共享资源、且有可能修改彼此的运行结果,且存在上下文切换。

在 JS 运行的时候可能会阻止 UI 渲染,这说明两个线程是互斥的。这是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。
JS 是单线程运行的,可以达到节省内存,节约上下文切换时间。

为了利用多核CPU的计算能力,html5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。

单线程的同步等待极大影响效率,任务不得不一个一个等待执行,对于网页应用是无法接受的。所以Javascript使用事件循环机制来解决异步任务的问题。

同步 vs 异步 宏任务 vs 微任务

首先了解下同步和异步的区别:

  • 同步:在一个函数返回的时候,调用者就能够得到预期结果。
  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
  • 异步:在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到。
  • 异步任务:不进入主线程、而放在"任务队列"中的任务,若有多个异步任务,则需排队等待进入主线程执行栈中被执行。
6317f9a6ef0ad.jpg

任务队列其实不止一种,根据任务种类的不同,可以分为微任务(micro task)队列宏任务(macro task)队列。常见的任务如下:

  • 宏任务: script(整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate;需要特定的异步线程去执行,有明确的异步任务去执行,有回调。
  • 微任务: Promise、MutaionObserver、process.nextTick(Node.js 环境,会先于其他微任务执行);不需要特定的异步线程去执行,没有明确的异步任务去执行,只有回调。

一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。 执行顺序如下图:

6317f9ad02478.jpg

第一个例子:

var req = new XMLHttpRequest();
req.open('GET', url);    
req.onload = function (){};    
req.onerror = function (){};    
req.send();
//等同于
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};    
req.onerror = function (){};

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。

第二个例子:

console.log('1 第一次循环 开始执行');
setTimeout(function () {
    console.log('2 第二次循环 开始执行');
    new Promise(function (resolve) {
        console.log('3 第二次循环 宏任务结束');
        resolve();
    }).then(function () {
        console.log('4 第二次循环 微任务执行')
    })
}, 0)
new Promise(function (resolve) {
    console.log('5 第一次循环 宏任务结束');
    resolve();
}).then(function () {
    console.log('6 第一次循环 微任务执行')
})

setTimeout(function () {
    console.log('7 第三次循环 开始执行');

    new Promise(function (resolve) {
        console.log('8 第三次循环 宏任务结束');
        resolve();
    }).then(function () {
        console.log('9 第三次循环 微任务执行')
    })
}, 0)

/*
结果
1 第一次循环 开始执行
5 第一次循环 宏任务结束
6 第一次循环 微任务执行
2 第二次循环 开始执行
3 第二次循环 宏任务结束
4 第二次循环 微任务执行
7 第三次循环 开始执行
8 第三次循环 宏任务结束
9 第三次循环 微任务执行
*/

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。主线程尽可能早得执行,但是没有办法保证回调函数一定会在setTimeout()指定的时间执行,因为必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。所以单线程无法实现真正的异步,因为还是存在阻塞。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。
在此之前,老版本的浏览器都将最短间隔设为10毫秒。
另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。
这时使用requestAnimationFrame()的效果要好于setTimeout()。

在Node.js环境下,还提供了另外两个方法:

process.nextTick方法可以在当前"执行栈"的尾部,下一次Event Loop之前,触发回调函数。也就是说,它指定的任务总是在本次"事件循环"触发,发生在所有异步任务之前,同时也是在所有微任务之前执行。

setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在之后的Event Loop执行,这与setTimeout(fn, 0)很像。

多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate则可能需要多次loop才能执行完。

To Be Continued...

Node.js使用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的api,事件循环机制也是它里面的实现的。(在Python中,uvloop,一个完整的asyncio事件循环的替代品,也是建立在libuv基础之上,是由Cython编写而成。)这个机制和浏览器中Javascript的事件循环机制是不太一样的。下次深入了解下libuv的Event Loop!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK