前言
ref 和 reactive 如何驱动页面更新?watch 为何能精准响应?要成为一名优秀的 Vue 开发者,这些问题不容回避。本文将为你深入浅出地剖析 Vue3 响应式原理,从 Proxy 替代 defineProperty 的优势,到 WeakMap 在依赖收集中扮演的关键角色,让你彻底告别知其然,不知其所以然。
观察者模式
class Subject {
constructor() {
// 存储观察者
this.observers = []
}
// 添加观察者
addObserver(observer) {
if (typeof observer !== 'function') {
throw new Error('Observer must be a function')
}
this.observers.push(observer)
}
// 移除观察者
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer)
}
// 通知所有观察者
notify(data) {
this.observers.forEach(observer => observer(data))
}
}
// 创建一个公众号平台
const pubsub = new Subject()
// 用户 A 订阅公众号
function userA(data) {
console.log('用户 A 收到通知:', data)
}
// 用户 B 订阅公众号
function userB(data) {
console.log('用户 B 收到通知:', data)
}
// 用户订阅公众号
pubsub.addObserver(userA)
pubsub.addObserver(userB)
// 公众号发布新文章
pubsub.notify('新文章《如何学习 JavaScript》已发布!')
// 用户 A 取消订阅
pubsub.removeObserver(userA)
// 公众号再次发布新文章
pubsub.notify('新文章《深入理解 Vue 响应式系统》已发布!')
直接的依赖关系,观察者知道被观察者(整体的类或者实例),被观察者也直接管理观察者,依赖收集和通知都是自动化操作的
发布 - 订阅模式
// 发布-订阅模式实现
class PubSub {
constructor() {
// 用于存储事件及其对应的回调函数
this.events = {}
}
/**
* 订阅事件
* @param {string} event - 事件名称
* @param {function} callback - 事件触发时的回调函数
*/
subscribe(event, callback) {
if (typeof event !== 'string') {
throw new Error('Event name must be a string')
}
if (typeof callback !== 'function') {
throw new Error('Callback must be a function')
}
// 如果事件不存在,则初始化为一个空数组
if (!this.events[event]) {
this.events[event] = []
}
// 将回调函数添加到事件的回调列表中
this.events[event].push(callback)
}
/**
* 取消订阅事件
* @param {string} event - 事件名称
* @param {function} callback - 要移除的回调函数
*/
unsubscribe(event, callback) {
if (typeof event !== 'string') {
throw new Error('Event name must be a string')
}
if (typeof callback !== 'function') {
throw new Error('Callback must be a function')
}
// 如果事件不存在,直接返回
if (!this.events[event]) return
// 过滤掉要移除的回调函数
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
/**
* 发布事件
* @param {string} event - 事件名称
* @param {*} data - 传递给回调函数的数据
* @param {boolean} once - 是否只执行一次,默认 false
*/
publish(event, data, once = false) {
if (typeof event !== 'string') {
throw new Error('Event name must be a string')
}
// 如果事件不存在,直接返回
if (!this.events[event]) return
// 执行所有订阅该事件的回调函数
this.events[event].forEach(callback => callback(data))
// 如果 once 为 true,清空该事件的回调列表
if (once) {
delete this.events[event]
}
}
}
// 创建一个公众号平台
const pubsub = new PubSub()
// 用户 A 订阅公众号
function userA(data) {
console.log('用户 A 收到通知:', data)
}
// 用户 B 订阅公众号
function userB(data) {
console.log('用户 B 收到通知:', data)
}
// 用户订阅公众号
pubsub.subscribe('公众号更新', userA)
pubsub.subscribe('公众号更新', userB)
// 公众号发布新文章
pubsub.publish('公众号更新', '新文章《如何学习 JavaScript》已发布!')
// 用户 A 取消订阅
pubsub.unsubscribe('公众号更新', userA)
// 公众号再次发布新文章
pubsub.publish('公众号更新', '新文章《深入理解 Vue 响应式系统》已发布!')
- 观察者模式(单一事件通知) :发布者和订阅者之间存在直接依赖关系,通知所有观察者,实现简单,扩展性差、耦合高,举例:公众号的订阅和通知
- 发布订阅模式(多种类事件通知) :通过事件总线(中介)解耦发布者和订阅者,扩展性好、耦合低、后续维护简单,公众号更新 删除 发布广告 等等以事件维度进行区分的通知
vue 采用观察者模式
// Vue 中的直接关系
响应式数据 (Subject) ←→ 组件/计算属性/侦听器 (Observer)
// 如果是发布订阅模式,会是这样:
响应式数据 → 事件中心 → 组件/计算属性/侦听器
vue2 使用观察者模式,是直接的依赖添加关系,vue3 是优化的观察者模式,用发布订阅的对象数据结构用来管理关系,并且以 变量名作为 key,但是会自动化收集依赖并处理
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 被观察者 | Dep 类 | 依赖收集系统 |
| 观察者 | Watcher 类 | Effect 函数 |
| 管理方式 | 面向对象 | 函数式 |
| 复杂度 | 较复杂 | 更简洁 |
defineProperty
概念
用于定义对象属性的工具,可以精确控制属性的特性(如可枚举性、可配置性、可写性),并通过 getter 和 setter 实现对属性的访问和修改的拦截
基本用法
const person = {
name: '张三',
age: 25,
}
const vm = {}
// 1. 使用数据描述符定义属性
Object.defineProperty(vm, 'name', {
value: '李四', // 属性值
writable: true, // 是否可写
enumerable: true, // 是否可枚举(可在for...in循环中出现)
configurable: true, // 是否可配置(可删除或再次修改特性)
})
// 2. 使用访问器描述符(getter/setter)
Object.defineProperty(vm, 'age', {
get() {
console.log('age属性被读取')
return person.age
},
set(newValue) {
console.log(`age属性被修改: ${person.age} → ${newValue}`)
// 数据变更前可以执行一些操作
// 例如:触发UI更新、记录变更等
person.age = newValue // 更新person对象的age属性值
// 数据变更后可以执行一些操作
// 例如:通知订阅者、触发回调等
},
enumerable: false, // 可枚举(在for...in循环中可见)
configurable: true, // 可配置(可以被删除或重新定义)
})
总结
数组的局限
- 长度变更无感知:
arr.length = newLength不触发更新 - 删除操作无感知:
delete arr[0]不触发响应 - 解决方案:Vue2 重写了七个数组方法 (push/pop/shift/unshift/splice/sort/reverse)
对象的局限
- 新增/删除属性无感知:
obj.newProp = value或delete obj.prop不触发响应 - 必须使用特殊API:需要
Vue.set()或this.$set()添加新属性 - 只能对初始属性响应:后添加的属性不具备响应式
技术实现局限
- 属性级监听:无法整体监听对象,只能监听每个具体属性
- 深层递归消耗:嵌套对象需要递归处理每个属性
- 性能开销大:初始化时需遍历定义各层级的 getter/setter
Proxy 响应式
概念
-
Proxy 用于创建一个对象的代理,处理器(handler) 从而可以拦截并重新定义对象的基本操作,如属性读取、赋值、枚举、函数调用等
-
Reflect 提供对象操作的统一 API,配合 Proxy 的处理器,能够指定 this 指向、函数式处理、出错会抛出异常,相较于传统代码清晰更易于维护
Proxy 核心特点总结如下:
- 全面拦截:可拦截对象的属性访问、赋值、删除等操作
- 动态代理:无需预定义属性,支持动态添加和删除
- 性能优化:按需代理,避免初始化时深度遍历
- 现代特性:结合 Reflect 提供更安全操作
基本用法
console.log('===== Proxy 基本用法 =====')
const target = {
name: '张三',
age: 30,
_greetWord: '',
hobbies: ['读书', '编程', '跑步'],
address: {
city: '北京',
district: '海淀',
next: {
city: '上海',
district: '浦东',
},
},
// 普通方法调用,this 指向 proxy 对象
greet() {
console.log(`当前 this 对象是 ${this === target ? 'target' : 'proxy'} 对象`, this)
console.log(`你好,我是 ${this.name},今年 ${this.age} 岁。`)
},
// 访问 greetWord 时会触发 Proxy 的 get trap,然后调用对象的 getter
get greetWord() {
// target 对象访问 name age 不会触发 Proxy 的 get trap
// proxy 对象访问 name age 会触发 Proxy 的 get trap
console.log(`当前 this 对象是 ${this === target ? 'target' : 'proxy'} 对象`, this)
return `${this._greetWord || '你好'},我是 ${this.name},今年 ${this.age} 岁。`
},
// 设置 greetWord 时会触发 Proxy 的 set trap,然后调用对象的 setter
set greetWord(word) {
this._greetWord = word
console.log(`问候语已设置为: ${this._greetWord}`)
},
}
const handler = {
// target: 被代理的原始对象
// property: 被访问的属性名
// receiver: 接收操作的对象(通常是代理对象本身,多层级对象需要进行区分)
get(target, property, receiver) {
console.log(`[GET] 访问了属性: ${property}`)
// console.log('target, property, receiver:', target, property, receiver)
// 数组/对象的访问会返回代理对象(这里递归简易实现)
const value = Reflect.get(target, property, receiver)
if (value && typeof value === 'object') {
return new Proxy(value, handler)
}
return value
},
set(target, property, value, receiver) {
console.log(`[SET] 属性 ${property} 被设置为: ${value}`)
return Reflect.set(target, property, value, receiver)
},
deleteProperty(target, property) {
console.log(`[DELETE] 属性 ${property} 被删除`)
return Reflect.deleteProperty(target, property)
},
has(target, property) {
console.log(`[HAS] 检查属性 ${property} 是否存在`)
return Reflect.has(target, property)
},
}
const proxy = new Proxy(target, handler)
关键点
为什么需要递归代理对象类型(数组、对象)?
- Proxy 只能拦截当前对象的直接属性访问,内部对象属性值无法追踪
- VUE 3 需要监控所有层级数据变化,对嵌套对象都需要创建代理 Proxy(有访问才创建)
我看到你在 Proxy 的 set 方法中使用了 receiver 参数,请详细介绍一下 receiver 的作用,以及为什么要传递给 Reflect.set()?
- receiver 代表操作的接收者,通常就是代理对象本身,确保在使用 Reflect 方法,this 指向正确
- 不使用 receiver 导致 this 指向原始对象而不是代理对象,VUE 响应式系统要求所有属性都通过代理进行访问,否则无法触发响应式更新,依赖收集也会不完整(计算属性、侦听器)
对比总结
| 特性 | Proxy | Object.defineProperty() |
|---|---|---|
| 监听范围 | 监听 整个对象 的所有操作 | 只能监听对象的 单个属性 |
| 属性新增/删除 | ✅ 可以监听到动态添加或删除的属性 | ❌ 无法监听新增或删除的属性,需要对新属性重新定义 |
| 数组变化 | ✅ 可以直接监听数组的索引变化、push、pop 等所有变化 | ❌ 无法直接监听通过索引修改数组和 length 属性的变化 |
| 嵌套对象 | ✅ 可以轻松实现对嵌套对象的递归代理 | ❌ 需要手动递归遍历对象的每一个属性来进行监听 |
| 性能 | 相对较好,在初始化时无需操作,在访问/修改时 按需拦截 | 需要在初始化时预先遍历对象的所有属性并进行定义,可能造成性能开销 |
| 兼容性 | ES6+,不支持 IE | 支持包括 IE9+ 在内的所有现代浏览器 |
| 拦截操作类型 | 13 种 操作 (如 get, set, has, deleteProperty 等) | 仅能拦截 get (获取) 和 set (设置) 两种操作 |
Map VS WeakMap
// 简单对比 Map(强引用) 与 WeakMap(弱引用)
console.log('===== 强引用 Map =====')
// 创建 Map (强引用)
let strongMap = new Map()
let obj = { name: '张三' }
strongMap.set(obj, '这是张三的数据')
obj = null
console.log('移除原始引用后,Map 仍保留引用:', strongMap.size) // 输出 1
console.log('\n===== 弱引用 WeakMap =====')
// 创建 WeakMap (弱引用)
let weakMap = new WeakMap()
let user = { name: '李四' }
// 将对象作为键存储在 WeakMap 中
weakMap.set(user, '这是李四的数据')
user = null
console.log('移除原始引用后,WeakMap 不再保留引用:', weakMap.size) // undefined
- 键类型:Map 可以是基本类型和对象,WeakMap 只能是对象(数组、函数、对象)
- 可枚举性:Map 可以枚举(forEach for...of values keys entries),WeakMap 不可以
- 引用类型:Map 是强引用,不会被垃圾回收。WeakMap 是弱引用,如果移除对这个值的引用(置为null),那么 WeakMap 里面就会自动移动这个键值对
- 适用场景:Map 适合频繁添加/删除、遍历键值对,WeakMap 适合避免内存泄漏、额外数据关联
VUE3 使用 WeakMap 用于存储响应式对象和依赖关系,确保内存管理的高效性和安全性
响应式原理
本质:声明式处理数据变化的编程范式(数据变化 => UI 更新)
副作用:可能会更改程序中的状态(数据变化、界面更新......)
- 创建(create):响应式对象(reactive、ref、computed、watch ......)
- 初始化(init):第一次加载使用(这里会触发 proxy get 方法)
- 副作用函数(effect):注册副作用函数,这里为数据更新时 页面数据渲染变化
- 收集依赖(track):先创建以 对象 为维度的 Map,然后创建以 属性 为维度的 Set 依赖,这里对应的是 上一步的 effect 函数,最后统一存放在 WeakMap 中
- 触发更新(trigger):获取 WeakMap 下的 对象 => 属性 => 副作用函数 => 执行
reactive
下方只是简易实现响应式原理,作为对 VUE3 核心原理的剖析,对于详细源码实现未在代码考虑中,希望能够循序渐进,了解响应性思想
变量定义
// 1. 响应式创建
const person = reactive({ name: '张三', age: 30 })
// 2. 副作用函数
whenDepsChange(() => {
console.log(`姓名: ${person.name}`)
})
// 3. 更新触发
person.name = '李四'
响应式创建
/**
* 创建响应式对象
* @param {Object} obj - 原始对象
* @returns {Proxy} - 代理对象
*/
function reactive(obj) {
// 如果不是对象或为 null,直接返回原值
if (typeof obj !== 'object' || obj === null) {
return obj
}
return new Proxy(obj, {
get(target, key, receiver) {
console.log('reactive get', target, key)
// 收集依赖
track(target, key)
// 使用 Reflect.get 确保正确的 this 绑定和原型链查找
const result = Reflect.get(target, key, receiver)
// 如果获取的值是对象,递归创建响应式代理
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
console.log('reactive set', target, key, value)
// 获取旧值用于比较
const oldValue = target[key]
// 如果新值和旧值相同,直接返回
if (Object.is(oldValue, value)) {
return true
}
// 使用 Reflect.set 设置值,确保正确的属性描述符处理
const result = Reflect.set(target, key, value, receiver)
// 如果设置成功,触发更新
if (result) {
trigger(target, key)
}
return result
},
})
}
实现核心的响应式原理,Proxy get/set 访问
这里需要注意多个对象层级需要递归 Proxy,修改对象属性使用 Reflect 操作
多个层级下的对象先寻找到根对象,get 访问,最后一个层级进行 set 设置
对于数组而言,以 person.hobbies.push('编程') 举例,找到 person.hobbies 后,.push 一开始也是作为一个对象进行查找,后续步骤如下:.push 在数组原型链找到 push 原生方法 => length 找到数组长度 => 最后通过数组索引进行修改
副作用函数
/**
* 响应式副作用:注册副作用函数
* @param {Function} fn - 要执行的副作用函数
*/
function whenDepsChange(update) {
const effect = () => {
// 1. 设置当前活跃的副作用函数
activeEffect = effect
// 2. 执行用户传入的更新函数
update()
// 3. 清空当前活跃的副作用函数
activeEffect = null
}
// 4. 立即执行一次副作用函数
effect()
}
副作用创建即第一次就会指向,相当于初始化渲染
依赖收集
// 存储响应式对象和其依赖(副作用函数)的映射关系
// targetMap 结构: Map<target, Map<key, Set<effect>>>
const targetMap = new WeakMap()
// 当前正在执行的副作用函数
let activeEffect = null
/**
* 收集依赖
* @param {Object} target - 目标对象
* @param {string} key - 属性名
*/
function track(target, key) {
console.log('track init', target, key, activeEffect)
if (!activeEffect) return
// 获取目标对象的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
console.log('track depsMap', target, key, targetMap)
// 获取特定属性的依赖集合
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
console.log('track deps', target, key, deps)
// 添加当前副作用函数到依赖集合
deps.add(activeEffect)
}
那上面依赖收集的流程是什么样的呢?
targetMap 结构: WeakMap<target, Map<key, Set>>
对象维度 WeakMap => 属性维度 Map => 事件维度 Set 集合
更新触发
/**
* 触发更新
* @param {Object} target - 目标对象
* @param {string} key - 属性名
*/
function trigger(target, key) {
// 获取目标对象的依赖映射
const depsMap = targetMap.get(target)
if (!depsMap) return
// 获取特定属性的依赖集合
const deps = depsMap.get(key)
if (!deps) return
console.log('trigger ', target, key, targetMap)
// 执行所有依赖函数
deps.forEach(effect => {
effect()
})
}
找到对应属性的 Set 事件,批量触发更新
ref
/**
* 创建响应式引用对象
* @param {any} value - 初始值
* @returns {Object} - 包装对象
* */
function ref(value) {
// 先判断是否为对象类型,如果是对象则立即转换为响应式
let innerValue = value
if (typeof value === 'object' && value !== null) {
innerValue = reactive(value)
}
// 创建一个包装对象
const refObject = {
// 用于存储实际值,私有属性
_value: innerValue,
// 通过 get value() 拦截对 .value 的读取操作
get value() {
console.log('ref get', this._value)
// 对基本类型值进行依赖追踪
// 这里将 refObject 作为目标对象,'value' 作为属性名
track(refObject, 'value')
return this._value
},
// 通过 set value() 拦截对 .value 的设置操作
set value(newValue) {
console.log('ref set', this._value, newValue)
const oldValue = this._value
// 如果值没有变化,则不触发更新
if (Object.is(oldValue, newValue)) return
// 如果新值是对象,也转换为响应式
this._value = typeof newValue === 'object' && newValue !== null ? reactive(newValue) : newValue
// 触发更新
trigger(refObject, 'value')
},
}
return refObject
}
基础类型直接使用 ES6 的定义语法,和 Object.defineProperty 用法差不多,只是较为简洁、适合创建新增对象,但无法精确控制属性描述符(可写、可枚举、可配置)
基本类型
const name = ref('张三')
whenDepsChange(() => {
console.log('名称:', name.value)
})
包裹创建一个 refObject 对象传参,value 作为访问的变量值绑定副作用函数,当下一次执行 .value 的时候就会触发,进行重新渲染
对象类型
const person = ref({
name: '张三',
age: 30,
})
whenDepsChange(() => {
console.log('名称:', person.value.name)
})
对象类型使用 ref 进行包裹,本质上就是 还是先进行 reactive 定义,然后再包裹一层 基本对象
那么访问或者触发的时候,.value 触发基本对象类型的 get/set,.value.name 触发 Proxy 的get/set,所以上面有两个 Map
watch
const score = reactive({
chinese: 98,
math: 88,
english: 97,
})
whenDepsChange(() => {
console.log('score changed:', score.chinese, score.math, score.english)
})
简单实现:利用副作用函数即可简单监听属性变化
下一章节内容将会重点介绍,computed watch watchEffect 原生语法实现
问题汇总
vue2 的响应式原理介绍一下?
VUE2 通过 Object.defineProperty 将对象的属性转换成 getter/setter 的形式监听数据变化
初始化阶段需要递归遍历所有属性并为每个属性单独定义访问器描述符,当属性被访问时触发 getter 进行依赖收集(将当前 Watcher 添加到 Dep 依赖列表中),属性值发生变化时触发 setter 执行依赖通知(调用 Dep.notify() 触发所有订阅的 Watcher 更新)
核心特点:属性劫持 + Dep/Watcher 依赖收集模式 + 递归遍历初始化 + 特殊场景需额外处理
vue3 的响应式原理介绍一下?
VUE3 通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据。
初始化在副作用函数内部访问的属性,会触发 Proxy getter 依赖收集操作,根据 对象维度 WeakMap => 属性维度 Map => 事件维度 Set 集合 进行存储,后续属性值发生更改,会进行触发更新并调用副作用函数。
并且由于 Proxy 只针对对象类型,所以对于基本数据类型(String、Number、Boolean、Undefined、Null、Symbol、BigInt),任然采用 ES6 语法的 get/set 对象语法(相比于 Object.defineProperty 作用一致,但语法简洁、性能更好、符合现代规范,单不能精准控制属性特性,如:可遍历、可修改 等)
核心特点:全面代理 + 三层依赖映射 + 惰性响应式 + 现代化 API
vue3 相比 vue2 在响应式系统的优势对比?
Vue3 相比 Vue2 在响应式系统方面的核心优势可概括为 “更全面、更高效、更现代” :
- API 全面性提升:Vue2 的
Object.defineProperty只能监听已存在属性,动态增删属性、数组索引操作需特殊方法;Vue3 的Proxy可拦截所有操作,实现全面响应式。 - 性能优化:Vue2 初始化时需递归遍历属性,大对象性能差;Vue3 采用惰性响应式,单个
Proxy覆盖对象,访问时才代理,避免深度遍历 - 依赖收集升级:Vue3 用
WeakMap→Map→Set三层映射结构,比 Vue2 的 Dep/Watcher 模式更精确,支持动态属性和复杂场景,且避免内存泄漏。 - 开发体验现代化:Vue3 利用 ES6+ 特性,提供 13 种
Proxy拦截操作和Reflect保证,提升 TypeScript 支持和错误调试,虽不支持 IE,但契合现代开发环境。
Vue3 响应式系统从 ES5 到 ES6+ 跃升,解决 Vue2 限制,实现强大高效响应式能力。
| 对比维度 | Vue2 (Object.defineProperty) | Vue3 (Proxy + Reflect + WeakMap) |
|---|---|---|
| API 层面 | ❌ 只能监听已存在属性 ❌ 新增/删除属性需要特殊方法 ❌ 数组索引操作需要特殊处理 ❌ 需要递归遍历嵌套对象 | ✅ 拦截所有对象操作 ✅ 自动追踪属性变化 ✅ 原生支持数组和集合类型 ✅ 按需代理嵌套对象 |
| 性能优势 | ❌ 初始化时递归遍历所有属性 ❌ 每个属性单独定义描述符 ❌ 大对象初始化性能差 ❌ 无法优化未访问的属性 | ✅ 惰性响应式,按需拦截 ✅ 单个代理覆盖整个对象 ✅ 避免深度遍历开销 ✅ 访问时才进行代理 |
| 依赖收集结构 | 📊 基于 Dep 和 Watcher 模式 📊 依赖关系相对简单 | 📊 三层映射结构(WeakMap→Map→Set) |
| 内存管理优势 | ❌ 可能产生内存泄漏 ❌ 手动管理依赖清理 ❌ 对象引用无法自动释放 | ✅ WeakMap 自动垃圾回收 ✅ 对象销毁时依赖自动清理 ✅ 更好的内存使用效率 |
| 基本类型处理 | 🔧 需要对象包装处理 🔧 实现相对复杂 🔧 类型推断有限 | 🔧 ref() API 简洁优雅 🔧 ES6 get/set 语法 🔧 更好的 TypeScript 支持 |
| 调试和开发体验 | 🛠️ 调试信息有限 🛠️ 某些操作不直观 🛠️ 错误追踪困难 | 🛠️ 13种 Proxy 拦截操作 🛠️ Reflect 确保操作正确性 🛠️ 更清晰的错误信息 |
| 现代特性集成 | ⚡ 基于 ES5 特性 ⚡ 与新特性兼容性差 ⚡ TypeScript 支持有限 | ⚡ 充分利用 ES6+ 特性 ⚡ 与现代生态系统契合 ⚡ 完善的 TypeScript 集成 |
| 兼容性权衡 | ✅ 支持 IE9+ 等老旧浏览器 ❌ 功能受限于 API 能力 ❌ 无法完全实现响应式 | ❌ 不支持 IE 浏览器 ✅ 现代浏览器全面支持 ✅ 功能完整性更好 |
vue2 新增响应式属性需要处理?
Object.defineProperty 只能监测属性,不能监测对象,因此 Vue2 创建了 Observer 类。Observer 类将对象及其子属性转换为响应式对象,当对象新增或删除属性时,通知对应的 Watcher 更新。
function set(target, key, val) {
const ob = target.__ob__
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
function del(target, key) {
const ob = target.__ob__
delete target[key]
ob.dep.notify()
}
响应式对象需要新增属性时,通过 vm.$set 将新增属性包装 getter/setter 并通知依赖更新
DefineProperty 能否监听数组变化?
// 为数组的每个索引设置 getter/setter
arr.forEach((val, index) => {
Object.defineProperty(arr, index, {
get() {
console.log(`读取数组索引 ${index},值为: ${val}`)
return val
},
set(newVal) {
console.log(`数组索引 ${index} 变化了:${val} → ${newVal}`)
val = newVal
},
enumerable: true,
configurable: true,
})
})
对数组进行响应式,对于下标的修改是可以监听到的,因为数组本质上也是对象 key 是索引,value 是值。
为什么还要重写,是因为通过索引进行访问/修改场景少,对于高频的数组增删改查操作方法 (push/pop/shift/unshift/splice/sort/reverse) 无法监控,为了性能问题考虑,所以还是需要重写
vue2 vue3 监控数组变化主要是重写 Array 原型上的方法实现的,这部分后续后单独出一章节内容进行讲解