From f31d782e4668050a188ac0f11ba8d5b861b913ca Mon Sep 17 00:00:00 2001
From: Doctor Wu <44631608+Doctor-wu@users.noreply.github.com>
Date: Tue, 6 Feb 2024 16:58:51 +0800
Subject: [PATCH 1/5] fix(runtime-dom): fix option selected update failed
(#10200)
close #10194
close #10267
---
packages/runtime-dom/src/directives/vModel.ts | 5 -----
1 file changed, 5 deletions(-)
diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts
index c581cb105..b2450b3cf 100644
--- a/packages/runtime-dom/src/directives/vModel.ts
+++ b/packages/runtime-dom/src/directives/vModel.ts
@@ -239,11 +239,6 @@ function setSelected(
return
}
- // fast path for updates triggered by other changes
- if (isArrayValue && looseEqual(value, oldValue)) {
- return
- }
-
for (let i = 0, l = el.options.length; i < l; i++) {
const option = el.options[i]
const optionValue = getValue(option)
From f0b5f7ed8ddf74f9f5ba47cb65e8300370875291 Mon Sep 17 00:00:00 2001
From: yangxiuxiu <79584569+yangxiuxiu1115@users.noreply.github.com>
Date: Tue, 6 Feb 2024 17:38:41 +0800
Subject: [PATCH 2/5] fix(hydration): fix SFC style v-bind hydration mismatch
warnings (#10250)
close #10215
---
.../runtime-core/__tests__/hydration.spec.ts | 16 ++++++++++++++++
packages/runtime-core/src/component.ts | 6 ++++++
packages/runtime-core/src/hydration.ts | 12 +++++++++++-
packages/runtime-dom/src/helpers/useCssVars.ts | 4 ++++
4 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index 127c0d88c..3fa0d7e73 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -19,6 +19,7 @@ import {
onMounted,
ref,
renderSlot,
+ useCssVars,
vModelCheckbox,
vShow,
withDirectives,
@@ -1538,5 +1539,20 @@ describe('SSR hydration', () => {
)
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
})
+
+ test('should not warn css v-bind', () => {
+ const container = document.createElement('div')
+ container.innerHTML = `
`
+ const app = createSSRApp({
+ setup() {
+ useCssVars(() => ({
+ foo: 'red',
+ }))
+ return () => h('div', { style: { color: 'var(--foo)' } })
+ },
+ })
+ app.mount(container)
+ expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+ })
})
})
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 1508627e5..ed1f8efee 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -519,6 +519,12 @@ export interface ComponentInternalInstance {
* @internal
*/
ut?: (vars?: Record) => void
+
+ /**
+ * dev only. For style v-bind hydration mismatch checks
+ * @internal
+ */
+ getCssVars?: () => Record
}
const emptyAppContext = createAppContext()
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index b22afdb7a..1e9200ce2 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -449,7 +449,10 @@ export function createHydrationFunctions(
) {
for (const key in props) {
// check hydration mismatch
- if (__DEV__ && propHasMismatch(el, key, props[key], vnode)) {
+ if (
+ __DEV__ &&
+ propHasMismatch(el, key, props[key], vnode, parentComponent)
+ ) {
hasMismatch = true
}
if (
@@ -718,6 +721,7 @@ function propHasMismatch(
key: string,
clientValue: any,
vnode: VNode,
+ instance: ComponentInternalInstance | null,
): boolean {
let mismatchType: string | undefined
let mismatchKey: string | undefined
@@ -748,6 +752,12 @@ function propHasMismatch(
}
}
}
+
+ const cssVars = instance?.getCssVars?.()
+ for (const key in cssVars) {
+ expectedMap.set(`--${key}`, String(cssVars[key]))
+ }
+
if (!isMapEqual(actualMap, expectedMap)) {
mismatchType = mismatchKey = 'style'
}
diff --git a/packages/runtime-dom/src/helpers/useCssVars.ts b/packages/runtime-dom/src/helpers/useCssVars.ts
index 72714e6f6..1666e3cb3 100644
--- a/packages/runtime-dom/src/helpers/useCssVars.ts
+++ b/packages/runtime-dom/src/helpers/useCssVars.ts
@@ -32,6 +32,10 @@ export function useCssVars(getter: (ctx: any) => Record) {
).forEach(node => setVarsOnNode(node, vars))
})
+ if (__DEV__) {
+ instance.getCssVars = () => getter(instance.proxy)
+ }
+
const setVars = () => {
const vars = getter(instance.proxy)
setVarsOnVNode(instance.subTree, vars)
From 91f058a90cd603492649633d153b120977c4df6b Mon Sep 17 00:00:00 2001
From: zhoulixiang <18366276315@163.com>
Date: Tue, 6 Feb 2024 17:54:06 +0800
Subject: [PATCH 3/5] fix(compiler-core): support v-bind shorthand syntax for
dynamic slot name (#10218)
close #10213
---
.../transforms/transformSlotOutlet.spec.ts | 16 ++++++++++++++++
.../src/transforms/transformSlotOutlet.ts | 12 +++++++++++-
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
index f8809ab6a..6420bdbbd 100644
--- a/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
@@ -389,4 +389,20 @@ describe('compiler: transform outlets', () => {
},
})
})
+
+ test('dynamically named slot outlet with v-bind shorthand', () => {
+ const ast = parseWithSlots(``)
+ expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+ type: NodeTypes.JS_CALL_EXPRESSION,
+ callee: RENDER_SLOT,
+ arguments: [
+ `$slots`,
+ {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: `name`,
+ isStatic: false,
+ },
+ ],
+ })
+ })
})
diff --git a/packages/compiler-core/src/transforms/transformSlotOutlet.ts b/packages/compiler-core/src/transforms/transformSlotOutlet.ts
index 310b6a94e..ea635e997 100644
--- a/packages/compiler-core/src/transforms/transformSlotOutlet.ts
+++ b/packages/compiler-core/src/transforms/transformSlotOutlet.ts
@@ -6,12 +6,14 @@ import {
type SlotOutletNode,
createCallExpression,
createFunctionExpression,
+ createSimpleExpression,
} from '../ast'
import { isSlotOutlet, isStaticArgOf, isStaticExp } from '../utils'
import { type PropsExpression, buildProps } from './transformElement'
import { ErrorCodes, createCompilerError } from '../errors'
import { RENDER_SLOT } from '../runtimeHelpers'
import { camelize } from '@vue/shared'
+import { processExpression } from './transformExpression'
export const transformSlotOutlet: NodeTransform = (node, context) => {
if (isSlotOutlet(node)) {
@@ -76,7 +78,15 @@ export function processSlotOutlet(
}
} else {
if (p.name === 'bind' && isStaticArgOf(p.arg, 'name')) {
- if (p.exp) slotName = p.exp
+ if (p.exp) {
+ slotName = p.exp
+ } else if (p.arg && p.arg.type === NodeTypes.SIMPLE_EXPRESSION) {
+ const name = camelize(p.arg.content)
+ slotName = p.exp = createSimpleExpression(name, false, p.arg.loc)
+ if (!__BROWSER__) {
+ slotName = p.exp = processExpression(p.exp, context)
+ }
+ }
} else {
if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) {
p.arg.content = camelize(p.arg.content)
From 6c7e0bd88f021b0b6365370e97b0c7e243d7d70b Mon Sep 17 00:00:00 2001
From: Johnson Chu
Date: Tue, 6 Feb 2024 18:23:56 +0800
Subject: [PATCH 4/5] fix(reactivity): handle `MaybeDirty` recurse (#10187)
close #10185
---
.../reactivity/__tests__/computed.spec.ts | 40 +++++++++++++++++
packages/reactivity/src/computed.ts | 12 ++---
packages/reactivity/src/constants.ts | 5 ++-
packages/reactivity/src/effect.ts | 44 +++++++++----------
4 files changed, 69 insertions(+), 32 deletions(-)
diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts
index 860e4dab1..5a0beb973 100644
--- a/packages/reactivity/__tests__/computed.spec.ts
+++ b/packages/reactivity/__tests__/computed.spec.ts
@@ -10,6 +10,7 @@ import {
isReadonly,
reactive,
ref,
+ shallowRef,
toRaw,
} from '../src'
import { DirtyLevels } from '../src/constants'
@@ -521,6 +522,45 @@ describe('reactivity/computed', () => {
expect(fnSpy).toBeCalledTimes(2)
})
+ // #10185
+ it('should not override queried MaybeDirty result', () => {
+ class Item {
+ v = ref(0)
+ }
+ const v1 = shallowRef()
+ const v2 = ref(false)
+ const c1 = computed(() => {
+ let c = v1.value
+ if (!v1.value) {
+ c = new Item()
+ v1.value = c
+ }
+ return c.v.value
+ })
+ const c2 = computed(() => {
+ if (!v2.value) return 'no'
+ return c1.value ? 'yes' : 'no'
+ })
+ const c3 = computed(() => c2.value)
+
+ c3.value
+ v2.value = true
+ expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
+ expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
+
+ c3.value
+ expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
+ expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
+ expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
+
+ v1.value.v.value = 999
+ expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
+ expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
+ expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
+
+ expect(c3.value).toBe('yes')
+ })
+
it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
const state = reactive({})
const consumer = computed(() => {
diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts
index 03459c7df..9eed5bc83 100644
--- a/packages/reactivity/src/computed.ts
+++ b/packages/reactivity/src/computed.ts
@@ -1,4 +1,4 @@
-import { type DebuggerOptions, ReactiveEffect, scheduleEffects } from './effect'
+import { type DebuggerOptions, ReactiveEffect } from './effect'
import { type Ref, trackRefValue, triggerRefValue } from './ref'
import { NOOP, hasChanged, isFunction } from '@vue/shared'
import { toRaw } from './reactive'
@@ -44,7 +44,6 @@ export class ComputedRefImpl {
this.effect = new ReactiveEffect(
() => getter(this._value),
() => triggerRefValue(this, DirtyLevels.MaybeDirty),
- () => this.dep && scheduleEffects(this.dep),
)
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
@@ -54,10 +53,11 @@ export class ComputedRefImpl {
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
- if (!self._cacheable || self.effect.dirty) {
- if (hasChanged(self._value, (self._value = self.effect.run()!))) {
- triggerRefValue(self, DirtyLevels.Dirty)
- }
+ if (
+ (!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) {
diff --git a/packages/reactivity/src/constants.ts b/packages/reactivity/src/constants.ts
index 1e3483eb3..5e9716dd8 100644
--- a/packages/reactivity/src/constants.ts
+++ b/packages/reactivity/src/constants.ts
@@ -24,6 +24,7 @@ export enum ReactiveFlags {
export enum DirtyLevels {
NotDirty = 0,
- MaybeDirty = 1,
- Dirty = 2,
+ QueryingDirty = 1,
+ MaybeDirty = 2,
+ Dirty = 3,
}
diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts
index a41cd4986..91d9105af 100644
--- a/packages/reactivity/src/effect.ts
+++ b/packages/reactivity/src/effect.ts
@@ -77,6 +77,7 @@ export class ReactiveEffect {
public get dirty() {
if (this._dirtyLevel === DirtyLevels.MaybeDirty) {
+ this._dirtyLevel = DirtyLevels.QueryingDirty
pauseTracking()
for (let i = 0; i < this._depsLength; i++) {
const dep = this.deps[i]
@@ -87,7 +88,7 @@ export class ReactiveEffect {
}
}
}
- if (this._dirtyLevel < DirtyLevels.Dirty) {
+ if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
this._dirtyLevel = DirtyLevels.NotDirty
}
resetTracking()
@@ -140,7 +141,7 @@ function preCleanupEffect(effect: ReactiveEffect) {
}
function postCleanupEffect(effect: ReactiveEffect) {
- if (effect.deps && effect.deps.length > effect._depsLength) {
+ if (effect.deps.length > effect._depsLength) {
for (let i = effect._depsLength; i < effect.deps.length; i++) {
cleanupDepEffect(effect.deps[i], effect)
}
@@ -291,35 +292,30 @@ export function triggerEffects(
) {
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 &&
- dep.get(effect) === effect._trackId
+ (tracking ??= dep.get(effect) === effect._trackId)
) {
- const lastDirtyLevel = effect._dirtyLevel
+ effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
effect._dirtyLevel = dirtyLevel
- if (lastDirtyLevel === DirtyLevels.NotDirty) {
- effect._shouldSchedule = true
- if (__DEV__) {
- effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
+ }
+ if (
+ effect._shouldSchedule &&
+ (tracking ??= dep.get(effect) === effect._trackId)
+ ) {
+ if (__DEV__) {
+ effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
+ }
+ effect.trigger()
+ if (!effect._runnings || effect.allowRecurse) {
+ effect._shouldSchedule = false
+ if (effect.scheduler) {
+ queueEffectSchedulers.push(effect.scheduler)
}
- effect.trigger()
}
}
}
- scheduleEffects(dep)
resetScheduling()
}
-
-export function scheduleEffects(dep: Dep) {
- for (const effect of dep.keys()) {
- if (
- effect.scheduler &&
- effect._shouldSchedule &&
- (!effect._runnings || effect.allowRecurse) &&
- dep.get(effect) === effect._trackId
- ) {
- effect._shouldSchedule = false
- queueEffectSchedulers.push(effect.scheduler)
- }
- }
-}
From 0bced13ee5c53a02d5f10e5db76fe38b6e131440 Mon Sep 17 00:00:00 2001
From: Johnson Chu
Date: Tue, 6 Feb 2024 18:44:09 +0800
Subject: [PATCH 5/5] fix(reactivity): avoid infinite recursion from side
effects in computed getter (#10232)
close #10214
---
.../reactivity/__tests__/computed.spec.ts | 38 +++++++++++++++++--
packages/reactivity/src/computed.ts | 12 ++++--
packages/reactivity/src/constants.ts | 5 ++-
packages/reactivity/src/effect.ts | 10 ++++-
packages/reactivity/src/ref.ts | 9 ++---
5 files changed, 58 insertions(+), 16 deletions(-)
diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts
index 5a0beb973..c3d0c7f15 100644
--- a/packages/reactivity/__tests__/computed.spec.ts
+++ b/packages/reactivity/__tests__/computed.spec.ts
@@ -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)', () => {
@@ -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)
@@ -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')
+ })
})
diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts
index 9eed5bc83..259d4e32c 100644
--- a/packages/reactivity/src/computed.ts
+++ b/packages/reactivity/src/computed.ts
@@ -43,7 +43,13 @@ export class ComputedRefImpl {
) {
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
@@ -60,8 +66,8 @@ export class ComputedRefImpl {
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
}
diff --git a/packages/reactivity/src/constants.ts b/packages/reactivity/src/constants.ts
index 5e9716dd8..baa75d616 100644
--- a/packages/reactivity/src/constants.ts
+++ b/packages/reactivity/src/constants.ts
@@ -25,6 +25,7 @@ export enum ReactiveFlags {
export enum DirtyLevels {
NotDirty = 0,
QueryingDirty = 1,
- MaybeDirty = 2,
- Dirty = 3,
+ MaybeDirty_ComputedSideEffect = 2,
+ MaybeDirty = 3,
+ Dirty = 4,
}
diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts
index 91d9105af..ca90544c0 100644
--- a/packages/reactivity/src/effect.ts
+++ b/packages/reactivity/src/effect.ts
@@ -76,7 +76,10 @@ export class ReactiveEffect {
}
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++) {
@@ -309,7 +312,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)
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index a3fdde483..1b9d60ef0 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -49,11 +49,10 @@ export function trackRefValue(ref: RefBase) {
ref = toRaw(ref)
trackEffect(
activeEffect,
- 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,