浅析JS线程与浏览器事件循环机制

632 阅读6分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

什么是 JS 线程

要了解什么是 JS 线程,首先我们应该大概了解一下进程与线程的概念。

对于浏览器,根据官方针对浏览器的介绍,有这么一句描述:浏览器是用来检索、展示以及传递 Web 信息资源的应用程序。简而言之,浏览器就是应用程序。凡是应用程序,都是由计算机的大脑-CPU 来负责解释和执行于操作系统上。而在运行过程中,就是以进程和线程的方式存在的。同一个应用程序中可以对应一个或者多个进程,每个程序有至少一个进程。

进程好比工厂中的一个车间,而 CPU 则类似工厂的供电设施,而比较特殊的是这个设施的供电量刚好只够一个车间开工。而线程就是车间里面的工人,每个工人负责着各自的那道工序,共同完成一件复杂的事情。

进程与线程之间的差别,有以下几个特点:

  • 进程和线程是包含关系,即进程包含线程;
  • 进程在创建时开销会比线程大;
  • 进程间通信通过 IPC 通信,比线程间的通信慢;
  • 线程间共享内存空间的资源,进程间不行。

而 JS 线程则是负责处理 JavaScript 的“工人”。

现代浏览器工作机制

目前,操作系统中一般存在三种模式:

  • 多进程模式
  • 多线程模式
  • 多进程 + 多线程模式 多进程模式比较有优势的一点就是稳定性比多线程高,在多进程模式运行的程序中,单一进程崩溃不会影响其他进程的运行。

而多线程模式运行的环境下,任何一个线程崩溃都会直接导致整个进程中的所有线程崩溃,稳定性相对较差,但是通信速度快。

多任务程序既可以由多进程模式实现,也可以由单进程内的多线程实现,还可以混合多进程 + 多线程模式实现。

我们熟知的现代浏览器,基本都是多进程 + 多线程的应用程序。浏览器的多进程主要包含有 Browser 进程(即浏览器主进程)、插件进程、GPU进程、浏览器渲染进程。

  • 浏览器主进程主要负责协调、主控。它控制应用中的包括地址栏,书签,回退与前进按钮等这些用户操作部分以及处理 web 浏览器不可见的特权部分,如网络请求与文件访问;负责各个标签页的管理,创建和销毁其他进程,并将浏览器渲染进程得到的结果绘制到用户界面上。
  • 插件进程的作用是控制站点使用的任意插件,如 Flash 等。
  • GPU进程用于3D绘制。
  • 浏览器渲染进程(又称浏览器内核),默认情况下,我们在浏览器中新打开一个标签页,就会创建一个渲染进程,就是说每一个标签页都会有一个独立的渲染进程,当然也不是绝对的,浏览器有时会将多个进程合并,比如打开多个空白标签页后,会发现多个空白标签页被合并成一个进程。这些进程间互不影响,主要负责各自标签页的页面渲染、脚本执行、事件处理等。

浏览器的多线程主要是指浏览器渲染进程是多线程的。渲染进程包含:

  • GUI渲染线程:负责渲染浏览器界面,解析HTML、CSS、构建DOM树、布局和绘制等。
  • JS引擎线程:也称为JS内核,负责处理JavaScript脚本程序(例如我们熟知的V8引擎),它负责解析JavaScript脚本,运行代码。
  • 事件触发线程:用来控制事件循环的,当对应的异步事件符合触发条件被触发时,该线程会把事件添加到一个待处理的任务队列的队尾,等待JS引擎空闲后执行。
  • 定时器触发线程:是setInterval和setTimeout所在的线程。浏览器定时计数器并不是由JS引擎计数,而是通过定时器触发线程来计时并触发,当计时完毕时,执行事件触发线程,把任务添加到任务队列中,等待JS引擎空闲后执行。
  • 异步http请求线程:是XHR在连接后通过浏览器新开的线程请求,设置有回调函数时,异步线程在检测到状态变更时,执行事件触发线程,把这个回调放到任务队列中,等待JS引擎空闲后执行。

事件循环机制

image.png 接下来我们通过上面这个流程图来分析, 同步任务都在 js 引擎线程上完成,当前的任务都存储在执行栈中;

js 引擎线程执行到 setTimeout/setInterval 的时候,通知定时器触发线程开始计时,等待计时结束后,将回调事件放入到由事件触发线程所管理的任务队列中,(任务队列分为宏任务队列和微任务队列,这个我们下面再讲);

js 引擎线程执行到 XHR/fetch 时,通知异步http请求线程,发送一个网络请求; 异步http请求线程在请求成功后,将回调事件放入到由事件触发线程所管理的任务队列中;

如果 JS 引擎线程中的执行栈没有任务了,JS 引擎线程会询问事件触发线程,在 任务队列中是否有待执行的回调函数,如果有就会推入到执行栈中交给 JS 引擎线程执行; JS 引擎线程空闲之后,GUI 渲染线程开始工作。

上文我们对JS事件循环机制梳理了一遍,接下来我们来对宏任务和微任务进行了解。

console.log('1');

setTimeout(() => {
  console.log('2');
  new Promise((resolve, reject) => {resolve()}).then(() => {
    console.log('3');
  })
}, 0);

setTimeout(() => {console.log('4');}, 0);

new Promise((resolve, reject) => {
  console.log('5');resolve();
})
.then(() => {
  console.log('6');
  setTimeout(() => {console.log('7');}, 0);
})

console.log('8');

第一个宏任务,执行主线程代码,输出 1 5 8 执行完了,开始执行当前微任务队列中任务,输出 6,同时这个有个异步任务,把它放入任务队列中。 到了这里,意味着一个事件循环结束,一般在此时页面UI会进行重新渲染。 之后开始执行下一个宏任务,输出 2 这个宏任务中执行完后检测到有微任务,执行输出 3 继续执行下个宏任务,输出 4, 继续执行下个宏任务,输出 7。 所以最后执行结果为:15862347。

事件循环机制会维护一个执行栈和任务队列,当执行代码时,把同步任务放在执行栈中顺序执行。当执行栈空闲,进入任务队列等待的异步任务会被执行。而任务会分成两种:宏任务和微任务。每执行完一个宏任务后就会执行当前所有等待的微任务,然后再执行下个宏任务。