Vue响应式原理

目录


响应式概述

什么是响应式

响应式是指当数据发生变化时,视图会自动更新。Vue的核心特性之一就是响应式系统,它使得开发者无需手动操作DOM,只需要关注数据的变化即可。

1
2
3
4
5
6
7
┌─────────────┐     变更数据      ┌─────────────┐
│ Data │ ───────────────> │ View │
│ (数据) │ │ (视图) │
└─────────────┘ └─────────────┘
▲ │
│ 自动追踪依赖 │
└────────────────────────────────┘

响应式数据流

1
2
3
4
5
6
7
// 数据驱动视图
data: {
message: 'Hello'
}

// 当修改message时,视图自动更新
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 // 访问,触发get
obj.message = 'Hello' // 设置,触发set

初始化过程

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
// Vue2源码简化版
function initState(vm) {
const opts = vm.$options

if (opts.data) {
initData(vm)
}
}

function initData(vm) {
let data = vm.$options.data
// 确保data函数返回对象
data = typeof data === 'function' ? data.call(vm) : data || {}

// 代理data属性到vm实例
proxy(vm, '_data', key)

// 响应式处理
observe(data)
}

function observe(value) {
// 如果不是对象则返回
if (typeof value !== 'object' || value === null) {
return
}

// 创建Observer实例
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() // 收集依赖

// 给value添加__ob__属性
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) {
// 创建Dep实例
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
// Dep类 - 管理依赖
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)
}
}

// 通知所有Watcher更新
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
// Watcher类 - 观察者
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)
}

// 初始化时获取值,触发getter进行依赖收集
this.value = this.get()
}

get() {
// 设置当前Watcher
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 // 访问,触发get
obj.message = 'Hello' // 设置,触发set
delete obj.message // 删除,触发deleteProperty

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
// Vue3响应式API
import { reactive, ref, computed } from 'vue'

// reactive - 深层响应式代理对象
const state = reactive({
count: 0,
user: {
name: '张三',
address: {
city: '北京'
}
}
})

// ref - 包装基本类型为响应式对象
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

// ref也支持对象,内部会调用reactive
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
// Vue3响应式系统简化实现

// 存储所有effect
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 // 新增的age属性不是响应式的

// 解决方案
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
// DOM不会立即更新
this.message = 'new message'
console.log(this.$el.textContent) // 仍是旧值

// 解决方案 - $nextTick
this.message = 'new message'
this.$nextTick(() => {
console.log(this.$el.textContent) // 新值
})

// Vue3
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 // count是普通数字

// 解决方案
import { toRefs } from 'vue'
const { count } = toRefs(state) // count是ref,保持响应式

// 放入普通对象丢失响应式
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
// 基本类型 - 使用ref
const count = ref(0)
const name = ref('张三')

// 对象/数组 - 使用reactive
const state = reactive({
count: 0,
items: []
})

// 函数参数传递
function increment(props) {
// props如果是ref需要用props.count访问
// props如果是reactive可以直接访问
}

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)

// state.count++ // 正常工作
// readonlyState.count++ // 警告:Cannot set readonly property

响应式性能优化

减少响应式开销

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 // 加下划线表示私有
}

// 使用Object.freeze冻结不需要变化的数据
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
// Vue3
import { watchEffect, watch } from 'vue'

// watchEffect - 自动追踪依赖,立即执行
watchEffect(() => {
console.log(state.count)
// 只有state.count变化时才会执行
})

// watch - 明确指定依赖
watch(() => state.count, (newVal, oldVal) => {
console.log(newVal, oldVal)
})

// 对于有条件执行的场景,watch更合适
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'

// shallowRef - 只对.value的变化响应,适合大数组
const list = shallowRef([])

// 需要手动触发更新
list.value = newList
list.value.push(newItem) // 不会触发更新

// shallowReactive - 只代理第一层
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
// Vue2
methods: {
async updateData() {
this.loading = true
await this.fetchData()
this.loading = false

// DOM已更新
this.$nextTick(() => {
this.initChart()
})
}
}

// Vue3
import { nextTick } from 'vue'

async updateData() {
this.loading = true
await this.fetchData()
this.loading = false

await nextTick()
this.initChart()
}

总结

核心要点

  1. Vue2使用Object.defineProperty - 只能代理对象的属性,需要递归处理深层对象
  2. Vue3使用Proxy - 代理整个对象,自动深层响应式
  3. 依赖收集 - 通过getter收集依赖,建立数据与Watcher的关联
  4. 发布订阅 - 通过setter触发更新通知所有依赖更新

面试常见问题

问题 答案
Vue2响应式原理 使用Object.defineProperty劫持属性
Vue3响应式原理 使用Proxy代理整个对象
Object.defineProperty的缺点 不能检测数组索引、新增属性;需要递归
Proxy的优点 可以检测任意属性变化;自动深层代理
数组为什么需要特殊处理 Object.defineProperty无法监听数组变化
$nextTick原理 使用Promise.setTimeout MutationObserver实现

相关链接

  • Vue基础入门
  • [Vue3新特性与Composition API](Vue3新特性与Composition API.md)