本文主要对W3C规范中关于script标签和event loop相关的篇幅作了简单的探讨,针对一些必要的相关概念进行了适当的标注和说明。虽然以前接触过,但都过于零散,但愿借此机会,可以对这些概念可以一个稍微全面一点的认识,也但愿和你们进行交流。因为知识的深度和广度以及英语水平的不足,若有错误,还望包涵指正。javascript
虽然以前查过W3C和WHATWG的关系,可是翻译得差很少的时候有个问题去WHATWG提了issue,才被domenic大大告知我可能看了"假规范"- -(具体可参考连接1,连接2,Fork tracking),最新的规范在这,大部分仍是基本一致的,新增了一些好比type=module的内容等等,还有排版呀,有的描述等等有一些变化,有兴趣的能够去看看。css
浏览器HTML解析过程以下:html
The exact processing details for these attributes are, for mostly historical reasons, somewhat non-trivial, involving a number of aspects of HTML.The implementation requirements are therefore by necessity scattered throughout the specification.html5
能够看到,规范也提到了规范只是一个参考,具体实现因人而异。在测试中,我列出如下发现和期待讨论的话题,但愿对本身和他人都能起到帮助:java
关于script标签基本信息的一些描述这里再也不过多介绍,本身有几个比较关心的点在如下列出。node
对于普通脚本,defer脚本,async脚本,有以下总结:css3
1.对于普通的脚本,有两点须要注意。git
第一:并非在fetch的时候彻底“阻止”后续标签的解析。咱们从timeline能够看到,在第一次praseHTML的最开始,就已经将页面所需的全部静态资源请求send出去了(具体可参考浏览器预解析加载机制)。因此脚本是没法“阻止”后续标签中引用外部资源的请求不被发送的。而且在解析这个脚本到finish load这段时间,还有不少其余操做,如扩展程序的脚本执行,某些VM语句执行,install这一脚本以前的脚本中的定时器等等。github
第二:fetch以后接收完全部数据包最后完成finish load以后,并非当即执行这个脚本内的内容。而是先要判断这个脚本在全部脚本中的顺序,必须肯定这个脚本以前的全部普通脚本执行完毕后,才会执行这个脚本。web
处理模型拥有如下7种状态属性:
"already started" ->> "parser-inserted" ->> "non-blocking" ->> "ready to be parser-executed" ->> "the script’s type" ->> "is from an external file" ->> "the script’s script"
最后一步也就是异步将预处理脚本(见下)的结果设置为脚本的script属性的值,不管这个值是正确的仍是错误的,都应标记脚本为ready状态,这意味着这以后能够触发其余行为。浏览器推迟load事件直到全部的脚本都处于ready状态。
关于这些状态的描述内容并很少,好比初始时尚未"already started",HTML解析器解析到以后当即置为"already started",初始时没有"parser-inserted",当HTML解析器把节点插入到父节点的时候,置为"parser-inserted",当HTML解析器在建立节点对象的时候默认是"non-blocking",当HTML解析器把节点插入到父节点的时候,置为"blocking"(实际是设置为false,便于理解故做此翻译,不要打我。。),若是脚本有async属性,那么又置为"non-blocking"避免阻塞解析,等等等等。
具体要了解的话建议查阅文档。这里咱们探讨下最关键的部分-预处理脚本(原文为prepare a script,感受翻译成准备,预备都不太合适,故做此翻译,若是有更好的翻译还但愿指正),"the script's type","from an external file","the script's script"都是在这一阶段肯定的:
当一个未被标记为"parser-inserted"的脚本元素经历如下3个事件中的任意一个时,浏览器必须当即预处理这个脚本元素:
1.在dom节点中顺序先于(先因而指按照前序进行深度优先遍历的时候)这个脚本的脚本被插入到dom树以后,这个脚本元素被插入到文档中。
2.在全部脚本元素都被插入完毕后,这个脚本元素在文档中且有其余节点被插入到这个脚本元素中。
3.脚本元素已经在文档中而且以前没有src属性,可是如今被设置了src属性。
为了预处理一个脚本,浏览器必须进行如下步骤:
前面1-18步主要考虑的是不须要执行或者说不符合执行条件的时候就中断预处理过程从而不执行这个脚本。好比发现尚未"already started",好比没有src属性且脚本的内容为空或者只有注释,脚本元素没有在文档中,type和language属性不符合规范,用户禁用了JS等等。除了这些以外,还有些诸如脚本有charset则设置,没有就用文档自己的charset。还有有些只是规范有说起,可是没有浏览器或者不是全部浏览器都实现了,好比for,event,nonce属性等等。另外还有一些其余的考虑,这里就不一一赘述了,详细的能够参考规范。
下面着重来看一下19-20步:
第19步:若是脚本元素没有src属性,则进行下列的步骤:
Let source text
等于yourScriptElement.text
的值。
将脚本的type属性设置为"classic"
Let script
做为用source text
和settings
建立的脚本的结果。
设置the script’s script
为上一步的script
。
让脚本处于ready状态
第20步:而后,选择符合下列情形的第一个进行执行:
Type 1:
the script’s type | 是否有src属性 | 是否有defer属性 | 是否有async属性 | 其余条件 |
---|---|---|---|---|
"classic" | 是 | 是 | 否 | 元素已经"parser-inserted" |
将脚本元素添加到将要执行的脚本的集合的末尾。
当脚本处于ready状态的时候,设置脚本元素的"ready to be parser-executed"标记。解析器将处理执行这个脚本。
the script’s type | 是否有src属性 | 是否有defer属性 | 是否有async属性 | 其余条件 |
---|---|---|---|---|
"classic" | 是 | 否 | 否 | 元素已经"parser-inserted" |
脚本元素为"等待解析阻塞的脚本"(见步骤末尾)的状态,同一时刻只能有一个这样的脚本存在。当脚本处于ready状态的时候,设置脚本元素的"ready to be parser-executed"标记。解析器将处理执行这个脚本。
the script’s type | 是否有src属性 | 是否有defer属性 | 是否有async属性 | 其余条件 |
---|---|---|---|---|
"classic" | 是 | 是或者否(意思为是或者否都是同样的) | 否 | 元素上没有"non-blocking"标记 |
尽快当预处理脚本一开始的时候按顺序将脚本元素添加到将要执行的脚本的集合的末尾。
当脚本为ready状态的时候,进行下面的步骤:
1.若是这个脚本如今不是将要执行的脚本的集合的第一个元素,则标记脚本为ready,可是中断剩余的步骤,不执行这个脚本。
2.执行脚本。
3.移除将要执行的脚本的集合中的第一个元素。
4.若是将要执行的脚本的集合仍然不为空且第一个元素被标记为ready,那么跳回到第2步。
the script’s type | 是否有src属性 | 是否有defer属性 | 是否有async属性 | 其余条件 |
---|---|---|---|---|
"classic" | 是 | 是或者否 | 是或者否 | 不适用 |
尽快当预处理脚本一开始的时候将脚本元素添加到将要执行的脚本的集合的末尾。
当脚本为ready状态的时候,执行脚本并将它从集合中移除。
the script’s type | 是否有src属性 | 是否有defer属性 | 是否有async属性 | 其余条件 |
---|---|---|---|---|
"classic" | 否 | 是或者否 | 是或者否 | 元素已经"parser-inserted",XML或者HTML解析器的script-nesting-level比建立这个脚本的低或者相等。建立这个脚本的解析器的文档有css正在阻塞脚本执行 |
脚本元素为"等待解析阻塞的脚本"的状态,同一时刻只能有一个这样的脚本存在。设置脚本元素的"ready to be parser-executed"标记。解析器将处理执行这个脚本。
当即执行这个脚本,即便有其余脚本正在执行。
总共就这6种情形,下面有一个上面提到的概念的补充说明
等待解析阻塞的脚本:
若是一个阻塞了解析的脚本元素在它中止阻塞解析前被移动到了另外一个document中,尽管如此,它仍然会阻塞解析直到形成它阻塞的缘由消除。(例如,若是这个脚本元素因为有一个css阻塞了它而变成一个等待解析阻塞的脚本,可是而后这个脚本在css加载完毕前被移动到了另外一个文档中,这个脚本仍然会阻塞解析直到css加载完毕(可是阻塞的是另外那个文档的解析了),但在这段期间,原来文档的脚本执行和HTML解析是畅通的)
在规范中user agents指的是实现了这些规范的应用。为了更好的叙述,如下咱们暂且用浏览器来代替这一描述。
为了协调事件,用户接口,脚本,渲染,网络等等,浏览器必须使用event loops
。对于event loops
,它有两种类型,一种是针对浏览器上下文(请务必先了解这一律念)而言,另外一种是针对Wokrer
而言。因为对Worker
不太熟悉,咱们这里也主要探讨浏览器相关的东西,因此如下都再也不叙述Worker
相关内容。
一个event loop
有一个或者多个任务队列。一个任何队列是一系列有序的任务集合,这样的队列是经过下面这些算法来工做的:
Events:一般对于专用任务而言,dispatch一个Event对象给一个特定的EventTarget对象。另外,并非全部的事件都是经过任务队列来dispatch(哪些不是呢,可参考区别)。
Parsing:HTML解析器将一个或多个字符转换为token表并处理,这个过程是一个典型的task。
Callbacks:调用一个回调函数常,常适用于专有任务。
Using a resource:当fetch一个资源的时候,若是fetch发生在一个非阻塞的方法,一旦资源的部分或者所有是可用的,也会被看成一个任务执行(即timeline中的receive data和finish loading)。
Reacting to DOM manipulation:为了响应dom变化也会致使一些元素产生task。如当一个元素被插入到文档中的时候。(意思就是说插入以后会致使浏览器从新计算布局,渲染,一些监听节点变化的事件也会被触发,这些都是task)
每个在浏览器上下文的event loop中的task都与Document对象(准确的说是实现了Document接口的对象,规范也说起过为了便于叙述不采用这种准确的说法,由于太长)相关联。若是某个task被加入了某个元素的context的队列,那么这个document对象就是这个元素的node document。若是某个task被加入了某个浏览器上下文的context的队列,那么在入队列的时候,这个document对象就是浏览器上下文的active document。若是某个task是经过脚本或者是针对脚本的,那么这个document对象就是经过脚本的配置对象指定的responsible document(如今想一想responsible这个词在这里仍是挺有意思,由于纯静态页面的document是不须要对任何东西负责的)。
当浏览器将一个task加入队列的时候,它必须将这个task加入相关的event loop中的某一个任务队列。
每个task在定义时都会有指定的task source(一共有4种,DOM manipulation task source,user interaction task source,networking task source,history traversal task source)。全部来自一个特定的task source的task都必须被添加到一个特定的相同的event loop(例如Document对象产生的回调函数,触发在Document对象上的mouseover事件,Document中等待解析的任务等等,他们都有相同的事件源-Document),可是不一样来自不一样task source的task也许会被添加到不一样的任务队列。
例如,浏览器也许有一个针对鼠标和键盘的任务队列(它们都来自user interaction这一task source)和其余的任务队列。那么相对其余任务队列而言,浏览器也许会给鼠标和键盘事件更高的优先级,来保持响应与用户的交互,可是这又不会饥饿其余任务队列。而且毫不会未来自同一task source的事件颠倒次序执行(意思就是task必须按照它添加时的顺序去执行)。
每个event loop都有一个当前执行任务。初始时为null
。它被用做处理reentrancy(可重入性,相似于generator,在内联脚本中直接使用document.write就是这样,由于这样是把write的参数写到以前的input stream(就是还未解析的字节流)里面)。每个event loop也有一个performing a microtask checkpoint 的flag,初始时为false。它被用做阻止对perform a microtask checkpoint这个算法的可重入性调用。
关于microtask:每个event loop都有一个microtask队列,处于microtask队列而不是普通的task队列中的task就叫作microtask。这里有两种类型的microtask,一种是单一回调函数microtask,一种是复合microtask。注意,规范中只针对单一回调函数microtask有具体描述。
一个event loop在它存在的期间必须不断重复如下步骤:
1.取出某一个任务队列队列头的任务(若是存在的话)。若是与浏览器上下文的event loop相关联的Documents对象不是fully active状态,那么忽略这个task。浏览器也许会选择任何一个任务队列。若是没有task能够取的话,跳到第6步。
2.将event loop的当前运行任务设置为上一步选择到的task。
3.运行这个task。
4.将event loop的当前运行任务设置为null
。
5.将第3步中运行的task从它的任务队列中移除。(这也说明以前取任务时进行的队列操做是peek,而不是poll)
6.执行一个microtask checkpoint操做。由于有点多,避免混乱我写在这7个步骤完毕后的位置。
7.更新渲染:若是这个event loop是浏览器上下文的event loop而非Worker的event loop,那么执行以下步骤:
Let now
等于now()方法的返回值。(能够理解为timeline中的start time)
Let docs
等于与这个event loop相关联的Document对象集合。这个集合是随意排序的,可是要遵循必定的原则,具体能够参照规范。简单举例来讲,A这个Document嵌套了B和C,B嵌套了D。那么顺序便可以是A,B,C,D也能够是A,B,D,C。只要保证C在B后面,B,C在A后面,D在B后面就行。
迭代docs
,对于其中的每一个doc
。若是这里存在一个顶级的浏览器上下文B(顶级就是指嵌套浏览器上下文状况下最祖先的那个浏览器上下文,形象一点的描述可参考连接)且不会从此次更新渲染中受益,那么将docs中全部浏览器上下文的顶级浏览器上下文为B的Document对象移除。
一个顶级浏览器上下文是否会从渲染更新中受益取决与几个方面,如更新频率。举例来讲,若是浏览器尝试60HZ的刷新频率,那么这些步骤只有在每16.7ms内才是有意义的。若是浏览器发现一个顶级浏览器上下文没法维持这个频率,它也许会将docs集合中的全部document对应的刷新频率下调到30HZ,而不是偶尔下调频率。(规范并不强制规定任何特定的模型用于什么时候更新渲染),相似的,若是一个顶级浏览器上下文是在background中(不太明白,猜想是dispaly:none之类的意思),那么浏览器也许会下调到4HZ,甚至更低。
另外一个关于浏览器可能会跳过更新渲染的例子是确保某些task在某些task以后被当即执行,这伴随着仅仅是microtask checkpoints的交替。(或者没有这些交替,例如requestAnimationFrame中animation帧的回调函数交替)。例如,浏览器也许但愿合并定时器回调函数,而不但愿在合并的时候存在渲染更新。
若是有一个浏览器认为不会从渲染更新中受益的嵌套的浏览器上下文B,那么从docs中移除那些浏览器上下文为B的元素。
正如顶级浏览器上下文同样,对于嵌套的浏览器行下午,不少因素也会影响到它是否会从更新渲染中受益。例如,浏览器也许但愿花费较少的资源渲染第三方的内容,特别是当前不可见的内容或者是受限制的内容。在这一的例子中,浏览器也许会决定不多或者根本不对这些内容更新渲染。
对于docs中每一个fully active的Document对象,触发resize
对于docs中每一个fully active的Document对象,触发scroll
对于docs中每一个fully active的Document对象,触发媒体查询和提交变化
对于docs中每一个fully active的Document对象,运行CSS animations并发送事件。
对于docs中每一个fully active的Document对象,运行全屏渲染步骤。
对于docs中每一个fully active的Document对象,运行animations回调函数。
对于docs中每一个fully active的Document对象,更新渲染或者用户接口,和浏览器上下文来反应当前的状态。
9.返回到第1步继续执行。
接上面提到的第6步,执行microtask checkpoint操做以下:
当一个算法须要将一个microtask加入队列时,它必须被追加到相关的event loop的microtask 队列。这个microtask的task source就被叫作microtask task source。
将一个microtask移动到普通的任务队列是颇有可能的,若是发生这样的移动的话,在它的初次运行时,将执行spins the event loop步骤。
当浏览器去执行一个microtask checkpoint的时候,若是这个performing a microtask checkpoint的falg为false,那么浏览器必须执行如下步骤:
1.将这个flag置为true
。
2.若是event loop的microtask队列为空,则跳到第8步:
3.取出microtask队列头的元素。
4.将event loop的当前运行任务设置为上一步取出的task。
5.运行这个task。
注意:这也许会涉及调用回调函数,最后会调用清理步骤,在清理步骤中也许又会执行microtask checkpoint操做,致使无终止条件的递归,这就是为何咱们须要用这个flag去避免这一状况。
6.设置event loop的当前运行任务为null
。
7.从microtask队列中移除上面运行的这个task。而后返回到第2步。
8.对于每个responsible event loop为这个event loop的环境配置对象,notify about rejected promises。
9.将flag置为false
。
就像咱们用迅雷同时下载10个文件同样,假设咱们是下行速度是1M/s,那么显然不可能10个资源每一个的下载速度都是100kb/s,由于每一个资源的资源热度是不一样的,因此有的是500kb/s,而有的可能只有20kb/s,有的甚至没法下载。
对于浏览器而已也是相似的道理,浏览器的资源调度算法以及每一个时间段的网络状况决定了下载资源的顺序,所花费的精力等等。以chrome的资源获取优先级算法为例,咱们不难看出,在获取到html以后,css的请求优先级是最高的,由于对于如今的web页面来讲,没有css的后果可能远远大于没有其余资源。对于脚本中发起的请求如经过接口获取数据等则为high,对于普通的js而已,优先级为medium,普通的图片和async脚本都为low等等等等,随着时间的推移,这个算法确定也会发生相应的变化来提高那个时候的应用体验。
关于这些点在network中与之相关的莫过于Queueing和Stalled属性了:
Queueing. The browser queues requests when:
There are higher priority requests.
There are already six TCP connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only.
Stalled: The request could be stalled for any of the reasons described in Queueing.
因此浏览器最开始会按照html中资源出现的顺序发送请求去获取,可是资源的接收顺序却不必定是按照这个顺序。一个请求发出去以后,后面又来了一个请求,而这个请求的优先级比当前的要高,那么极可能就会先去接收这个优先级更高的资源的数据。而对于优先级相同的多个资源,则极可能采用你接收一段数据,我接收一段数据这样的方式交叉运行。也就是咱们经常看到的页面中的图片加载的时候每每是多个图片同时慢慢从白屏到加载完毕,而不是一个加载完毕后再加载另外一个。
另外前面已经提到过了,对于普通脚本则是确定会按照html中的顺序执行的,也就是说若是脚本a只有500kb,而在他后面的脚本b只有1kb,那么即便脚本b获取所有字节后完成finish load也不能当即执行,必须等到脚本a获取所有字节后且执行完毕后它才能执行。而若是a和b都是async的脚本化则没必要遵循这一原则,谁先获取到谁就先执行。为何呢,由于async设计的本意就是为了抽离与页面无关的逻辑的,它们之间也不该该存在连贯性和依赖性,然后面的普通脚本更不用说了,更不该依赖它们去工做。
因此后面连接提到的视频中提问者说只要不操做dom和获取dom,就应该把这些公共代码提取出来放在head中async引入来达到性能优化的效果,实际上是不稳当的。好比loadsh就符合这个要求,咱们显然不能这么作,一是由于lodash体积太大,没法保证在body尾部用到lodash的代码所处的脚本必定晚于lodash后执行,二是因为网络缘由,就是lodash是一个只有1kb的资源,也很难保证。
此次阅读规范的过程,了解了不少知识,也早已超出了当初想要得到的知识,这即是学习的乐趣。固然也有不少地方花了很长时间才弄清楚究竟是表达的什么意思,也还存留一些问题到目前也仍未理解,你们有不明白或者以为错误的地方但愿多多交流,也但愿随着岁月,再来回头探索的时候可以明白。
the-javascript-event-loop-explained
浏览器如何构建dom树(chrome官方文档,另外里面有配套的视频,很是不错)