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,