setTimeout(() => { console.log(1); }, 0) console.log(2);
答案:输出 2 , 1。javascript
setTimeout
setTimeout
和setInterval
requestAnimationFrame
requestidlecallback
JavaScript语言的一大特色就是单线程,也就是说,同一时间只能作一件事,前面的任务没作完,后面的任务只能等着。html
这主要与JavaScript用途有关。它的主要用途是与用户互动,以及操做DOM。若是JavaScript是多线程的,会带来不少复杂的问题,假如 JavaScript有A和B两个线程,A线程在DOM节点上添加了内容,B线程删除了这个节点,应该是哪一个为准呢? 因此,为了不复杂性,因此设计成了单线程。前端
虽然 HTML5 提出了Web Worker
标准。Web Worker 的做用,就是为 JavaScript 创造多线程环境,容许主线程建立 Worker 线程,将一些任务分配给后者运行。可是子线程彻底不受主线程控制,且不得操做DOM。因此这个并无改变JavaScript单线程的本质。通常使用 Web Worker 的场景是代码中有不少计算密集型或高延迟的任务,能够考虑分配给 Worker 线程。
可是使用的时候必定要注意,worker 线程是为了让你的程序跑的更快,可是若是 worker 线程和主线程之间通讯的时间大于了你不使用worker线程的时间,结果就得不偿失了。java
brower进程(主进程)react
GPU进程web
插件进程浏览器
浏览器渲染进程(浏览器内核)微信
GUI渲染进程多线程
js引擎线程架构
事件触发
能够看到 js引擎是浏览器渲染进程的一个线程。
GUI渲染线程和JS引擎线程互斥
JS阻塞页面加载
进程(process)和线程(thread)是操做系统的基本概念。
因为每一个进程至少要作一件事,因此一个进程至少有一个线程。系统会给每一个进程分配独立的内存,所以进程有它独立的资源。同一进程内的各个线程之间共享该进程的内存空间(包括代码段,数据集,堆等)。
进程能够理解为一个工厂不不一样车间,相互独立。线程是车间里的工人,能够本身作本身的事情,也能够相互配合作同一件事情。
若是你想知道更多,推荐看 《WebKit技术内幕》这本书。
单线程就意味着,全部任务都要排队执行,前一个任务结束,才会执行后一个任务。若是一个任务须要执行,但此时JavaScript引擎正在执行其余任务,那么这个任务就须要放到一个队列中进行等待。等到线程空闲时,就能够从这个队列中取出最先加入的任务进行执行(相似于咱们去银行排队办理业务,单线程至关于说这家银行只有一个服务窗口,一次只能为一我的服务,后面到的就须要排队,而任务队列就是排队区,先到的就优先服务)
注意:若是当前线程空闲,而且队列为空,那每次加入队列的函数将当即执行。
setTimeout的运行机制:执行该语句时,设置一个定时器,定时时间置为多设置的延时,当计数结束后,将传入的函数加入任务队列,以后的执行就交给任务队列负责。
如今咱们回到最开始的一个例子
setTimeout(() => { console.log(1); }, 0) console.log(2);
输出 2, 1;
setTimeout的第二个参数表示在执行代码前等待的毫秒数。上面代码中,设置为0,表面意思为 执行代码前等待的毫秒数为0,即当即执行。但实际上的运行结果咱们也看到了,并非表面上看起来的样子,千万不要被欺骗了。
实际上,上面的代码并非当即执行的,这是由于setTimeout有一个最小执行时间,HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔)不得低于4毫秒。 当指定的时间低于该时间时,浏览器会用最小容许的时间做为setTimeout的时间间隔,也就是说即便咱们把setTimeout的延迟时间设置为0,实际上可能为 4毫秒后才事件推入任务队列
setTimeout(() => { console.log(111); }, 100);
上面代码表示100ms后执行console.log(111)
,但实际上实行的时间确定是大于100ms后的, 100ms 只是表示 100ms 后将任务加入到"任务队列"中,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等好久,因此并无办法保证,回调函数必定会在setTimeout()
指定的时间执行。
下面的例子引用 深刻理解定时器系列第一篇——理解setTimeout和setInterval 这篇文章的例子
btn.onclick = function(){ setTimeout(function(){ console.log(1); },250); }
点击该按钮后,首先将onclick事件处理程序加入队列。该程序执行后才设置定时器,再有250ms后,指定的代码才被添加到队列中等待执行。
若是上面代码中的onclick事件处理程序执行了300ms,那么定时器的代码至少要在定时器设置以后的300ms后才会被执行。队列中全部的代码都要等到javascript进程空闲以后才能执行,而无论它们是如何添加到队列中的。
如图所示,尽管在255ms处添加了定时器代码,但这时候还不能执行,由于onclick事件处理程序仍在运行。定时器代码最先能执行的时机是在300ms处,即onclick事件处理程序结束以后。
JavaScript中使用 setInterval 开启轮询。定时器代码可能在代码再次被添加到队列以前尚未完成执行,结果致使定时器代码连续运行好几回,而之间没有任何停顿。而javascript引擎对这个问题的解决是:当使用setInterval()时,仅当没有该定时器的任何其余代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。
可是,这样会致使两个问题:
假设,某个onclick事件处理程序使用setInterval()设置了200ms间隔的定时器。若是事件处理程序花了300ms多一点时间完成,同时定时器代码也花了差很少的时间,就会同时出现跳过某间隔的状况
例子中的第一个定时器是在205ms
处添加到队列中的,可是直到过了300ms
处才能执行。当执行这个定时器代码时,在405ms处又给队列添加了另外一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中
使用setTimeout构造轮询能保证每次轮询的间隔。
setTimeout(function () { console.log('我被调用了'); setTimeout(arguments.callee, 100); }, 100);
callee 是 arguments 对象的一个属性。它能够用于引用该函数的函数体内当前正在执行的函数。在严格模式下,第5版 ECMAScript (ES5) 禁止使用
arguments.callee()
。当一个函数必须调用自身的时候, 避免使用 arguments.callee(), 经过要么给函数表达式一个名字,要么使用一个函数声明.
setTimeout(function fn(){ console.log('我被调用了'); setTimeout(fn, 100); },100);
这个模式链式调用了setTimeout()
,每次函数执行的时候都会建立一个新的定时器。第二个setTimeout()
调用当前执行的函数,并为其设置另一个定时器。这样作的好处是,在前一个定时器代码执行完以前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。并且,它能够保证在下一次定时器代码执行以前,至少要等待指定的间隔,避免了连续的运行。
60fps
与设备刷新率目前大多数设备的屏幕刷新率为60次/秒
,若是在页面中有一个动画或者渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也须要跟设备屏幕的刷新率保持一致。
卡顿:其中每一个帧的预算时间仅比16毫秒
多一点(1秒/ 60 = 16.6毫秒
)。但实际上,浏览器有整理工做要作,所以您的全部工做是须要在10毫秒
内完成。若是没法符合此预算,帧率将降低,而且内容会在屏幕上抖动。此现象一般称为卡顿,会对用户体验产生负面影响。
跳帧: 假如动画切换在 16ms, 32ms, 48ms时分别切换,跳帧就是假如到了32ms,其余任务还未执行完成,没有去执行动画切帧,等到开始进行动画的切帧,已经到了该执行48ms的切帧。就比如你玩游戏的时候卡了,过了一会,你再看画面,它不会停留你卡的地方,或者这时你的角色已经挂掉了。必须在下一帧开始以前就已经绘制完毕;
Chrome devtool 查看实时 FPS, 打开 More tools => Rendering, 勾选 FPS meter
requestAnimationFrame
实现动画requestAnimationFrame
是浏览器用于定时循环操做的一个接口,相似于setTimeout,主要用途是按帧对网页进行重绘。
在 requestAnimationFrame
以前,主要借助 setTimeout/ setInterval 来编写 JS 动画,而动画的关键在于动画帧之间的时间间隔设置,这个时间间隔的设置有讲究,一方面要足够小,这样动画帧之间才有连贯性,动画效果才显得平滑流畅;另外一方面要足够大,确保浏览器有足够的时间及时完成渲染。
显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame
的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动中止刷新。这就节省了CPU、GPU和电力。
requestAnimationFrame
是在主线程上完成。这意味着,若是主线程很是繁忙,requestAnimationFrame
的动画效果会大打折扣。
requestAnimationFrame
使用一个回调函数做为参数。这个回调函数会在浏览器重绘以前调用。
requestID = window.requestAnimationFrame(callback);
window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })();
上面的代码按照1秒钟60次(大约每16.7毫秒一次),来模拟requestAnimationFrame
。
MDN上的解释:
requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者可以在主事件循环上执行后台和低优先级工做,而不会影响延迟关键事件,如动画和输入响应。函数通常会按先进先调用的顺序执行,然而,若是回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
requestAnimationFrame
会在每次屏幕刷新的时候被调用,而requestIdleCallback
则会在每次屏幕刷新时,判断当前帧是否还有多余的时间,若是有,则会调用requestAnimationFrame
的回调函数,
图片中是两个连续的执行帧,大体能够理解为两个帧的持续时间大概为16.67,图中黄色部分就是空闲时间。因此,requestIdleCallback
中的回调函数仅会在每次屏幕刷新而且有空闲时间时才会被调用.
利用这个特性,咱们能够在动画执行的期间,利用每帧的空闲时间来进行数据发送的操做,或者一些优先级比较低的操做,此时不会使影响到动画的性能,或者和requestAnimationFrame搭配,能够实现一些页面性能方面的的优化,
react 的fiber
架构也是基于requestIdleCallback
实现的, 而且在不支持的浏览器中提供了 polyfill
setTimeout(fn, 0)
,并非当即执行。requestAnimationFrame
会比 setInterval
效果更好requestIdleCallback()
经常使用来切割长任务,利用空闲时间执行,避免主线程长时间阻塞。最近发起了一个100天前端进阶计划,主要是深挖每一个知识点背后的原理,欢迎关注 微信公众号「牧码的星星」,咱们一块儿学习,打卡100天。