Skip to content

Commit

Permalink
fix(reactivity): avoid infinite recursion from side effects in comput…
Browse files Browse the repository at this point in the history
…ed getter (vuejs#10232)

close vuejs#10214
  • Loading branch information
johnsoncodehk authored and lynxlangya committed May 30, 2024
1 parent a778d0e commit 6b96e09
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 58 deletions.
38 changes: 34 additions & 4 deletions packages/reactivity/__tests__/computed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,12 @@ describe('reactivity/computed', () => {
c3.value

expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
expect(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
expect(c3.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
})

it('should work when chained(ref+computed)', () => {
Expand Down Expand Up @@ -550,8 +554,12 @@ describe('reactivity/computed', () => {

c3.value
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
expect(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
expect(c3.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)

v1.value.v.value = 999
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
Expand Down Expand Up @@ -581,4 +589,26 @@ describe('reactivity/computed', () => {
await nextTick()
expect(serializeInner(root)).toBe(`2`)
})

it('should not trigger effect scheduler by recurse computed effect', async () => {
const v = ref('Hello')
const c = computed(() => {
v.value += ' World'
return v.value
})
const Comp = {
setup: () => {
return () => c.value
},
}
const root = nodeOps.createElement('div')

render(h(Comp), root)
await nextTick()
expect(serializeInner(root)).toBe('Hello World')

v.value += ' World'
await nextTick()
expect(serializeInner(root)).toBe('Hello World World World World')
})
})
12 changes: 9 additions & 3 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ export class ComputedRefImpl<T> {
) {
this.effect = new ReactiveEffect(
() => getter(this._value),
() => triggerRefValue(this, DirtyLevels.MaybeDirty),
() =>
triggerRefValue(
this,
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
)
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
Expand All @@ -60,8 +66,8 @@ export class ComputedRefImpl<T> {
triggerRefValue(self, DirtyLevels.Dirty)
}
trackRefValue(self)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty) {
triggerRefValue(self, DirtyLevels.MaybeDirty)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
}
return self._value
}
Expand Down
5 changes: 3 additions & 2 deletions packages/reactivity/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export enum ReactiveFlags {
export enum DirtyLevels {
NotDirty = 0,
QueryingDirty = 1,
MaybeDirty = 2,
Dirty = 3,
MaybeDirty_ComputedSideEffect = 2,
MaybeDirty = 3,
Dirty = 4,
}
10 changes: 8 additions & 2 deletions packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ export class ReactiveEffect<T = any> {
}

public get dirty() {
if (this._dirtyLevel === DirtyLevels.MaybeDirty) {
if (
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
this._dirtyLevel === DirtyLevels.MaybeDirty
) {
this._dirtyLevel = DirtyLevels.QueryingDirty
pauseTracking()
for (let i = 0; i < this._depsLength; i++) {
Expand Down Expand Up @@ -310,7 +313,10 @@ export function triggerEffects(
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
}
effect.trigger()
if (!effect._runnings || effect.allowRecurse) {
if (
(!effect._runnings || effect.allowRecurse) &&
effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
) {
effect._shouldSchedule = false
if (effect.scheduler) {
queueEffectSchedulers.push(effect.scheduler)
Expand Down
51 changes: 4 additions & 47 deletions packages/reactivity/src/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,15 @@ type RefBase<T> = {
value: T
}

/**
* trackRefValue 用于跟踪依赖
* @param ref
*/
export function trackRefValue(ref: RefBase<any>) {
// 只有在需要跟踪且当前有活跃的effect时,才会进行依赖收集
if (shouldTrack && activeEffect) {
// 获取ref的原始值,去除响应式对象的包装
ref = toRaw(ref)
// trackEffect 用于建立当前活跃的effect与ref之间的依赖关系
trackEffect(
// 当前活跃的effect
activeEffect,
// 获取或创建 ref 的依赖管理器 dep
ref.dep ||
(ref.dep = createDep(
() => (ref.dep = undefined),
ref instanceof ComputedRefImpl ? ref : undefined,
)),

// 开发模式,提供额外调试信息
(ref.dep ??= createDep(
() => (ref.dep = undefined),
ref instanceof ComputedRefImpl ? ref : undefined,
)),
__DEV__
? {
target: ref,
Expand All @@ -76,23 +64,14 @@ export function trackRefValue(ref: RefBase<any>) {
}
}

/**
* triggerRefValue 用于触发更新
* @param ref
* @param dirtyLevel
* @param newVal
*/
export function triggerRefValue(
ref: RefBase<any>,
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
newVal?: any,
) {
// 获取ref的原始值,去除响应式对象的包装
ref = toRaw(ref)
// 获取 ref 的依赖管理器
const dep = ref.dep
if (dep) {
// triggerEffects 用于出发所有依赖此 ref 的 effect 更新
triggerEffects(
dep,
dirtyLevel,
Expand Down Expand Up @@ -169,58 +148,36 @@ function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
debugger
return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
// 存储ref的值
private _value: T
// 存储ref的原始值
private _rawValue: T

// 这是一个依赖对象,用于收集依赖,当依赖发生变化时,触发更新
public dep?: Dep = undefined
// 用于标识是否是一个ref对象
public readonly __v_isRef = true

constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
// 使用__v_isShallow标识是否是一个浅层的ref
// 如果是浅层的ref,那么value就是原始值,否则就是一个响应式对象
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}

// 获取ref的值
// 如果ref的值是一个响应式对象,那么会对这个响应式对象进行依赖追踪
get value() {
debugger
trackRefValue(this)
return this._value
}

// 设置ref的值
// 在设置值时,会根据条件决定是否需要转换值
set value(newVal) {
debugger
// 确定是否可以直接使用newVal
// 取决于 __v_isShallow、newVal是否是一个浅层或只读的响应式对象
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)

// 根据 useDirectValue 判断是否需要将 newVal 转换为原始值
newVal = useDirectValue ? newVal : toRaw(newVal)

// 判断新值是否发生了变化
if (hasChanged(newVal, this._rawValue)) {
// 如果发生了变化,那么就更新值
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)

// 触发更新
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
Expand Down

0 comments on commit 6b96e09

Please sign in to comment.