《Vue.js设计与实现》笔记 第4章:响应系统的作用与实现
Vue.js设计与实现 第4章:响应系统的作用与实现
本章导读
Vue 最核心的能力:
数据变化
↓
视图自动更新
例如:
<template>
<h1>{{ count }}</h1>
</template>
<script setup>
const count = ref(0)
setTimeout(() => {
count.value++
}, 1000)
</script>
当数据变化时:
count
↓
自动更新DOM
开发者无需手动操作页面。
这种机制称为:
响应式(Reactivity)
本章将一步一步实现:
effect
track
trigger
三个核心模块。
一、什么是响应式
最简单的理解
响应式:
数据变化
↓
自动执行相关逻辑
例如:
let text = 'hello'
function update() {
document.body.innerText = text
}
text = 'world'
update()
这里需要:
手动调用 update()
并不是真正响应式。
理想状态
希望:
text = 'world'
自动执行:
update()
即:
修改数据
↓
自动更新
这就是响应式系统。
二、副作用函数(Effect)
什么是副作用
副作用:
函数执行时
会影响外部环境
例如:
document.body.innerText = 'Vue'
修改了页面。
Effect概念
例如:
function effect() {
document.body.innerText = obj.text
}
这里:
effect依赖obj.text
当:
obj.text
变化时:
effect需要重新执行
Effect本质
依赖响应式数据的函数
三、最简单的响应式实现
原始数据
const data = {
text: 'hello world'
}
使用Proxy代理
const obj = new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
return true
}
})
注册副作用函数
function effect() {
document.body.innerText = obj.text
}
执行:
effect()
页面显示:
hello world
问题
修改:
obj.text = 'Vue3'
页面不会更新。
因为:
set触发后
不知道执行哪个effect
四、建立响应联系
核心目标:
obj.text
↓
effect
建立映射关系。
全局变量保存当前Effect
let activeEffect
注册Effect
function effect(fn) {
activeEffect = fn
fn()
}
使用:
effect(() => {
document.body.innerText = obj.text
})
执行过程
effect()
↓
activeEffect记录
↓
读取obj.text
↓
建立依赖
五、依赖收集(track)
读取属性时收集依赖
修改:
get(target, key) {
track(target, key)
return target[key]
}
创建依赖集合
const bucket = new Set()
track实现
function track() {
bucket.add(activeEffect)
}
工作流程
effect执行
↓
读取obj.text
↓
触发get
↓
track()
↓
收集effect
当前结果
bucket
┌──────────┐
│ effect() │
└──────────┘
六、触发更新(trigger)
修改属性时触发
set(target,key,newVal){
target[key] = newVal
trigger()
return true
}
trigger实现
function trigger() {
bucket.forEach(fn => fn())
}
整体流程
obj.text = 'Vue3'
↓
set
↓
trigger()
↓
effect()
↓
页面更新
第一个完整版本
const bucket = new Set()
let activeEffect
const data = {
text: 'hello'
}
const obj = new Proxy(data, {
get(target, key) {
bucket.add(activeEffect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
function effect(fn) {
activeEffect = fn
fn()
}
七、分支切换问题
示例
const data = {
ok: true,
text: 'hello'
}
副作用:
effect(() => {
document.body.innerText =
obj.ok ? obj.text : 'not'
})
初次收集
依赖:
ok
text
修改
obj.ok = false
页面显示:
not
再修改
obj.text = 'Vue3'
理论上:
不应该重新执行effect
因为:
当前已经不依赖text
实际却会执行
原因:
旧依赖没有清理
八、依赖清理
Effect增加deps属性
function effect(fn){
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
cleanup实现
function cleanup(effectFn) {
effectFn.deps.forEach(deps => {
deps.delete(effectFn)
})
effectFn.deps.length = 0
}
track改进
function track(target,key){
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
效果
重新执行effect时:
先删除旧依赖
↓
重新收集依赖
避免无效更新。
九、嵌套Effect问题
示例
effect(() => {
effect(() => {
console.log(obj.bar)
})
console.log(obj.foo)
})
问题
执行内部effect后:
activeEffect
被覆盖
导致:
外层effect丢失
十、Effect栈
解决方案:
effectStack
创建栈
const effectStack = []
effect实现
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect =
effectStack[
effectStack.length - 1
]
}
effectFn()
}
工作流程
外层effect
↓
入栈
内层effect
↓
入栈
执行结束
↓
出栈
恢复外层effect
十一、避免无限递归
问题
effect(() => {
obj.count++
})
执行:
effect
↓
count++
↓
trigger
↓
effect
↓
count++
↓
无限循环
解决方案
trigger时跳过当前Effect
effectsToRun.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectFn()
}
})
十二、调度执行(Scheduler)
问题
obj.foo++
obj.foo++
会触发:
effect执行两次
Vue解决方案
允许用户指定:
scheduler
effect实现
effect(
fn,
{
scheduler(effectFn) {
}
}
)
trigger修改
if(effectFn.options.scheduler){
effectFn.options.scheduler(
effectFn
)
}else{
effectFn()
}
优势
开发者可以:
- 控制执行时机
- 合并更新
- 异步更新
十三、计算属性与Lazy
Lazy执行
默认:
effect(fn)
立即执行。
支持:
effect(fn,{
lazy:true
})
实现
if (!options.lazy) {
effectFn()
}
返回值
return effectFn
这样:
const runner = effect(fn,{
lazy:true
})
执行:
runner()
才会运行。
十四、计算属性(Computed)思想
例如:
const sum =
computed(
() => obj.foo + obj.bar
)
本质:
lazy effect
+
缓存机制
工作流程
读取value
↓
执行getter
↓
缓存结果
再次读取
↓
直接返回缓存
依赖变化
↓
缓存失效
十五、Watch实现思想
示例
watch(
() => obj.foo,
(newVal, oldVal) => {
}
)
本质:
effect
+
scheduler
工作流程
数据变化
↓
trigger
↓
scheduler
↓
执行回调
第四章核心知识图谱
响应式系统
Proxy
│
├── get
│ └── track()
│
└── set
└── trigger()
track()
│
└── 收集依赖
trigger()
│
└── 触发依赖
effect()
│
├── activeEffect
├── cleanup
├── effectStack
├── scheduler
└── lazy
高级能力
├── computed
└── watch
高频面试题
什么是响应式?
数据变化
自动触发相关逻辑
effect作用是什么?
注册副作用函数。
track作用是什么?
收集依赖。
trigger作用是什么?
触发依赖执行。
为什么需要cleanup?
解决分支切换导致的遗留依赖问题。
为什么需要effectStack?
解决嵌套effect覆盖问题。
scheduler作用是什么?
控制effect执行时机。
computed本质是什么?
lazy effect
+
缓存
watch本质是什么?
effect
+
scheduler
本章总结
Vue3响应式系统核心链路:
响应式数据
│
▼
Proxy
│
┌───┴────┐
▼ ▼
track trigger
│ │
▼ ▼
effect ←───┘
核心实现:
- effect(副作用函数)
- track(依赖收集)
- trigger(触发更新)
- cleanup(依赖清理)
- effectStack(嵌套处理)
- scheduler(调度器)
后续章节将在此基础上扩展:
- 第5章:非原始值响应式(Object、Array、Map、Set)
- 第6章:原始值响应式(ref、toRef、toRefs)
- 第7章:渲染器实现
第四章是整本《Vue.js设计与实现》中最重要的章节之一,也是 Vue 面试源码题出现频率最高的部分。

浙公网安备 33010602011771号