Vue 响应式原理概述
- Vue 的响应式系统是其核心功能之一,主要用于实现数据和视图之间的自动同步。
- 响应式数据:Vue2中的data属性,watch属性,computed属性,vue3中的ref,reactive,computed,watch..等
- 以下是 Vue 响应式的关键点:
响应式定义
响应式是指当数据发生变化时,视图会自动更新以反映最新的数据状态,而开发者无需手动操作 DOM。
实现机制
- Object.defineProperty (Vue 2.x) Vue 2 使用
Object.defineProperty
方法对数据对象的属性进行拦截,通过getter
和setter
来追踪数据的变化。 - Proxy (Vue 3.x) Vue 3 引入了 ES6 的
Proxy
对象,替代了Object.defineProperty
,提供了更强大和灵活的数据拦截能力,支持直接添加或删除属性。
依赖收集与派发更新
- 当组件初始化时,Vue 会遍历数据对象的所有属性,并通过
getter
收集依赖(如计算属性或模板中的引用)。 - 当数据发生变化时,
setter
会被触发,通知所有相关的依赖进行更新。
响应式的核心类
- Observer:负责将数据转换为响应式对象。
- Watcher:监听数据变化并执行相应的更新操作。
- Dep:依赖管理器,用于存储依赖并通知它们更新。
限制与注意事项
- Vue 2 中无法检测到对象属性的新增或删除(需使用
$set
或Vue.set
)。 - 数组的变异方法(如
push
、pop
等)被重写以触发更新,但直接修改索引不会触发响应式。
vue3响应式和vue2响应式的对比
特性 | Vue 2 | Vue 3 |
---|---|---|
响应式实现 | Object.defineProperty |
Proxy |
属性新增/删除支持 | 需要 $set 或 Vue.set |
直接支持 |
数组索引修改支持 | 不支持 | 支持 |
依赖追踪精度 | 较低 | 更高 |
TypeScript 支持 | 有限 | 更好 |
API 设计 | Options API | Options API + Composition API |
新增工具 | 无 | ref 、reactive 、toRefs 等 |
包体积 | 较大 | 更小 |
Vue3的响应式实现:reactive
测试代码
ini
const { reactive, effect } = VueReactivity
const info = reactive({
name: 'why',
})
const titleH1 = document.querySelector('h1')
effect(() => {
titleH1.innerHTML = info.name
})
setTimeout(() => {
info.name = 'kobe'
}, 1000)
注册响应式数据的过程:
对reactive的分析
1.reactive: 调用createReactiveObject创建响应式对象
reactive函数干的事情很简单,只读对象直接返回,否则调用createReactiveObject创建响应式对象
javascript
export function reactive(target: object) {
// 判断传入的 target 是否是只读对象,如果是则直接返回该对象
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target, // 目标对象
false, // 是否为只读(false 表示可变)
mutableHandlers, // 处理普通对象的响应式处理器
mutableCollectionHandlers, // 处理集合类型的响应式处理器
reactiveMap, // 用于追踪已转换对象的映射表防止重复转换,它是一个WeakMap
)
}
2.createReactiveObject: 创建一个代理对象
createReactiveObject函数的终极目的是创建一个代理对象并为其设置上对应的执行各种操作时代理对象的行为(处理器)
请先忽略前面的参数合法性检测步骤,直接看最后的new对象代码
这里是Vue3使用Proxy实现响应式的证明
typescript
// 定义一个函数,用于创建响应式对象
function createReactiveObject(
target: Target, // 目标对象
isReadonly: boolean, // 是否为只读
baseHandlers: ProxyHandler<any>, // 基础处理器,用于普通对象的代理
collectionHandlers: ProxyHandler<any>, // 集合处理器,用于集合类型(如数组、Map等)的代理
proxyMap: WeakMap<Target, any>, // 一个WeakMap,用于存储原始对象和它们对应的代理对象
) {
// 如果目标不是一个对象
if (!isObject(target)) {
return target // 直接返回目标值
}
// 如果目标已经是一个Proxy对象,并且不是在一个响应式对象上调用readonly()的情况,则直接返回它
if (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
return target
}
// 只有特定的值类型可以被观察
const targetType = getTargetType(target) // 获取目标对象的类型
if (targetType === TargetType.INVALID) {
return target // 如果目标类型无效,则直接返回目标值
}
// 如果目标已经有一个对应的Proxy对象
const existingProxy = proxyMap.get(target) // 从proxyMap中获取目标对应的代理对象
if (existingProxy) {
return existingProxy // 如果已经存在代理对象,则直接返回它
}
// 创建一个新的Proxy对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, // 根据目标类型选择处理器
)
// 将原始对象和它的代理对象存储在proxyMap中
proxyMap.set(target, proxy)
// 返回新创建的Proxy对象
return proxy
}
3.先分析一下这个代理对象吧
因为案例中是一个普通的对象所以使用的是mutableHandlers 这个处理器 简化一下这个处理器的操作我们直接把结果放在这里并忽略函数实现 那么这个代理对象就是:
scala
{
[handler]: {
//这两个属性继承自BaseReactiveHandler
_isReadonly: false //是否为只读代理
__isShallow: false //是否为浅层响应式代理
get: ƒ get(target, key, receiver)
//以下属性来自MutableReactiveHandler (mutableHandlers = new MutableReactiveHandler())
set: ƒ set(target, key, value, receiver)
constructor: class extends
deleteProperty: ƒ deleteProperty(target, key)
has: ƒ has(target, key)
ownKeys: ƒ ownKeys(target)
}
[Target]:{
name: "why"
}
}
现在我们已经完成了reactive的操作并得到了代理对象(get set 方法均已重写实现)
4.总结
reactive执行后我们得到了一个代理对象,这个代理对象[handler]上的set属性被重写了(请记住这一点!),这里解释了为什么说vue3靠proxy实现了响应式
对effect的分析
在使用reactive创建proxy对象后,还必须要调用effect函数才能使其真正具有响应式的特性,那么接下来我们就来看以下effect是如何把这个proxy对象变成响应式对象的吧
1.effect:创建ReactiveEffect实例调用run
我们先忽略参数合法性检测的逻辑直接看核心逻辑,它做了以下几件事
- 创建一个ReactiveEffect实例并将传入的函数作为参数 (之后我们再分析创建实例过程都做了什么)
- 调用这个实例的run方法
- 创建这个实例run方法的副本runner并将的this绑定到这个实例身上
- 将runner的effect属性设置为上面创建的实例
- 返回这个runner
第二步以后的操作我们可以先忽略因为它不影响我们之后的分析
那么这一步主要做的事情就是创建一个ReactiveEffect实例并调用run
typescript
export function effect<T = any>(
fn: () => T, // 一个无参数函数,返回类型为 T
options?: ReactiveEffectOptions, // 可选的配置对象,用于定义效应的行为
): ReactiveEffectRunner<T> { // 返回一个响应式效应的运行器,该运行器调用时会执行原始的 fn 函数
// 如果传入的 fn 实际上是一个响应式效应的运行器(即其 effect 属性是一个 ReactiveEffect 实例)
if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
// 则提取出该运行器内部的效应函数,并重新赋值给 fn
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 创建一个新的 ReactiveEffect 实例,将传入的 fn 作为其执行函数
const e = new ReactiveEffect(fn)
// 如果提供了 options 配置对象,则将该对象的属性扩展到 ReactiveEffect 实例上
if (options) {
extend(e, options)
}
try {
// 尝试运行该效应一次,以初始化其依赖收集等逻辑
e.run()
} catch (err) {
// 如果运行过程中发生错误,则停止该效应,并重新抛出错误
e.stop()
throw err
}
// 创建一个新的函数 runner,该函数绑定到 ReactiveEffect 实例 e 上 并将 runner 的 effect 属性设置为 e,以便外部可以访问到该效应实例
const runner = e.run.bind(e) as ReactiveEffectRunner
runner.effect = e
// 返回 runner 函数,该函数现在可以作为响应式效应的运行器使用
return runner
}
2. 得到的ReactiveEffect对象实例
得到的实例如下: 先记一下结构
yaml
{
// 清理函数,当ReactiveEffect停止时调用,用于执行必要的清理工作,如移除事件监听器等
cleanup: undefined,
// 存储ReactiveEffect依赖的Dep对象的链表头部,Dep对象代表响应式数据的一个依赖收集器
deps: Link {sub: ReactiveEffect, dep: Dep, version: 1, prevActiveLink: undefined, prevSub: undefined, ...}
// 存储ReactiveEffect依赖的Dep对象的链表尾部,与deps头部共同构成依赖链表
depsTail: Link {sub: ReactiveEffect, dep: Dep, version: 1, prevActiveLink: undefined, prevSub: undefined, ...}
// 标志位,用于控制ReactiveEffect的行为,如是否处于活动状态、是否应该被触发等
flags: 5,
// 指向下一个ReactiveEffect的引用,在某些情况下用于链接多个副作用
next: undefined,
// 调度器函数,用于控制ReactiveEffect的执行时机,默认为undefined,表示没有特定的调度逻辑
scheduler: undefined,
// ReactiveEffect的构造函数引用,通常不会直接访问,但在内部可能用于创建新的ReactiveEffect实例
constructor: class,
// 表示ReactiveEffect是否"脏"的布尔值,如果为true,则表示该effect需要重新运行以响应依赖的变化
dirty: false,
// 副作用函数,当依赖的数据发生变化时,这个函数会被调用以执行相应的逻辑
fn: () => { titleH1.innerHTML = info.name },
// 通知依赖项已经发生变化的函数,通常由内部响应式系统调用
notify: ƒ notify(),
// 暂停ReactiveEffect的函数,暂停后该effect将不会响应依赖的变化
pause: ƒ pause(),
// 恢复已暂停的ReactiveEffect的函数,恢复后该effect将重新响应依赖的变化
resume: ƒ resume(),
// 运行ReactiveEffect的函数,无论其是否"脏"都会执行
run: ƒ run(),
// 如果ReactiveEffect是"脏"的,则运行它的函数
runIfDirty: ƒ runIfDirty(),
// 停止ReactiveEffect的函数,停止后将不再响应依赖的变化,并可能执行清理工作
stop: ƒ stop(),
// 触发ReactiveEffect重新运行的函数,通常由内部响应式系统调用以响应依赖的变化
trigger: ƒ trigger(),
// getter函数,用于获取dirty属性的值
get dirty: ƒ dirty()
}
3.effect调用run方法:调用fn
run方法干的事情:
- 检查该效应的标志位,看其是否处于非活动状态(即已被停止)(检查未通过不执行if)
- 将该效应的标志位设置为正在运行状态
- 调用清理效应的逻辑(实际上什么都没做)
- 准备依赖收集的逻辑(案例中也是什么都没做)
- 存当前活动的订阅者 (undefine)
- 保存当前是否应该跟踪依赖的标志 (ture)
- 将当前效应设置为活动的订阅者
- 设置应该跟踪依赖的标志为true
- 执行该效应的函数并返回结果
- 清理和恢复
从以上过程可以得出其实run方法主要就是为了执行以下我们在effect函数中写的那个函数
kotlin
run(): T {
// TODO: 清理效应的逻辑尚未实现
// 检查该效应的标志位,看其是否处于非活动状态(即已被停止)
if (!(this.flags & EffectFlags.ACTIVE)) {
// 如果在清理过程中被停止,则直接执行该效应的函数并返回结果
return this.fn()
}
// 将该效应的标志位设置为正在运行状态
this.flags |= EffectFlags.RUNNING
// 调用清理效应的逻辑(注意:此处的 TODO 表示该逻辑尚未实现)
cleanupEffect(this)
// 准备依赖收集的逻辑,即设置效应所需的环境或上下文以便正确地收集依赖
prepareDeps(this)
// 保存当前活动的订阅者(目的是对嵌套调用进行处理)
const prevEffect = activeSub
// 保存当前是否应该跟踪依赖的标志
const prevShouldTrack = shouldTrack
// 将当前效应设置为活动的订阅者
activeSub = this
// 设置应该跟踪依赖的标志为 true
shouldTrack = true
try {
// 执行该效应的函数并返回结果
return this.fn()
} finally {
// 无论 try 块中的代码是否抛出异常,finally 块中的代码都会执行
// 清理该效应的依赖
cleanupDeps(this)
// 恢复之前保存的活动订阅者
activeSub = prevEffect
// 恢复之前保存的是否应该跟踪依赖的标志
shouldTrack = prevShouldTrack
// 将该效应的标志位从正在运行状态清除
this.flags &= ~EffectFlags.RUNNING
}
}
4.调用fn:执行get
执行fn时会执行titleH1.innerHTML = info.name这条语句,那么它到底有什么用呢?
没错,在reactive的分析中我们得到了一个代理对象,这个info就是这个代理对象,那么对他=取name属性会触发get操作,而上面我强调过这个get操作被重写实现了
5.get: 依赖收集开始,调用tarck函数
get方法做的事情:
-
检查特殊属性:
- 如果访问的属性是
ReactiveFlags.SKIP
,直接返回目标对象上的该属性值。 - 检查并返回关于代理对象的一些内置标志位(如是否为响应式、是否为只读、是否为浅层响应式)。
- 如果访问的是
ReactiveFlags.RAW
,根据条件返回原始对象或undefined
。
- 如果访问的属性是
-
处理数组:
- 判断目标对象是否为数组,如果是,并且访问的是数组特有的方法(通过
arrayInstrumentations
映射),则返回这些方法。
- 判断目标对象是否为数组,如果是,并且访问的是数组特有的方法(通过
-
处理
hasOwnProperty
方法:- 如果访问的是
hasOwnProperty
方法,返回自定义的hasOwnProperty
方法,以确保在代理对象上正确调用。
- 如果访问的是
-
使用
Reflect.get
获取属性值:- 使用
Reflect.get
获取目标对象上的属性值。如果目标是一个ref
对象(即一个包含.value
属性的响应式引用),则使用目标自身作为接收者,以避免调用toRaw
。
- 使用
-
处理内置符号和不可追踪的键:
- 如果访问的是内置符号或不可追踪的键(如
Symbol.iterator
或一些内部属性),直接返回结果,不进行依赖追踪。
- 如果访问的是内置符号或不可追踪的键(如
-
依赖追踪:
- 如果不是只读代理,使用
track
函数追踪依赖关系。这是响应式系统的核心部分,用于记录哪些依赖(如计算属性或侦听器)依赖于当前访问的属性。
- 如果不是只读代理,使用
-
处理浅层响应式:
- 如果是浅层响应式代理,直接返回获取到的属性值,不进行深层响应式转换。
-
解包
ref
对象:- 如果获取到的结果是一个
ref
对象,并且不是数组中的整数索引访问,则解包该ref
对象,返回其.value
属性。
- 如果获取到的结果是一个
-
递归创建响应式代理:
- 如果获取到的结果是一个对象,并且不是只读代理,则递归地将其转换为响应式对象(使用
reactive
或readonly
)。
- 如果获取到的结果是一个对象,并且不是只读代理,则递归地将其转换为响应式对象(使用
-
返回最终结果:
- 返回经过上述处理后的最终结果。
所以这里的重点是调用track进行依赖收集
kotlin
get(target: Target, key: string | symbol, receiver: object): any {
// 如果访问的是 ReactiveFlags.SKIP 属性,直接返回目标对象上的该属性值
if (key === ReactiveFlags.SKIP) return target[ReactiveFlags.SKIP]
const isReadonly = this._isReadonly, // 获取当前处理器是否为只读
isShallow = this._isShallow // 获取当前处理器是否为浅层响应式
// 特殊属性处理:判断是否是内置的响应式标志位
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly // 返回是否为非只读(即是否为可变)
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly // 返回是否为只读
} else if (key === ReactiveFlags.IS_SHALLOW) {
return isShallow // 返回是否为浅层响应式
} else if (key === ReactiveFlags.RAW) {
// 如果访问的是原始对象引用:
// - 如果接收者是对应的代理映射中的代理对象,或者具有相同的原型链,则返回原始对象
// - 否则返回 undefined
if (
receiver ===
(isReadonly
? isShallow
? shallowReadonlyMap
: readonlyMap
: isShallow
? shallowReactiveMap
: reactiveMap
).get(target) ||
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
return target
}
return undefined
}
// 判断目标是否为数组
const targetIsArray = isArray(target)
// 如果不是只读代理,处理特殊方法和属性
if (!isReadonly) {
let fn: Function | undefined
// 如果目标是数组且有对应的数组拦截方法,则返回该方法
if (targetIsArray && (fn = arrayInstrumentations[key])) {
return fn
}
// 如果访问的是 hasOwnProperty 方法,返回自定义的 hasOwnProperty 方法
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// 使用 Reflect.get 获取目标对象上的属性值
// 如果目标是一个 ref 对象,则使用原始 ref 作为接收者以避免调用 toRaw
const res = Reflect.get(target, key, isRef(target) ? target : receiver)
// 如果访问的是内置符号或不可追踪的键,则直接返回结果
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 如果不是只读代理,跟踪依赖关系
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 如果是浅层响应式代理,直接返回结果
if (isShallow) {
return res
}
// 如果结果是一个 ref 对象,进行解包(除非是数组且键为整数索引)
if (isRef(res)) {
return targetIsArray && isIntegerKey(key) ? res : res.value
}
// 如果结果是一个对象,递归创建响应式代理
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
// 返回最终结果
return res
}
}
6.track:实施依赖收集,写入targetMap调用实例track方法
它做的事情
-
条件检查:
- 首先,它检查是否应该追踪依赖关系(
shouldTrack
)以及当前是否有活动的订阅者(activeSub
)。这两个条件通常与响应式系统的上下文管理有关,确保只有在需要时才进行追踪,并且当前有侦听器或计算属性等订阅者存在。
- 首先,它检查是否应该追踪依赖关系(
-
获取依赖映射:
- 接着,它从全局的
targetMap
中获取目标对象的依赖映射(depsMap
)。targetMap
是一个存储对象和它们对应的依赖映射的Map
。如果目标对象之前没有对应的依赖映射,它会创建一个新的Map
并将其存储在targetMap
中。
- 接着,它从全局的
-
获取或创建依赖对象:
- 然后,它从依赖映射中获取特定键(
key
)对应的依赖对象(dep
)。如果该键没有对应的依赖对象,它会创建一个新的Dep
实例,并将其存储在依赖映射中。同时,它还会设置依赖对象的map
和key
属性,用于内部引用。
- 然后,它从依赖映射中获取特定键(
-
追踪依赖:
- 最后,它调用依赖对象的
track
方法来追踪依赖关系
- 最后,它调用依赖对象的
typescript
export function track(target: object, type: TrackOpTypes, key: unknown): void {
debugger
// 判断是否应该追踪依赖关系,并且当前是否有活动的订阅者
if (shouldTrack && activeSub) {
// 从 targetMap 中获取目标对象的依赖映射(depsMap)
// targetMap 是一个全局的 Map,用于存储对象和它们对应的依赖映射
let depsMap = targetMap.get(target)
// 如果目标对象没有对应的依赖映射,则创建一个新的 Map 并存储在 targetMap 中
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 从依赖映射中获取特定键(key)对应的依赖对象(dep)
let dep = depsMap.get(key)
// 如果该键没有对应的依赖对象,则创建一个新的 Dep 实例,并存储在依赖映射中
if (!dep) {
depsMap.set(key, (dep = new Dep()))
// 设置依赖对象的 map 和 key 属性,用于内部引用
dep.map = depsMap
dep.key = key
}
// 调用 dep.track
dep.track()
}
}
这一步产生的targetMap结构如下:
yaml
targetMap :{
{name: 'why'}: Map(1) {
'name' => Dep{
activeLink: undefined
computed: undefined
key: "name"
map: Map(1) {'name' => Dep} //这里的dep就是自己本身
sc: 0
subs: undefined
subsHead: undefined
version: 0
constructor: class
notify: ƒ notify(debugInfo)
track: ƒ track(debugInfo)
trigger: ƒ trigger(debugInfo)
}
}
}
7.调用track真正记录依赖
-
条件检查:
- 首先,它检查是否有活动的订阅者(
activeSub
)、是否应该追踪依赖关系(shouldTrack
),以及活动的订阅者不是当前计算属性的订阅者(通过比较activeSub
和this.computed
)。如果这些条件不满足,函数将直接返回undefined
,表示没有追踪到依赖关系。
- 首先,它检查是否有活动的订阅者(
-
获取或创建链接对象:
- 接着,它尝试获取当前活动的链接对象(
link
),这个对象表示当前的依赖关系。如果链接对象不存在,或者链接对象的订阅者不是当前活动的订阅者,它会创建一个新的Link
实例,并将其设置为当前活动的链接对象。
- 接着,它尝试获取当前活动的链接对象(
-
管理依赖列表:
- 对于新创建的链接对象,它会将其添加到活动订阅者的依赖列表中。如果活动订阅者还没有依赖列表,它会初始化这个列表。否则,它会将新的链接对象插入到依赖列表的尾部,并更新相关指针以保持列表的完整性。
-
处理重用的链接对象:
- 如果链接对象存在,但其版本号为
-1
,表示它是从上一次运行中重用的。此时,它会同步版本号,并可能调整链接对象在依赖列表中的位置,以确保依赖列表中的顺序与它们被访问的顺序一致。这通常是为了优化性能,避免不必要的重复操作。
- 如果链接对象存在,但其版本号为
-
全局依赖管理:
- 在创建新的链接对象时,它还会调用
addSub(link)
方法(尽管这一步在代码片段中没有直接显示,但根据上下文可以推断)。这个方法可能是将新的链接对象添加到全局的依赖管理系统中,以支持嵌套依赖或其他高级功能。
- 在创建新的链接对象时,它还会调用
-
返回链接对象:
- 最后,它返回表示依赖关系的链接对象。这个对象可以用于后续的数据变化通知和依赖更新。
ini
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
// 检查是否有活动的订阅者、是否应该追踪依赖关系、以及活动的订阅者不是当前计算属性的订阅者
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return // 如果没有满足条件,则直接返回 undefined
}
// 获取当前活动的链接对象(Link),它表示当前的依赖关系
let link = this.activeLink
// 如果链接对象不存在,或者链接对象的订阅者不是当前活动的订阅者,则需要创建一个新的链接对象
if (link === undefined || link.sub !== activeSub) {
// 创建新的链接对象,并将其设置为当前活动的链接对象
link = this.activeLink = new Link(activeSub, this)
// 将新的链接对象添加到活动订阅者的依赖列表中(作为尾部)
if (!activeSub.deps) {
// 如果活动订阅者还没有依赖列表,则初始化它
activeSub.deps = activeSub.depsTail = link
} else {
// 否则,将新的链接对象插入到依赖列表的尾部,并更新相关指针
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}
// 将新的链接对象添加到全局的依赖管理系统中
addSub(link)
} else if (link.version === -1) {
// 如果链接对象存在,但其版本号为 -1,表示它是从上一次运行中重用的
// 此时,只需要同步版本号,并可能调整链接对象在依赖列表中的位置
link.version = this.version
// 如果链接对象有下一个依赖对象,说明它不在尾部,需要将其移动到尾部
// 这确保了依赖列表中的顺序与它们被访问的顺序一致
if (link.nextDep) {
const next = link.nextDep
next.prevDep = link.prevDep
if (link.prevDep) {
link.prevDep.nextDep = next
}
link.prevDep = activeSub.depsTail
link.nextDep = undefined
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
// 如果链接对象是依赖列表的头部,则需要更新头部的指针
if (activeSub.deps === link) {
activeSub.deps = next
}
}
}
// 返回表示依赖关系的链接对象
return link
}
最后得到的Link对象如下 这是一个双向链表
yaml
{
dep: Dep {computed: undefined, version: 0, activeLink: Link, subs: Link, map: Map(1), ...}
nextDep: undefined
nextSub: undefined
prevActiveLink: undefined
prevDep: undefined
prevSub: undefined
sub: ReactiveEffect {deps: Link, depsTail: Link, flags: 7, next: undefined, fn: ƒ, ...}
version: 0
}
到此依赖收集完毕(还有一个addSub函数没有分析)
总结
vue3的依赖收集过程是,
- 创建一个ReactiveEffect实例并调用run,
- 在run方法里调用fn函数,
- 在执行fn函数的过程中触发proxy的get方法
- get方法里调用tarck函数
- tarck函数将依赖写入全局targetMap,并调用实例track方法
- track方法中将依赖信息写入link完成依赖的收集
完成依赖收集以后得到的东西是:
全局targetMap写入依赖,link里记录依赖
数据响应式触发的过程:
触发响应式其实就是当数据改变时自动调用我们给effect函数传递的函数的过程
当定时器到时间时,proxy对象的set方法将被执行,上文也强调过set方法被重写实现
1.set:设置属性触发trigger
-
获取旧值:
- 通过
target[key]
获取当前键key
对应的旧值oldValue
。
- 通过
-
检查是否为浅响应式模式:
- 判断
this._isShallow
是否为false
,以确定是否处于非浅响应式模式。
- 判断
-
非浅响应式模式处理:
-
如果不是浅响应式模式,则进行以下检查和处理:
a.检查旧值是否为只读:
- 使用
isReadonly(oldValue)
检查旧值是否为只读。
b. 转换新旧值为原始值:
- 如果新值不是浅层的且不是只读的,则使用
toRaw(oldValue)
和toRaw(value)
将旧值和新值转换为它们的原始值(去除响应式包装)。
c. 处理旧值为引用(ref)且新值不是引用的情况:
-
如果目标不是数组,旧值是一个引用(ref),而新值不是一个引用,则进行以下操作:
- 如果旧值是只读的,则不进行设置操作,直接返回
false
。 - 否则,将新值赋给旧值的
.value
属性,并返回true
。
- 如果旧值是只读的,则不进行设置操作,直接返回
- 使用
-
-
浅响应式模式处理:
- 如果是浅响应式模式,则不进行上述的转换和处理,对象按原样设置。
-
检查键是否存在:
- 使用一系列条件判断来确定目标对象中是否已存在该键
key
: a. 如果目标是数组且键是整数,则检查整数键是否小于数组长度。 b. 否则,使用hasOwn(target, key)
检查目标对象是否自身拥有该键。
- 使用一系列条件判断来确定目标对象中是否已存在该键
-
使用
Reflect.set
设置属性值:- 调用
Reflect.set
方法设置属性值,并根据目标是否为ref
来选择接收者receiver
或target
。
- 调用
-
触发响应式逻辑:
- 如果目标是接收者的原始值(即没有通过原型链继承),则根据键是否存在以及新旧值是否变化来触发相应的响应式逻辑: a. 如果之前不存在该键,则触发添加操作
trigger(target, TriggerOpTypes.ADD, key, value)
。 b. 如果旧值和新值不同,则触发设置操作trigger(target, TriggerOpTypes.SET, key, value, oldValue)
。
- 如果目标是接收者的原始值(即没有通过原型链继承),则根据键是否存在以及新旧值是否变化来触发相应的响应式逻辑: a. 如果之前不存在该键,则触发添加操作
-
返回设置操作的结果:
- 返回
Reflect.set
方法调用的结果。
这一步设置新属性并触发trigger函数
- 返回
scss
set(
target: Record<string | symbol, unknown>, // 目标对象,键为字符串或符号,值为任意类型。
key: string | symbol, // 要设置的属性的键。
value: unknown, // 要设置的新值。
receiver: object, // 触发设置的接收者对象(Proxy 代理对象)。
): boolean {
// 获取旧值
let oldValue = target[key]
// 如果不是浅响应式模式
if (!this._isShallow) {
// 检查旧值是否为只读
const isOldValueReadonly = isReadonly(oldValue)
// 如果新值不是浅层的且不是只读的,则将它们转换为原始值(去除响应式包装)
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue) // 将旧值转换为原始值
value = toRaw(value) // 将新值转换为原始值
}
// 如果目标不是数组,旧值是一个引用(ref),而新值不是一个引用
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 如果旧值是只读的,则不进行设置操作,返回 false
if (isOldValueReadonly) {
return false
} else {
// 否则,将新值赋给旧值的 .value 属性,并返回 true
oldValue.value = value
return true
}
}
} else {
// 在浅响应式模式下,对象按原样设置,不考虑是否为响应式
}
// 检查目标对象中是否已存在该键
const hadKey =
isArray(target) && isIntegerKey(key) // 如果目标是数组且键是整数
? Number(key) < target.length // 检查整数键是否小于数组长度
: hasOwn(target, key) // 否则,检查目标对象是否自身拥有该键
// 使用 Reflect.set 设置属性值,并考虑 ref 的情况
const result = Reflect.set(
target, // 目标对象
key, // 要设置的键
value, // 新值
isRef(target) ? target : receiver, // 如果是 ref,则使用 target 作为接收者,否则使用 receiver
)
// 如果目标是接收者的原始值(即没有通过原型链继承),则触发相应的响应式逻辑
if (target === toRaw(receiver)) {
// 如果之前不存在该键,则触发添加操作
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 如果旧值和新值不同,则触发设置操作
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
// 返回设置操作的结果
return result
}
2.trigger: 运行对应的实例trigger
-
获取依赖项映射:还记得吗,我们前面记录了targetMap,所以这里是可以找到的
- 从全局的
targetMap
中获取目标对象target
的依赖项映射depsMap
。
- 从全局的
-
检查依赖项映射:
- 如果没有找到依赖项映射(
depsMap
为undefined
),说明目标对象从未被追踪过,此时增加全局版本号globalVersion
并直接返回。
- 如果没有找到依赖项映射(
-
定义运行依赖项的函数:
- 定义一个
run
函数,该函数接受一个依赖项dep
作为参数。如果存在依赖项,则根据开发模式或生产模式触发依赖项。
- 定义一个
-
开始批处理:
- 调用
startBatch
函数开始一个批处理,用于合并多个触发操作,以提高性能。
- 调用
-
根据操作类型执行逻辑:这里走set分支
-
根据type参数的值执行不同的逻辑分支:
a.
TriggerOpTypes.CLEAR
:- 如果是清除操作(通常针对集合类型如
Map
或Set
),则遍历depsMap
并运行所有依赖项。
b. 数组和非数组处理:
- 判断目标是否是数组
targetIsArray
和键是否是数组索引isArrayIndex
。 - 对于数组且键是
'length'
的情况,处理数组长度的变化,并运行相关的依赖项。
c.
SET | ADD | DELETE
操作:- 如果指定了键
key
或依赖项映射中存在空键依赖项,则运行对应键的依赖项。 - 如果是数组索引变化,运行
ARRAY_ITERATE_KEY
依赖项。 - 根据操作类型
type
和目标类型(数组、对象、Map
),运行相应的迭代键依赖项(ITERATE_KEY
、MAP_KEY_ITERATE_KEY
)。
- 如果是清除操作(通常针对集合类型如
-
-
结束批处理:
- 调用
endBatch
函数结束批处理,执行合并后的触发操作。
- 调用
这一步是为了执行对应记录的 Dep上的 dep.trigger方法
scss
export function trigger(
target: object, // 目标响应式对象
type: TriggerOpTypes, // 操作类型,如添加、设置、删除等
key?: unknown, // 可选的属性键,用于指定要触发副作用的具体属性
newValue?: unknown, // 可选的新值
oldValue?: unknown, // 可选的旧值
oldTarget?: Map<unknown, unknown> | Set<unknown>, // 可选的旧目标集合,用于集合操作
): void {
// 从 targetMap 中获取目标对象的依赖项映射
const depsMap = targetMap.get(target)
// 如果没有找到依赖项映射,说明目标对象从未被追踪过
if (!depsMap) {
// 增加全局版本号(用于检测依赖项的过时状态)
globalVersion++
// 没有依赖项,直接返回
return
}
// 定义一个运行依赖项的函数
const run = (dep: Dep | undefined) => {
// 如果存在依赖项
if (dep) {
dep.trigger()
}
}
// 开始一个批处理,用于合并多个触发操作
startBatch()
// 根据操作类型执行不同的逻辑
if (type === TriggerOpTypes.CLEAR) {
// 如果是清除操作(针对集合类型)
// 触发目标对象的所有依赖项
depsMap.forEach(run)
} else {
// 目标是否是数组
const targetIsArray = isArray(target)
// 键是否是数组索引
const isArrayIndex = targetIsArray && isIntegerKey(key)
// 如果是数组且键是 'length',则处理数组长度的变化
if (targetIsArray && key === 'length') {
const newLength = Number(newValue)
depsMap.forEach((dep, depKey) => {
// 如果键是 'length'、ARRAY_ITERATE_KEY 或非符号键且大于等于新长度
if (
depKey === 'length' ||
depKey === ARRAY_ITERATE_KEY ||
(!isSymbol(depKey) && depKey >= newLength)
) {
// 运行依赖项
run(dep)
}
})
} else {
// 根据 SET | ADD | DELETE 操作类型安排依赖项的运行
if (key !== void 0 || depsMap.has(void 0)) {
// 如果指定了键或依赖项映射中存在空键依赖项
run(depsMap.get(key))
}
// 如果是数组索引变化,安排 ARRAY_ITERATE_KEY 依赖项的运行
if (isArrayIndex) {
run(depsMap.get(ARRAY_ITERATE_KEY))
}
// 对于 ADD | DELETE 操作,如果是非数组或 Map 的 set 操作,安排 ITERATE_KEY 依赖项的运行
// 对于 Map 的 set 操作,还需要安排 MAP_KEY_ITERATE_KEY 依赖项的运行
switch (type) {
case TriggerOpTypes.ADD:
if (!targetIsArray) {
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isArrayIndex) {
// 如果是数组且添加了新索引,则长度会变化
run(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!targetIsArray) {
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
run(depsMap.get(ITERATE_KEY))
}
break
}
}
}
// 结束批处理,执行合并后的触发操作
endBatch()
}
3.实例trigger:执行对应的notify方法
不多赘述,执行对应的notify方法
kotlin
trigger(debugInfo?: DebuggerEventExtraInfo): void {
this.version++
globalVersion++
this.notify(debugInfo)
}
4.执行对应的notify:调用对应ReactiveEffect实例的notify方法
-
开始批处理周期:
- 调用
startBatch()
函数开始一个新的批处理周期。这是为了将所有后续的更新操作合并,以减少不必要的性能开销,比如多次重绘或多次更新 DOM。
- 调用
-
遍历订阅者列表:
- 使用一个循环从
this.subs
开始遍历订阅者列表。由于订阅者列表可能是一个链表结构,循环通过link.prevSub
反向遍历。 this.subs
通常指向链表中的最后一个订阅者(即最新添加的订阅者),因此反向遍历可以确保按照添加顺序的反向来通知订阅者。
- 使用一个循环从
-
通知订阅者:
- 对于每个订阅者(通过
link.sub
访问),调用其notify
方法。 - 如果
notify
方法返回true
,则表示该订阅者是一个计算属性(ComputedRefImpl
)。
- 对于每个订阅者(通过
-
通知计算属性的依赖:
- 如果订阅者是一个计算属性(
ComputedRefImpl
),则通过类型断言(link.sub as ComputedRefImpl)
访问其dep
属性。 - 调用计算属性的
dep.notify()
方法,以通知其依赖项。这一步是为了减少调用栈深度,因为计算属性本身可能已经在其notify
方法中进行了某些更新操作,而这些更新操作可能会触发其依赖项的更新。
- 如果订阅者是一个计算属性(
-
异常处理:
- 使用
try...finally
结构确保无论是否发生异常,都会执行finally
块中的代码。 - 在
finally
块中,调用endBatch()
函数结束批处理周期。
- 使用
-
结束批处理周期:
endBatch()
函数负责处理所有批量的更新操作。这可能包括执行所有已合并的 DOM 更新、触发所有已合并的副作用等。- 结束批处理周期后,系统回到一个稳定状态,准备接收下一个更新周期的通知。
这里调用对应的ReactiveEffect实例的notify方法
scss
notify(debugInfo?: DebuggerEventExtraInfo): void {
// 开始一个新的批处理周期,将所有后续的更新操作合并
startBatch()
try {
// 遍历订阅者列表(从尾部开始,即反向遍历)
for (let link = this.subs; link; link = link.prevSub) {
// 调用订阅者的 notify 方法,如果它返回 true,则表示这是一个计算属性
if (link.sub.notify()) {
// 如果是计算属性,则还需要通知它的依赖(dep),以减少调用栈深度
// 这里使用类型断言将 link.sub 断言为 ComputedRefImpl 类型
;(link.sub as ComputedRefImpl).dep.notify()
}
}
} finally {
// 无论是否发生异常,都结束批处理周期,并处理所有批量的更新操作
endBatch()
}
}
5.ReactiveEffect实例的notify方法:调用batch
kotlin
notify(): void {
// 检查当前效果(effect)是否正在运行,并且不允许递归触发
if (
// 使用位运算检查 flags 是否包含 EffectFlags.RUNNING
this.flags & EffectFlags.RUNNING &&
// 同时检查 flags 是否不包含 EffectFlags.ALLOW_RECURSE
!(this.flags & EffectFlags.ALLOW_RECURSE)
) {
// 如果满足上述条件,则直接返回,避免递归触发导致的问题
return
}
// 检查当前效果是否已经被通知过
if (!(this.flags & EffectFlags.NOTIFIED)) {
// 如果尚未被通知,则将其加入批处理队列
// batch 函数可能是将当前效果封装成一个任务,等待后续统一执行
// 这样可以减少多次状态更新导致的多次DOM操作,提高效率
batch(this)
// 注意:这里虽然调用了 batch 函数,但并未直接修改 flags 中的 NOTIFIED 标志位
// 这可能是在 batch 函数内部或在后续某个时刻统一处理,确保效果只被通知一次
// 但在当前代码块中,我们主要关注是否需要将效果加入批处理队列
}
// 需要注意的是,此 notify 方法并未直接触发效果的执行
// 它只是将效果加入批处理队列或根据条件决定是否加入
// 实际的执行逻辑可能是在 batch 函数的处理流程中或后续某个时刻
}
6.batch
javascript
export function batch(sub: Subscriber, isComputed = false): void {
// 设置订阅者的 NOTIFIED 标志位,表示它已被通知
sub.flags |= EffectFlags.NOTIFIED
// 如果订阅者是一个计算属性订阅者
if (isComputed) {
// 将订阅者的 next 属性指向当前的 batchedComputed 订阅者(如果存在)
// 这样做可能是为了维护一个计算属性订阅者的链表
sub.next = batchedComputed
// 更新 batchedComputed 为当前的订阅者,使其成为最新的计算属性订阅者
batchedComputed = sub
// 提前返回,因为计算属性订阅者的处理可能与普通订阅者不同
return
}
// 对于普通订阅者
// 将订阅者的 next 属性指向当前的 batchedSub 订阅者(如果存在)
// 同样是为了维护一个普通订阅者的链表
sub.next = batchedSub
// 更新 batchedSub 为当前的订阅者,使其成为最新的普通订阅者
batchedSub = sub
}
7.全部执行完毕执行最后的批处理endBatch
这一步执行所有已经记录过的ReactiveEffect的trigger方法
javascript
export function endBatch(): void {
// 减少批次深度,如果批次深度仍然大于0,则直接返回,因为还有未结束的批次
if (--batchDepth > 0) {
return
}
// 如果存在批处理的计算属性(computed),则处理它们
if (batchedComputed) {
let e: Subscriber | undefined = batchedComputed // 获取批处理计算属性的第一个订阅者
batchedComputed = undefined // 清空批处理计算属性的队列,表示当前批次已处理完
// 遍历批处理计算属性的所有订阅者
while (e) {
const next: Subscriber | undefined = e.next // 获取当前订阅者的下一个订阅者
e.next = undefined // 断开当前订阅者的next链接,避免后续处理时的循环引用
e.flags &= ~EffectFlags.NOTIFIED // 清除已通知标志,表示当前订阅者需要重新计算
e = next // 移动到下一个订阅者
}
}
// 用于存储错误信息的变量,如果有错误发生,则在此变量中保存第一个错误
let error: unknown
// 如果存在批处理的订阅者(watchers),则处理它们
while (batchedSub) {
let e: Subscriber | undefined = batchedSub // 获取批处理订阅者的第一个订阅者
batchedSub = undefined // 清空批处理订阅者的队列,表示当前批次已处理完
// 遍历批处理订阅者的所有订阅者
while (e) {
const next: Subscriber | undefined = e.next // 获取当前订阅者的下一个订阅者
e.next = undefined // 断开当前订阅者的next链接,避免后续处理时的循环引用
e.flags &= ~EffectFlags.NOTIFIED // 清除已通知标志,表示当前订阅者需要重新触发
// 如果当前订阅者是激活状态(ACTIVE),则尝试触发它
if (e.flags & EffectFlags.ACTIVE) {
try {
// 强制类型转换为ReactiveEffect,并触发它
;(e as ReactiveEffect).trigger()
} catch (err) {
// 如果触发过程中发生错误,且error变量尚未保存错误信息,则保存当前错误
if (!error) error = err
}
}
e = next // 移动到下一个订阅者
}
}
// 如果在处理过程中有错误发生,则抛出第一个错误
if (error) throw error
}
8.trigger方法
执行runIfDirty方法
kotlin
trigger(): void {
// 检查当前依赖或效果是否被暂停
if (this.flags & EffectFlags.PAUSED) {
// 如果被暂停,则将其添加到暂停队列中
// pausedQueueEffects 可能是一个全局或局部的集合,用于存储所有被暂停的效果
// 以便在后续某个时刻恢复执行
pausedQueueEffects.add(this)
}
// 检查当前依赖或效果是否有自定义的调度器(scheduler)
else if (this.scheduler) {
// 如果有自定义调度器,则调用该调度器
// 调度器是一个函数,用于控制依赖或效果的执行时机
// 它可能用于实现防抖(debounce)、节流(throttle)等功能
this.scheduler()
}
// 如果既没有被暂停自定义,调度也没有器
else {
// 则直接检查并运行当前依赖或效果(如果它已变脏)
// runIfDirty 方法会检查依赖或效果的状态
// 如果 它检测到依赖的数据已经发生变化(即"变脏"),则会执行相应的回调函数
this.runIfDirty()
}
}
9.runIfDirty方法
执行run
kotlin
runIfDirty(): void {
if (isDirty(this)) {
this.run() //这个run会调用我们传入的那个回调函数
}
}
run分析过结束
总结
整个触发的过程是:
- 触发set,set调用trigger
- trigger调用实例的notify
- notify调用RE实例的notify
- RE实例的notify调用batch
- 最后调用endBatch执行统一处理
- 统一处理里遍历链表调用trigger
- trigger调用runIfDirty,在里面调用run执行fn
Vue3响应式实现:ref
测试代码:
ini
const { effect, ref } = VueReactivity
const info = ref("one")
const titleH1 = document.querySelector('h1')
debugger
effect(() => {
titleH1.innerHTML = info.value
})
setTimeout(() => {
info.value = 'kobe'
}, 1000)
注册响应式数据的过程
对ref的分析
1.ref: 调用createRef
javascript
export function ref(value?: unknown) {
return createRef(value, false)
}
2.createRef: 创建RefImpl对象
如果是ref则直接返回,不是则创建RefImpl对象实例
php
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
3.先分析一下这个对象吧
忽略函数具体实现,对象实例如下:
yaml
{
dep: Dep {computed: undefined, version: 0, activeLink: undefined, subs: undefined, map: undefined, ...}
__v_isRef: true
__v_isShallow: false
_rawValue: "one"
_value: "one"
value: "one"
constructor: class
get value: ƒ value()
set value: ƒ value(newValue)
}
这个对象的get set 方法均已重写实现
需要说明的是RefImpl的构造函数,如你所见,当你传入的是一个对象的时候,这个value会被赋值为reactive生成的proxy,所以ref传入对象走的是reactive的逻辑
kotlin
constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value)
this._value = isShallow ? value : toReactive(value)
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
总结
调用ref之后,他把我们传入的普通值包装成了一个对象,并将值设置为value属性,同时重写get,set方法
对effect的分析
前四步和reactive里写的一样,我们主要看第5步调用get的过程,这里就从5开始写了
5.get: 调用自己dep属性对象上的track
kotlin
get value() {
this.dep.track()
return this._value
}
6.track:
这一步也不用多说,它和reactive的第七步一样
总结
ref执行后也是把创建好的响应式对象放入Link,但是它和reactive不同的是没有建立一个全局映射去记录
数据响应式触发的过程
这个操作也是执行set方法,跳过reactive的第二步直接开始第三步,后面都是一样的
附表:
Link类:
typescript
typescript
export class Link {
//属性
version: number;
nextDep?: Link;
prevDep?: Link;
nextSub?: Link;
prevSub?: Link;
prevActiveLink?: Link;
//构造函数
constructor(
public sub: Subscriber,
public dep: Dep,
);
}
Dep类:
typescript
class Dep {
//属性
version: number;
activeLink?: Link;
subs?: Link;
subsHead?: Link;
map?: KeyToDepMap; //Map<any, Dep>
key?: unknown;
sc: number;
//构造函数
constructor(computed?: ComputedRefImpl | undefined);
//方法
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined;
trigger(debugInfo?: DebuggerEventExtraInfo): void;
notify(debugInfo?: DebuggerEventExtraInfo): void;
}
ReactiveEffect类:
typescript
export class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
// 属性
deps?: Link;
depsTail?: Link;
flags: EffectFlags;
next?: Subscriber;
cleanup?: () => void;
scheduler?: EffectScheduler;
onStop?: () => void;
onTrack?: (event: DebuggerEvent) => void;
onTrigger?: (event: DebuggerEvent) => void;
// 构造函数
constructor(public fn: () => T);
// 方法
pause(): void;
resume(): void;
notify(): void;
run(): T;
stop(): void;
trigger(): void;
runIfDirty(): void;
get dirty(): boolean;
}
RefImpl类
typescript
class RefImpl<T = any> {
//属性
private _value: T;
private _rawValue: T;
dep: Dep;
public readonly [ReactiveFlags.IS_REF]: true;
public readonly [ReactiveFlags.IS_SHALLOW]: boolean;
//构造函数
constructor(value: T, isShallow: boolean);
//方法
get value(): T;
set value(newValue: T): void;
}