SpiderMonkey
Brendan Eich
网景
主流浏览器 | 内核 | 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 分为同步任务和异步任务,同步任务在主线程上执行,形成一个执行栈;事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件,一旦执行栈里中的同步任务执行完毕,系统就会读取任务队列,将可执行的异步任务添加到可执行栈中,开始执行。
看到这里,我们会明白一件事,那就是为什么有时候 setTimeOut 推入队列里的事件执行时间不准确。原因便是推入的事件被推入队列的时候,js 引擎线程比较繁忙,没有立即执行,所以有误差。
下边通过图片辅助理解,对事件循环的进一步补充:
上图的大致描述:
为什么要单独的定时器线程?
什么时候会用到定时器线程?
来个例子帮助大家理解一下:
setTimeout(function(){
console.log('setTimeOut');
}, 0);
console.log('Hi');
这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行
在来看一下运行结果:
Hi
setTimeOut
惊讶吧!此处,虽然代码的本意是 0 毫秒后就推入事件队列,但是 W3C 在 HTML 标准中规定,规定要求setTimeout中低于 4ms 的时间间隔算为4ms。再退一步讲,即使不用等待 4ms 结果依然如此。因为在 JS 引擎线程执行空闲时才会执去行被定时器推入到事件队列中的回调函数。
鉴于这么多但问题,目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame
补充:JS高程中有提到,JS引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。不过,仍然是有很多问题。。。
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中分为两种任务类型:macrotask
和 microtask
,在 ECMAScript 中,microtask 称为 jobs
,macrotask 可称为 task
。
macrotask(宏任务)script(整体代码)
setTimeout
setInterval
setImmediate
I/O
UI rendering
可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
macrotask -> 渲染 -> macrotask -> ····
microtask(微任务)process.nextTick
Promises
Object.observe
MutationObserver
可以理解是在当前 macrotask 执行结束后立即执行的任务
__补充:在node环境下,process.nextTick的优先级高于Promise__,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。
从线程的角度理解一下,宏任务和微任务:
macrotask 中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护。
microtask 中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前 macrotask 执行完毕后执行,而这个队列由 JS 引擎线程维护。