Vue 核心原理笔记:响应式原理与渲染机制

1. Vue 响应式原理

核心目标:实现「数据与视图自动同步」—— 当 Model(数据)变化时,View(视图)自动更新;当用户操作 View 时,Model 同步更新(双向绑定是响应式的典型应用,而非响应式的全部)。

响应式系统的三大核心组成:

  • 数据层(Model):应用状态数据及业务逻辑(如组件的 datarefreactive 数据)。

  • 视图层(View):数据的可视化展示(如模板、DOM 元素)。

  • 响应式桥接层(ViewModel):框架封装的核心(Vue2 的 Object.defineProperty + Watcher,Vue3 的 Proxy + Effect),负责监听数据变化、触发视图更新。

在这里插入图片描述

1.1 响应式核心模块

Vue 响应式系统的核心是「数据监听-依赖收集-触发更新」的闭环,核心模块可明确为以下四部分:

1.1.1 监听器(Observer)

作用:将普通数据(对象/数组)转换为响应式数据,监听数据的「读取」和「修改」操作,是响应式的入口。

版本差异

  • Vue2:通过 Object.defineProperty 重写数据的 getter/setter,仅能监听对象属性的读写。

  • Vue3:通过 Proxy 代理整个对象(天然支持对象/数组),通过 getter/setter 包裹处理 ref 基础类型数据。

1.1.2 依赖收集器(Dep)

作用:为每个响应式属性维护一个「订阅者集合」(Set<Effect/Watcher>),记录哪些逻辑依赖该属性,是连接监听器与副作用的桥梁。

核心逻辑

  • 数据被「读取」时,将当前活跃的副作用(Effect)加入集合。

  • 数据被「修改」时,遍历集合中所有副作用并触发重新执行。

1.1.3 副作用(Effect / Watcher)

作用:依赖响应式数据的「执行逻辑」,数据变化时需重新运行,是响应式的「消费端」。

常见类型

  • 渲染副作用:组件渲染函数(数据变 → 重新渲染 DOM)。

  • 计算副作用:computed 回调(依赖变 → 重新计算值)。

  • 监听副作用:watch/watchEffect 回调(依赖变 → 触发自定义逻辑)。

版本差异:Vue2 中称为 Watcher,Vue3 统一为 Effect(底层实现逻辑一致,命名更贴合「副作用」语义)。

1.1.4 编译器(Compiler)

作用:编译阶段(构建时/运行时)将模板转换为渲染函数,同时标记动态节点(如 {{}}:bind),为运行时优化提供关键信息(如修补标记)。

运行时配合:渲染函数执行时触发数据「读取」,完成依赖收集;数据变化时,副作用重新执行渲染函数,生成新的虚拟 DOM。

1.2 双向绑定与响应式的关系

  • 响应式是「数据 → 视图」的基础能力(Vue 核心的单向数据流依赖此实现)。

  • 双向绑定(v-model)是响应式的「增强应用」:在「数据 → 视图」的基础上,通过监听视图事件(如 inputchange)反向更新数据,本质是 v-bind + v-on 的语法糖。

  • 注意:Vue 核心是「单向响应式」,双向绑定仅用于表单等特定场景,避免混淆两者概念。

1.3 响应式底层实现差异(Vue2 vs Vue3)

1.3.1 Vue2:Object.defineProperty 实现

核心逻辑:遍历对象属性,重写 getter(收集依赖)和 setter(触发更新)。


// Vue2 响应式核心伪代码
function defineReactive(obj, key, value) {
  // 递归处理嵌套对象
  if (typeof value === 'object') new Observer(value);
  const dep = new Dep(); // 为当前属性创建依赖收集器
  Object.defineProperty(obj, key, {
    get() {
      // 读取时收集依赖(当前活跃副作用加入 Dep)
      if (Dep.target) dep.addSub(Dep.target);
      return value;
    },
    set(newVal) {
      if (newVal === value) return;
      value = newVal;
      // 修改时触发更新(通知 Dep 中所有副作用执行)
      dep.notify();
    }
  });
}

// 数组兼容:重写数组原型方法(push、pop 等)
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'splice'].forEach(method => {
  arrayMethods[method] = function() {
    arrayProto[method].apply(this, arguments);
    // 触发数组所在对象的更新
    dep.notify();
  };
});

局限性(核心缺陷)

  1. 无法监听数组索引变化(如 arr[0] = 1)和数组长度变化(如 arr.length = 0),需通过重写数组原型方法实现兼容。

  2. 无法监听对象新增属性(如 obj.newKey = 1),需通过 Vue.set(obj, 'newKey', 1) 手动触发响应式。

  3. 需递归遍历嵌套对象才能实现深响应,初始化性能开销较大。

1.3.2 Vue3:Proxy + Ref 实现

核心逻辑

  1. reactive:通过 Proxy 代理整个对象,天然支持对象新增属性、数组索引/长度变化,默认深响应。

  2. ref:通过 getter/setter 包裹基础类型(stringnumber 等),解决 Proxy 无法代理基础类型的问题。


// Vue3 响应式核心伪代码
function reactive(obj) {
  // 代理整个对象,支持对象/数组
  return new Proxy(obj, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      track(target, key); // 收集依赖
      // 懒代理:访问嵌套对象时才动态代理
      return typeof result === 'object' ? reactive(result) : result;
    },
    set(target, key, value, receiver) {
      const oldVal = Reflect.get(target, key, receiver);
      const success = Reflect.set(target, key, value, receiver);
      if (success && oldVal !== value) {
        trigger(target, key); // 触发更新
      }
      return success;
    }
  });
}

function ref(value) {
  // 包裹基础类型,通过 .value 访问/修改
  const refObject = {
    get value() {
      track(refObject, 'value'); // 收集依赖
      return value;
    },
    set value(newValue) {
      if (newValue === value) return;
      value = newValue;
      trigger(refObject, 'value'); // 触发更新
    }
  };
  return refObject;
}

优势

  1. 无需递归遍历初始对象,访问嵌套属性时才动态代理(懒代理),初始化性能更优。

  2. 天然支持对象新增属性、数组索引/长度变化,无需手动调用 API。

  3. 提供 shallowReactiveshallowRef 支持浅响应,满足灵活场景需求。

  4. 通过 toRef/toRefs 解决响应式对象解构后断开连接的问题。

1.3.3 实战补充:解构响应式对象的解决方案

响应式对象解构后,普通变量会丢失 getter/setter,导致响应式断开,需用 toRefs 处理:


import { reactive, toRefs } from 'vue'

const user = reactive({ name: '张三', age: 20 })

// 问题:解构后 name 是普通变量,无响应式
const { name } = user 
name = '李四' // 不触发更新

// 解决方案:toRefs 转换后,属性仍为响应式
const { name: reactiveName, age } = toRefs(user)
reactiveName.value = '李四' // 触发响应式更新
age.value = 21 // 触发响应式更新

1.4 响应式原理闭环(完整链路)

以 Vue3 为例,响应式从数据定义到视图更新的完整流程:

  1. 步骤1:创建响应式数据:调用 reactive/ref 生成响应式数据(Proxy 代理或 getter/setter 包裹)。

  2. 步骤2:执行副作用:运行副作用(如渲染函数、watchEffect),过程中读取响应式数据。

  3. 步骤3:依赖收集:触发数据的 getter(ref)或 Proxy.get(reactive),调用 track 方法将当前副作用加入对应属性的 Dep 集合。

  4. 步骤4:修改数据触发更新:修改响应式数据,触发 setter(ref)或 Proxy.set(reactive),调用 trigger 方法通知 Dep 中所有副作用重新执行。

  5. 步骤5:副作用执行更新视图
    渲染副作用:重新执行渲染函数生成新虚拟 DOM,对比旧虚拟 DOM 后更新真实 DOM。

  6. 计算副作用:重新计算值并同步到计算属性。

  7. 监听副作用:执行自定义监听回调。

2. Vue 渲染机制

Vue 渲染机制的核心是「将响应式数据转换为真实 DOM」,依赖「虚拟 DOM + 编译优化 + 响应式副作用」实现高效更新,核心流程分为「编译 → 挂载 → 修补」三步。

2.1 虚拟 DOM(Virtual DOM)

虚拟 DOM 是一个与平台无关的 JavaScript 对象,包含创建真实 DOM 所需的所有信息(类型、属性、子节点等),是连接数据与真实 DOM 的中间层。


// 虚拟 DOM 示例(vnode)
const vnode = {
  type: 'div',       // 节点类型
  props: { id: 'app' }, // 节点属性
  children: [        // 子节点(可嵌套其他 vnode)
    { type: 'span', children: 'Hello Vue' }
  ]
}

2.1.1 虚拟 DOM 核心价值

  1. 跨平台能力:虚拟 DOM 与平台无关,可被渲染为浏览器 DOM、小程序节点、Native 组件(如 Vue Native)等。

  2. 减少 DOM 操作开销:DOM 操作比 JavaScript 运算慢得多,虚拟 DOM 通过批量对比差异(diff),只更新变化的部分,减少频繁 DOM 操作导致的回流重绘。

  3. 声明式编程支持:开发者只需描述「目标视图状态」,虚拟 DOM 负责处理「如何从旧状态更新到新状态」,无需手动操作 DOM。

2.2 渲染管线(完整流程)

Vue 渲染分为「编译、挂载、修补」三个阶段,形成完整的渲染管线,其中「挂载、修补」阶段与响应式系统深度联动。
在这里插入图片描述

2.2.1 阶段1:编译(Template → 渲染函数)

将模板字符串转换为可执行的渲染函数(返回虚拟 DOM 树),可通过「构建时编译」(借助 vue-loader)或「运行时编译」(Vue 完整版)完成,核心分三步:

  1. 解析(Parse):将模板字符串解析为抽象语法树(AST),标记出静态节点、动态节点、指令等信息。

  2. 优化(Optimize):标记静态节点(如纯文本节点)和动态节点的更新类型(如仅 class 变化),生成修补标记(patch flag),为运行时优化提供依据。

  3. 生成(Generate):将优化后的 AST 转换为渲染函数代码(如 _sfc_render 函数)。

示例:模板 <div :class="cls">{{ msg }}</div> 编译后生成的渲染函数:


function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", {
    class: _ctx.cls
  }, _toDisplayString(_ctx.msg), 2 /* CLASS */))
  // 最后一个参数 2 是修补标记,标识该节点仅 class 动态变化
}

2.2.2 阶段2:挂载(渲染函数 → 真实 DOM)

将渲染函数生成的虚拟 DOM 树转换为真实 DOM 并插入页面,同时完成依赖收集,核心分两步:

  1. 生成虚拟 DOM:执行渲染函数,生成初始虚拟 DOM 树。

  2. 创建真实 DOM:渲染器遍历虚拟 DOM 树,调用原生 DOM API(如 document.createElement)创建真实 DOM 节点,插入到挂载点(如 #app)。

与响应式联动:挂载阶段会将渲染函数作为响应式副作用执行,过程中读取的响应式数据会被收集到 Dep 中,建立「数据 → 视图」的依赖关联。

2.2.3 阶段3:修补(数据变化 → 视图更新)

当响应式数据变化时,触发副作用重新执行,生成新虚拟 DOM 树,通过对比新旧虚拟 DOM 树的差异,仅将变化部分应用到真实 DOM,核心分两步:

  1. 差异对比(diff):渲染器采用「同层对比」策略遍历新旧虚拟 DOM 树,通过节点类型、key、修补标记等信息,快速定位变化的节点。

  2. 应用更新(patch):对变化的节点执行最小化 DOM 操作(如修改 class、更新文本、新增节点),避免全量重绘。

2.3 带编译时信息的虚拟 DOM(Vue 核心优化)

Vue 同时控制编译器和运行时,编译器可在编译阶段为虚拟 DOM 注入「编译时信息」(如修补标记、静态节点标识),让运行时渲染器跳过无变化的节点,实现「高效 diff」,这是 Vue 虚拟 DOM 区别于 React 等纯运行时虚拟 DOM 的核心优势。

2.3.1 优化1:静态提升(Static Hoisting)

核心逻辑:模板中完全静态的节点(无动态绑定)会被提升到渲染函数外部,每次渲染时直接复用同一个 vnode,避免重复创建和对比。

进阶优化:连续的静态节点会被「预字符串化」为 HTML 字符串,挂载时直接通过 innerHTML 创建 DOM,后续复用通过原生 cloneNode() 克隆,性能更优。


// 编译后:静态节点被提升到渲染函数外部
const _hoisted_1 = _createElementVNode("div", null, "静态文本1")
const _hoisted_2 = _createElementVNode("div", null, "静态文本2")

function _sfc_render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _hoisted_1, // 直接复用静态 vnode
    _hoisted_2, // 直接复用静态 vnode
    _createElementVNode("div", null, _ctx.dynamic) // 动态节点
  ]))
}

2.3.2 优化2:修补标记(Patch Flags)

核心逻辑:编译器为动态节点添加「修补标记」,标识该节点的更新类型(如仅 class 变化、仅文本变化),运行时渲染器通过位运算快速判断更新范围,跳过无变化的属性和子节点。

常用修补标记表

修补标记常量 数值 含义 应用场景
TEXT 1 仅文本子节点动态 {{ dynamic }}
CLASS 2 仅 class 动态 :class="cls"
STYLE 4 仅 style 动态 :style="style"
PROPS 8 仅 props 动态 :id="id":value="val"
STABLE_FRAGMENT 64 片段子节点顺序稳定 多根节点模板(无 v-if/v-for)
DYNAMIC_FRAGMENT 128 片段子节点顺序动态 含 v-for 的多根节点
运行时判断逻辑:通过位运算快速匹配更新类型,仅执行必要操作:

// 若节点仅 class 动态,则只更新 class
if (vnode.patchFlag & PatchFlags.CLASS) {
  updateElementClass(el, vnode.props.class);
}

2.3.3 优化3:树结构打平(Tree Flattening)

核心概念:区块(Block)—— 由结构性指令(v-ifv-for)分割的、内部结构稳定的虚拟 DOM 片段。

核心逻辑:编译时,每个区块会「打平」其内部的「动态后代节点」为一个数组(静态后代节点无需追踪),重渲染时仅遍历该数组,而非整棵虚拟 DOM 树,大幅减少遍历节点数量。

示例


<div> 
  <div>静态文本</div> 
  <div :id="id"> 
    <span>{{ text }}</span> 
  </div>
  <div v-if="show"> 
    {{ dynamic }}
  </div>
</div>

根区块打平后的动态数组:[div:id, 区块v-if],重渲染时仅遍历这两个节点,跳过所有静态节点。

2.3.4 优化4:其他编译优化

  1. 事件处理函数缓存:模板中 @click="handleClick" 会被编译为 cacheHandlers: true,避免每次渲染创建新函数,导致子组件不必要的重渲染。

  2. 动态指令合并:多个动态绑定(如 :id="id" :class="cls")会被合并为一个对象,减少 vnode props 的创建开销。

  3. SSR 激活优化:修补标记和树结构打平让 SSR 激活时仅遍历动态节点,实现高效的部分激活。

2.4 虚拟 DOM Diff 算法核心原则

Vue diff 算法基于「同层对比」和「key 匹配」,时间复杂度优化为 O(n),核心原则如下:

  1. 同层对比:仅对比虚拟 DOM 树的同一层级节点,不跨层级对比(如父节点不与子节点对比),降低算法复杂度。

  2. 节点类型判断
    节点类型不同(如 divp):直接销毁旧节点,创建新节点,无需深入子节点对比。

  3. 节点类型相同:对比 props 和子节点,结合修补标记执行最小化更新。

  4. 列表 Diff 与 key:通过 key 建立新旧列表节点的映射关系,最小化移动、新增、删除操作(无 key 时会采用「就地更新」,可能导致节点状态错误)。

3. 核心原理总结

模块 Vue2 核心实现 Vue3 核心实现 核心优势
响应式 Object.defineProperty + Watcher Proxy + Effect + Ref 支持数组/新增属性,懒代理,性能更优
渲染机制 纯运行时虚拟 DOM,全量 diff 编译时优化 + 虚拟 DOM,定向 diff 静态提升、修补标记等优化,更新效率更高
核心链路串联:响应式数据变化 → 触发 Effect 副作用 → 重新执行渲染函数生成新虚拟 DOM → 基于编译时信息高效 diff → 最小化更新真实 DOM,最终实现「数据与视图自动同步」。
Logo

Vue社区为您提供最前沿的新闻资讯和知识内容

更多推荐