首先说共同点:Vue 2 和 Vue 3 的核心架构都是基于响应式系统和虚拟 DOM 的。这是 Vue.js 框架设计的基石;区别是Vue 3 在实现这两个核心系统的方式上进行了大量的重构和优化,这使得它在性能、可维护性和开发体验上都有了显著的提升。

  • Vue 2:基于 Object.defineProperty 的响应式系统,选项式 API,代码组织相对松散。

  • Vue 3:基于 Proxy 的响应式系统,组合式 API,采用模块化架构,性能更高,灵活性更强。

  • Vue 2 和 Vue 3 都坚守了“响应式数据 + 虚拟 DOM Diff "

1. 响应式系统工作原理

1.1 vue2

1.1.1 vue2 的响应式系统工作

Vue 2 通过Object.defineProperty实现响应式系统。vue2 通过递归地遍历数据对象的所有属性,并使用 Object.defineProperty 将它们转换为 getter 和 setter。

  • 依赖收集(Dependency Collection):当组件首次渲染时,会“读取‘‘数据属性,触发 Getter。Vue 会把这个“读取操作”(即这个组件对应的 Watcher 实例)收集为这个属性的“依赖”。
  • 派发更新(Dependency Notification):当数据被修改时,会触发 Setter。Setter 会通知之前收集的所有依赖(Watcher),数据被修改。
  • Watcher:每个组件实例都有一个对应的 Watcher。当它收到属性的变化通知后,会触发组件的重新渲染。

局限性:

  • 无法检测对象属性的添加或删除(需要使用 Vue.set 或 this.$set)。
  • 对数组的某些操作(如通过索引直接设置项 arr[index] = newValue,或修改数组长度 arr.length = newLength)无法被检测,Vue 重写了数组的变异方法(如 push, pop, splice 等)来弥补这一点。

1.1.2 vue2 的响应式实现

数据对象转为响应式的方法:

const data = {
  user: {
    name: '张三',
    address: {
      city: '北京',
      street: '长安街'
    }
  },
  scores: [85, 90, 78]
}

// obj - 目标对象, key - 属性名,val - 属性值
//使用示例 defineReactive(data , 'scores', data .scores)
function defineReactive(obj, key, val) {
  // 关键代码:如果值是对象,递归处理
  if (typeof val === 'object' && val !== null) {
    observe(val)  // 递归调用
  }
  
  Object.defineProperty(obj, key, {
    get() {
      return val
     // return obj[key]  // 这里又会触发 getter,无限递归!
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      //obj[key] = newVal  // 这里又会触发 setter,无限递归!
      
      // 新值也可能是对象,需要递归处理
      if (typeof newVal === 'object' && newVal !== null) {
        observe(newVal)
      }
    }
  })
}

递归方法observe(val)的实现

// 观察者构造函数
class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    
    // 标记该对象已被观察
    def(value, '__ob__', this)
    
    if (Array.isArray(value)) {
      // 数组的特殊处理
      this.observeArray(value)
    } else {
      // 对象的处理
      this.walk(value)
    }
  }
  
  // 遍历对象的所有属性
  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
    //defineReactive调用了observe,observe有在此使用了defineReactive
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
  
  // 观察数组的每个元素
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

// 主要的 observe 函数
function observe(value) {
  // 如果不是对象或者是 VNode 实例,直接返回
  if (typeof value !== 'object' || value === null) {
    return
  }
  
  let ob
  // 如果已经有 Observer 实例,直接返回
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    // 创建新的 Observer 实例
    ob = new Observer(value)
  }
  
  return ob
}

遍历过程:

  1. 创建 Observer 实例,调用 walk(data)
  2. 遍历 data 的属性,调用 defineReactive(data, ‘level1’, data.level1)
  3. 发现 data.level1 是对象,调用 observe(data.level1)
  4. 创建 Observer 实例,调用 walk(data.level1)
  5. 遍历 data.level1 的属性,调用 defineReactive(data.level1, ‘level2’, data.level1.level2)
  6. 发现 data.level1.level2 是对象,调用 observe(data.level1.level2)
  7. 创建 Observer 实例,调用 walk(data.level1.level2)
  8. 遍历 data.level1.level2 的属性,调用 defineReactive(data.level1.level2, ‘level3’, ‘最终值’)

用递归确保对象的深度响应式,但是数组使用重写数组方法的方式实现响应式。
可以说Vue 2 中所有对象的属性和基本类型数据在初始化时都会用到 defineReactive,但数组有特殊的处理机制,后期动态添加的对象属性(需要 Vue.set)和methods中的函数都不使用 defineReactive 。

1.1.3 声明对象数据后调用 defineReactive的过程

//声明数据,此时只是普通的 JavaScript 对象声明 还没有任何 Vue 的响应式处理
const data = {
  user: { name: '张三' },
  list: [1, 2, 3],
  count: 0
}
//当这个对象被 Vue 实例使用时,才会开始响应式处理:
new Vue({
  data() {
    return {
      user: { name: '张三' },
      list: [1, 2, 3],
      count: 0
    }
  }
})

vue文件结构:

<template>
  <div>
    <p>{{ message }}</p>
    <p>{{ user.name }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello Vue',
      user: { name: '张三' },
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

.vue文件中声明的对象数据会经过一个编译和构建过程才被 Vue 实例使用

阶段 1: Vue Loader 处理 .vue 文件
vue-loader 的工作流程:

  1. 解析 .vue 文件,分离 template、script、style
  2. 将 template 编译为 render 函数
  3. 将 script 部分转换为 JavaScript 模块

阶段 2: 编译后的结果

// 编译后的 JavaScript 文件大致如下:
export default {
  data() {
    return {
      message: 'Hello Vue',
      user: { name: '张三' },
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  // template 被编译为 render 函数
  render(h) {
    return h('div', [
      h('p', this.message),
      h('p', this.user.name)
    ])
  }
}

main.js文件执行new Vue({})进行数据的初始化时会调用defineReactivedefineReactive将数据改为响应式。

1.1.4 总结

在 main.js 中执行 new Vue() 时:

全局组件:已经在注册时处理完成,new Vue() 时直接可用

局部组件:在渲染过程中按需解析和初始化

根组件数据:new Vue() 时立即初始化根组件的响应式数据

子组件数据:在组件被渲染创建实例时才初始化

所以 new Vue() 会触发组件初始化链,从根组件开始,逐步初始化所有用到的组件及其数据。

1.2 Vue3

1.2.1 Vue3 的响应式系统工作

Vue 3 使用 ES6 的 Proxy 来重写了响应式系统, Proxy 的主要作用是拦截并监听对象的各种操作,从而实现数据的响应式更新。它解决了 Vue 2 的局限性。

  • Proxy:可以创建一个对象的代理,从而拦截并定义该对象的基本操作(如属性读取、赋值、删除、遍历等)。

  • 工作原理:

     拦截:当你访问或修改一个响应式对象的任何属性时,Proxy 都能拦截到。
     依赖收集:拦截 get 操作,收集依赖。
     派发更新:拦截 set 或 delete 操作,通知所有依赖。
    

优势:

  • 完美支持对象和数组:可以检测到属性的添加、删除,以及通过索引修改数组等操作。
  • 性能更好:不需要像 Vue 2 那样递归遍历所有属性并一次性转换,而是“按需”转换(只有在访问某个嵌套属性时才会将其转换为响应式)。

1.2.2 Proxy 在 Vue 3 中的具体作用

import { reactive } from 'vue';

const target = {
  name: '小明',
  age: 18,
  hobbies: ['篮球', '编程']
};

const state = reactive(target); // 这里返回的就是一个 Proxy 代理对象

当你操作 state 时,Proxy 会进行拦截:
拦截 get 操作(读取属性)
当你 console.log(state.name),Proxy 会触发 get 陷阱。
Vue 会在这里进行依赖收集(Track),记录下“这个组件或计算属性依赖了state.name”。
拦截 set 操作(设置属性)
当你 state.age = 19,Proxy 会触发 set 陷阱。
Vue 会在这里触发更新(Trigger),通知所有依赖 state.age 的地方(例如组件模板)进行更新。
拦截其他操作
delete state.property: 拦截 delete 操作,使属性删除也是响应式的。
state.hobbies.push(‘音乐’): 首先会 get hobbies 数组,然后执行 push。因为 hobbies 本身也是一个 Proxy 代理数组,所以 push 操作也会被拦截,并触发视图更新。
‘name’ in state: 拦截 in 操作符。
Object.keys(state): 拦截 Object.keys() 等迭代操作。

reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:console.log( reactive(target ) === target ) // false

只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。

对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:
// 在同一个对象上调用 reactive() 会返回相同的代理
const state = reactive(target);
console.log(reactive(target ) === state ) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(state ) === state ) // true

1.2.3 基础 Proxy 响应化实现

// 原始数据
const data = {
  user: { name: '张三' },
  list: [1, 2, 3],
  count: 0
};

// 存储依赖关系
const targetMap = new WeakMap();
let activeEffect = null;

// 创建响应式代理
function reactive(obj) {
  return new Proxy(obj, {
  
    get(target, key, receiver) {
      console.log(`读取属性: ${key}`, target[key]);
      // 依赖收集
      track(target, key);
      const result = Reflect.get(target, key, receiver);
      // 如果是对象,递归代理(惰性代理)
      if (result !== null && typeof result === 'object') {
        return reactive(result);
      }
      return result;
    },
    
    set(target, key, value, receiver) {
      console.log(`设置属性: ${key}`, value);
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      // 值发生变化时触发更新
      if (!Object.is(oldValue, value)) {
        trigger(target, key, value, oldValue);
      }
      return result;
    },
    
    deleteProperty(target, key) {
      console.log(`删除属性: ${key}`);
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) {
        trigger(target, key, undefined, target[key]);
      }
      return result;
    }
  });
}

// 依赖收集
function track(target, key) {
  if (!activeEffect) return;
  
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  
  dep.add(activeEffect);
}

// 触发更新
function trigger(target, key, newValue, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => {
      effect();
    });
  }
}

// 创建副作用函数
function effect(fn) {
  activeEffect = fn;
  fn(); // 首次执行,建立依赖关系
  activeEffect = null;
}

1.2.4 reactive使用proxy的局限

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。
  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
  1. 对解构操作不友好:当将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,将丢失响应性连接:
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)

由于这些限制,建议使用 ref() 作为声明响应式状态的主要 API。

1.2.5 ref 在模板中解包

在模板渲染上下文中,只有顶级的 ref 属性才会被解包。

const count = ref(0)
const object = { id: ref(1) }

<div> {{count + 1 }}</div> 正常工作
<div> {{object.id + 1 }}</div> 不会正常工作

{{object.id + 1 }}不正常工作原因是在计算表达式时 object.id 没有被解包仍然是一个 ref 对象。
可以先进行结构将 id 解构为一个顶级属性:const { id } = object;
{{ id + 1 }}正常工作
如果 ref 是文本插值的最终计算值 (即 {{ }} 标签),那么它将被解包,因此以下内容将渲染为 1
{{ object.id }}

2. 虚拟 DOM(Virtual DOM)与渲染机制

即使响应式系统通知了“数据变了”,直接操作真实 DOM 也是非常消耗性能的(因为 DOM 操作会触发浏览器的重绘和重排)。

什么是虚拟 DOM?
它是一个用 JavaScript 对象(VNode)来描述真实 DOM 结构和属性的轻量级副本。

2.1虚拟 DOM工作流程:渲染和更新

Vue 2 还是 Vue 3,其虚拟 DOM 的核心工作流程都可以概括为以下三个步骤:

  1. 编译阶段:将模板编译成渲染函数。

  2. 运行时-渲染阶段:执行渲染函数,生成虚拟 DOM 树。

  3. 运行时-挂载与更新阶段:将虚拟 DOM 树转换为真实 DOM,并在数据变化时进行高效的比对和更新。

2.2 Vue 2 的虚拟 DOM 工作流程

Vue 2 的流程相对直接,但存在一些全量比对的开销。
在这里插入图片描述

流程详解:

  1. 编译:Vue 2 的编译器将模板解析成 AST(抽象语法树),然后优化静态节点,最后生成渲染函数。这个渲染函数本质上是一个 createElement(或 h)函数的调用集合。

Vue 的模板()会被编译成一个 渲染函数(Render Function)。这个渲染函数被执行时,会返回一个虚拟 DOM 树(VNode Tree)

  1. 渲染:当响应式数据发生变化或组件初次渲染时,会执行渲染函数。这个函数会递归地创建并返回一个完整的 虚拟 DOM 树(VNode Tree)。每个 VNode 都是一个普通的 JavaScript 对象,描述了对应节点的信息(标签名、属性、子节点等)。

将生成的虚拟 DOM 树转换成真实的 DOM 节点,并插入到页面中。

  1. 挂载与更新(Patch):

    • 初次渲染:将生成的 VNode 树递归地创建成真实的 DOM 节点,并挂载到页面上。

    • 更新:当数据变化导致重新渲染时,会再次执行渲染函数,生成一个新的 VNode (虚拟 DOM)树。然后,Vue 会将 新旧两棵 VNode 树 进行比对,这个过程叫做 Diff。

    • Diff 算法:Vue 2 采用了一种高效的 同层比较 算法。它只会对同一层级的节点进行比较,而不会跨层级。这大大减少了计算量。比较过程中,它会尽可能地复用相同类型的节点,只更新发生变化的属性或文本内容。这个算法的目标是找出最小差异。

    • Patch:根据 Diff 的结果将计算出的差异(需要进行的 DOM 操作),将需要更新的变更批量应用到真实的 DOM 上。

Vue 2 的特点与瓶颈:

  • 全量 Diff:只要数据变化,就会从变化的组件开始,生成完整的 VNode 树,并对整棵树进行 Diff。即使其中只有一部分节点是动态的。

  • 静态节点优化有限:虽然编译器会标记静态节点并在后续渲染中复用它们,但在 Diff 过程中,仍然需要去遍历这些静态节点的父级。

2.3 Vue 3 的虚拟 DOM 工作流程

Vue 3 在 Vue 2 的基础上,通过编译时的优化和运行时的改进,实现了更快的更新速度。
在这里插入图片描述
动态节点指的是在组件的模板中,其内容、属性或结构会随着组件响应式数据的变化而改变的 DOM 节点。

流程详解与优化点:

  1. 编译时优化:这是 Vue 3 性能提升的关键。编译器会进行非常积极的静态分析。

    • Patch Flags(补丁标志):编译器会分析模板中的动态绑定(如 :class、:id、{{ text }})。在为动态节点创建 VNode 时,会为其打上一个或多个 patchFlag。例如,1 代表文本动态,8 代表 props 动态。这样在运行时,Vue 就知道这个节点具体是哪部分需要更新,无需全量对比属性。

    • Block Tree(块树):Vue 3 将模板划分为“块”。一个“块”是一个内部结构,它追踪其所有 动态后代节点。根节点是一个“Block”。带有 v-if、v-for 等结构指令的节点也会创建一个新的“Block”。这样,在更新时,Vue 只需要遍历这些 Block 中的动态节点列表,而无需遍历整棵完整的树。

    • 静态提升(Hoisting):在模板中完全静态的节点会被提升到渲染函数之外。这意味着它们只会在首次渲染时创建一次,在后续的每次渲染中都会被复用,避免了重复创建 VNode 的开销。

    • 树结构拍平:得益于 Block Tree,一个 Block 内部的所有动态节点会被收集到一个扁平数组中。当更新发生时,Vue 直接遍历这个数组进行 Diff,完全跳过了静态节点和静态父节点。这大大减少了需要遍历的 VNode 数量。

  2. 运行时优化:

    • 靶向更新:由于在编译阶段已经通过 Patch Flags 和 Block Tree 标记了动态节点及其类型,在运行时的 Diff 过程中,Vue 3 可以做到 “靶向更新”。它直接找到动态节点,并根据其 patchFlag 只更新对应的部分(如只更新文本内容或只更新某个 class)。

    • 更高效的 Diff 算法:在遇到动态子节点数组(如 v-for)时,Vue 3 使用了 最长递增子序列 算法来优化对比过程,尽可能多地复用 DOM 元素,减少移动操作。

Vue 3如何利用“动态节点”进行优化
Vue 3 的编译器会像侦探一样,在编译阶段就找出所有这些动态节点,并做好标记:

  1. Patch Flags(补丁标志):给每个动态节点打上一个数字标签,指明其动态类型。
    • 例如,1 代表文本动态,8 代表属性动态。
    • 好处:运行时无需做“全量对比”。看到一个节点有 PATCH_FLAG_TEXT,就直接 更新其 textContent,看到 PATCH_FLAG_PROPS,就只对比更新其动态属性列表。
  2. Block Tree(块树) 与 树结构拍平:
    • 编译器会将模板组织成一个个“块”(Block)。每个块都知道自己内部包含了哪些动态子节点。
    • 在更新时,Vue 不再需要递归地遍历整棵 VNode 树。它只需要直接进入这个“块”,然后遍历其内部维护的一个扁平化的动态节点数组。
    • 这就意味着,所有静态节点和它们的静态父节点都被完全跳过,根本不会进入 Diff 流程。

2.4 核心差异对比

特性 vue2 vue3
Diff 策略 全量 Diff:从根组件开始,对整棵 VNode 树进行同层比较。 靶向 Diff:通过 Block Tree 只对比动态节点,跳过静态内容。
静态内容处理 静态节点会被复用,但其父级仍需被遍历。 静态提升 和 树结构拍平,完全跳过静态节点的创建和比对。
动态节点识别 无明确标志,需要对比所有属性来判断是否更新。 Patch Flags 明确标记动态绑定的类型,更新时精准定位。
性能影响 与模板大小相关,即使大部分是静态内容,更新时也有固定开销。 更新开销主要与动态节点的数量相关,模板大小影响减小。
编译产物 渲染函数生成的 VNode 结构相对统一,信息量少。 渲染函数包含优化信息(如 openBlock, createBlock),VNode 携带 patchFlag、dynamicChildren 等。

Diff 算法避免了“推倒重来”,只更新必要的部分,减少了直接且频繁的 DOM 操作提高了效率。

3. 整体工作流程总结

  1. 初始化:

    • 创建 Vue 实例。

    • 初始化响应式系统:Vue 遍历 data, props 等选项,使用 Object.defineProperty(Vue 2)或 Proxy(Vue 3)将它们转换为响应式数据。

    • 编译模板:将模板编译成渲染函数。

  2. 首次渲染

    • 执行渲染函数。在执行过程中,会读取响应式数据,触发它们的 Getter。

    • 依赖收集:响应式系统将当前组件的 Watcher(或副作用函数)记录为这些数据的“依赖”。

    • 渲染函数返回一个虚拟 DOM 树。

    • 通过 patch 函数将虚拟 DOM 转换为真实 DOM,显示在页面上。

  3. 数据变化与更新:

    • 用户交互或异步操作导致数据发生变化。

    • 触发 Setter:修改数据时,触发响应式数据的 Setter。

    • 通知更新:Setter 通知该数据的“依赖”(即组件的 Watcher):“数据已更新”。

    • 异步更新队列:Vue 不会立即执行更新,而是将 Watcher 推进一个异步队列。在下一个事件循环“tick”中,Vue 会清空这个队列,执行所有 Watcher 的更新。这避免了同一个数据多次变化导致的重复渲染。

    • 重新渲染与打补丁:

    • 被触发的 Watcher 执行组件的渲染函数,生成一个新的虚拟 DOM 树。

    • 将新旧虚拟 DOM 树进行 Diff 对比,找出差异。

    • 将差异通过 patch 函数应用到真实 DOM上,完成视图更新。

4. 总结

Vue.js 的工作原理可以概括为:
通过响应式系统自动追踪数据依赖,在数据变化时通过虚拟 DOM 的 Diff 算法计算出最小化更新,并高效地 patch 到真实 DOM 上,从而实现数据到视图的自动、高性能同步。
这种机制让开发者可以从繁琐的 DOM 操作中解放出来,只需声明式地描述“视图应该是什么样子”,而将如何高效更新的复杂问题交给 Vue 框架本身。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐