系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)
- 第 13 篇:Promise 与 async/await
- 第 14 篇:数据结构基础
- 第 15 篇:垃圾回收与内存
- 第 16 篇:DOM 基础全面解析
- 第 17 篇:DOM 性能与渲染
- 第 18 篇:DOM 交互补充
- 第 19 篇:DOM 实战案例
- 第 20 篇:CSS 布局与可视化高频
- 第 21 篇:移动端与 viewport
- 第 22 篇:BOM 核心对象
- 第 23 篇:前端路由原理
- 第 24 篇:浏览器存储对比
- 第 25 篇:网络与跨域
- 第 26 篇:网络请求与实时通道
- 第 27 篇:Service Worker、PWA 与 Web Worker
- 第 28 篇:浏览器高级 API
- 第 29 篇:图片懒加载
- 第 30 篇:ES6+ 模块
- 第 31 篇:Symbol 与 Iterator / Generator
- 第 32 篇:Proxy 与 Reflect
- 第 33 篇:Map / Set / WeakMap / WeakSet
- 第 34 篇:Vue 响应式原理(本文)
文章目录
- 系列文章目录
- 前言
- 一、什么是响应式
-
- [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.defineProperty和Proxy各自如何实现响应式?ref和reactive的区别是什么?computed、watch、watchEffect有什么不同?
一、什么是响应式
1.1 定义
Vue 通过拦截对象属性的读取和修改操作,在数据变化时自动触发视图更新,这种机制称为响应式系统。
响应式系统的核心流程为:
- 数据读取时收集副作用(依赖)------当组件渲染或计算属性求值时,会触发 getter,将当前 watcher(观察者)记录到该属性的依赖集合中。
- 数据修改时触发副作用重新执行------当属性被修改时触发 setter,通知所有收集到的 watcher 重新执行,从而触发组件重新渲染或计算属性重算。
1.2 应用场景
- 表单双向绑定:输入框内容变化自动同步到数据层,数据变化自动回显到视图。
- 计算属性缓存:依赖的响应式数据未变化时直接返回缓存结果,避免重复计算。
- 侦听器执行副作用:监听特定数据变化后自动发起网络请求或执行日志记录等异步操作。
- 跨组件状态同步:父组件修改共享的响应式对象,所有子组件视图自动更新。
二、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 的局限
- 无法检测新增属性 :只有初始化时被遍历的属性才会被劫持,后续新增的属性需要
$set。 - 无法检测数组索引和长度 :
arr[0] = 10和arr.length = 0不会触发响应式。 - 性能问题:初始化时需要递归遍历整个 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
流程:
effect执行fn,将fn设为activeEffectfn访问state.count,触发 getter- getter 中调用
track,将activeEffect收集到count的依赖集合 state.count++触发 setter- setter 中调用
trigger,通知所有依赖重新执行
五、ref 与 reactive
5.1 定义
ref和reactive都是创建响应式数据,但ref用于基础类型值,reactive用于对象/数组。shallowRef和shallowReactive只代理第一层,深层属性的修改不会触发视图更新。toRaw可以获取响应式对象的原始对象,适合在非响应式场景(如作为 WeakMap 键)中使用。- 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 定义
computed是基于响应式依赖进行缓存的计算属性,只有依赖变化时才会重新计算,否则返回缓存值。watch用于监听特定响应式数据的变化,在回调中执行副作用逻辑,不返回值。computed适合从已有数据派生新数据,watch适合在数据变化后执行异步操作或较重的副作用。watchEffect是watch的自动依赖收集版本,无需显式指定监听源,会立即执行一次。
6.2 应用场景
- 使用
computed实现商品总价计算:依赖单价和数量,任一变化自动重算总价。 - 使用
watch监听搜索关键词变化并触发防抖请求,避免频繁调用接口。 - 使用
computed实现表单校验状态:根据多个输入值实时计算错误提示信息。 - 使用
watch监听路由参数变化,在页面切换时重新加载数据。
6.3 易混淆点
computed有缓存机制,多次访问不会重复执行;watch每次数据变化都会触发回调。computed不应在 getter 中执行副作用(如异步请求),否则会导致不可预测的行为和缓存失效。watch默认是懒执行的(只在数据变化后触发),而watchEffect会立即执行一次。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 定义
nextTick是 Vue 提供的 API,用于在下一次 DOM 更新循环结束后执行回调函数。- Vue 的响应式更新是异步批量执行的,同一个事件循环内的多次数据修改只会触发一次 DOM 更新。
nextTick内部利用微任务(Promise.then / MutationObserver / queueMicrotask)实现延迟执行。- 在修改响应式数据后立即读取 DOM 状态可能获取到旧值,需要通过
nextTick等待更新完成。
7.2 应用场景
- 修改数据后立即获取更新后的 DOM 尺寸或位置信息(如
getBoundingClientRect)。 - 动态添加列表项后需要操作新 DOM 元素(如聚焦新添加的输入框)。
- 在自定义指令的
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)
}
八、易混淆点
- Vue 2 vs Vue 3 :Vue 2 用
Object.defineProperty递归劫持,无法感知动态新增属性(需$set);Vue 3 用Proxy,可拦截动态操作。 - 响应式与双向绑定 :响应式是数据变化自动更新视图;双向绑定(
v-model)是响应式 + 事件监听的语法糖。 shallowRef与ref:shallowRef只追踪.value的变化,不深度代理内部对象,性能更优但丢失深层响应性。effect与watchEffect:effect是底层 API,立即执行并自动追踪依赖;watchEffect是其封装,增加了清理函数和异步刷新支持。
九、思考与练习
1. 为什么 Vue 2 的 Object.defineProperty 无法监听数组索引变化?
解析:Object.defineProperty 需要预先知道属性名。数组索引是动态的(arr[0], arr[1]...),无法预先劫持所有可能的索引。Vue 2 通过重写数组方法解决。
2. reactive 和 ref 什么时候用哪个?
解析:
- 对象用
reactive,原始值用ref - 需要整体替换时用
ref(.value = newObj) reactive不支持解构,需要用toRefs
3. computed 和 watch 如何选择?
解析:
- 从已有数据派生新数据 →
computed - 数据变化后执行副作用(异步请求、日志) →
watch - 自动追踪依赖 →
watchEffect
4. Vue 3 的响应式系统使用了哪些 ES6 特性?
解析:
Proxy:拦截对象操作Reflect:正确传递 receiverWeakMap:存储依赖映射,键对象可被 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)。