feat: support ref in v-for, remove compat deprecation warnings

This commit is contained in:
Evan You
2021-12-10 23:49:01 +08:00
parent a1167c57e5
commit 41c18effea
11 changed files with 224 additions and 334 deletions

View File

@@ -365,4 +365,81 @@ describe('api: template refs', () => {
expect(elRef1.value).toBeNull()
expect(elRef1.value).toBe(elRef2.value)
})
// compiled output of <script setup> inline mode
test('raw ref with ref_key', () => {
let refs: any
const el = ref()
const App = {
mounted() {
refs = (this as any).$refs
},
render() {
return h(
'div',
{
ref: el,
ref_key: 'el'
},
'hello'
)
}
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(el.value)).toBe('hello')
expect(serializeInner(refs.el)).toBe('hello')
})
// compiled output of v-for + template ref
test('ref in v-for', async () => {
const show = ref(true)
const list = reactive([1, 2, 3])
const listRefs = ref([])
const mapRefs = () => listRefs.value.map(n => serializeInner(n))
const App = {
render() {
return show.value
? h(
'ul',
list.map(i =>
h(
'li',
{
ref: listRefs,
ref_for: true
},
i
)
)
)
: null
}
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(mapRefs()).toMatchObject(['1', '2', '3'])
list.push(4)
await nextTick()
expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
list.shift()
await nextTick()
expect(mapRefs()).toMatchObject(['2', '3', '4'])
show.value = !show.value
await nextTick()
expect(mapRefs()).toMatchObject([])
show.value = !show.value
await nextTick()
expect(mapRefs()).toMatchObject(['2', '3', '4'])
})
})

View File

@@ -12,7 +12,7 @@ import {
} from '../src/vnode'
import { Data } from '../src/component'
import { ShapeFlags, PatchFlags } from '@vue/shared'
import { h, reactive, isReactive, setBlockTracking } from '../src'
import { h, reactive, isReactive, setBlockTracking, ref } from '../src'
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
import { setCurrentRenderingInstance } from '../src/componentRenderContext'
@@ -236,20 +236,24 @@ describe('vnode', () => {
setCurrentRenderingInstance(mockInstance1)
const original = createVNode('div', { ref: 'foo' })
expect(original.ref).toStrictEqual({ i: mockInstance1, r: 'foo' })
expect(original.ref).toMatchObject({
i: mockInstance1,
r: 'foo',
f: false
})
// clone and preserve original ref
const cloned1 = cloneVNode(original)
expect(cloned1.ref).toStrictEqual({ i: mockInstance1, r: 'foo' })
expect(cloned1.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
// cloning with new ref, but with same context instance
const cloned2 = cloneVNode(original, { ref: 'bar' })
expect(cloned2.ref).toStrictEqual({ i: mockInstance1, r: 'bar' })
expect(cloned2.ref).toMatchObject({ i: mockInstance1, r: 'bar', f: false })
// cloning and adding ref to original that has no ref
const original2 = createVNode('div')
const cloned3 = cloneVNode(original2, { ref: 'bar' })
expect(cloned3.ref).toStrictEqual({ i: mockInstance1, r: 'bar' })
expect(cloned3.ref).toMatchObject({ i: mockInstance1, r: 'bar', f: false })
// cloning with different context instance
setCurrentRenderingInstance(mockInstance2)
@@ -257,16 +261,35 @@ describe('vnode', () => {
// clone and preserve original ref
const cloned4 = cloneVNode(original)
// #1311 should preserve original context instance!
expect(cloned4.ref).toStrictEqual({ i: mockInstance1, r: 'foo' })
expect(cloned4.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
// cloning with new ref, but with same context instance
const cloned5 = cloneVNode(original, { ref: 'bar' })
// new ref should use current context instance and overwrite original
expect(cloned5.ref).toStrictEqual({ i: mockInstance2, r: 'bar' })
expect(cloned5.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: false })
// cloning and adding ref to original that has no ref
const cloned6 = cloneVNode(original2, { ref: 'bar' })
expect(cloned6.ref).toStrictEqual({ i: mockInstance2, r: 'bar' })
expect(cloned6.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: false })
const original3 = createVNode('div', { ref: 'foo', ref_for: true })
expect(original3.ref).toMatchObject({
i: mockInstance2,
r: 'foo',
f: true
})
const cloned7 = cloneVNode(original3, { ref: 'bar', ref_for: true })
expect(cloned7.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: true })
const r = ref()
const original4 = createVNode('div', { ref: r, ref_key: 'foo' })
expect(original4.ref).toMatchObject({
i: mockInstance2,
r,
k: 'foo'
})
const cloned8 = cloneVNode(original4)
expect(cloned8.ref).toMatchObject({ i: mockInstance2, r, k: 'foo' })
setCurrentRenderingInstance(null)
})
@@ -277,14 +300,14 @@ describe('vnode', () => {
setCurrentRenderingInstance(mockInstance1)
const original = createVNode('div', { ref: 'foo' })
expect(original.ref).toStrictEqual({ i: mockInstance1, r: 'foo' })
expect(original.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
// clone and preserve original ref
setCurrentRenderingInstance(mockInstance2)
const cloned1 = cloneVNode(original, { ref: 'bar' }, true)
expect(cloned1.ref).toStrictEqual([
{ i: mockInstance1, r: 'foo' },
{ i: mockInstance2, r: 'bar' }
expect(cloned1.ref).toMatchObject([
{ i: mockInstance1, r: 'foo', f: false },
{ i: mockInstance2, r: 'bar', f: false }
])
setCurrentRenderingInstance(null)

View File

@@ -46,7 +46,6 @@ export const enum DeprecationTypes {
WATCH_ARRAY = 'WATCH_ARRAY',
PROPS_DEFAULT_THIS = 'PROPS_DEFAULT_THIS',
V_FOR_REF = 'V_FOR_REF',
V_ON_KEYCODE_MODIFIER = 'V_ON_KEYCODE_MODIFIER',
CUSTOM_DIR = 'CUSTOM_DIR',
@@ -298,13 +297,6 @@ export const deprecationData: Record<DeprecationTypes, DeprecationData> = {
link: `https://v3.vuejs.org/guide/migration/custom-directives.html`
},
[DeprecationTypes.V_FOR_REF]: {
message:
`Ref usage on v-for no longer creates array ref values in Vue 3. ` +
`Consider using function refs or refactor to avoid ref usage altogether.`,
link: `https://v3.vuejs.org/guide/migration/array-refs.html`
},
[DeprecationTypes.V_ON_KEYCODE_MODIFIER]: {
message:
`Using keyCode as v-on modifier is no longer supported. ` +

View File

@@ -1,45 +0,0 @@
import { isArray, remove } from '@vue/shared'
import { ComponentInternalInstance, Data } from '../component'
import { VNode } from '../vnode'
import { DeprecationTypes, warnDeprecation } from './compatConfig'
export function convertLegacyRefInFor(vnode: VNode) {
// refInFor
if (vnode.props && vnode.props.refInFor) {
delete vnode.props.refInFor
if (vnode.ref) {
if (isArray(vnode.ref)) {
vnode.ref.forEach(r => (r.f = true))
} else {
vnode.ref.f = true
}
}
}
}
export function registerLegacyRef(
refs: Data,
key: string,
value: any,
owner: ComponentInternalInstance,
isInFor: boolean | undefined,
isUnmount: boolean
) {
const existing = refs[key]
if (isUnmount) {
if (isArray(existing)) {
remove(existing, value)
} else {
refs[key] = null
}
} else if (isInFor) {
__DEV__ && warnDeprecation(DeprecationTypes.V_FOR_REF, owner)
if (!isArray(existing)) {
refs[key] = [value]
} else if (!existing.includes(value)) {
existing.push(value)
}
} else {
refs[key] = value
}
}

View File

@@ -40,7 +40,8 @@ import {
hasOwn,
invokeArrayFns,
isArray,
getGlobalThis
getGlobalThis,
remove
} from '@vue/shared'
import {
queueJob,
@@ -86,7 +87,6 @@ import { initFeatureFlags } from './featureFlags'
import { isAsyncWrapper } from './apiAsyncComponent'
import { isCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig'
import { registerLegacyRef } from './compat/ref'
export interface Renderer<HostElement = RendererElement> {
render: RootRenderFunction<HostElement>
@@ -2407,40 +2407,53 @@ export function setRef(
}
}
if (isString(ref)) {
const doSet = () => {
if (__COMPAT__ && isCompatEnabled(DeprecationTypes.V_FOR_REF, owner)) {
registerLegacyRef(refs, ref, refValue, owner, rawRef.f, isUnmount)
} else {
refs[ref] = value
}
if (hasOwn(setupState, ref)) {
setupState[ref] = value
}
}
// #1789: for non-null values, set them after render
// null values means this is unmount and it should not overwrite another
// ref with the same key
if (value) {
;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense)
} else {
doSet()
}
} else if (isRef(ref)) {
const doSet = () => {
ref.value = value
}
if (value) {
;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense)
} else {
doSet()
}
} else if (isFunction(ref)) {
if (isFunction(ref)) {
callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
} else if (__DEV__) {
warn('Invalid template ref type:', value, `(${typeof value})`)
} else {
const _isString = isString(ref)
const _isRef = isRef(ref)
if (_isString || _isRef) {
const doSet = () => {
if (rawRef.f) {
const existing = _isString ? refs[ref] : ref.value
if (isUnmount) {
isArray(existing) && remove(existing, refValue)
} else {
if (!isArray(existing)) {
if (_isString) {
refs[ref] = [refValue]
} else {
ref.value = [refValue]
if (rawRef.k) refs[rawRef.k] = ref.value
}
} else if (!existing.includes(refValue)) {
existing.push(refValue)
}
}
} else if (_isString) {
refs[ref] = value
if (hasOwn(setupState, ref)) {
setupState[ref] = value
}
} else if (isRef(ref)) {
ref.value = value
if (rawRef.k) refs[rawRef.k] = value
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}
}
if (value) {
// #1789: for non-null values, set them after render
// null values means this is unmount and it should not overwrite another
// ref with the same key
;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense)
} else {
doSet()
}
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}
}
}

View File

@@ -42,7 +42,6 @@ import { hmrDirtyComponents } from './hmr'
import { convertLegacyComponent } from './compat/component'
import { convertLegacyVModelProps } from './compat/componentVModel'
import { defineLegacyVNodeProperties } from './compat/renderFn'
import { convertLegacyRefInFor } from './compat/ref'
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined) as any as {
__isFragment: true
@@ -73,7 +72,8 @@ export type VNodeRef =
export type VNodeNormalizedRefAtom = {
i: ComponentInternalInstance
r: VNodeRef
f?: boolean // v2 compat only, refInFor marker
k?: string // setup ref key
f?: boolean // refInFor marker
}
export type VNodeNormalizedRef =
@@ -92,6 +92,8 @@ export type VNodeHook =
export type VNodeProps = {
key?: string | number | symbol
ref?: VNodeRef
ref_for?: boolean
ref_key?: string
// vnode hooks
onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
@@ -380,11 +382,15 @@ export const InternalObjectKey = `__vInternal`
const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
key != null ? key : null
const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
const normalizeRef = ({
ref,
ref_key,
ref_for
}: VNodeProps): VNodeNormalizedRefAtom | null => {
return (
ref != null
? isString(ref) || isRef(ref) || isFunction(ref)
? { i: currentRenderingInstance, r: ref }
? { i: currentRenderingInstance, r: ref, k: ref_key, f: !!ref_for }
: ref
: null
) as any
@@ -468,7 +474,6 @@ function createBaseVNode(
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
convertLegacyRefInFor(vnode)
defineLegacyVNodeProperties(vnode)
}