• 经常使用js的前端同学们肯定都知道js是单线程的脚本语言,但是js为什么会设计成单线程?
  • js中也有同步任务和异步任务,既然js是单线程是怎么实现异步的?为什么要使用异步?

带着上面这些问题,我们来了解下js中的事件循环和js的运行机制。



1浏览器内核(渲染进程)

对于普通的前台操作来说,最重要的就是渲染进程。可以这么理解,页面的渲染、js的执行、时间循环都是在渲染进程中执行的。接下来我们来分析下这个进程(浏览器内核),首先请牢记,渲染进程是多线程的渲染进程中会包含如下几种线程:

    (1)GUI渲染线程

  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树,布局、回流和重绘。
  • GUI渲染线程与JS引擎线程是互斥的 ,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

    (2)JS引擎线程

  • 也称为js内核,负责解析、运行js脚本程序。由于js是单线程的,所以一个浏览器tab页中永远只有一个js引擎线程在运行。

    (3)事件触发线程

  • 用来控制js事件循环,当js引擎执行代码块如setTimeout时(或来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将任务添加到事件触发线程中,当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理

    (4)定时触发器线程

  • 传说中的setIntervalsetTimeout所在线程,由于js是单线程,所以需要一个专门的计数器来帮助主线程为定时任务计时,并且计时结束之后将触发任务放入任务队列中,等待js引擎空闲时执行。

    (5)异步http请求线程

  • 当js主线程中有异步请求时,浏览器会新开一个单独的线程用于请求数据,所以js中的异步任务是由浏览器开辟的异步请求线程来完成的,因为js是单线程的,所有有必要通过异步来处理大数据量的异步请求,防止页面阻塞失去响应。


2事件循环

首先看一下js事件循环的循环图

jquery 渲染结束后事件_ViewUI

1.js引擎线程将执行 执行栈中的任务,当遇到setTimeout,异步请求,浏览器中的点击、加载等事件时发给事件触发线程进行管理

2.当事件满足触发条件,如异步请求数据获取完成,setTimeout计数完成等情况,由事件触发线程将任务推进任务队列的队尾

3.当js引擎线程清空执行栈的任务之后 ,开始从任务队列中的队头开始消费任务,并且重复执行第一步,如此就形成了一个事件循环

上述事件循环的核心是 js引擎线程 和 事件触发线程,但是其中还有一些细节,比如是谁在计时特定时间之后触发setTimeout中的任务?答案就是定时器线程

setTimeout(function(){
    console.log('hello!');
}, 1000);

上面的一段代码的作用是,当1000ms计时结束之后(由定时器线程计时),由事件触发线程将回调方法放入任务队列中,等待js引擎线程执行,如果当前时间js引擎线程正在执行任务,则回调方法并不能像传入的参数那样1000ms之后执行,所以代码的意思是1000ms之后将回调方法放入任务队列,具体什么时间运行就要看js引擎线程的运行状况。

setTimeout(function(){
    console.log('hello!');
}, 0);

console.log('begin');

这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行

注意:

  • 执行结果是:先beginhello!
  • 虽然代码的本意是0毫秒后就推入事件队列,但是W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

(不过也有一说是不同浏览器有不同的最小时间设定)

  • 就算不等待4ms,就算假设0毫秒就推入事件队列,也会先执行begin(因为只有可执行栈内空了后才会主动读取事件队列)


3事件循环进阶:macrotask与microtask 

    上文中将JS事件循环机制梳理了一遍,在ES5的情况是够用了,但是在ES6盛行的现在,仍然会遇到一些问题,譬如下面这题

console.log('script start');

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

Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});

console.log('script end');

他的执行顺序是

script start -> script end -> promise1 -> promise2 -> setTimeout

为什么呢?

js中分为两种任务类型:macroktask 和 microktask, 在ECMAScript中,microtask称为jobs,macrotask可称为task

那么两种任务有什么不同?

  • macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从任务队列中获取一个事件回调并放到执行栈中执行)
  • 每一个task会从头到尾将这个任务执行完毕,不会执行其它
  • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
  • 宏任务包含 主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
  • macrotask中的事件都是放在一个任务队列中的,而这个队列由事件触发线程维护
  • microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务
  • 也就是说,在当前task任务后,下一个task之前,在渲染之前
  • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
  • 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)
  • 微任务包含 Promise,process.nextTick等
  • microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护

所以,总结下运行机制:

  • step1 执行一个宏任务(栈中没有就从任务队列中获取)
  • step2 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • step3 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • step4 当前微任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • step5 渲染完毕后,JS线程继续接管,开始下一个宏任务(从任务队列中获取)