JS笔记《浏览器与<script>》

104 阅读7分钟

浏览器组成

  • 浏览器的核心是两部分:渲染引擎和JS引擎。

渲染引擎

  • 渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。不同浏览器有不同的渲染引擎:

    • Firefox:Gecko
    • Safari:Webkit
    • Chrome:Blink
    • IE:Tridnet
    • opera:presto
  • 渲染引擎处理网页,通常分为四个阶段:

    • 解析代码:HTML代码解析为DOM,CSS代码解析为CSSOM
    • 对象合成:将DOM和CSSOM合成一颗渲染树(render tree)。
    • 布局:计算出渲染树的布局(layout)。
    • 绘制:将渲染树绘制到屏幕(paint)。
  • 以上四步并非严格按照顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。

重排

  • reflow的本质就是重新计算布局树,引发layout。从而导致发生 layout后续所有的步骤解析。

  • 为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作。当JS代码全部解析执行完成后再进行统一计算,所以改动属性造成的reflow是异步完成的,也正因为如此,当JS获取布局属性时,就可能造成无法获取到最新的布局信息,最终决定在获取元素几何信息时立即 reflow。

  • 触发重排的条件(不完全):

    • dom节点的删除、添加
    • dom节点的宽高、位置或 display变化。
    • 查询几何信息clientWidth、offsetWidth等。

重绘

  • 重绘发生在元素的可见的外观被改变,会引发paint。重绘不会带来重新布局,所以并不一定伴随重排,但重排一定伴随重绘。

  • 触发重绘的条件(不完全):

    • 修改文字颜色、背景颜色等。

优化重排与重绘

  • 读取DOM或者写入DOM,尽量写在一起,不要混杂。不要读取一个DOM节点,然后立刻写入,接着再读取一个DOM节点。
  • 缓存DOM信息。
  • 不要一项一项的改变样式,而是使用CSS的class一次性改变样式。
  • 使用documentFragment节点操作DOM。
  • 动画使用absolutefixed定位,减少对其他元素的影响。
  • 使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重绘之前执行,而不是立即要求页面重绘。

JS引擎

  • JS引擎的主要作用是读取网页中的JS代码,对其处理后运行。JS是一种解释型语言,不需要编译,由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就可以重接解释。缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。

  • 为了运行速度提升,目前的浏览器都将JS进行一定程度的编译,生成类似字节码的中间代码,提高运行速度。早期,浏览器内部对 JavaScript 的处理过程如下:

    1. 读取代码,进行词法分析,将代码分解成词元。
    2. 对词元进行语法分析,将代码整理成“语法树”。
    3. 使用“翻译器”,将代码转为字节码。
    4. 使用“字节码解释器”,将字节码转为机器码。
  • 逐行解释将字节码转为机器码,是很低效的。为了提高运行速度,现代浏览器改为采用“即时编译”(Just In Time compiler,缩写 JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存。通常,一个程序被经常用到的,只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提升。

  • 字节码不能直接运行,而是运行在一个虚拟机之上,一般也把虚拟机称为JS引擎。并非所有的JS虚拟机运行时都有字节码,有的JS虚拟机基于源码,即只要有可能,就通过JIT编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其他采用虚拟机(比如 Java)的语言不尽相同。这样做的目的,是为了尽可能地优化代码、提高性能。下面是目前最常见的一些 JavaScript 虚拟机:

    • IE: Chakra
    • Safari:Nitro/JavaScript Core
    • Opera:Carakan
    • Firefox:SpiderMonkey
    • Chrome, Chromium:V8

<script>概述

  • JS是浏览器内置的脚本语言,浏览器内置了JS引擎,并且提供了各种接口让JS脚本可以控制浏览器的各种功能。

嵌入方法

<script>嵌入代码

<script type="application/javascript">
  console.log('Hello World');
</script>

<script>加载外部

  • 加载外部脚本和嵌入代码块,这两种方法不能混用。
<script src="https://www.example.com/script.js"></script>

事件属性

<button id="myBtn" onclick="console.log(this.id)">点击</button>

URL协议

<a href="javascript:console.log('Hello')">点击</a>

动态加载

  • 动态生成的<script>不会阻塞页面渲染,生成后再插入页面,从而实现脚本的动态加载。
// 无法保证脚本的执行顺序 哪个先下载完就执行哪个
['a.js', 'b.js'].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  // 添加以下代码可以保证脚本的执行顺序
  script.async = false
  document.head.appendChild(script);
});
  • 为动态加载的脚本指定回调函数
function loadScript(src, done) {
  var js = document.createElement('script');
  js.src = src;
  // 加载完成后执行回调函数
  js.onload = function() {
    done();
  };
  js.onerror = function() {
    done(new Error('Failed to load script ' + src));
  };
  document.head.appendChild(js);
}

<script>工作原理

  • 浏览器加载JS脚本主要是通过<script>元素完成。正常的网页加载流程为:

    • 浏览器一边下载HTML网页,一边开始解析。也就是说不等到下载完就开始解析。
    • 当解析过程遇到<script>元素时,暂停解析。把网页渲染的控制权交给JS引擎。
    • 如果<script>元素引用了外部脚本,就下载该脚本再执行,否则就直接执行JS代码。
    • JS引擎执行完毕,控制权还给渲染引擎,恢复往下解析HTML网页。
  • 加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后再继续渲染。原因是JS代码可以修改DOM,所以必须把控制权让给它,否则会导致复杂的线程问题。如果外部脚本加载时间很长,那么浏览器会一直等待脚本下载完成,造成网页长时间失去响应,呈现假死状态。这被称为阻塞效应

  • 为了避免这种情况,较好的做法是将<script>标签放在页面底部。这样即使遇到脚本失去响应,用户至少可以看到内容。还有一个好处是可以直接调用DOM节点,因为脚本在页面底部时,DOM 肯定已经生成了。

<body>
  <script>
    console.log(document.body.innerHTML);
  </script>
</body>
  • 还有一种方法是监听DOMContentLoaded事件。
<head>
  <script>
    document.addEventListener(
      'DOMContentLoaded',
      function (event) {
        console.log(document.body.innerHTML);
      }
    );
  </script>
</head>
  • 如果遇到多个<script>标签(a.js、b.js),浏览器会同时并行下载a.jsb.js,但是执行时会保证先执行a.js,然后再执行b.js,即使后者先下载完成也是如此。

defer属性

  • 延迟脚本的执行,等到DOM加载生成后再执行脚本。
<script src="a.js" defer></script>
<script src="b.js" defer></script>
  • 运行流程如下:

    • 浏览器开始解析HTML网页。
    • 解析过程中发现带有defer属性的\<script>元素。
    • 浏览器继续往下解析HTML网页,同时并行下载\<script>元素加载的外部脚本。
    • 浏览器完成解析HTML网页(DOMContentLoaded事件触发前),开始按照出现的顺序执行已经下载完成的脚本。
  • 对于内置的代码块及动态生成的<script>标签,defer属性不起作用。

async属性

  • 使用另一个进程下载脚本,下载时不会阻塞渲染。
<script src="a.js" async></script>
<script src="b.js" async></script>
  • 运行流程如下:

    • 浏览器开始解析HTML网页。
    • 解析过程中,发现带有async属性的<script>标签。
    • 浏览器继续往下解析HTML网页,同时并行下载<script>标签中的外部脚本。
    • 脚本下载完成,浏览器暂停解析HTML网页,开始执行下载的脚本。无法保证执行顺序,哪个脚本先下载完就先执行哪个脚本。
    • 脚本执行完成,浏览器恢复解析HTML网页。
  • 如果脚本之间没有依赖关系,就使用async属性。如果脚本之间有依赖关系,就使用defer属性。如果两个属性同时拥有,defer不起作用。