上篇回顾:HTML Standard系列:浏览器是如何解析页面和脚本的javascript
能够带着这些问题阅读本文:前端
在浏览器的实现上,诸如渲染任务、JavaScript 脚本执行、User Interaction、网络处理都跑在同一个线程上,当执行其中一个类型的任务的时候意味着其余任务的阻塞,为了有序的对各个任务按照优先级进行执行浏览器实现了咱们称为 Event Loop 调度流程。java
这种设计模型致使了 JavaScript 天生异步的特色,意味着诸如 Ajax 等浏览器接口调用的回调将会发生在 Event Loop 将来的循环中,而不是在阻塞当前 Task。react
与这种模型相对的并发模型,相似 iOS 中渲染和脚本执行也是在同一个线程中,当 iOS 开发人员发起网络请求的时候,通常经过一个优先级较低的线程去完成请求任务,任务线程完成任务后唤起主线程执行网络请求后的工做。web
咱们看一段简单的代码对比。swift
JavaScript中请求一张图片URL的表现:浏览器
// 将一个网络请求任务推入Event Loop中的任务队列中 http.request('some.img').then( // 将回调推入任务队列中排队,并在网络任务完成后置位可执行任务,等待执行(这里暂时忽视微任务和宏任务的区别) (imgUrl) => imgElement.src = imgUrl; ) 复制代码
咱们看看iOS中不阻塞UI的请求一张图片如何作:markdown
// 将请求任务主动推到额外线程的队列上 DispatchQueue.global().async { // 在额外的线程中同步请求 let imageData = doRequestStuff() // 完成后主动将后续任务推入到主线程中完成。 DispatchQueue.main.async { UIImage(data: imageData) } } 复制代码
能够看到二者对于开发者而言最大的区别在于主动和被动,JavaScript的开发者只负责发起请求,随后的任务调度所有交给了隐藏在幕后的Event Loop,iOS开发者则须要主动维护多线程之间的关系。网络
为何这两个方法要和 Event Loop 一块儿讲?根据上面的例子,咱们发现 Event Loop 的实现大大简化UI开发,通常开发者只须要将要执行的任务推入 Event Loop 的队列上就好了,有 Event Loop 天然不会阻塞UI渲染。多线程
Event Loop 虽然优势不少,但仍是存在必定问题,Event Loop 对多线程开发模型就好了抽象,隐藏了复杂的细节,好比咱们压根不用管网络请求在浏览器内部是并发请求的,隐藏细节就意味着复杂场景开发者拥有的自由度会下降。
典型的问题的就是 long task,当 event loop 中某个任务执行时间超过了50ms,用户就可能会感到卡顿;另一个问题就是 evetloop 中任务过多,致使高优先级的任务没法及时执行(咱们没法控制任务的优先级);好比Js动画效果。
了解JS定时器同窗应该知道,settimeout 和 setinterval 的定时并不是准确的,考虑以下代码:
// 咱们期待这个动画帧数为20帧 var i = 1; setInterval(() => { element.style.width = `${i++}px` }, 50) // 在某些状况下我插入了一堆任务到队列中 for(var j = 0; j < 10000, j++) { setTimeout(() => { doSomeStuff() }, 99) } 复制代码
显然这个动画在要执行第二帧的JS脚本的时候,前面排了10000个任务,而这段脚本排在队列中10001(实际状况应该更复杂),虽然浏览器老是在执行任务后进行渲染工做,但关键的脚本没有执行,渲染的界面天然仍是原来的,这就形成视觉效果上的卡顿。
因而乎 requestAnimationFrame 就出现了,它的定义就是在浏览器下次绘制以前将会执行这个方法的回调,具体浏览器如何实现了这个方法,能够保证咱们的动画避免被长队列任务所延迟咱们接下来再讲。
// 例子,来自MDN var start = null; var element = document.getElementById('SomeElementYouWantToAnimate'); element.style.position = 'absolute'; function step(timestamp) { if (!start) start = timestamp; var progress = timestamp - start; element.style.left = Math.min(progress / 10, 200) + 'px'; if (progress < 2000) { window.requestAnimationFrame(step); } } window.requestAnimationFrame(step); 复制代码
对于 requestIdleCallback 而言,最出名的应用应该就是 react 了,react 使用了 requestIdleCallback 进行 diff 任务的调度工做,避免了单个 diff 任务耗时过长,而致使界面卡顿的问题,这个方法的回调将在 Event Loop 空闲的时候唤起,并提供浏览器接下来可使用的空闲时间(即下一帧渲染以前拥有的时间)。
在 HTML Standard 中是这么描述 Event loop 的:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.
咱们每一个浏览器界面都有对应的 Event loop,细心的同窗可能看到上面写的是 evet loops ,咱们每一个界面并不止一个 event loop ,而是有多个 event loop,不一样的 event loop 管理的方向是彻底不一样的:
咱们接下来说的 event loop 都默认指代 window event loop,后者咱们应用并很少,worker 能够看做一个不能访问 dom 的 JavaScript 运行环境,而 worklet 还处于草案阶段,主要应用于须要超高性能场景( worklet 中跑的是机器码而不是 JavaScript )。
所谓的 Event Loop 就是咱们界面从建立到销毁,浏览器中不停执行的一些步骤,以及为了执行这些步骤而持有的一些固有属性。
每一个 event loop 持有如下属性:
疑问点1:为何要有多个 task queque?由于浏览器能够为不一样的 queque 分配不一样的优先级,从而优先处理某种类型任务。
疑问点2:为何 task queue 不是队列,而是集合?由于浏览器老是会挑选可执行的任务去执行,而不是根据进入队列的时间。
event loop 存在期间将会一直执行下列的步骤:
可见浏览器并不是每次tick都会执行绘制工做,而是根据物理环境的实际状况决定。
好比:某次task插入一个p元素,task结束后并不意味着本次tick会将相应的element绘制到界面上。
咱们习惯把宏任务和微任务都理解成 JavaScript 异步执行的一种形式。
事实上只有宏任务是异步的,而微任务是对宏任务代码执行顺序的再分配;宏任务执行完后老是会执行完全部微任务,这种意义上微任务是阻塞主线程的,若是你在某个微任务中不断建立新的微任务,毫无疑问界面会出现假死。全部微任务的意义在于执行一些老是想在某个任务完成后再执行的代码。
这时候可能不少小伙伴想到 Promise,我我的认为 Promise 的控制反转特性才是它大放异彩的缘由(大多时候咱们喜欢的是 Promise 的语法、链式调用;其实并不关心是不是微任务),而不是由于 Promise.then 是个微任务。
这里有一些奇怪的点,Promise 的规范是应该属于 ECMAScript 编写,本应和 HTML Standard 没有关系,但由于 Promise 的特殊性,浏览器基本是照着 HTML Standard 的规范去实现的 Promise,在 ECMAScript 中 Promise.then 注册的不叫 microtask 而是称为 job。
讲完 Event Loop 彷佛前端开发者压根没法把握住渲染前的那一个点,为了解决这个问题 w3c 定义了 requestAnimationFrame 方法,该方法的回调将会在浏览器的下一次绘制前。
调用 requestAnimationFrame,将会将回调推入 animation frame request callback list,而一个非空的 animation frame request callback list,将会使浏览器周期性的向 event loop 中添加一个任务去执行 requestAnimationFrame 注册的回调,这里的周期没有指明,但咱们很容易推测和刚刚 event loop 中的渲染时机(rendering opportunity)有关。
直到如今咱们依然没法看到使用 requestAnimationFrame 相对于 setinterval 构建 JS 动画的优越性,你们都是周期性向 event loop 推送任务,为何 requestAnimationFrame 就要更稳定呢?
答案藏在 event loop 中在多个 task queue 之间优先级不一致中,每一个 task 拥有一个 task source 属性,决定了 task 归属到哪一个 task queque,而 task queque 拥有不一样的执行优先级,显然由 animation frame request callback list 非空而建立的任务优先级是要高于 timer 的。
animation frame request callback list 中全部的回调函数,将会在一次任务内所有执行,意味着同步的屡次调用 requestAnimationFrame,将会在下一次渲染前的一次任务内按顺序所有执行。
在讲 requestIdleCallback 以前,咱们先回忆一下 react16 新推出的基于 fiber 架构,在 react16 以前 react 使用 stack reconciler,那 react 声称 fiber 解决了什么问题呢?
首先咱们先看看 stack reconciler 存在什么问题,依然是 JS 动画的例子,若是咱们使用 requestAnimationFrame 调制咱们的动画,若是不存在 long task,动画的帧数将获得保证;可是若是某个 task 执行时间超过50ms,没人能够保证界面不卡顿;这就是 stack reconciler 的问题,存在单次 diff 时间过长的问题。
而 react 推出 fiber 就是为了解决这个问题,提升动画的流畅度,将任务切分到多个帧之间,保证子任务不会出现成为 long task,提供 fiber 这种核心能力的即是 requestIdleCallback。
调用 requestIdleCallback 方法,将使浏览器在空闲时段调用该方法的回调函数。
让咱们回到 Event Loop 的执行流程,能够得知在 Event Loop 没有须要执行的任务的时候会计算空闲时间,空闲时间的计算有两种状况。
50ms这个魔法数字来自大数据的分析,有研究表面高于50ms/帧的画面会让人以为卡顿,因此咱们时常要求当个任务不能过长,就是这个缘由。
一样调用 requestIdleCallback 方法的回调并不会直接进入 task queque,而是在每轮 event loop 结束以前会计算 idleperiod,若是 idleperiod 大于0,才会将任务放进队列中。
提示:idleperiod 的时间除了和渲染频率有关,还和最近要执行的定时器有着必定的关系,idleperiod 老是会小于下个定时器要执行的时间。
同步的调用屡次 requestIdleCallback,该方法的回调执行可能会分布在不一样的帧上,每执行完一次回调,浏览器会检查是否还有剩余的空闲时间,若是没有,会将执行控制权交还 event loop,若是有才会继续执行下一个回调,听起来是否是和 react fiber 的调度很像。
本文和上一篇这个系统的文章的重点都在于弄清楚整个界面的生命周期和运转过程,理解绘制和脚本执行直接的关系。
我相信 react 的开发人员若是没读过规范,是不能设计出fiber这样的架构的,这些规范知识提供了高性能 web 开发的理论基础。