异步加载 js

dom解析

image.png
绘制 dom 树,符合深度优先(纵向)原则,比如先看 head → title → meta → body → div → strong → span。
dom 树是节点解析,dom 树解析完毕代表 dom 数所有的节点解析完毕,不代表加载
(下载完毕)完毕。如看到 img 标签就放到 dom 树上,然后同时下载。 dom 树形成完了以后,就等 css 树形成【cssTree 也是深度优先原则】。
domTree + cssTree = randerTree,randerTree 形成以后才,渲染引擎才会绘制页面, domTree 改变,randerTree 也会改变,会重排,影响效率,要尽量避免重排。
randerTree 触发重排(reflow)的情况:

  • dom 节点的删除,添加;
  • dom 节点的宽高变 化,位置变化;
  • display none ==> block,
  • 查看宽高offsetWidth,offsetLeft,需要保证宽高是实时的

repaint 重绘:效率也比较低,效率影响较小。触发情况:改颜色,图片

js加载

js 是单线程的,会阻断 HTML,css 加载(因为 js 会修改 html 和 css, 一起加载会乱)
javascript 异步加载的三种方案

  • defer 异步加载,和htmlcss一起下载,但要等到 dom 文档全部解析完(dom 树生成完)才会被执行
    • dom 文档全部解析完,不代表整个页面加载完
    • 只有 IE 能用
    • html内部js代码块也可以用
  • async 异步加载,加载完就执行,async 只能加载外部脚本,不能把 js 写在 script 标签里
    • ie9 以上可以用,w3c 标准
    • 不阻塞页面

image.png
image.png
image.png

js 浏览器加载时间线

js 加载时间线:依据 js 出生的那一刻起,记录了一系列浏览器按照顺序做的事(就 是一个执行顺序)
js 时间线步骤(创建 document 对象==>文档解析完==>文档解析完加载完执行完)
1、创建 Document 对象,开始解析 web 页面。解析 HTML 元素和他们的文本内容 后添加 Element 对象和 Text 节点到文档中。这个阶段 document.readyState = ‘loading’。
2、遇到 link 外部 css,创建线程,进行异步加载,并继续解析文档。
3、遇到 script 外部 js,并且没有设置 async、defer,浏览器同步加载,并阻塞,等 待 js 加载完成并执行该脚本,然后继续解析文档。
4、遇到 script 外部 js,并且设置有 async、defer,浏览器创建线程异步加载,并继 续解析文档。
对于 async 属性的脚本,脚本加载完成后立即执行。(异步禁止使用 document.write(), 因为当你整个文档解析到差不多,再调用 document.write(),会把之前所有的文档流 都清空,用它里面的文档代替)
5、遇到 img 等(带有 src),先正常解析 dom 结构,然后浏览器异步加载 src,并继 续解析文档。 看到标签直接生产 dom 树,不用等着 img 加载完 scr。
6、当文档解析完成(domTree 建立完毕,不是加载完毕),document.readyState = ‘interactive’。
7、文档解析完成后,所有设置有 defer 的脚本会按照顺序执行。(注意与 async 的不 同,但同样禁止使用 document.write());
8、document 对象触发 DOMContentLoaded 事件,这也标志着程序执行从同步脚本 执行阶段,转化为事件驱动阶段。
9、当所有 async 的脚本加载完成并执行后、img 等加载完成后(页面所有的都执行 加载完之后),document.readyState = ‘complete’,window 对象触发 load 事件。
10、从此,以异步响应方式处理用户输入、网络事件等。

image.png
DOMContentLoaded = $(document).ready,dom解析完,第7,8步
onload 第9步
image.png

单线程模型

JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合

JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制?所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。

如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 操作(输入输出)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript 语言的设计者意识到,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop)

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质

同步任务和异步任务

程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。

同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有”堵塞“效应。

举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。

任务队列和事件循环

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。

异步操作的模式

回调函数

回调函数是异步操作最基本的方法

回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数

事件监听

另一种思路是采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合“(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程

  1. f1.on('done', f2);
  2. function f1() {
  3. setTimeout(function () {
  4. // ...
  5. f1.trigger('done');
  6. }, 1000);
  7. }

发布/订阅

事件完全可以理解成”信号“,如果存在一个”信号中心“,某个任务执行完成,就向信号中心”发布“(publish)一个信号,其他任务可以向信号中心”订阅“(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)

  1. jQuery.subscribe('done', f2);
  2. function f1() {
  3. setTimeout(function () {
  4. // ...
  5. jQuery.publish('done');
  6. }, 1000);
  7. }
  8. jQuery.unsubscribe('done', f2);

异步操作的流程控制

如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序