JS 引擎运行机制讲解

SpiderMonkey Brendan Eich 网景

JS 引擎运行机制讲解_第1张图片
主流浏览器 内核 js引擎
IE -> Edge trident->EdgeHTML JScript(IE3.0-IE8.0) / Chakra(IE9+之后,查克拉,微软也看火影么…)
Chrome webkit->blink V8(大名鼎鼎)
Firefox Gecko SpiderMonkey(1.0-3.0)/ TraceMonkey(3.5-3.6)/ JaegerMonkey(4.0-
Safari webkit Nitro(4-)
Opera Presto->blink Linear A(4.0-6.1)/ Linear B(7.0-9.2)/ Futhark(9.5-10.2)/ Carakan(10.5-)

读到这片文章,相信大家已经清楚 JS 引擎是单线程,开题先给大家来个灵魂四问:

Q0:浏览器内核和 js 引擎的关系?
Q1:JS 引擎为什么是单线程的?
Q2:为什么需要异步?
Q3:单线程又是如何实现异步的呢?


A0: 浏览器内核名字有很多,渲染引擎排版引擎解释引擎,英文(Rendering Engine) ,在早期内核也是包含 js 引擎的,而现在 js 引擎越来越独立了,可以把它单独提出来,所以,我们所说的内核更偏向于指渲染引擎
A1: 假设 JS 引擎是多线程的。那么我们现在有 2 个进程, process1 和 process2,如果它们对同一个 dom 同时进行操作。其中 process1 删除了该 dom ,而 process2 编辑了该 dom ,同时下达 2 个矛盾的命令,浏览器究竟该如何执行呢? 这样想的话,JS 引擎被被设计成单线程应该就容易理解了吧。

A2: 如果 JS 中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验

A3: 通过事件循环(event loop)

下边我们就通过 Event Loop 来看看 JS 引擎的运行机制。

这里我们还需要回顾其他几个名词 :JS 引擎线程事件触发线程定时触发线程

JS 分为同步任务和异步任务,同步任务在主线程上执行,形成一个执行栈;事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件,一旦执行栈里中的同步任务执行完毕,系统就会读取任务队列,将可执行的异步任务添加到可执行栈中,开始执行。
JS 引擎运行机制讲解_第2张图片
看到这里,我们会明白一件事,那就是为什么有时候 setTimeOut 推入队列里的事件执行时间不准确。原因便是推入的事件被推入队列的时候,js 引擎线程比较繁忙,没有立即执行,所以有误差。

下边通过图片辅助理解,对事件循环的进一步补充:
JS 引擎运行机制讲解_第3张图片
上图的大致描述:

  • 主线程在执行时产生执行栈,栈中的代码调用 API 时,会往任务队列里添加各种事件(当满足触发条件后推入任务队列,如 ajax 请求完成)
  • 栈中的代码执行完毕,就会读取任务队列中的事件,去执行那些回调,如此循环

定时器

定时器线程

  • 为什么要单独的定时器线程?

    • JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。
  • 什么时候会用到定时器线程?

    • 当使用setTimeout或setInterval时,定时器需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。

来个例子帮助大家理解一下:

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

console.log('Hi');

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

Hi
setTimeOut

惊讶吧!此处,虽然代码的本意是 0 毫秒后就推入事件队列,但是 W3C 在 HTML 标准中规定,规定要求setTimeout中低于 4ms 的时间间隔算为4ms。再退一步讲,即使不用等待 4ms 结果依然如此。因为在 JS 引擎线程执行空闲时才会执去行被定时器推入到事件队列中的回调函数。

setTimeOut 和 setInterval 用来计时的区别

  • setTimeOut 计时完成,执行回调函数候,才会进行下一次计时,这中间就会多出执行回调函数所用的时间。所以计时会有误差。
  • setInterval 虽然会每次都精确的隔一段时间把回调函数推入执行队列一次,但是执行回调函数的时间如果大于间隔时间,那么事件队里就会累积同一定时器的推入的事件,这样就会导致代码执行好几次,而没有时间间隔;即使正常时间间隔执行,多个setInterval的代码执行时间可能会比预期小(因为代码执行需要一定时间)。甚至有更致命的问题:当把浏览器最小化显示等操作时,setInterval并不是不执行程序,它会把setInterval的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行。

鉴于这么多但问题,目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame

补充:JS高程中有提到,JS引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。不过,仍然是有很多问题。。。

es6 中对事件循环的重新考量

话不多说,直接看图上例子:
JS 引擎运行机制讲解_第4张图片

setTimeout(function(){
  console.log("1:setTimeOut")
})
new Promise(function(resolve){
  console.log("2:马上执行 for 循环")
  for (var i = 0; i < 1000; i++){
    i == 99 && resolve();
  }
}).then(function(){
  console.log('3:Execute then function')
})
console.log('4:end of the code')

首先执行 script 下的宏任务,遇到 setTimeout ,将其放到宏任务的【队列】里
遇到 new Promise 直接执行,打印**“2:马上执行for循环啦”**
遇到 then 方法,是微任务,将其放到微任务的【队列里】
打印 "4:end of the code"
本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印**“3:Execute then function”**
到此,本轮的 event loop 全部完成。
下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印**“1:setTimeOut”**

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

看来 promise 里边有一个新的概念:microtask,接下来对此展开来研究一番:
JS中分为两种任务类型:macrotaskmicrotask,在 ECMAScript 中,microtask 称为 jobs ,macrotask 可称为 task

  • macrotask(宏任务)script(整体代码) setTimeout setInterval setImmediate I/O UI rendering
    可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

    • 每一个 macrotask 会从头到尾将这个任务执行完毕,不会执行其它.
    • 浏览器为了能够使得 JS 内部 macrotask 与 DOM 任务能够有序的执行,会在一个 macrotask 执行结束后,在下一个 macrotask 执行开始前,对页面进行重新渲染,即macrotask -> 渲染 -> macrotask -> ····
    • 使用场景:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个 macrotask)
  • microtask(微任务)process.nextTick Promises Object.observe MutationObserver
    可以理解是在当前 macrotask 执行结束后立即执行的任务

    • 也就是说,在当前 macrotask 任务后,下一个 macrotask 之前,在渲染之前
    • 所以它的响应速度相比 setTimeout( setTimeout 是宏任务)会更快,因为无需等渲染。也就是说,某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕
    • 使用场景:Promise,process.nextTick等

__补充:在node环境下,process.nextTick的优先级高于Promise__,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。

从线程的角度理解一下,宏任务和微任务:

  • macrotask 中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护。

  • microtask 中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前 macrotask 执行完毕后执行,而这个队列由 JS 引擎线程维护。

总结

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
JS 引擎运行机制讲解_第5张图片

你可能感兴趣的:(前端技术知识)