『前端进阶』🌋 基础不牢地动山摇

3,198 阅读10分钟

HTML 标签特性

交互实现

  • meta 标签:自动刷新/跳转
<meta http-equiv="Refresh" content="5; URL=page2.html">

性能优化

<script> 标签:调整加载顺序提升渲染速度

浏览器的底层运行机制,渲染引擎在解析 HTML 时,若遇到 script 标签引用文件,则会暂停解析过程,同时通知网络线程加载文件,文件加载后会切换至 JavaScript 引擎来执行对应代码,代码执行完成之后切换至渲染引擎继续渲染页面。

  • async 属性。立即请求文件,但不阻塞渲染引擎,而是文件加载完毕后阻塞渲染引擎并立即执行文件内容。
  • defer 属性。立即请求文件,但不阻塞渲染引擎,等到解析完 HTML 之后再执行文件内容。
  • HTML5 标准 type 属性,对应值为“module”。让浏览器按照 ECMA Script 6 标准将文件当作模块进行解析,默认阻塞效果同 defer,也可以配合 async 在请求完成后立即执行。

image.png

link 标签:通过预处理提升渲染速度

  • dns-prefetch:当 link 标签的 rel 属性值为“dns-prefetch”时,浏览器会对某个域名预先进行 DNS 解析并缓存。这样,当浏览器在请求同域名资源的时候,能省去从域名查询 IP 的过程,从而减少时间损耗
  • preconnect:让浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析、TLS 协商、TCP 握手,通过消除往返延迟来为用户节省时间。
  • prefetch/preload:两个值都是让浏览器预先下载并缓存某个资源,但不同的是,prefetch 可能会在浏览器忙时被忽略,而 preload 则是一定会被预先下载。
  • prerender:浏览器不仅会加载资源,还会解析执行页面,进行预渲染。

浏览器获取资源文件的流程: image.png

搜索优化

  • meta 标签:提取关键信息
  • link 标签:减少重复
  • <link href="https://lagou.com/a.html" rel="canonical"> 这样可以让搜索引擎避免花费时间抓取重复网页。不过需要注意的是,它还有个限制条件,那就是指向的网站不允许跨域。

延伸内容:OGP(开放图表协议)

高效操作 DOM 元素

为什么说 DOM 操作耗时

线程切换

浏览器包含渲染引擎(也称浏览器内核)和 JavaScript 引擎,它们都是单线程运行。单线程的优势是开发方便,避免多线程下的死锁、竞争等问题,劣势是失去了并发能力

  • 浏览器为了避免两个引擎同时修改页面而造成渲染结果不一致的情况,增加了另外一个机制,这两个引擎具有互斥性,也就是说在某个时刻只有一个引擎在运行,另一个引擎会被阻塞。
  • 操作系统在进行线程切换的时候需要保存上一个线程执行时的状态信息并读取下一个线程的状态信息,俗称上下文切换。而这个操作相对而言是比较耗时的。
  • 每次 DOM 操作就会引发线程的上下文切换——从 JavaScript 引擎切换到渲染引擎执行对应操作,然后再切换回 JavaScript 引擎继续执行,这就带来了性能损耗。单次切换消耗的时间是非常少的,但是如果频繁地大量切换,那么就会产生性能问题。

重新渲染

另一个更加耗时的因素是元素及样式变化引起的再次渲染,在渲染过程中最耗时的两个步骤为重排(Reflow)重绘(Repaint)

可能会影响到其他元素排布的操作就会引起重排,继而引发重绘,比如:

  • 修改元素边距、大小
  • 添加、删除元素
  • 改变窗口大小 与之相反的操作则只会引起重绘,比如:
  • 设置背景图片
  • 修改字体颜色
  • 改变 visibility 属性值

如何高效操作 DOM

  • 在循环外操作元素
  • 批量操作元素
  • 缓存元素集合

总结

  • 尽量不要使用复杂的匹配规则和复杂的样式,从而减少渲染引擎计算样式规则生成 CSSOM 树的时间;
  • 尽量减少重排和重绘影响的区域;
  • 使用 CSS3 特性来实现动画效果。
  • 利用绝对定位 脱离文档流,这样操作定位里面的内容不会引起外部的重排
  • 有动画的话,也可以考虑分层渲染的机制。加上will-change
  • dom 的操作进行 json 数据化,只操作一个根节点即可

3个使用场景助你用好 DOM 事件

防抖

为函数的执行设置一个合理的时间间隔,避免事件在时间间隔内频繁触发,同时又保证用户输入后能即时看到搜索结果。

// 代码2
const debounce = (func, wait = 0) => {
  let timeout = null
  let args
  function debounced(...arg) {
    args = arg
    if(timeout) {
      clearTimeout(timeout)
      timeout = null
    }
    // 以Promise的形式返回函数执行结果
    return new Promise((res, rej) => {
      timeout = setTimeout(async () => {
        try {
          const result = await func.apply(this, args)
          res(result)
        } catch(e) {
          rej(e)
        }
      }, wait)
    })
  }
  // 允许取消
  function cancel() {
    clearTimeout(timeout)
    timeout = null
  }
  // 允许立即执行
  function flush() {
    cancel()
    return func.apply(this, args)
  }
  debounced.cancel = cancel
  debounced.flush = flush
  return debounced
}

节流

const throttle = (func, wait = 0, execFirstCall) => {
  let timeout = null
  let args
  let firstCallTimestamp


  function throttled(...arg) {
    if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
    if (!execFirstCall || !args) {
      console.log('set args:', arg)
      args = arg
    }
    if (timeout) {
      clearTimeout(timeout)
      timeout = null
    }
    // 以Promise的形式返回函数执行结果
    return new Promise(async(res, rej) => {
      if (new Date().getTime() - firstCallTimestamp >= wait) {
        try {
          const result = await func.apply(this, args)
          res(result)
        } catch (e) {
          rej(e)
        } finally {
          cancel()
        }
      } else {
        timeout = setTimeout(async () => {
          try {
            const result = await func.apply(this, args)
            res(result)
          } catch (e) {
            rej(e)
          } finally {
            cancel()
          }
        }, firstCallTimestamp + wait - new Date().getTime())
      }
    })
  }
  // 允许取消
  function cancel() {
    clearTimeout(timeout)
    args = null
    timeout = null
    firstCallTimestamp = null
  }
  // 允许立即执行
  function flush() {
    cancel()
    return func.apply(this, args)
  }
  throttled.cancel = cancel
  throttled.flush = flush
  return throttled
}

节流与防抖都是通过延迟执行,减少调用次数,来优化频繁调用函数时的性能。不同的是,对于一段时间内的频繁调用,防抖是延迟执行后一次调用,节流是延迟定时多次调用

代理

事件触发流程如图所示,主要分为 3 个阶段:

  • 捕获,事件对象 Window 传播到目标的父对象,图 1 的红色过程;
  • 目标,事件对象到达事件对象的事件目标,图 1 的蓝色过程;
  • 冒泡,事件对象从目标的父节点开始传播到 Window,图 1 的绿色过程。

image.png

关于 DOM 事件标准

// 方式1
<input type="text" onclick="click()"/>
// 方式2
document.querySelector('input').onClick = function(e) {
  // ...
}
// 方式3
document.querySelector('input').addEventListener('click', function(e) {
  //...
})
  • 方式 1 和方式 2 同属于 DOM0 标准,通过这种方式进行事件监会覆盖之前的事件监听函数。
  • 方式 3 属于 DOM2 标准,推荐使用这种方式。同一元素上的事件监听函数互不影响,而且可以独立取消,调用顺序和监听顺序一致。

浏览器如何渲染页面?

从 HTML 到 DOM

  1. 字节流解码
  2. 输入流预处理
  3. 令牌化

第一步是将字符数据转化成令牌(Token),第二步是解析HTML 生成 DOM 树。

遇到 script 标签时的处理

  • 如果遇到的是内联代码,那么解析过程会暂停,执行权限会转给 JavaScript 脚本引擎,待 JavaScript 脚本执行完成之后再交由渲染引擎继续解析。
  • 有一种情况例外,那就是脚本内容中调用了改变 DOM 结构的 document.write() 函数,此时渲染引擎会回到第二步,将这些代码加入字符流,重新进行解析。
  • 如果遇到的是外链脚本,根据标签属性来执行对应的操作。

构建 DOM 树

解析 HTML 的第二步是树构建。

  • 浏览器在创建解析器的同时会创建一个 Document 对象。
  • 在树构建阶段,Document 会作为根节点被不断地修改和扩充。标记步骤产生的令牌会被送到树构建器进行处理。
  • HTML 5 标准中定义了每类令牌对应的 DOM 元素,当树构建器接收到某个令牌时就会创建该令牌对应的 DOM 元素并将该元素插入到 DOM 树中。
  • 为了纠正元素标签嵌套错位的问题和处理未关闭的元素标签,树构建器创建的新 DOM 元素还会被插入到一个开放元素栈中。
              Document
             /        \
DocumentType           HTMLHtmlElement
                      /               \
       HTMLHeadElement                 HTMLBodyElement
                                              |
                                          TextNode

image.png

从 CSS 到 CSSOM

与 DOM 树不同的是,CSSOM 树的节点具有继承特性,也就是会先继承父节点样式作为当前样式,然后再进行补充或覆盖。

body { font-size: 12px }
p { font-weight: light }
span { color: blue }
p span { display: none }
img { float: left }

对于上面的代码,会解析生成类似下面结构的 DOM 树:

image.png

上图中的 CSSOM 树并不完整,完整的 CSSOM 树还应当包括浏览器提供的默认样式(也称为“User Agent 样式”)。

构建渲染树

  • DOM 树包含的结构内容与 CSSOM 树包含的样式规则都是独立的,为了更方便渲染,先需要将它们合并成一棵渲染树。
  • 这个过程会从** DOM 树的根节点开始遍历**,然后在 CSSOM 树上找到每个节点对应的样式
  • 遍历过程中会自动忽略那些不需要渲染的节点(比如脚本标记、元标记等)以及不可见的节点(比如设置了“display:none”样式)。同时也会将一些需要显示的伪类元素加到渲染树中。

布局

  • 布局就是计算元素的大小及位置。
  • 计算元素布局是一个比较复杂的操作,包括字体大小、换行位置等,这些因素会影响段落的大小和形状,进而影响下一个段落的位置。
  • 布局完成后会输出对应的“盒模型”,它会精确地捕获每个元素的确切位置和大小,将所有相对值都转换为屏幕上的绝对像素

绘制

  • 绘制就是将渲染树中的每个节点转换成屏幕上的实际像素的过程。得到布局树这份“施工图”之后,渲染引擎并不能立即绘制,因为还不知道绘制顺序,如果没有弄清楚绘制顺序,那么很可能会导致页面被错误地渲染。
    • 例如,对于使用 z-index 属性的元素(如遮罩层)如果未按照正确的顺序绘制,则将导致渲染结果和预期不符(失去遮罩作用)。
  • 所以绘制过程中的第一步就是遍历布局树,生成绘制记录,然后渲染引擎会根据绘制记录去绘制相应的内容。
  • 对于无动画效果的情况,只需要考虑空间维度,生成不同的图层,然后再把这些图层进行合成,当然这个绘制过程并不是静态不变的,会随着页面滚动不断合成新的图形。

渲染页面总结

  • 前面 4 个步骤为 DOM 树的生成过程,后面 3 个步骤是利用 DOM 树和 CSSOM 树来渲染页面的过程
  • 字节 → 字符 → 令牌 → 树 → 页面

JavaScript 的数据类型

  • Null、Undefined、Number、String、Boolean、Symbol、Object
  • 基础类型 的数据在被引用或拷贝时,是值传递,也就是说会创建一个完全相等的变量;
  • 引用类型 只是创建一个指针指向原有的变量,实际上两个变量是“共享”这个数据的,并没有重新创建一个新的数据

Undefined

  • Undefined 是一个很特殊的数据类型,它只有一个值,也就是 undefined。可以通过下面几种方式来得到 undefined:
引用已声明但未初始化的变量;
引用未定义的对象属性;
执行无返回值函数;
执行 void 表达式;
全局常量 window.undefinedundefined

var a; // undefined
var o = {}
o.b // undefined
(() => {})() // undefined
void 0 // undefined
window.undefined // undefined

未完待续。。。