Skip to content
大纲

核心原理

数据劫持

vue2: 通过 Object.defineProperty

劫持对象

js
const span = document.createElement("span");
document.body.appendChild(span);
const updateNode = (key, val) => {
  if (key === "span") {
    span.innerHTML = val;
  }
};
const data = { span: "" };
const copyData = JSON.parse(JSON.stringify(data));
for (let key in copyData) {
  Object.defineProperty(copyData, key, {
    get() {},
    set(val) {
      data[key] = val;
      updateNode(key, val);
    },
  });
}
copyData.span = "666";

劫持数组:通过覆写数组原生方法进行接持

js
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
// hack 以下几个函数
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];
methodsToPatch.forEach(function (method) {
  // 获得原生函数
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    // 调用原生函数
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // 触发更新
    ob.dep.notify();
    return result;
  });
});

vue3: 通过 Proxy

使用 Proxy 进行代理,相比 Object.defineProperty 的方式,具有以下优点

  • 可以直接监听对象而非属性
  • 可以直接监听数组
  • 具有更多的拦截方法,扩展性高
  • 无需在操作前拷贝原对象
js
const span = document.createElement("span");
document.body.appendChild(span);
const updateNode = (key, val) => {
  if (key === "span") {
    span.innerHTML = val;
  }
};
const data = { span: "" };
const proxy = new Proxy(data, {
  set(target, prop, value, receiver) {
    Reflect.set(target, prop, value, receiver);
    updateNode(prop, value);
  },
});
proxy.span = "777";

组件渲染

模板编译

模板是对组件渲染结构的描述, 相比于直接编写 render,对开发者更加友好, 且易读性更高
vuereact 都支持 jsx, 相比于通过 h 函数描述的 render 可读性更高, vue 同时还独立出 template, 其较 jsx 更接近 html
vue 内部是通过 vnode 描述组件的, 因而 template 在使用前还需要经过编译流程

  1. 解析: 将模板解析为 AST
  2. 优化: 对静态内容进行优化(静态结点渲染后不会经过 patch 流程)
    • 标记静态和非静态结点
    • 标记静态根节点
  3. 转化: 对处理后的 AST 转换为 render 函数

虚拟 DOM

虚拟 DOM 是对真实 DOM 的抽象, 相比于真实 DOM 有以下特点

  • 本质为数据对象, 因而可以独立于平台
  • 由于是真实 DOM 的数据抽象, 因而性能开支较小
  • 在响应式数据变动时,多次同步的数据变动只触发一次真实 DOM 更新

vue 中使用 VNode 指代 虚拟 DOM, 其包含的信息包括

  • 当前标签: 元素标签或组件名称
  • 相关数据: classstyleattrspropson(事件回调)、directivesref
  • 子节点: 文本节点、VNode

真实映射

渲染器负责将 虚拟 DOM 渲染为真实 DOM,其工作分为两个阶段

  • 挂载(mount)元素: 实例或 app 挂载到指定 容器 上
  • 更新(patch)元素: 响应式数据变动导致真实 DOM 变动

diff 流程

diff算法 是 patch 阶段, 为了尽可能减少元素变动使用的算法, vue 相比 react 采用了双端比较, 实际性能略高

  • 信息记录
    • 原列表:使用 oldHead 与 oldTail 指向头尾节点
    • 现列表:使用 newHead 与 newTail 指向头尾节点
  • 双端比较:在一次比较中,按顺序进行以下最多四次比较,若节点相同则更新,并修改节点指向
    1. oldHead 与 newHead 比较,若相同则更新,oldHead 与 newHead 分别指向下一个节点
    2. oldTail 与 newHead 比较,若相同则更新,oldTail 与 newHead 分别指向上一个节点
    3. oldHead 与 newTail 比较,若相同则更新,oldHead 节点移动到 oldTail 之后,oldHead 后移,newTail 前移
    4. oldTail 与 newHead 比较,若相同则更新,oldTail 节点移动到 oldHead 之前,oldTail 前移,newTail 后移
    5. 若以上都不满足,则遍历原列表查看 newHead 是否存在
      • 若存在,则更新节点,并将该节点移动到 oldHead 之前;
      • 若不存在,则添加到 oldHead 之前, newHead 后移
  • 双端比较结束后,判断新增或删除节点
    • 若 oldTail < oldHead,loop: newHead -> newTail 新增节点
    • 若 newTail < newTail,loop: oldHead -> oldTail 移除节点
code
[a, b, c, d] // 原列表
[b, a, d, c] // 现列表

b, [a, c, d] // 不满双端比较(5),b 移动到 a 之前,oldHead 指向 a,newHead 指向 a
b, a, [c, d] // 满足双端比较(1),oldHead 指向 c,newHead 指向 d
b, a, d, [c] // 满足双端比较(4),d 移动到 c 之前,oldTail 指向 c,newTail 指向 c
b, a, d, c   // 满足双端比较(1),遍历结束

vue3 的优化

  • 静态标记: diff 中跳过静态节点的比较和更新
  • 静态提升: 编译阶段对静态节点打上标记, 渲染时只创建一次
  • 事件缓存: 更新时不再解绑和重新绑定

nextTick

在响应式数据变动时,DOM 的更新是非同步的, nextTick 可在 DOM 更新后立即执行

  • vue2 为this.$nextTick(cb)
  • vue3 还提供了独立的 nextTick()

v2.6+ 使用内部会优先使用微任务处理(并按浏览器支持程度降序)

  • Promise > MutationObserver > setImmediate > setTimeout

vue3 因为不再兼容低端浏览器则默认使用 Promise

路由监听

hash 模式

  • 监听 popstate 事件, 对 历史记录条目 变动进行响应
  • 若不支持 popstate 则监听 hashchange 事件,对 url#(包括) 后的哈希值变化进行响应

history 模式

  • 服务端部分: 当页面刷新时,请求路径未匹配资源时,服务器返回 index.html 即可
  • 浏览器部分
    • 导航部分: 浏览器导航 和 router 导航
      • 浏览器导航: 该方式发生页面加载, 通过监听 popstate 事件, 对历史记录变动进行响应
        • 用户操作: 浏览器前进后退跳转
        • 代码操作: history.go().back().forward()location.href
      • router 导航: 该方式页面不加载
        • <router-link>: 禁用默认事件 ev.preventDefault, 调用 router.push()router.replace()
        • router.push()router.replace(): 对 history 对应的方法进行了包装
    • 渲染部分: <router-view> 对路由变动进行监听并响应