依赖收集与触发机制
核心问题:Vue 是怎么知道"count 变了,要更新 name 显示"的?
问题的本质
javascript
const state = reactive({ count: 0 })
// 这个函数依赖于 state.count
function render() {
console.log(`计数: ${state.count}`)
}
// 当 count 变化时,Vue 如何自动调用 render() ?
state.count = 5 // ❓ Vue 怎么知道要调用 render() ?1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
答案:Vue 在函数执行时"偷偷"记录了它访问了哪些属性。
核心概念:Effect
Effect = 副作用函数 = "当数据变化时,需要重新执行的函数"
javascript
// 常见的 effect 示例:
effect(() => {
// 这个函数依赖于 count 和 name
document.getElementById('app').innerText = state.count + state.name
})
// 组件的渲染函数本质上就是一个 effect1
2
3
4
5
6
7
2
3
4
5
6
7
实现 effect
javascript
/**
* 全局变量:记录当前正在执行的 effect
* 这是依赖收集的关键!
*/
let currentEffect = null
/**
* 全局 ID 生成器:为每个 effect 分配唯一 ID
*/
let effectId = 0
/**
* 创建 effect(副作用函数)
* @param {Function} fn - 要执行的函数
* @returns {Function} 带有 run 方法的 effect 对象
*/
function effect(fn) {
const _effect = {
id: effectId++, // 唯一标识
fn: fn, // 实际要执行的函数
active: true // 是否激活
}
// 包装原始函数
const wrappedFn = function() {
try {
// 1. 设置当前正在执行的 effect
currentEffect = _effect
// 2. 执行函数(此时会触发 getter,收集依赖)
return fn()
} finally {
// 3. 清理当前 effect
currentEffect = null
}
}
// 保存原始函数引用
_effect.fn = wrappedFn
// 立即执行一次,收集初始依赖
_effect.fn()
return _effect
}
// 使用示例
effect(() => {
// 当执行这个函数时,currentEffect 指向这个 effect
// 所以任何响应式属性的读取都会触发 track()
console.log(`count 是 ${state.count}`)
})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
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
依赖收集的时机
在 getter 中收集 — 当读取一个属性时,Vue 知道"有东西在用这个属性"。
javascript
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 👇 这里是关键!
// 只要读取这个属性,就收集依赖
track(target, key)
const value = Reflect.get(target, key, receiver)
return typeof value === 'object' && value !== null
? reactive(value)
: value
},
set(target, key, value, receiver) {
const oldValue = target[key]
Reflect.set(target, key, value, receiver)
// 只有值真的变了才触发
if (oldValue !== value) {
// 👇 触发所有依赖这个属性的 effect
trigger(target, key)
}
return true
}
})
}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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
依赖图的数据结构
targetMap (WeakMap)
│
├── { count: 0 } (target)
│ │
│ └── depsMap (Map)
│ │
│ ├── 'count' ──────► [ effect1, effect2 ] (Set)
│ └── 'name' ──────► [ effect3 ]
│
└── { user: {...} } (target)
│
└── depsMap (Map)
│
└── 'name' ──────► [ effect1 ]1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
简化版数据结构
javascript
// 依赖图
const targetMap = new WeakMap()
/**
* 收集依赖
* targetMap[target][key] = [effect1, effect2, ...]
*/
function track(target, key) {
if (!currentEffect) return // 没有正在执行的 effect,跳过
// 获取 target 的依赖图
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取 key 的依赖列表
let deps = depsMap.get(key)
if (!deps) {
deps = new Set() // Set 确保不重复
depsMap.set(key, deps)
}
// 添加当前 effect
deps.add(currentEffect)
}
/**
* 触发更新
*/
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
if (effects) {
effects.forEach(effect => {
effect.fn() // 重新执行 effect
})
}
}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
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
完整示例:追踪渲染流程
javascript
// ========== 完整示例 ==========
// 模拟 DOM 更新
function updateDOM() {
console.log('🎨 DOM 更新了!')
}
// 响应式状态
const state = reactive({
count: 0,
name: '张三'
})
// 渲染函数(effect)
effect(() => {
// 这个函数依赖 state.count
console.log(`📊 当前计数: ${state.count}`)
// 这个函数依赖 state.name
console.log(`👤 当前用户: ${state.name}`)
})
console.log('\n--- 第一次修改 count ---')
state.count = 5
console.log('\n--- 第二次修改 name ---')
state.name = '李四'
console.log('\n--- 同时修改两个 ---')
state.count = 10
state.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
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
输出:
📊 当前计数: 0
👤 当前用户: 张三
--- 第一次修改 count ---
📊 当前计数: 5
--- 第二次修改 name ---
👤 当前用户: 李四
--- 同时修改两个 ---
📊 当前计数: 10
👤 当前用户: 王五1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
分析:
- effect 首次执行时,读取了
count和name,所以建立了两个依赖关系 - 修改
count时,只触发依赖count的 effect - 修改
name时,只触发依赖name的 effect - 这就是 Vue 的按需更新 — 只更新需要更新的部分
嵌套 effect
组件之间可能有嵌套关系:
javascript
// 父组件
effect(() => {
// 父组件的渲染
state.parentCount
// 嵌套的子组件 effect
effect(() => {
state.childCount
})
})1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Vue 需要正确处理这种嵌套情况:
javascript
// 收集嵌套的 effect 到栈中
const effectStack = []
function effect(fn) {
const _effect = {
id: effectId++,
fn: fn,
active: true
}
const wrappedFn = function() {
try {
currentEffect = _effect
effectStack.push(_effect) // 👈 入栈
return fn()
} finally {
effectStack.pop() // 👈 出栈
currentEffect = effectStack[effectStack.length - 1] || null
}
}
_effect.fn = wrappedFn
_effect.fn()
return _effect
}
// 收集时使用栈顶的 effect
function track(target, key) {
if (currentEffect) {
// 收集到当前正在执行的 effect
// 即使它是嵌套的也没问题
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(currentEffect)
}
}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
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
computed 的依赖收集
计算属性需要特殊处理,因为它有缓存机制:
javascript
function computed(getter) {
let value = undefined
let dirty = true // 标记是否需要重新计算
const _effect = effect(getter)
return {
get value() {
if (dirty) {
value = _effect.fn() // 重新执行 getter
dirty = false
}
// 访问 value 时也要收集依赖
track(value, 'value')
return value
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
watch 的实现
watch 本质上是一个特殊的 effect,它在数据变化时执行回调:
javascript
/**
* 监听数据变化
* @param {Function|Ref} source - 要监听的数据源
* @param {Function} callback - 变化时的回调
*/
function watch(source, callback) {
let getter
// 支持多种数据源格式
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue = undefined
let newValue = undefined
const _effect = effect(() => {
// 先获取新值
newValue = getter()
// TODO: 比较新旧值
callback(newValue, oldValue)
oldValue = newValue
})
// 立即执行一次,设置初始 oldValue
oldValue = getter()
}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
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
常见问题
Q: 为什么要用 WeakMap?
javascript
// WeakMap 的键是弱引用,不会阻止垃圾回收
// 当对象被删除时,对应的依赖图也会自动清理
const targetMap = new WeakMap()
// 如果用 Map:
const map = new Map()
map.set(obj, deps) // obj 永远不会被回收 ❌
// WeakMap:
targetMap.set(obj, deps) // obj 没有其他引用时可以回收 ✅1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Q: 为什么用 Set 而不是数组存储 effects?
javascript
// Set 自动去重,避免重复添加相同的 effect
const deps = new Set()
deps.add(effect1)
deps.add(effect1) // 不会重复添加
deps.add(effect2)
console.log(deps.size) // 2
// 如果用数组:
const deps = []
deps.push(effect1)
deps.push(effect1) // 重复添加了!
console.log(deps.length) // 2(但其实有问题)1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Q: 依赖什么时候会被清除?
javascript
// 组件卸载时,相关依赖会被清除
// 因为 WeakMap 的键(target 对象)没有被其他引用时会被回收
const state = reactive({ count: 0 })
effect(() => {
console.log(state.count)
})
// state 变成不可达时,整个依赖链都会被清理
state = null // 依赖图自动清理1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
总结
| 概念 | 说明 |
|---|---|
| Effect | 当数据变化时需要重新执行的副作用函数 |
| track | 在 getter 中建立"谁用了这个属性"的记录 |
| trigger | 在 setter 中找出并执行所有依赖方 |
| targetMap | 全局依赖图,存储 target→key→effects 的映射 |
| computed | 带有缓存的 effect,只有依赖变化时才重新计算 |
核心思想:
- 读取时收集 — 谁用了这个数据,就记录下来
- 写入时触发 — 数据变化时,通知所有依赖方
这就是 Vue 响应式系统的核心原理!