夜间模式暗黑模式
字体
阴影
滤镜
圆角
JavaScript Event Loop

这篇当做Jake Archibald在2018 JSConf Asia和Philip Roberts在2014 JSConf EU讲的Event Loop笔记,视频在Youtube上可以搜到。

引子

A single-threaded non-blocking asynchronous concurrent language —— JavaScript

What the heck is the event loop anyway? | Philip Roberts at 2014 JSConf EU

JavaScript是单线程非阻塞的。单线程很容易理解,因为用户在浏览网页的时候常常要进行页面交互,以及JavaScript背后也会进行一些操作DOM的操作。要是搞成多线程,就会出现DOM操作的条件竞争。所以还是干脆一点弄成单线程就好。

当然也有“多线程”的解决方案,Web Worker就是其中一个,但是用Worker可以操作的东西是有限制的,而且Worker一多起来资源开销也是很雷普的。(并不是这篇文章的主题,就略过了)

非阻塞是JavaScript中一个有趣的点,像JavaScript Runtime中(比如V8),存在一个堆空间用来存数据,也有一个栈空间来存储函数的调用。但是说到异步非阻塞,粗略看看V8的源码也并没有类似诸如setTimeout、DOM操作还有HTTP请求这样的东西在,也没有和异步相关的东西。这其实很有意思——我的意思是JavaScript非阻塞实现很有意思。

这种“有意思”,主要还是在于“从0开始的非阻塞实现”这方面的有意思。JavaScript是单线程,也意味着单一call stack。

Call Stack

function multiply(a, b) {
    return a * b;
}

function square(n) {
    return multiply(n, n);
}

function printSquare(n) {
    var squared = square(n);
    console.log(squared);
}
// 一切的开始 entry point
printSquare(4);

稍稍有点Binary的基础就很能理解call stack的压栈弹栈的操作。对于单线程来说,所谓的阻塞可以理解为在某个阶段,或者说某个函数中,代码需要执行很久。造成了用户在界面交互的时候卡顿的现象。

var foo = $.getSync('//foo.com');
var bar = $.getSync('//bar.com');
var qux = $.getSync('//qux.com');
// 每一次getSync请求 我都得等一会儿 不能干其他事情
console.log(foo);
console.log(bar);
console.log(qux);

但是对于诸如setTimeout这样的异步回调函数,它在一般的调用栈上有着异于其他函数的表现。对于setTimeout(callback, ms)来说,setTimeout仅会短时间在调用栈上出现一下,随后很快就消失:

console.log('hi');

setTimeout(function () {
    console.log('there');
}, 5000);

console.log('JSConfEU');

等到所有的代码都执行完,5秒过去后就会在调用栈上出现console.log('there'),然后就消失了。这对于单线程来说是匪夷所思的。能够解释这种现象的东西,就得谈谈Event Loop了。

Event Loop

JavaScript能够实现“并发”,其实是浏览器所赋予的能力。像setTimeout是浏览器提供的API,而不是存在于V8源码中的东西。

承接Call Stack中讲的东西,Web API会帮JavaScript承包计时器Handler,当时间到了,就会把callback回调函数放到所谓task queue任务队列中。

这个时候Event Loop登场了,事件循环干的事情其实也非常简单。它会查看call stack和task queue中的内容,如果call stack空了,就把task queue里面排在第一个的task塞到call stack里面去。call stack是V8的管辖领域,所以之后就按照原来的“常识”继续工作。如果call stack中有东西,并且task queue中也有东西,事件循环会等到call stack里面的东西全部执行完了在把task queue中的东西塞到调用栈。

现在也可以理解一下Ajax的请求流程,当代码执行到xhr请求时,先扔给浏览器处理handler。这里就是接受返回的响应信息(比如json),接受完就扔到任务队列里面。然后重复上面setTimeout的流程。

像比较常见的onClick事件,通常是在代码执行到定义回调函数的地方,把监听handler一直放在浏览器里,每次点击按钮的时候,就会扔一个事件到任务队列里面。

页面渲染也是浏览器的任务,理想情况下1秒钟渲染60次(满足绝大多数显示器的刷新频率)。渲染也可以看做一种callback,也要等待调用栈清空以后执行。不同地方在于render的优先级更高,也就是所render queue排在callback queue的前面。普通的callback要等render queue清空以后才能得到执行。

macro/micro Task

macrotask:包含执行整体的js代码,事件回调,XHR回调,定时器(setTimeout/setInterval/setImmediate),IO操作,UI render

microtask:更新应用程序状态的任务,包括promise回调,MutationObserver,process.nextTick,Object.observe

宏任务和微任务

macro task宏任务和micro task微任务是规范里面的内容。通俗点来说,微任务是由宏任务产生的,但是微任务的执行优先级却高于宏任务。可以认为微任务紧紧跟在宏任务的屁股后面,在微任务的后面是其他的宏任务。

render视图渲染是在本轮事件循环的微任务队列执行完后执行,执行任务的耗时会影响视图渲染的时间。

1、执行宏任务队列的一个任务

2、执行完当前微任务队列的所有任务

3、视图渲染

事件循环的步骤

视图渲染也不是必然会执行的步骤,浏览器的优化策略有可能将几次视图更新累计到一起进行渲染,执行渲染时会调用requestAnimationFrame回调函数执行重绘。

Loop in Node.js

node.js

Node.js和浏览器又有点不太一样,它搞了一套自己的模型。node中实现Event Loop主要依赖libuv。在较新的node版本中(指大于11版本号),事件循环模型是这样的:

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

先从poll轮询阶段开始,这时候会拿到外部数据并等待还未返回的I/O事件,轮询事件占用时间比较长。如果没有其他的异步任务需要处理就会一直停留在这个阶段。随后到check检查阶段,这个阶段主要执行setImmediate()的callback。接下来是关闭事件回调阶段close callbacks,这一步处理的是关闭请求的回调函数(socket.on('close', ...))。定时器timer阶段是处理setTimeout()setInterval()的回调函数,在这个阶段主线程main thread会检查当前时间是否满足定时器的条件。如果满足就执行回调函数后退出阶段,没有满足就直接退出。经过上面的阶段剩下来的callback全部在I/O callback阶段执行。

暂无评论

发送评论 编辑评论


				
上一篇
下一篇