refactor(template-ref): improve template ref handling

close #836, close #839
This commit is contained in:
Evan You 2020-03-16 12:40:58 -04:00
parent 8a58dce603
commit 9ad65b1653
6 changed files with 55 additions and 114 deletions

View File

@ -1,49 +0,0 @@
import { baseParse as parse } from '../../src/parse'
import { transform } from '../../src/transform'
import { transformRef } from '../../src/transforms/transformRef'
import { ElementNode, NodeTypes } from '../../src/ast'
function transformWithRef(template: string) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformRef]
})
return ast.children[0] as ElementNode
}
describe('compiler: transform ref', () => {
const getExpected = (key: any) => ({
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `ref`
},
exp: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`[_ctx, `, key, `]`]
}
})
test('static', () => {
const node = transformWithRef(`<div ref="test"/>`)
expect(node.props[0]).toMatchObject(
getExpected({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `test`,
isStatic: true
})
)
})
test('dynamic', () => {
const node = transformWithRef(`<div :ref="test"/>`)
expect(node.props[0]).toMatchObject(
getExpected({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `test`,
isStatic: false
})
)
})
})

View File

@ -4,7 +4,6 @@ import { transform, NodeTransform, DirectiveTransform } from './transform'
import { generate, CodegenResult } from './codegen' import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast' import { RootNode } from './ast'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
import { transformRef } from './transforms/transformRef'
import { transformIf } from './transforms/vIf' import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor' import { transformFor } from './transforms/vFor'
import { transformExpression } from './transforms/transformExpression' import { transformExpression } from './transforms/transformExpression'
@ -28,7 +27,6 @@ export function getBaseTransformPreset(
): TransformPreset { ): TransformPreset {
return [ return [
[ [
transformRef,
transformOnce, transformOnce,
transformIf, transformIf,
transformFor, transformFor,

View File

@ -1,40 +0,0 @@
import { NodeTransform } from '../transform'
import {
NodeTypes,
ElementTypes,
createSimpleExpression,
createCompoundExpression
} from '../ast'
import { findProp } from '../utils'
// Convert ref="foo" to `:ref="[_ctx, 'foo']"` so that the ref contains the
// correct owner instance even inside slots.
export const transformRef: NodeTransform = node => {
if (
!(
node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT)
)
) {
return
}
const ref = findProp(node, 'ref')
if (!ref) return
const refKey =
ref.type === NodeTypes.ATTRIBUTE
? ref.value
? createSimpleExpression(ref.value.content, true, ref.value.loc)
: null
: ref.exp
if (refKey) {
node.props[node.props.indexOf(ref)] = {
type: NodeTypes.DIRECTIVE,
name: `bind`,
arg: createSimpleExpression(`ref`, true, ref.loc),
exp: createCompoundExpression([`[_ctx, `, refKey, `]`]),
modifiers: [],
loc: ref.loc
}
}
}

View File

@ -22,9 +22,7 @@ describe('api: template refs', () => {
} }
}, },
render() { render() {
// Note: string refs are compiled into [ctx, key] tuples by the compiler return h('div', { ref: 'refKey' })
// to ensure correct context.
return h('div', { ref: [this, 'refKey'] as any })
} }
} }
render(h(Comp), root) render(h(Comp), root)
@ -45,7 +43,7 @@ describe('api: template refs', () => {
} }
}, },
render() { render() {
return h('div', { ref: [this, refKey.value] as any }) return h('div', { ref: refKey.value })
} }
} }
render(h(Comp), root) render(h(Comp), root)
@ -70,7 +68,7 @@ describe('api: template refs', () => {
} }
}, },
render() { render() {
return toggle.value ? h('div', { ref: [this, 'refKey'] as any }) : null return toggle.value ? h('div', { ref: 'refKey' }) : null
} }
} }
render(h(Comp), root) render(h(Comp), root)
@ -178,4 +176,28 @@ describe('api: template refs', () => {
await nextTick() await nextTick()
expect(el.value).toBe(null) expect(el.value).toBe(null)
}) })
test('string ref inside slots', async () => {
const root = nodeOps.createElement('div')
const spy = jest.fn()
const Child = {
render(this: any) {
return this.$slots.default()
}
}
const Comp = {
render() {
return h(Child, () => {
return h('div', { ref: 'foo' })
})
},
mounted(this: any) {
spy(this.$refs.foo.tag)
}
}
render(h(Comp), root)
expect(spy).toHaveBeenCalledWith('div')
})
}) })

View File

@ -8,7 +8,8 @@ import {
VNodeArrayChildren, VNodeArrayChildren,
createVNode, createVNode,
isSameVNodeType, isSameVNodeType,
Static Static,
VNodeNormalizedRef
} from './vnode' } from './vnode'
import { import {
ComponentInternalInstance, ComponentInternalInstance,
@ -30,8 +31,7 @@ import {
isFunction, isFunction,
PatchFlags, PatchFlags,
ShapeFlags, ShapeFlags,
NOOP, NOOP
isArray
} from '@vue/shared' } from '@vue/shared'
import { import {
queueJob, queueJob,
@ -44,7 +44,6 @@ import {
stop, stop,
ReactiveEffectOptions, ReactiveEffectOptions,
isRef, isRef,
Ref,
toRaw, toRaw,
DebuggerEvent DebuggerEvent
} from '@vue/reactivity' } from '@vue/reactivity'
@ -1789,21 +1788,22 @@ function baseCreateRenderer<
} }
const setRef = ( const setRef = (
ref: string | Function | Ref | [ComponentPublicInstance, string], rawRef: VNodeNormalizedRef,
oldRef: string | Function | Ref | [ComponentPublicInstance, string] | null, oldRawRef: VNodeNormalizedRef | null,
parent: ComponentInternalInstance, parent: ComponentInternalInstance,
value: HostNode | ComponentPublicInstance | null value: HostNode | ComponentPublicInstance | null
) => { ) => {
if (isArray(ref)) { const [owner, ref] = rawRef
// template string refs are compiled into tuples like [ctx, key] to if (__DEV__ && !owner) {
// ensure refs inside slots are set on the correct owner instance. warn(
const [{ $: owner }, key] = ref `Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
setRef(key, oldRef && (oldRef as any[])[1], owner, value) `A vnode with ref must be created inside the render function.`
)
return return
} }
const oldRef = oldRawRef && oldRawRef[1]
const refs = parent.refs === EMPTY_OBJ ? (parent.refs = {}) : parent.refs const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
const renderContext = toRaw(parent.renderContext) const renderContext = toRaw(owner.renderContext)
// unset old ref // unset old ref
if (oldRef !== null && oldRef !== ref) { if (oldRef !== null && oldRef !== ref) {
@ -1827,7 +1827,7 @@ function baseCreateRenderer<
} else if (isRef(ref)) { } else if (isRef(ref)) {
ref.value = value ref.value = value
} else if (isFunction(ref)) { } else if (isFunction(ref)) {
callWithErrorHandling(ref, parent, ErrorCodes.FUNCTION_REF, [value]) callWithErrorHandling(ref, parent, ErrorCodes.FUNCTION_REF, [value, refs])
} else if (__DEV__) { } else if (__DEV__) {
warn('Invalid template ref type:', value, `(${typeof value})`) warn('Invalid template ref type:', value, `(${typeof value})`)
} }

View File

@ -52,10 +52,17 @@ export type VNodeTypes =
| typeof PortalImpl | typeof PortalImpl
| typeof SuspenseImpl | typeof SuspenseImpl
export type VNodeRef =
| string
| Ref
| ((ref: object | null, refs: Record<string, any>) => void)
export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef]
export interface VNodeProps { export interface VNodeProps {
[key: string]: any [key: string]: any
key?: string | number key?: string | number
ref?: string | Ref | ((ref: object | null) => void) ref?: VNodeRef
// vnode hooks // vnode hooks
onVnodeBeforeMount?: (vnode: VNode) => void onVnodeBeforeMount?: (vnode: VNode) => void
@ -95,7 +102,7 @@ export interface VNode<HostNode = any, HostElement = any> {
type: VNodeTypes type: VNodeTypes
props: VNodeProps | null props: VNodeProps | null
key: string | number | null key: string | number | null
ref: string | Ref | ((ref: object | null) => void) | null ref: VNodeNormalizedRef | null
scopeId: string | null // SFC only scopeId: string | null // SFC only
children: VNodeNormalizedChildren<HostNode, HostElement> children: VNodeNormalizedChildren<HostNode, HostElement>
component: ComponentInternalInstance | null component: ComponentInternalInstance | null
@ -261,7 +268,10 @@ export function createVNode(
type, type,
props, props,
key: props !== null && props.key !== undefined ? props.key : null, key: props !== null && props.key !== undefined ? props.key : null,
ref: (props !== null && props.ref) || null, ref:
props !== null && props.ref !== undefined
? [currentRenderingInstance!, props.ref]
: null,
scopeId: currentScopeId, scopeId: currentScopeId,
children: null, children: null,
component: null, component: null,