3.4版本响应式
结合讲述3.4源码依赖收集的过程,以及在computed中,当依赖的属性(比如当依赖count1变成依赖count2)发生变化,3.4版本是如何清楚无效依赖的
在vue3到3.4版本中,响应式一直都是有两个核心类,Dep, effect也称为Sub 组成的,常见的 Dep 有 ref, reactive, computed,常见的 effect 有computed, watch, watchEffect, render函数等
依赖收集过程
我们知道,在vue2或者vue3中定义的响应式数据都是通过Object.defineProperty或者Proxy实现,不管是哪种方法,都是通过属性拦截,在读取属性的时候,记录一下当前正在执行的effect是谁, 当这个属性发生改变的时候,我们把这个记录的effect拿出来执行一下, 而这个记录操作,正是通过Dep实现的,我们给每一个属性都挂一个dep,dep记录我在哪个effect执行过即可,具体看下ref的实现 下面都只展示最核心代码,方便理解
class RefImpl<T> {
// 给ref对象一个dep属性
public dep?: Dep = undefined
private _value: T
...
constructor(
value: T,
) {
this._value = value
}
// 当在render函数或者computed中以及其它effect中访问ref时,会触发这个方法
// 此写法相当于Object.defineProperty(obj, {get...})
get value() {
trackRefValue(this)
return this._value
}
}trackRefValue实现
export function trackRefValue(ref: RefBase<any>) {
trackEffect(
activeEffect,
// 初始化ref上的dep,如果ref.dep没有的话
(ref.dep ??= createDep()),
)
}trackEffect实现
export function trackEffect(
effect: ReactiveEffect,
dep: Dep,
) {
dep.set(effect, effect._trackId)
effect.deps[effect._depsLength++] = dep
}可以看到,当我们访问 ref.value 的时候,会被RefImpl类中value方法所拦截,从而执行trackRefValue 而在执行trackRefValue,会给当前的ref对象挂一个dep属性, 用来做依赖收集,看下dep对象长什么样子
export const createDep = (
cleanup: () => void,
computed?: ComputedRefImpl<any>,
): Dep => {
const dep = new Map() as Dep
return dep
}dep实际上就是一个map对象,为什么是map? 因为一个属性,我们既可以在render函数中使用,也可以在computed中使用,而render函数和computed它们属于effect,同样,一个属性可以在A组件,B组件...中使用,而一个组件对应一个render函数,一个render函数中也可以有很多个属性,因此dep和effect属于多对多关系。
接下来我们真正执行依赖收集的函数trackEffect,核心就是下面两行代码
...
dep.set(effect, effect._trackId)
...
effect.deps[effect._depsLength++]我们把dep和activeEffect互相关联了起来,而activeEffect,可以理解为当前正在执行的effect。
举个例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp, computed, ref } from './vue.runtime.esm-bundler.js'
const app = createApp({
template: `
<p>{{ val }}</p>
<button @click="changeVal">改变val</button>
`,
setup() {
const val = ref(1)
const changeVal = () => {
val.value = Math.random()
}
return {
val,
changeVal,
}
}
})
app.mount('#app')
</script>
</body>
</html>当执行render函数的时候,此时这个activeEffect可以理解为我们的render函数,由于在render函数中访问ref对象,会自动.value,因此我们会进入ref的get value方法,命中了上面依赖收集的流程,dep收集了render effect, render effect记住了我依赖了哪些dep, 当修改value的时候,会把ref上的dep拿出来,看看自己上面有哪些effect,都拿出来执行一遍,这就是依赖收集的过程。下面为简化后的源码,方便理解
set value(newVal) {
// 执行triggerRefValue方法
triggerRefValue(this, newVal, oldVal)
}
export function triggerRefValue(
ref: RefBase<any>,
newVal?: any,
oldVal?: any,
) {
// 拿出ref上的dep
const dep = ref.dep
if (dep) {
triggerEffects(
dep
)
}
}
export function triggerEffects(
dep: Dep
) {
// 遍历dep收集的effect,挨个拿出来执行
for (const effect of dep.keys()) {
effect.trigger()
}
}清除不用的依赖
以下面这个例子举例,当点击按钮时,doubleCount 的依赖从 flag, count1 变成了 flag, count2, 此时的 count1 就变成了无效依赖,3.4版本是如何解决的?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp, computed, ref } from './vue.runtime.esm-bundler.js'
const app = createApp({
template: `
<p>{{ doubleCount }}</p>
<button @click="changeFlag">切换flag</button>
`,
setup() {
const count1 = ref(1)
const count2 = ref(10)
const flag = ref(true)
const doubleCount = computed(() => {
console.log('computed');
if(flag.value) {
return count1.value * 2
} else {
return count2.value * 2
}
})
const changeFlag = () => {
flag.value = !flag.value
}
return {
count1,
count2,
flag,
doubleCount,
changeFlag,
}
}
})
app.mount('#app')
</script>
</body>
</html>当修改flag之后,会触发 set value 的拦截,里面会执行triggerRefValue方法,记住,此时的flag上的dep属性收集的依赖是 computed effect
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
const oldVal = this._rawValue
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 执行 triggerRefValue方法
triggerRefValue(this, DirtyLevels.Dirty, newVal, oldVal)
}
}
export function triggerRefValue(
ref: RefBase<any>,
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
newVal?: any,
oldVal?: any,
) {
ref = toRaw(ref)
// 取出 computed 上的依赖收集器 dep, 在我们这个例子中, dep收集的是 computed effect
const dep = ref.dep
if (dep) {
triggerEffects(
dep,
dirtyLevel,
)
}
}
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
pauseScheduling()
for (const effect of dep.keys()) {
// dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
let tracking: boolean | undefined
if (
effect._dirtyLevel < dirtyLevel &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
// 把该 effect 变为脏的,因为 例子中 computed 依赖的属性发生了变化
// 把收集到的 effect 变为脏的,代表该 effect 待会一定要执行
effect._dirtyLevel = dirtyLevel
}
if (
effect._shouldSchedule &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
if (__DEV__) {
// eslint-disable-next-line no-restricted-syntax
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
}
// 执行 computed effect 上的 trigger, 待会讲
effect.trigger()
if (
(!effect._runnings || effect.allowRecurse) &&
effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
) {
effect._shouldSchedule = false
// computed effect 没有 scheduler 因此不会放到调度队列中
if (effect.scheduler) {
queueEffectSchedulers.push(effect.scheduler)
}
}
}
}
resetScheduling()
}
export enum DirtyLevels {
NotDirty = 0, // 不脏
QueryingDirty = 1, // 查询脏不脏中
MaybeDirty_ComputedSideEffect = 2, // 还没发现这个有啥用
MaybeDirty = 3, // 可能是脏的
Dirty = 4, // 脏
}可以看到,代码执行到了 effect.trigger(), 我们看下computed上的 effect 是长什么样子的
export class ComputedRefImpl<T> {
...
// effect 是 ReactiveEffect 的一个实例
this.effect = new ReactiveEffect(
() => getter(this._value),
() =>
triggerRefValue(
this,
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
)
// 代表该 effect 是 computed effect
this.effect.computed = this
...
}
// ReactvieEffect 是长这个样子的
export class ReactiveEffect<T = any> {
deps: Dep[] = []
// 是否是 computed effect的标识
computed?: ComputedRefImpl<T>
// 初始值是脏的,因为初始化的时候,如果用到了,肯定要执行一下 effect.run()
_dirtyLevel = DirtyLevels.Dirty
// 该effect依赖了几个dep
_depsLength = 0
constructor(
public fn: () => T, // fn 可以理解为传给 computed 的 getter函数 computed(() => {...}),此时就是 () => {...}
public trigger: () => void, // 依赖的dep发生变化,执行该函数
public scheduler?: EffectScheduler,
) {
}
run() {
// run 过了, 已经不脏了
this._dirtyLevel = DirtyLevels.NotDirty
let lastShouldTrack = shouldTrack
let lastEffect = activeEffect
try {
shouldTrack = true
// 代表当前正在执行的 effect 是我,this.fn 里面访问到的变量都要收集我自己
activeEffect = this
this._runnings++
preCleanupEffect(this)
return this.fn()
} finally {
postCleanupEffect(this)
this._runnings--
// 还原 activeEffect
activeEffect = lastEffect
shouldTrack = lastShouldTrack
}
}
}可以看到 effect.trigger() 就是下面这段逻辑
triggerRefValue(
this, // 传进去了 computed, 而此时 computed dep 上收集的是 render effect
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
export function triggerRefValue(
ref: RefBase<any>,
dirtyLevel: DirtyLevels = DirtyLevels.Dirty, // 这次传进来的是3 可能是脏的
newVal?: any,
oldVal?: any,
) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
triggerEffects(
dep,
dirtyLevel
)
}
}
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels, // 传进来的是 3
) {
pauseScheduling()
// 此时 computed dep 上只有 render effect
for (const effect of dep.keys()) {
let tracking: boolean | undefined
if (
effect._dirtyLevel < dirtyLevel &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
effect._dirtyLevel = dirtyLevel // 3 可能是脏的
}
if (
effect._shouldSchedule &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
effect.trigger() // render effect 的 trigger 是一个空函数,因为更新是异步的
if (
(!effect._runnings || effect.allowRecurse) &&
effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
) {
effect._shouldSchedule = false
// 更新逻辑放在 调度队列里,异步执行
if (effect.scheduler) {
queueEffectSchedulers.push(effect.scheduler)
}
}
}
}
resetScheduling()
}接下来异步执行 effect.scheduler,可以理解为重新执行下 render effect.run,就会开启新一轮的依赖收集流程 但是在执行的时候,我们需要判断 render effect 是不是脏的,如果不脏,完全可以不更新 在上面的流程中 render effect._dirtyLevel 被标记为 3, 代表可能是脏的,还需要进一步判断脏不脏
const update: SchedulerJob = (instance.update = () => {
// 执行 render effect run之前需要判断脏不脏
if (effect.dirty) {
effect.run()
}
})
// 访问 effect.dirty属性会被拦截,判断脏不脏
public get dirty() {
if (
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
// 此时为3 DirtyLevels.MaybeDirty 命中 if 逻辑
this._dirtyLevel === DirtyLevels.MaybeDirty
) {
// 标记正在查询是不是脏的
this._dirtyLevel = DirtyLevels.QueryingDirty
pauseTracking()
// 遍历 render effect 上所有的 dep, 找出 computed dep
for (let i = 0; i < this._depsLength; i++) {
const dep = this.deps[i]
// 找出 computed dep
if (dep.computed) {
triggerComputed(dep.computed) // 核心是这一句,下面有代码
if (this._dirtyLevel >= DirtyLevels.Dirty) {
break
}
}
}
if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
this._dirtyLevel = DirtyLevels.NotDirty
}
resetTracking()
}
return this._dirtyLevel >= DirtyLevels.Dirty
}
function triggerComputed(computed: ComputedRefImpl<any>) {
// 重新访问 computed.value
return computed.value
}
// 访问 value 会被拦截
get value() {
const self = toRaw(this)
if (
// 判断 comouted 的 effect 是不是脏的
// 很明显,在上面的流程中,computed effect._dirtyLevel为4,已经脏了
// 会重新执行计算属性的 effect.run,就是重新执行传给computed的函数
(!self._cacheable || self.effect.dirty) &&
hasChanged(self._value, (self._value = self.effect.run()!))
) {
triggerRefValue(self, DirtyLevels.Dirty)
}
trackRefValue(self)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
if (__DEV__ && (__TEST__ || this._warnRecursive)) {
warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter)
}
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
}
return self._value
}
// effect.run代码
run() {
// run 过就不脏了,computed effect在这里被标记为不脏
this._dirtyLevel = DirtyLevels.NotDirty
let lastShouldTrack = shouldTrack
let lastEffect = activeEffect
try {
shouldTrack = true
activeEffect = this
preCleanupEffect(this) // 核心就是 effect._depsLength = 0,方便重新计算依赖
// 执行依赖收集,执行computed传进去的函数,里面访问到的响应式属性,如 ref,都会被依赖收集
return this.fn()
} finally {
postCleanupEffect(this)
activeEffect = lastEffect
shouldTrack = lastShouldTrack
}
}
function preCleanupEffect(effect: ReactiveEffect) {
effect._depsLength = 0
}代码走到了这里,就会去重新执行 computed effect的依赖收集流程,还记得computed effect 的依赖从flag 和 count1变为 flag 和 count2吗? 下面就是3.4版本的变换流程
当我们再一次访问到 flag的时候,由于上次我们已经收集过flag了,
// 访问 flag1.value 会走到这里
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
trackEffect(
activeEffect,
// 上次收集过,所以 dep 不用重新初始化
(ref.dep ??= createDep(
() => (ref.dep = undefined),
ref instanceof ComputedRefImpl ? ref : undefined,
))
)
}
}
export function trackEffect(
effect: ReactiveEffect,
dep: Dep,
) {
// dep 为 flag 的 dep,新一轮 run了,effect._trackId肯定和之前不一样了
if (dep.get(effect) !== effect._trackId) {
// 放进dep
dep.set(effect, effect._trackId)
// 取出之前该位置所对应的dep
// 什么叫该位置,就是在computed传进去的函数中,flag 是第一个访问的,该位置就是1
// 重新执行传进去的函数,flag 还是第1个访问的,没问题,复用dep即可 !! 核心复用流程
const oldDep = effect.deps[effect._depsLength]
// 都是 flag 的 dep,复用
if (oldDep !== dep) {
if (oldDep) {
cleanupDepEffect(oldDep, effect)
}
effect.deps[effect._depsLength++] = dep
} else {
// 复用的话直接让 effect._depsLength++即可
effect._depsLength++
}
}
}当我们访问count2的时候,也会执行依赖收集
// 此时 ref是 count2
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
trackEffect(
activeEffect,
// count2 之前没有被收集过,需要给 count2 初始化 dep
(ref.dep ??= createDep(
() => (ref.dep = undefined),
ref instanceof ComputedRefImpl ? ref : undefined,
))
)
}
}
export function trackEffect(
effect: ReactiveEffect,
dep: Dep, // count2
) {
if (dep.get(effect) !== effect._trackId) {
// count2 的 dep收集 该effect
dep.set(effect, effect._trackId)
// 访问之前的位置,之前的位置是 count1
const oldDep = effect.deps[effect._depsLength]
// 现在变成 count2 了,肯定不相等
if (oldDep !== dep) {
if (oldDep) {
// 清理旧的依赖
cleanupDepEffect(oldDep, effect)
}
// 让 effect.deps对应的位置由 count1 变为 count2
// 至此,dep 和 effect都完成了各自的清理工作
effect.deps[effect._depsLength++] = dep
} else {
effect._depsLength++
}
}
}
function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
const trackId = dep.get(effect)
if (trackId !== undefined && effect._trackId !== trackId) {
// count1的dep删除掉effect
dep.delete(effect)
...
}
}有没有发现一个问题,我们从判断render effect脏不脏,搞了半天没有看到对应的逻辑,反而看到了依赖清理的流程 其实,依赖清理逻辑只是判断render effect脏不脏其中的一小个环节而已, 我们继续判断render effect脏不脏,我们刚刚到了 get value,再回顾下
const update: SchedulerJob = (instance.update = () => {
// 执行 render effect前判断 render effect 脏不脏,不脏则不重新渲染
if (effect.dirty) {
effect.run()
}
})
// 访问 effect.dirty属性会被拦截,判断脏不脏
public get dirty() {
if (
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
// 此时为3 DirtyLevels.MaybeDirty 命中 if 逻辑
this._dirtyLevel === DirtyLevels.MaybeDirty
) {
// 标记正在查询是不是脏的
this._dirtyLevel = DirtyLevels.QueryingDirty
pauseTracking()
// 遍历 render effect 上所有的 dep, 找出 computed dep
for (let i = 0; i < this._depsLength; i++) {
const dep = this.deps[i]
// 找出 computed dep
if (dep.computed) {
triggerComputed(dep.computed) // 核心是这一句,下面有代码
if (this._dirtyLevel >= DirtyLevels.Dirty) {
break
}
}
}
if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
this._dirtyLevel = DirtyLevels.NotDirty
}
resetTracking()
}
return this._dirtyLevel >= DirtyLevels.Dirty
}
function triggerComputed(computed: ComputedRefImpl<any>) {
return computed.value
}
get value() {
const self = toRaw(this)
if (
(!self._cacheable || self.effect.dirty) &&
// 上面一大坨依赖清理流程,就是在这个self.effect.run()中执行的
hasChanged(self._value, (self._value = self.effect.run()!))
) {
// 此时computed计算的值,跟之前的值不一样了,代表 render effect脏了
triggerRefValue(self, DirtyLevels.Dirty)
}
trackRefValue(self)
return self._value
}
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels, // 传进来是4
) {
pauseScheduling()
for (const effect of dep.keys()) {
// dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
let tracking: boolean | undefined
if (
effect._dirtyLevel < dirtyLevel &&
(tracking ??= dep.get(effect) === effect._trackId)
) {
effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
effect._dirtyLevel = dirtyLevel // render effect 变脏了
}
...
}
resetScheduling()
}到了上面,render effect已经变脏了 triggerComputed流程执行结束,回到dirty属性的拦截中来,继续往下走
public get dirty() {
if (
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
this._dirtyLevel === DirtyLevels.MaybeDirty
) {
this._dirtyLevel = DirtyLevels.QueryingDirty
pauseTracking()
for (let i = 0; i < this._depsLength; i++) {
const dep = this.deps[i]
if (dep.computed) {
// 这里就是把 render effect 变脏的地方
triggerComputed(dep.computed)
// 已经脏了,break
if (this._dirtyLevel >= DirtyLevels.Dirty) {
break
}
}
}
// 脏了,而不是正在查询脏不脏的状态了
if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
this._dirtyLevel = DirtyLevels.NotDirty
}
resetTracking()
}
// 脏了,返回true
return this._dirtyLevel >= DirtyLevels.Dirty
}
const update: SchedulerJob = (instance.update = () => {
// 脏了,要重新渲染
if (effect.dirty) {
// 渲染
effect.run()
}
})至此,依赖收集流程,依赖改变触发更新,清理无效依赖流程已经结束 那么有一个问题,为什么要判断脏不脏呢?因为虽然计算属性的依赖发生了变化,但是最后计算出来的值可能是不变的,页面依然可以不刷新。 我们只需要改变computed effect收集的dep,清理掉无效依赖即可。
给自己看的总结
在最后判断脏不脏中,是拿 render effect上所有的computed dep,执行一下computed.value计算新的值,此时会对computed effect进行新一轮的 依赖收集,以及清除无效依赖,(计算完后computed effect已经不脏了)。在render effect中执行的时候,computed effect已经不脏,不需要重新计算 和依赖收集,仅仅让computed的dep收集render effect即可。