Merge remote-tracking branch 'upstream/main'

This commit is contained in:
三咲智子 Kevin Deng 2024-02-06 20:24:11 +08:00
commit 3c3b56ac04
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
12 changed files with 186 additions and 50 deletions

View File

@ -389,4 +389,20 @@ describe('compiler: transform <slot> outlets', () => {
},
})
})
test('dynamically named slot outlet with v-bind shorthand', () => {
const ast = parseWithSlots(`<slot :name />`)
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,
},
],
})
})
})

View File

@ -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)

View File

@ -10,6 +10,7 @@ import {
isReadonly,
reactive,
ref,
shallowRef,
toRaw,
} from '../src'
import { DirtyLevels } from '../src/constants'
@ -481,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)', () => {
@ -521,6 +526,49 @@ 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_ComputedSideEffect,
)
expect(c3.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
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<any>({})
const consumer = computed(() => {
@ -541,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')
})
})

View File

@ -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'
@ -43,8 +43,13 @@ export class ComputedRefImpl<T> {
) {
this.effect = new ReactiveEffect(
() => getter(this._value),
() => triggerRefValue(this, DirtyLevels.MaybeDirty),
() => this.dep && scheduleEffects(this.dep),
() =>
triggerRefValue(
this,
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
)
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
@ -54,14 +59,15 @@ export class ComputedRefImpl<T> {
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) {
triggerRefValue(self, DirtyLevels.MaybeDirty)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
}
return self._value
}

View File

@ -24,6 +24,8 @@ export enum ReactiveFlags {
export enum DirtyLevels {
NotDirty = 0,
MaybeDirty = 1,
Dirty = 2,
QueryingDirty = 1,
MaybeDirty_ComputedSideEffect = 2,
MaybeDirty = 3,
Dirty = 4,
}

View File

@ -76,7 +76,11 @@ 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++) {
const dep = this.deps[i]
@ -87,7 +91,7 @@ export class ReactiveEffect<T = any> {
}
}
}
if (this._dirtyLevel < DirtyLevels.Dirty) {
if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
this._dirtyLevel = DirtyLevels.NotDirty
}
resetTracking()
@ -140,7 +144,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 +295,33 @@ 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._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
) {
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)
}
}
}

View File

@ -49,11 +49,10 @@ export function trackRefValue(ref: RefBase<any>) {
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,

View File

@ -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 = `<div style="--foo:red;color:var(--foo);" />`
const app = createSSRApp({
setup() {
useCssVars(() => ({
foo: 'red',
}))
return () => h('div', { style: { color: 'var(--foo)' } })
},
})
app.mount(container)
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
})
})
})

View File

@ -518,6 +518,12 @@ export interface ComponentInternalInstance {
* @internal
*/
ut?: (vars?: Record<string, string>) => void
/**
* dev only. For style v-bind hydration mismatch checks
* @internal
*/
getCssVars?: () => Record<string, string>
}
const emptyAppContext = createAppContext()

View File

@ -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'
}

View File

@ -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)

View File

@ -32,6 +32,10 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>) {
).forEach(node => setVarsOnNode(node, vars))
})
if (__DEV__) {
instance.getCssVars = () => getter(instance.proxy)
}
const setVars = () => {
const vars = getter(instance.proxy)
setVarsOnVNode(instance.subTree, vars)