Vue 响应式原理

系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)


文章目录

  • 系列文章目录
  • 前言
  • 一、什么是响应式
    • [1.1 定义](#1.1 定义)
    • [1.2 应用场景](#1.2 应用场景)
  • [二、Object.defineProperty 实现响应式(Vue 2)](#二、Object.defineProperty 实现响应式(Vue 2))
    • [2.1 基本原理](#2.1 基本原理)
    • [2.2 Vue 2 的局限](#2.2 Vue 2 的局限)
  • [三、Proxy 实现响应式(Vue 3)](#三、Proxy 实现响应式(Vue 3))
    • [3.1 为什么切换到 Proxy](#3.1 为什么切换到 Proxy)
    • [3.2 Proxy 的优势](#3.2 Proxy 的优势)
  • [四、核心实现:track 与 trigger](#四、核心实现:track 与 trigger)
    • [4.1 依赖收集的实现](#4.1 依赖收集的实现)
    • [4.2 effect 函数](#4.2 effect 函数)
    • [4.3 完整流程](#4.3 完整流程)
  • [五、ref 与 reactive](#五、ref 与 reactive)
    • [5.1 定义](#5.1 定义)
    • [5.2 ref 的实现](#5.2 ref 的实现)
    • [5.3 示例](#5.3 示例)
  • [六、computed 与 watch](#六、computed 与 watch)
    • [6.1 定义](#6.1 定义)
    • [6.2 应用场景](#6.2 应用场景)
    • [6.3 易混淆点](#6.3 易混淆点)
    • [6.4 示例](#6.4 示例)
  • [七、nextTick 原理](#七、nextTick 原理)
    • [7.1 定义](#7.1 定义)
    • [7.2 应用场景](#7.2 应用场景)
    • [7.3 示例](#7.3 示例)
  • 八、易混淆点
  • 九、思考与练习
  • 总结

前言

上一篇讲了 Proxy 能拦截对象操作;本篇进入 Vue 响应式原理------这是前端面试的高频考点。

Vue 的核心设计哲学是数据驱动视图 :你修改数据,视图自动更新。这背后是一套完整的响应式系统:依赖收集 + 派发更新。

本篇会讲清楚:

  • 响应式系统的核心流程是什么?
  • Object.definePropertyProxy 各自如何实现响应式?
  • refreactive 的区别是什么?
  • computedwatchwatchEffect 有什么不同?

一、什么是响应式

1.1 定义

Vue 通过拦截对象属性的读取和修改操作,在数据变化时自动触发视图更新,这种机制称为响应式系统

响应式系统的核心流程为:

  1. 数据读取时收集副作用(依赖)------当组件渲染或计算属性求值时,会触发 getter,将当前 watcher(观察者)记录到该属性的依赖集合中。
  2. 数据修改时触发副作用重新执行------当属性被修改时触发 setter,通知所有收集到的 watcher 重新执行,从而触发组件重新渲染或计算属性重算。

1.2 应用场景

  1. 表单双向绑定:输入框内容变化自动同步到数据层,数据变化自动回显到视图。
  2. 计算属性缓存:依赖的响应式数据未变化时直接返回缓存结果,避免重复计算。
  3. 侦听器执行副作用:监听特定数据变化后自动发起网络请求或执行日志记录等异步操作。
  4. 跨组件状态同步:父组件修改共享的响应式对象,所有子组件视图自动更新。

二、Object.defineProperty 实现响应式(Vue 2)

2.1 基本原理

Vue 2 使用 Object.defineProperty 对每个属性添加 getter/setter 进行依赖收集和派发更新:

javascript 复制代码
const obj = {};
let value = "hello";

Object.defineProperty(obj, "name", {
  get() {
    console.log("读取 name,收集依赖");
    return value;
  },
  set(newVal) {
    console.log("设置 name,触发更新");
    value = newVal;
  }
});

obj.name;       // 触发 getter
obj.name = "hi"; // 触发 setter

2.2 Vue 2 的局限

  1. 无法检测新增属性 :只有初始化时被遍历的属性才会被劫持,后续新增的属性需要 $set
  2. 无法检测数组索引和长度arr[0] = 10arr.length = 0 不会触发响应式。
  3. 性能问题:初始化时需要递归遍历整个 data 对象,对每个属性进行劫持。

三、Proxy 实现响应式(Vue 3)

3.1 为什么切换到 Proxy

Vue 3 改用 Proxy 代理整个对象,可拦截属性的增删及数组的索引操作,彻底解决了 Vue 2 的响应式缺陷:

javascript 复制代码
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key); // 依赖收集
      const result = Reflect.get(target, key, receiver);
      // 惰性代理:访问对象时才代理子对象
      if (typeof result === "object" && result !== null) {
        return reactive(result);
      }
      return result;
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key); // 触发更新
      return result;
    }
  });
}

3.2 Proxy 的优势

对比项 Object.defineProperty Proxy
新增属性 需要 $set 自动监听
数组索引 需要重写方法 自动监听
删除属性 无法监听 deleteProperty trap
性能 初始化递归遍历 惰性代理

四、核心实现:track 与 trigger

4.1 依赖收集的实现

响应式系统的核心是 track(收集依赖)和 trigger(触发更新):

javascript 复制代码
// 依赖存储:target → key → effect
const depMap = new WeakMap();
let activeEffect = null;

const track = (target, key) => {
  let deps = depMap.get(target);
  if (!deps) deps = depMap.set(target, new Map()).get(target);
  let dep = deps.get(key);
  if (!dep) deps.set(key, (dep = new Set()));
  dep.add(activeEffect);
};

const trigger = (target, key) => {
  const dep = depMap.get(target)?.get(key);
  dep?.forEach((fn) => fn());
};

4.2 effect 函数

effect 是副作用函数,执行时会触发 getter,从而收集依赖:

javascript 复制代码
const effect = (fn) => {
  activeEffect = fn;
  fn(); // 执行时触发 getter,收集依赖
  activeEffect = null;
};

4.3 完整流程

javascript 复制代码
const state = reactive({ count: 0 });

effect(() => console.log("count:", state.count)); // count: 0
state.count++; // count: 1

流程

  1. effect 执行 fn,将 fn 设为 activeEffect
  2. fn 访问 state.count,触发 getter
  3. getter 中调用 track,将 activeEffect 收集到 count 的依赖集合
  4. state.count++ 触发 setter
  5. setter 中调用 trigger,通知所有依赖重新执行

五、ref 与 reactive

5.1 定义

  1. refreactive 都是创建响应式数据,但 ref 用于基础类型值,reactive 用于对象/数组。
  2. shallowRefshallowReactive 只代理第一层,深层属性的修改不会触发视图更新。
  3. toRaw 可以获取响应式对象的原始对象,适合在非响应式场景(如作为 WeakMap 键)中使用。
  4. Vue 3 的 markRaw 可将对象标记为非响应式,避免大对象被代理带来的性能开销。

5.2 ref 的实现

ref 本质是用 reactive 包装了一个 { value: xxx } 对象:

javascript 复制代码
function ref(rawValue) {
  return reactive({ value: rawValue });
}

在模板中自动解包,不需要 .value

vue 复制代码
<template>
  <div>{{ count }}</div> <!-- 不需要 .value -->
</template>

5.3 示例

javascript 复制代码
import { ref, reactive, toRaw, markRaw } from 'vue'

// ref 用于基础类型
const count = ref(0)
count.value++ // JS 中需要 .value

// reactive 用于对象
const state = reactive({ name: 'Alice', age: 25 })
state.age++ // 直接修改

// toRaw 获取原始对象
const raw = toRaw(state) // 原始对象,非响应式

// markRaw 标记非响应式
const config = markRaw({ theme: 'dark' }) // 不会被代理

六、computed 与 watch

6.1 定义

  1. computed 是基于响应式依赖进行缓存的计算属性,只有依赖变化时才会重新计算,否则返回缓存值。
  2. watch 用于监听特定响应式数据的变化,在回调中执行副作用逻辑,不返回值。
  3. computed 适合从已有数据派生新数据,watch 适合在数据变化后执行异步操作或较重的副作用。
  4. watchEffectwatch 的自动依赖收集版本,无需显式指定监听源,会立即执行一次。

6.2 应用场景

  1. 使用 computed 实现商品总价计算:依赖单价和数量,任一变化自动重算总价。
  2. 使用 watch 监听搜索关键词变化并触发防抖请求,避免频繁调用接口。
  3. 使用 computed 实现表单校验状态:根据多个输入值实时计算错误提示信息。
  4. 使用 watch 监听路由参数变化,在页面切换时重新加载数据。

6.3 易混淆点

  1. computed 有缓存机制,多次访问不会重复执行;watch 每次数据变化都会触发回调。
  2. computed 不应在 getter 中执行副作用(如异步请求),否则会导致不可预测的行为和缓存失效。
  3. watch 默认是懒执行的(只在数据变化后触发),而 watchEffect 会立即执行一次。
  4. watch 监听 reactive 对象时默认开启深度监听,监听 ref 基础类型则不需要加 .value

6.4 示例

javascript 复制代码
import { ref, computed, watch, watchEffect } from 'vue'

const price = ref(10)
const qty = ref(2)
const total = computed(() => price.value * qty.value) /* 缓存计算 */

watch(price, (newVal, oldVal) => {
  console.log(`价格: ${oldVal} → ${newVal}`)
})

/* 自动追踪依赖并执行副作用 */
watchEffect(() => {
  console.log(`count: ${price.value}`)
})

七、nextTick 原理

7.1 定义

  1. nextTick 是 Vue 提供的 API,用于在下一次 DOM 更新循环结束后执行回调函数。
  2. Vue 的响应式更新是异步批量执行的,同一个事件循环内的多次数据修改只会触发一次 DOM 更新。
  3. nextTick 内部利用微任务(Promise.then / MutationObserver / queueMicrotask)实现延迟执行。
  4. 在修改响应式数据后立即读取 DOM 状态可能获取到旧值,需要通过 nextTick 等待更新完成。

7.2 应用场景

  1. 修改数据后立即获取更新后的 DOM 尺寸或位置信息(如 getBoundingClientRect)。
  2. 动态添加列表项后需要操作新 DOM 元素(如聚焦新添加的输入框)。
  3. 在自定义指令的 updated 钩子中确保 DOM 完全更新后再执行计算逻辑。

7.3 示例

javascript 复制代码
import { ref, nextTick } from 'vue'

const count = ref(0)
const inc = async () => {
  count.value++
  await nextTick()
  console.log('DOM 已更新', document.querySelector('.count')?.textContent)
}

八、易混淆点

  1. Vue 2 vs Vue 3 :Vue 2 用 Object.defineProperty 递归劫持,无法感知动态新增属性(需 $set);Vue 3 用 Proxy,可拦截动态操作。
  2. 响应式与双向绑定 :响应式是数据变化自动更新视图;双向绑定(v-model)是响应式 + 事件监听的语法糖。
  3. shallowRefrefshallowRef 只追踪 .value 的变化,不深度代理内部对象,性能更优但丢失深层响应性。
  4. effectwatchEffecteffect 是底层 API,立即执行并自动追踪依赖;watchEffect 是其封装,增加了清理函数和异步刷新支持。

九、思考与练习

1. 为什么 Vue 2 的 Object.defineProperty 无法监听数组索引变化?

解析:Object.defineProperty 需要预先知道属性名。数组索引是动态的(arr[0], arr[1]...),无法预先劫持所有可能的索引。Vue 2 通过重写数组方法解决。

2. reactiveref 什么时候用哪个?

解析:

  • 对象用 reactive,原始值用 ref
  • 需要整体替换时用 ref.value = newObj
  • reactive 不支持解构,需要用 toRefs

3. computedwatch 如何选择?

解析:

  • 从已有数据派生新数据 → computed
  • 数据变化后执行副作用(异步请求、日志) → watch
  • 自动追踪依赖 → watchEffect

4. Vue 3 的响应式系统使用了哪些 ES6 特性?

解析:

  • Proxy:拦截对象操作
  • Reflect:正确传递 receiver
  • WeakMap:存储依赖映射,键对象可被 GC 回收
  • Map/Set:存储依赖关系

5. 为什么 Vue 3 的响应式性能更好?

解析:

  • 惰性代理:只有访问子对象时才代理,而不是初始化时递归遍历
  • 更精确的依赖收集:Proxy 可以拦截所有操作,减少不必要的更新

6. nextTick 的作用是什么?

解析:Vue 的响应式更新是异步批量执行的,nextTick 确保在 DOM 更新完成后再执行回调,避免读取到旧的 DOM 状态。


总结

  • 响应式的核心:依赖收集(谁在用)+ 派发更新(数据变了通知谁)
  • Vue 2 用 Object.defineProperty :无法监听新增属性、数组索引,需要 $set
  • Vue 3 用 Proxy:自动监听所有操作,惰性代理性能更好
  • ref 包装原始值.value 访问;reactive 包装对象
  • computed 缓存计算watch/watchEffect 监听变化执行副作用
  • nextTick 等待 DOM 更新完成后再执行回调

下一篇讲 虚拟 DOM :VNode、更新流程、key 与列表稳定标识(系列第 35 篇,大纲 §35)。

相关推荐
提子拌饭1331 小时前
模态窗鸿蒙PC Electron框架实现技术详解 - 饮料含糖量应用案例分析
前端·javascript·华为·electron·前端框架·开源·鸿蒙
丑八怪大丑1 小时前
前端工程化
vue.js
佛山个人技术开发2 小时前
个人建站接单|汽车汽配行业宽屏自适应官网模板 工厂企业定制建站源码
前端·css·前端框架·html·汽车·php
光影少年2 小时前
react的Context 和 Redux 区别?
前端·javascript·react.js·前端框架
前端 贾公子2 小时前
uni-app工程化实战:基于vue-i18n和i18n-ally的国际化方案 (上)
前端·javascript·vue.js
喵个咪2 小时前
基于 Flutter 的 Headless CMS 全平台前端架构:技术解析与二次开发导引
前端·flutter·cms
vim怎么退出2 小时前
Dive into React——Diff 算法
前端·react.js·源码阅读
半个落月2 小时前
面试必问的 JS 原型链,我用 16 个示例给你彻底讲明白
javascript
拾年2752 小时前
别调 BERT 了:我用 Prompt 做了套 NLP 系统,20 分钟搞定
前端·人工智能