1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > js 多个定时器_从浏览器多进程到JS单线程 JS运行机制最全面的一次梳理(二)

js 多个定时器_从浏览器多进程到JS单线程 JS运行机制最全面的一次梳理(二)

时间:2022-03-31 00:32:49

相关推荐

js 多个定时器_从浏览器多进程到JS单线程 JS运行机制最全面的一次梳理(二)

作者:撒网要见鱼

/a/1190000012925872

本文接上篇《从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理(一)》

从Event Loop谈JS的运行机制

到此时,已经是属于浏览器页面初次渲染完毕后的事情,JS引擎的一些运行机制分析.

注意,这里不谈可执行上下文,VO,scop chain等概念(这些完全可以整理成另一篇文章了),这里主要是结合Event Loop来谈JS代码是如何执行的.

读这部分的前提是已经知道了JS引擎是单线程,而且这里会用到上文中的几个概念:(如果不是很理解,可以回头温习)

JS引擎线程

事件触发线程

定时触发器线程

然后再理解一个概念:

JS分为同步任务和异步任务

同步任务都在主线程上执行,形成一个执行栈

主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件.

一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行.

看图:

看到这里,应该就可以理解了:为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到事件列表时,主线程还不空闲,正在执行其它代码,所以自然有误差.

事件循环机制进一步补充

这里就直接引用一张图片来协助理解:(参考自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)

上图大致描述就是:

主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)-

而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调

如此循环

注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

单独说说定时器

上述事件循环机制的核心是:JS引擎线程和事件触发线程

但事件上,里面还有一些隐藏细节,譬如调用setTimeout后,是如何等待特定时间后才添加到事件队列中的?

是JS引擎检测的么?当然不是了.它是由定时器线程控制(因为JS引擎自己都忙不过来,根本无暇分身)

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

什么时候会用到定时器线程?当使用setTimeoutsetInterval时,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中.

譬如:

setTimeout(function(){

console.log('hello!');

},1000);

这段代码的作用是当1000毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行

setTimeout(function(){

console.log('hello!');

},0);

console.log('begin');

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

注意:

执行结果是:先begin后hello!

虽然代码的本意是0毫秒后就推入事件队列,但是W3CHTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms.(不过也有一说是不同浏览器有不同的最小时间设定)-

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

setTimeout而不是setInterval

setTimeout模拟定期计时和直接用setInterval是有区别的.

因为每次setTimeout计时到后就会去执行,然后执行一段时间后才会继续setTimeout,中间就多了误差(误差多少与代码执行时间有关)

setInterval则是每次都精确的隔一段时间推入一个事件(但是,事件的实际执行时间不一定就准确,还有可能是这个事件还没执行完毕,下一个事件就来了)

而且setInterval有一些比较致命的问题就是:

累计效应(上面提到的),如果setInterval代码在(setInterval)再次添加到队列之前还没有完成执行,就会导致定时器代码连续运行好几次,而之间没有间隔.就算正常间隔执行,多个setInterval的代码执行时间可能会比预期小(因为代码执行需要一定时间)

譬如像iOS的webview,或者Safari等浏览器中都有一个特点,在滚动的时候是不执行JS的,如果使用了`setInterval`,会发现在滚动结束后会执行多次由于滚动不执行JS积攒回调,如果回调执行时间过长,就会非常容器造成卡顿问题和一些不可知的错误(这一块后续有补充,setInterval自带的优化,不会重复添加回调)

而且把浏览器最小化显示等操作时,setInterval并不是不执行程序,它会把setInterval的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行时

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

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

事件循环进阶:macrotask与microtask

这段参考了参考来源中的第2篇文章(英文版的),(加了下自己的理解重新描述了下),强烈推荐有英文基础的同学直接观看原文,作者描述的很清晰,示例也很不错,如下:

//tasks-microtasks-queues-and-schedules/

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

console.log('scriptstart');

setTimeout(function(){

console.log('setTimeout');

},0);

Promise.resolve().then(function(){

console.log('promise1');

}).then(function(){

console.log('promise2');

});

console.log('scriptend');

嗯哼,它的正确执行顺序是这样子的:

scriptstart

scriptend

promise1

promise2

setTimeout

为什么呢?因为Promise里有了一个一个新的概念:microtask

或者,进一步,JS中分为两种任务类型:macrotaskmicrotask,在ECMAScript中,microtask称为jobs,macrotask可称为task

它们的定义?区别?简单点可以按如下理解:

`macrotask`(又称之为`宏任务`),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

每一个`task`会从头到尾将这个任务执行完毕,不会执行其它

浏览器为了能够使得JS内部`task`与DOM任务能够有序的执行,会在一个`task`执行结束后,在下一个 `task` 执行开始前,对页面进行重新渲染

(`task->渲染->task->...`)

microtask(又称为微任务),可以理解是在当前task执行结束后立即执行的任务

也就是说,在当前task任务后,下一个task之前,在渲染之前

所以它的响应速度相比setTimeout(setTimeouttask)会更快,因为无需等渲染

也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

分别什么样的场景会形成macrotaskmicrotask呢?

macrotask:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)

microtask:Promise,process.nextTick等

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

参考:/q/1010000011914016

再根据线程来理解下:

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

microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护(这点由自己理解+推测得出,因为它是在主线程下无缝执行的)

所以,总结下运行机制:

执行一个宏任务(栈中没有就从事件队列中获取)

执行过程中如果遇到微任务,就将它添加到微任务的任务队列中

宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)

当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

如图:

另外,请注意下Promisepolyfill与官方版本的区别:

官方版本中,是标准的microtask形式

polyfill,一般都是通过setTimeout模拟的,所以是macrotask形式

请特别注意这两点区别

注意:有一些浏览器执行结果不一样(因为它们可能把microtask当成macrotask来执行了),但是为了简单,这里不描述一些不标准的浏览器下的场景(但记住,有些浏览器可能并不标准)

写在最后的话

看到这里,不知道对JS的运行机制是不是更加理解了,从头到尾梳理,而不是就某一个碎片化知识应该是会更清晰的吧?

同时,也应该注意到了JS根本就没有想象的那么简单,前端的知识也是无穷无尽,层出不穷的概念、N多易忘的知识点、各式各样的框架、

底层原理方面也是可以无限的往下深挖,然后你就会发现,你知道的太少了…

另外,本文也打算先告一段落,其它的,如JS词法解析,可执行上下文以及VO等概念就不继续在本文中写了,后续可以考虑另开新的文章.

最后,喜欢的话,就请给个赞吧!

关联阅读

进入公众号查看:

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理(一)

关注UU

关注公众号【前端UU】,定期获取好文推荐哟~

如果文章对您有微微帮助,请帮忙【转发】

如果同是天涯沦落前端人,请点亮【在看】

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。