Vue响应式原理
目录
响应式概述
什么是响应式
响应式是指当数据发生变化时,视图会自动更新。Vue的核心特性之一就是响应式系统,它使得开发者无需手动操作DOM,只需要关注数据的变化即可。
1 2 3 4 5 6 7
| ┌─────────────┐ 变更数据 ┌─────────────┐ │ Data │ ───────────────> │ View │ │ (数据) │ │ (视图) │ └─────────────┘ └─────────────┘ ▲ │ │ 自动追踪依赖 │ └────────────────────────────────┘
|
响应式数据流
1 2 3 4 5 6 7
| data: { message: 'Hello' }
this.message = 'Hello Vue'
|
Vue2响应式原理
Object.defineProperty
Vue2使用Object.defineProperty来实现响应式系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const obj = {}
Object.defineProperty(obj, 'message', { enumerable: true, configurable: true, get() { console.log('访问message') return value }, set(newValue) { console.log('设置message为:', newValue) value = newValue } })
obj.message obj.message = 'Hello'
|
初始化过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function initState(vm) { const opts = vm.$options
if (opts.data) { initData(vm) } }
function initData(vm) { let data = vm.$options.data data = typeof data === 'function' ? data.call(vm) : data || {}
proxy(vm, '_data', key)
observe(data) }
function observe(value) { if (typeof value !== 'object' || value === null) { return }
return new Observer(value) }
|
Observer类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| 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(obj, keys[i], obj[keys[i]]) } }
observeArray(items) { for (let i = 0; i < items.length; i++) { observe(items[i]) } } }
function defineReactive(obj, key, val) { const dep = new Dep()
let childOb = observe(val)
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return val }, set(newValue) { if (val === newValue) return val = newValue dep.notify() } }) }
|
依赖收集与Watcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Dep { constructor() { this.subs = [] }
addSub(sub) { this.subs.push(sub) }
removeSub(sub) { const index = this.subs.indexOf(sub) if (index > -1) { this.subs.splice(index, 1) } }
notify() { const subs = this.subs.slice() for (let i = 0; i < subs.length; i++) { subs[i].update() } } }
Dep.target = null
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| class Watcher { constructor(vm, expOrFn, cb, options) { this.vm = vm this.expOrFn = expOrFn this.cb = cb this.options = options this.deps = []
if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) }
this.value = this.get() }
get() { Dep.target = this const obj = this.vm let value try { value = this.getter.call(vm, vm) } finally { Dep.target = null } return value }
update() { queueWatcher(this) }
run() { const oldValue = this.value const newValue = this.get() if (newValue !== oldValue) { this.value = newValue this.cb.call(this.vm, newValue, oldValue) } } }
|
响应式流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| ┌──────────────────────────────────────────────────────────────┐ │ 渲染Watcher │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ new Watcher() { │ │ │ │ this.get() { │ │ │ │ Dep.target = this │ │ │ │ this.getter.call(vm, vm) // 读取数据,触发get │ │ │ │ // 在get中,dep.depend()收集依赖 │ │ │ │ Dep.target = null │ │ │ │ } │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 数据读取 │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Object.defineProperty { │ │ │ │ get() { │ │ │ │ dep.depend() // 将当前Watcher添加到dep.subs │ │ │ │ return value │ │ │ │ } │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 数据修改 │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Object.defineProperty { │ │ │ │ set(newValue) { │ │ │ │ val = newValue │ │ │ │ dep.notify() // 通知所有Watcher更新 │ │ │ │ } │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 视图更新 │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Watcher.update() { │ │ │ │ queueWatcher(this) // 放入更新队列 │ │ │ │ nextTick(flushSchedulerQueue) // 异步执行更新 │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘
|
Vue3响应式原理
Proxy
Vue3使用Proxy替代Object.defineProperty来实现响应式系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const obj = new Proxy(data, { get(target, key, receiver) { console.log('访问:', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('设置:', key, value) return Reflect.set(target, key, value, receiver) }, deleteProperty(target, key) { console.log('删除:', key) return Reflect.deleteProperty(target, key) }, has(target, key) { console.log('in操作:', key) return Reflect.has(target, key) } })
obj.message obj.message = 'Hello' delete obj.message
|
Reactive与Ref
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { reactive, ref, computed } from 'vue'
const state = reactive({ count: 0, user: { name: '张三', address: { city: '北京' } } })
const count = ref(0) console.log(count.value) count.value++ console.log(count.value)
const obj = ref({ name: '张三' }) obj.value.name = '李四'
|
响应式系统实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
let activeEffect = null
class ReactiveEffect { constructor(fn) { this.fn = fn this.deps = [] }
run() { activeEffect = this this.fn() activeEffect = null } }
function effect(fn) { const reactiveEffect = new ReactiveEffect(fn) reactiveEffect.run() }
const targetMap = new WeakMap()
function track(target, key) { if (activeEffect) { 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) activeEffect.deps.push(dep) } }
function trigger(target, key) { const depsMap = targetMap.get(target) if (!depsMap) return
const dep = depsMap.get(key) if (dep) { dep.forEach(effect => { if (effect.fn) { effect.schedulers ? effect.schedulers() : effect.run() } }) } }
|
reactive实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| function reactive(target) { if (typeof target !== 'object' || target === null) { return target }
return new Proxy(target, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver)
track(target, key)
if (typeof res === 'object' && res !== null) { return reactive(res) }
return res },
set(target, key, value, receiver) { const oldValue = Reflect.get(target, key, receiver)
const res = Reflect.set(target, key, value, receiver)
if (value !== oldValue) { trigger(target, key) }
return res },
deleteProperty(target, key) { const hadKey = Reflect.has(target, key) const res = Reflect.deleteProperty(target, key)
if (hadKey && res) { trigger(target, key) }
return res },
has(target, key) { const res = Reflect.has(target, key) track(target, key) return res } }) }
|
ref实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class RefImpl { constructor(value) { this._rawValue = value this._value = typeof value === 'object' ? reactive(value) : value this.dep = new Set() }
get value() { if (activeEffect) { this.dep.add(activeEffect) } return this._value }
set value(newValue) { if (newValue !== this._rawValue) { this._rawValue = newValue this._value = typeof newValue === 'object' ? reactive(newValue) : newValue this.dep.forEach(effect => effect.run()) } } }
function ref(value) { return new RefImpl(value) }
|
Vue3响应式流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| ┌──────────────────────────────────────────────────────────────┐ │ effect() │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ const reactiveEffect = new ReactiveEffect(fn) │ │ │ │ reactiveEffect.run() { │ │ │ │ activeEffect = this │ │ │ │ this.fn() // 读取响应式数据,触发get │ │ │ │ activeEffect = null │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ Proxy.get │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ get(target, key, receiver) { │ │ │ │ track(target, key) // 收集activeEffect到targetMap │ │ │ │ return reactive(result) // 深层响应式处理 │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 数据修改 │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ set(target, key, value, receiver) { │ │ │ │ const oldValue = Reflect.get(target, key) │ │ │ │ Reflect.set(target, key, value) │ │ │ │ trigger(target, key) // 从targetMap查找并执行effect │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 视图更新 │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ trigger(target, key) { │ │ │ │ const depsMap = targetMap.get(target) │ │ │ │ const dep = depsMap.get(key) │ │ │ │ dep.forEach(effect => effect.run()) │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘
|
响应式实现对比
Vue2 vs Vue3响应式对比
| 特性 |
Vue2 (Object.defineProperty) |
Vue3 (Proxy) |
| 原理 |
劫持对象的属性 |
代理整个对象 |
| 深层对象 |
需要递归处理 |
自动深层代理 |
| 新增属性 |
需要Vue.set |
自动响应 |
| 删除属性 |
需要Vue.delete |
自动响应 |
| 数组索引 |
需要Vue.set |
自动响应 |
| 性能 |
相对较差 |
更好 |
| 兼容性 |
IE9+ |
现代浏览器 |
原理对比图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ┌────────────────────────────────────────────────────────────────┐ │ Vue2 响应式 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ data: { │ │ │ │ user: { name: '张三' } │ │ │ │ } │ │ │ │ │ │ │ │ 响应式处理: │ │ │ │ Object.defineProperty(data, 'user', { │ │ │ │ get() { return value }, │ │ │ │ set(newVal) { value = newVal } │ │ │ │ }) │ │ │ │ │ │ │ │ user.name = '李四' // 触发set但不触发user的dep.notify() │ │ │ │ // 需要在user对象内部也需要defineProperty │ │ │ └──────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐ │ Vue3 响应式 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ const state = reactive({ │ │ │ │ user: { name: '张三' } │ │ │ │ }) │ │ │ │ │ │ │ │ state.user.name = '李四' // Proxy自动追踪到所有层级 │ │ │ │ │ │ │ │ state.user = { name: '王五' } // 正常工作 │ │ │ │ delete state.user.name // 正常工作 │ │ │ └──────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘
|
常见响应式问题
Vue2响应式问题
1. 对象新增属性不响应
1 2 3 4 5 6 7 8 9
| this.user.age = 18
this.$set(this.user, 'age', 18)
Vue.set(this.user, 'age', 18)
this.user = { ...this.user, age: 18 }
|
2. 数组直接用索引修改不响应
1 2 3 4 5 6 7
| this.items[0] = { name: '新商品' }
this.$set(this.items, 0, { name: '新商品' })
this.items.splice(0, 1, { name: '新商品' })
|
3. 数组长度直接修改不响应
1 2 3 4 5
| this.items.length = 0
this.items.splice(0)
|
4. 异步更新队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| this.message = 'new message' console.log(this.$el.textContent)
this.message = 'new message' this.$nextTick(() => { console.log(this.$el.textContent) })
import { nextTick } from 'vue' nextTick(() => { console.log(this.$el.textContent) })
|
Vue3响应式”问题”
1. 丢失响应式的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const state = reactive({ count: 0 })
const { count } = state
import { toRefs } from 'vue' const { count } = toRefs(state)
const obj = { count: state.count }
import { toRaw } from 'vue' const raw = toRaw(state)
|
2. reactive vs ref 选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const count = ref(0) const name = ref('张三')
const state = reactive({ count: 0, items: [] })
function increment(props) { }
|
3. readonly
1 2 3 4 5 6 7 8 9
| import { reactive, readonly } from 'vue'
const state = reactive({ count: 0 })
const readonlyState = readonly(state)
|
响应式性能优化
减少响应式开销
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const data = { user: reactive({ name: '张三', avatar: '/default-avatar.png' }),
MAX_COUNT: 100,
_cache: null }
const staticData = Object.freeze({ list: [1, 2, 3] })
|
合理使用watchEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { watchEffect, watch } from 'vue'
watchEffect(() => { console.log(state.count) })
watch(() => state.count, (newVal, oldVal) => { console.log(newVal, oldVal) })
watch(() => state.isLogin, (isLogin) => { if (isLogin) { fetchUserData() } })
|
使用shallowRef和shallowReactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { shallowRef, shallowReactive } from 'vue'
const list = shallowRef([])
list.value = newList list.value.push(newItem)
const state = shallowReactive({ user: { name: '张三' } })
state.user.name = '李四' state.user = { name: '李四' }
|
nextTick优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| methods: { async updateData() { this.loading = true await this.fetchData() this.loading = false
this.$nextTick(() => { this.initChart() }) } }
import { nextTick } from 'vue'
async updateData() { this.loading = true await this.fetchData() this.loading = false
await nextTick() this.initChart() }
|
总结
核心要点
- Vue2使用Object.defineProperty - 只能代理对象的属性,需要递归处理深层对象
- Vue3使用Proxy - 代理整个对象,自动深层响应式
- 依赖收集 - 通过getter收集依赖,建立数据与Watcher的关联
- 发布订阅 - 通过setter触发更新通知所有依赖更新
面试常见问题
| 问题 |
答案 |
| Vue2响应式原理 |
使用Object.defineProperty劫持属性 |
| Vue3响应式原理 |
使用Proxy代理整个对象 |
| Object.defineProperty的缺点 |
不能检测数组索引、新增属性;需要递归 |
| Proxy的优点 |
可以检测任意属性变化;自动深层代理 |
| 数组为什么需要特殊处理 |
Object.defineProperty无法监听数组变化 |
| $nextTick原理 |
使用Promise.setTimeout MutationObserver实现 |
相关链接
- Vue基础入门
- [Vue3新特性与Composition API](Vue3新特性与Composition API.md)