feat: support ref in v-for, remove compat deprecation warnings
This commit is contained in:
		
							parent
							
								
									a1167c57e5
								
							
						
					
					
						commit
						41c18effea
					
				@ -936,7 +936,7 @@ describe('compiler: element transform', () => {
 | 
			
		||||
      expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('the binding exists (inline ref input)', () => {
 | 
			
		||||
    test('script setup inline mode template ref (binding exists)', () => {
 | 
			
		||||
      const { node } = parseWithElementTransform(`<input ref="input"/>`, {
 | 
			
		||||
        inline: true,
 | 
			
		||||
        bindingMetadata: {
 | 
			
		||||
@ -949,31 +949,30 @@ describe('compiler: element transform', () => {
 | 
			
		||||
          {
 | 
			
		||||
            type: NodeTypes.JS_PROPERTY,
 | 
			
		||||
            key: {
 | 
			
		||||
              type: NodeTypes.SIMPLE_EXPRESSION,
 | 
			
		||||
              content: 'ref_key',
 | 
			
		||||
              isStatic: true
 | 
			
		||||
            },
 | 
			
		||||
            value: {
 | 
			
		||||
              content: 'input',
 | 
			
		||||
              isStatic: true
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: NodeTypes.JS_PROPERTY,
 | 
			
		||||
            key: {
 | 
			
		||||
              content: 'ref',
 | 
			
		||||
              isStatic: true
 | 
			
		||||
            },
 | 
			
		||||
            value: {
 | 
			
		||||
              type: NodeTypes.JS_FUNCTION_EXPRESSION,
 | 
			
		||||
              params: ['_value', '_refs'],
 | 
			
		||||
              body: {
 | 
			
		||||
                type: NodeTypes.JS_BLOCK_STATEMENT,
 | 
			
		||||
                body: [
 | 
			
		||||
                  {
 | 
			
		||||
                    content: `_refs['input'] = _value`
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    content: 'input.value = _value'
 | 
			
		||||
                  }
 | 
			
		||||
                ]
 | 
			
		||||
              }
 | 
			
		||||
              content: 'input',
 | 
			
		||||
              isStatic: false
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('the binding not exists (inline ref input)', () => {
 | 
			
		||||
    test('script setup inline mode template ref (binding does not exist)', () => {
 | 
			
		||||
      const { node } = parseWithElementTransform(`<input ref="input"/>`, {
 | 
			
		||||
        inline: true
 | 
			
		||||
      })
 | 
			
		||||
@ -983,96 +982,12 @@ describe('compiler: element transform', () => {
 | 
			
		||||
          {
 | 
			
		||||
            type: NodeTypes.JS_PROPERTY,
 | 
			
		||||
            key: {
 | 
			
		||||
              type: NodeTypes.SIMPLE_EXPRESSION,
 | 
			
		||||
              content: 'ref',
 | 
			
		||||
              isStatic: true
 | 
			
		||||
            },
 | 
			
		||||
            value: {
 | 
			
		||||
              type: NodeTypes.JS_FUNCTION_EXPRESSION,
 | 
			
		||||
              params: ['_value', '_refs'],
 | 
			
		||||
              body: {
 | 
			
		||||
                type: NodeTypes.JS_BLOCK_STATEMENT,
 | 
			
		||||
                body: [
 | 
			
		||||
                  {
 | 
			
		||||
                    content: `_refs['input'] = _value`
 | 
			
		||||
                  }
 | 
			
		||||
                ]
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('the binding not exists (inline maybe ref input)', () => {
 | 
			
		||||
      const { node } = parseWithElementTransform(`<input ref="input"/>`, {
 | 
			
		||||
        inline: true,
 | 
			
		||||
        bindingMetadata: {
 | 
			
		||||
          input: BindingTypes.SETUP_MAYBE_REF
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      expect(node.props).toMatchObject({
 | 
			
		||||
        type: NodeTypes.JS_OBJECT_EXPRESSION,
 | 
			
		||||
        properties: [
 | 
			
		||||
          {
 | 
			
		||||
            type: NodeTypes.JS_PROPERTY,
 | 
			
		||||
            key: {
 | 
			
		||||
              type: NodeTypes.SIMPLE_EXPRESSION,
 | 
			
		||||
              content: 'ref',
 | 
			
		||||
              content: 'input',
 | 
			
		||||
              isStatic: true
 | 
			
		||||
            },
 | 
			
		||||
            value: {
 | 
			
		||||
              type: NodeTypes.JS_FUNCTION_EXPRESSION,
 | 
			
		||||
              params: ['_value', '_refs'],
 | 
			
		||||
              body: {
 | 
			
		||||
                type: NodeTypes.JS_BLOCK_STATEMENT,
 | 
			
		||||
                body: [
 | 
			
		||||
                  {
 | 
			
		||||
                    content: `_refs['input'] = _value`
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    content: '_isRef(input) && (input.value = _value)'
 | 
			
		||||
                  }
 | 
			
		||||
                ]
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('the binding not exists (inline let ref input)', () => {
 | 
			
		||||
      const { node } = parseWithElementTransform(`<input ref="input"/>`, {
 | 
			
		||||
        inline: true,
 | 
			
		||||
        bindingMetadata: {
 | 
			
		||||
          input: BindingTypes.SETUP_LET
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      expect(node.props).toMatchObject({
 | 
			
		||||
        type: NodeTypes.JS_OBJECT_EXPRESSION,
 | 
			
		||||
        properties: [
 | 
			
		||||
          {
 | 
			
		||||
            type: NodeTypes.JS_PROPERTY,
 | 
			
		||||
            key: {
 | 
			
		||||
              type: NodeTypes.SIMPLE_EXPRESSION,
 | 
			
		||||
              content: 'ref',
 | 
			
		||||
              isStatic: true
 | 
			
		||||
            },
 | 
			
		||||
            value: {
 | 
			
		||||
              type: NodeTypes.JS_FUNCTION_EXPRESSION,
 | 
			
		||||
              params: ['_value', '_refs'],
 | 
			
		||||
              body: {
 | 
			
		||||
                type: NodeTypes.JS_BLOCK_STATEMENT,
 | 
			
		||||
                body: [
 | 
			
		||||
                  {
 | 
			
		||||
                    content: `_refs['input'] = _value`
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    content:
 | 
			
		||||
                      '_isRef(input) ? input.value = _value : input = _value'
 | 
			
		||||
                  }
 | 
			
		||||
                ]
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,6 @@ export const enum CompilerDeprecationTypes {
 | 
			
		||||
  COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
 | 
			
		||||
  COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
 | 
			
		||||
  COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE',
 | 
			
		||||
  COMPILER_V_FOR_REF = 'COMPILER_V_FOR_REF',
 | 
			
		||||
  COMPILER_NATIVE_TEMPLATE = 'COMPILER_NATIVE_TEMPLATE',
 | 
			
		||||
  COMPILER_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE',
 | 
			
		||||
  COMPILER_FILTERS = 'COMPILER_FILTER'
 | 
			
		||||
@ -79,13 +78,6 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
 | 
			
		||||
    link: `https://v3.vuejs.org/guide/migration/v-if-v-for.html`
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  [CompilerDeprecationTypes.COMPILER_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`
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  [CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE]: {
 | 
			
		||||
    message:
 | 
			
		||||
      `<template> with no special directives will render as a native template ` +
 | 
			
		||||
 | 
			
		||||
@ -19,10 +19,7 @@ import {
 | 
			
		||||
  TemplateTextChildNode,
 | 
			
		||||
  DirectiveArguments,
 | 
			
		||||
  createVNodeCall,
 | 
			
		||||
  ConstantTypes,
 | 
			
		||||
  JSChildNode,
 | 
			
		||||
  createFunctionExpression,
 | 
			
		||||
  createBlockStatement
 | 
			
		||||
  ConstantTypes
 | 
			
		||||
} from '../ast'
 | 
			
		||||
import {
 | 
			
		||||
  PatchFlags,
 | 
			
		||||
@ -48,8 +45,7 @@ import {
 | 
			
		||||
  KEEP_ALIVE,
 | 
			
		||||
  SUSPENSE,
 | 
			
		||||
  UNREF,
 | 
			
		||||
  GUARD_REACTIVE_PROPS,
 | 
			
		||||
  IS_REF
 | 
			
		||||
  GUARD_REACTIVE_PROPS
 | 
			
		||||
} from '../runtimeHelpers'
 | 
			
		||||
import {
 | 
			
		||||
  getInnerRange,
 | 
			
		||||
@ -467,20 +463,32 @@ export function buildProps(
 | 
			
		||||
    const prop = props[i]
 | 
			
		||||
    if (prop.type === NodeTypes.ATTRIBUTE) {
 | 
			
		||||
      const { loc, name, value } = prop
 | 
			
		||||
      let valueNode = createSimpleExpression(
 | 
			
		||||
        value ? value.content : '',
 | 
			
		||||
        true,
 | 
			
		||||
        value ? value.loc : loc
 | 
			
		||||
      ) as JSChildNode
 | 
			
		||||
      let isStatic = true
 | 
			
		||||
      if (name === 'ref') {
 | 
			
		||||
        hasRef = true
 | 
			
		||||
        if (context.scopes.vFor > 0) {
 | 
			
		||||
          properties.push(
 | 
			
		||||
            createObjectProperty(
 | 
			
		||||
              createSimpleExpression('ref_for', true),
 | 
			
		||||
              createSimpleExpression('true')
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        // in inline mode there is no setupState object, so we can't use string
 | 
			
		||||
        // keys to set the ref. Instead, we need to transform it to pass the
 | 
			
		||||
        // actual ref instead.
 | 
			
		||||
        if (!__BROWSER__ && context.inline && value?.content) {
 | 
			
		||||
          valueNode = createFunctionExpression(['_value', '_refs'])
 | 
			
		||||
          valueNode.body = createBlockStatement(
 | 
			
		||||
            processInlineRef(context, value.content)
 | 
			
		||||
        if (
 | 
			
		||||
          !__BROWSER__ &&
 | 
			
		||||
          value &&
 | 
			
		||||
          context.inline &&
 | 
			
		||||
          context.bindingMetadata[value.content]
 | 
			
		||||
        ) {
 | 
			
		||||
          isStatic = false
 | 
			
		||||
          properties.push(
 | 
			
		||||
            createObjectProperty(
 | 
			
		||||
              createSimpleExpression('ref_key', true),
 | 
			
		||||
              createSimpleExpression(value.content, true, value.loc)
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -504,7 +512,11 @@ export function buildProps(
 | 
			
		||||
            true,
 | 
			
		||||
            getInnerRange(loc, 0, name.length)
 | 
			
		||||
          ),
 | 
			
		||||
          valueNode
 | 
			
		||||
          createSimpleExpression(
 | 
			
		||||
            value ? value.content : '',
 | 
			
		||||
            isStatic,
 | 
			
		||||
            value ? value.loc : loc
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    } else {
 | 
			
		||||
@ -555,6 +567,15 @@ export function buildProps(
 | 
			
		||||
        shouldUseBlock = true
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isVBind && isStaticArgOf(arg, 'ref') && context.scopes.vFor > 0) {
 | 
			
		||||
        properties.push(
 | 
			
		||||
          createObjectProperty(
 | 
			
		||||
            createSimpleExpression('ref_for', true),
 | 
			
		||||
            createSimpleExpression('true')
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // special case for v-bind and v-on with no argument
 | 
			
		||||
      if (!arg && (isVBind || isVOn)) {
 | 
			
		||||
        hasDynamicKeys = true
 | 
			
		||||
@ -654,25 +675,6 @@ export function buildProps(
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      __COMPAT__ &&
 | 
			
		||||
      prop.type === NodeTypes.ATTRIBUTE &&
 | 
			
		||||
      prop.name === 'ref' &&
 | 
			
		||||
      context.scopes.vFor > 0 &&
 | 
			
		||||
      checkCompatEnabled(
 | 
			
		||||
        CompilerDeprecationTypes.COMPILER_V_FOR_REF,
 | 
			
		||||
        context,
 | 
			
		||||
        prop.loc
 | 
			
		||||
      )
 | 
			
		||||
    ) {
 | 
			
		||||
      properties.push(
 | 
			
		||||
        createObjectProperty(
 | 
			
		||||
          createSimpleExpression('refInFor', true),
 | 
			
		||||
          createSimpleExpression('true', false)
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let propsExpression: PropsExpression | undefined = undefined
 | 
			
		||||
@ -914,30 +916,3 @@ function stringifyDynamicPropNames(props: string[]): string {
 | 
			
		||||
function isComponentTag(tag: string) {
 | 
			
		||||
  return tag === 'component' || tag === 'Component'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function processInlineRef(
 | 
			
		||||
  context: TransformContext,
 | 
			
		||||
  raw: string
 | 
			
		||||
): JSChildNode[] {
 | 
			
		||||
  const body = [createSimpleExpression(`_refs['${raw}'] = _value`)]
 | 
			
		||||
  const { bindingMetadata, helperString } = context
 | 
			
		||||
  const type = bindingMetadata[raw]
 | 
			
		||||
  if (type === BindingTypes.SETUP_REF) {
 | 
			
		||||
    body.push(createSimpleExpression(`${raw}.value = _value`))
 | 
			
		||||
  } else if (type === BindingTypes.SETUP_MAYBE_REF) {
 | 
			
		||||
    body.push(
 | 
			
		||||
      createSimpleExpression(
 | 
			
		||||
        `${helperString(IS_REF)}(${raw}) && (${raw}.value = _value)`
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  } else if (type === BindingTypes.SETUP_LET) {
 | 
			
		||||
    body.push(
 | 
			
		||||
      createSimpleExpression(
 | 
			
		||||
        `${helperString(
 | 
			
		||||
          IS_REF
 | 
			
		||||
        )}(${raw}) ? ${raw}.value = _value : ${raw} = _value`
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  return body
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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'])
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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. ` +
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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})`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -83,7 +83,7 @@ export const isIntegerKey = (key: unknown) =>
 | 
			
		||||
 | 
			
		||||
export const isReservedProp = /*#__PURE__*/ makeMap(
 | 
			
		||||
  // the leading comma is intentional so empty string "" is also included
 | 
			
		||||
  ',key,ref,' +
 | 
			
		||||
  ',key,ref,ref_for,ref_key,' +
 | 
			
		||||
    'onVnodeBeforeMount,onVnodeMounted,' +
 | 
			
		||||
    'onVnodeBeforeUpdate,onVnodeUpdated,' +
 | 
			
		||||
    'onVnodeBeforeUnmount,onVnodeUnmounted'
 | 
			
		||||
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
import Vue from '@vue/compat'
 | 
			
		||||
import { nextTick } from '../../runtime-core/src/scheduler'
 | 
			
		||||
import {
 | 
			
		||||
  DeprecationTypes,
 | 
			
		||||
  deprecationData,
 | 
			
		||||
  toggleDeprecationWarning
 | 
			
		||||
} from '../../runtime-core/src/compat/compatConfig'
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  toggleDeprecationWarning(true)
 | 
			
		||||
  Vue.configureCompat({
 | 
			
		||||
    MODE: 2,
 | 
			
		||||
    GLOBAL_MOUNT: 'suppress-warning'
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
afterEach(() => {
 | 
			
		||||
  toggleDeprecationWarning(false)
 | 
			
		||||
  Vue.configureCompat({ MODE: 3 })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('V_FOR_REF', async () => {
 | 
			
		||||
  const vm = new Vue({
 | 
			
		||||
    data() {
 | 
			
		||||
      return {
 | 
			
		||||
        ok: true,
 | 
			
		||||
        list: [1, 2, 3]
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    template: `
 | 
			
		||||
    <template v-if="ok">
 | 
			
		||||
      <li v-for="i in list" ref="list">{{ i }}</li>
 | 
			
		||||
    </template>
 | 
			
		||||
    `
 | 
			
		||||
  }).$mount() as any
 | 
			
		||||
 | 
			
		||||
  const mapRefs = () => vm.$refs.list.map((el: HTMLElement) => el.textContent)
 | 
			
		||||
  expect(mapRefs()).toMatchObject(['1', '2', '3'])
 | 
			
		||||
 | 
			
		||||
  expect(deprecationData[DeprecationTypes.V_FOR_REF].message).toHaveBeenWarned()
 | 
			
		||||
 | 
			
		||||
  vm.list.push(4)
 | 
			
		||||
  await nextTick()
 | 
			
		||||
  expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
 | 
			
		||||
 | 
			
		||||
  vm.list.shift()
 | 
			
		||||
  await nextTick()
 | 
			
		||||
  expect(mapRefs()).toMatchObject(['2', '3', '4'])
 | 
			
		||||
 | 
			
		||||
  vm.ok = !vm.ok
 | 
			
		||||
  await nextTick()
 | 
			
		||||
  expect(mapRefs()).toMatchObject([])
 | 
			
		||||
 | 
			
		||||
  vm.ok = !vm.ok
 | 
			
		||||
  await nextTick()
 | 
			
		||||
  expect(mapRefs()).toMatchObject(['2', '3', '4'])
 | 
			
		||||
})
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user