feat(compiler-vapor): ref for `v-for` (#167)

Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Jevon 2024-04-13 02:54:34 +08:00 committed by GitHub
parent 98bae0c4a9
commit a0bd0e9c5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 268 additions and 30 deletions

View File

@ -0,0 +1,51 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: template ref transform > dynamic ref 1`] = `
"import { setRef as _setRef, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
export function render(_ctx) {
const n0 = t0()
_setRef(n0, _ctx.foo)
return n0
}"
`;
exports[`compiler: template ref transform > ref + v-for 1`] = `
"import { setRef as _setRef, createFor as _createFor, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
export function render(_ctx) {
const n0 = _createFor(() => ([1,2,3]), (_block) => {
const n2 = t0()
_setRef(n2, "foo", true)
return [n2, () => {}]
})
return n0
}"
`;
exports[`compiler: template ref transform > ref + v-if 1`] = `
"import { setRef as _setRef, createIf as _createIf, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
export function render(_ctx) {
const n0 = _createIf(() => (true), () => {
const n2 = t0()
_setRef(n2, "foo")
return n2
})
return n0
}"
`;
exports[`compiler: template ref transform > static ref 1`] = `
"import { setRef as _setRef, template as _template } from 'vue/vapor';
const t0 = _template("<div></div>")
export function render(_ctx) {
const n0 = t0()
_setRef(n0, "foo")
return n0
}"
`;

View File

@ -1,4 +1,127 @@
// TODO: add tests for this transform
describe('compiler: template ref transform', () => {
test.todo('basic')
import {
DynamicFlag,
type ForIRNode,
IRNodeTypes,
type IfIRNode,
transformChildren,
transformElement,
transformRef,
transformVFor,
transformVIf,
} from '../../src'
import { makeCompile } from './_utils'
const compileWithTransformRef = makeCompile({
nodeTransforms: [
transformVIf,
transformVFor,
transformRef,
transformElement,
transformChildren,
],
})
describe('compiler: template ref transform', () => {
test('static ref', () => {
const { ir, code } = compileWithTransformRef(`<div ref="foo" />`)
expect(ir.block.dynamic.children[0]).toMatchObject({
id: 0,
flags: DynamicFlag.REFERENCED,
})
expect(ir.template).toEqual(['<div></div>'])
expect(ir.block.operation).lengthOf(1)
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.SET_REF,
element: 0,
value: {
content: 'foo',
isStatic: true,
loc: {
start: { line: 1, column: 10, offset: 9 },
end: { line: 1, column: 15, offset: 14 },
},
},
})
expect(code).matchSnapshot()
expect(code).contains('_setRef(n0, "foo")')
})
test('dynamic ref', () => {
const { ir, code } = compileWithTransformRef(`<div :ref="foo" />`)
expect(ir.block.dynamic.children[0]).toMatchObject({
id: 0,
flags: DynamicFlag.REFERENCED,
})
expect(ir.template).toEqual(['<div></div>'])
expect(ir.block.operation).lengthOf(1)
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.SET_REF,
element: 0,
value: {
content: 'foo',
isStatic: false,
loc: {
start: { line: 1, column: 12, offset: 11 },
end: { line: 1, column: 15, offset: 14 },
},
},
})
expect(code).matchSnapshot()
expect(code).contains('_setRef(n0, _ctx.foo)')
})
test('ref + v-if', () => {
const { ir, code } = compileWithTransformRef(
`<div ref="foo" v-if="true" />`,
)
expect(ir.block.operation).lengthOf(1)
expect(ir.block.operation[0].type).toBe(IRNodeTypes.IF)
const { positive } = ir.block.operation[0] as IfIRNode
expect(positive.operation).lengthOf(1)
expect(positive.operation[0]).toMatchObject({
type: IRNodeTypes.SET_REF,
element: 2,
value: {
content: 'foo',
isStatic: true,
loc: {
start: { line: 1, column: 10, offset: 9 },
end: { line: 1, column: 15, offset: 14 },
},
},
})
expect(code).matchSnapshot()
expect(code).contains('_setRef(n2, "foo")')
})
test('ref + v-for', () => {
const { ir, code } = compileWithTransformRef(
`<div ref="foo" v-for="item in [1,2,3]" />`,
)
const { render } = ir.block.operation[0] as ForIRNode
expect(render.operation).lengthOf(1)
expect(render.operation[0]).toMatchObject({
type: IRNodeTypes.SET_REF,
element: 2,
value: {
content: 'foo',
isStatic: true,
loc: {
start: { line: 1, column: 10, offset: 9 },
end: { line: 1, column: 15, offset: 14 },
},
},
refFor: true,
})
expect(code).matchSnapshot()
expect(code).contains('_setRef(n2, "foo", true)')
})
})

View File

@ -102,9 +102,9 @@ export function getBaseTransformPreset(
return [
[
transformOnce,
transformRef,
transformVIf,
transformVFor,
transformRef,
transformText,
transformElement,
transformComment,

View File

@ -14,6 +14,7 @@ export function genSetRef(
vaporHelper('setRef'),
[`n${oper.element}`],
genExpression(oper.value, context),
oper.refFor && 'true',
),
]
}

View File

@ -36,6 +36,7 @@ export {
export { transformElement } from './transforms/transformElement'
export { transformChildren } from './transforms/transformChildren'
export { transformRef } from './transforms/transformRef'
export { transformText } from './transforms/transformText'
export { transformVBind } from './transforms/vBind'
export { transformVHtml } from './transforms/vHtml'

View File

@ -138,6 +138,7 @@ export interface SetRefIRNode extends BaseIRNode {
type: IRNodeTypes.SET_REF
element: number
value: SimpleExpressionNode
refFor: boolean
}
export interface SetModelValueIRNode extends BaseIRNode {

View File

@ -72,8 +72,9 @@ export interface TransformContext<T extends AllNode = AllNode> {
comment: CommentNode[]
inVOnce: boolean
inVFor: number
enterBlock(ir: TransformContext['block']): () => void
enterBlock(ir: TransformContext['block'], isVFor?: boolean): () => void
reference(): number
increaseId(): number
registerTemplate(): number
@ -122,23 +123,26 @@ function createRootContext(
index: 0,
root: null!, // set later
block: root.block,
enterBlock(ir) {
enterBlock(ir, inVFor = false) {
const { block, template, dynamic, childrenTemplate } = this
this.block = ir
this.dynamic = ir.dynamic
this.template = ''
this.childrenTemplate = []
inVFor && this.inVFor++
return () => {
// exit
this.block = block
this.template = template
this.dynamic = dynamic
this.childrenTemplate = childrenTemplate
inVFor && this.inVFor--
}
},
options: extend({}, defaultOptions, options),
dynamic: root.block.dynamic,
inVOnce: false,
inVFor: 0,
comment: [],
increaseId: () => globalId++,

View File

@ -11,6 +11,7 @@ import { EMPTY_EXPRESSION } from './utils'
export const transformRef: NodeTransform = (node, context) => {
if (node.type !== NodeTypes.ELEMENT) return
const dir = findProp(node, 'ref', false, true)
if (!dir) return
@ -22,10 +23,12 @@ export const transformRef: NodeTransform = (node, context) => {
? createSimpleExpression(dir.value.content, true, dir.value.loc)
: EMPTY_EXPRESSION
}
return () =>
context.registerOperation({
type: IRNodeTypes.SET_REF,
element: context.reference(),
value,
refFor: !!context.inVFor,
})
}

View File

@ -60,7 +60,7 @@ export function processFor(
operation: [],
returns: [],
}
const exitBlock = context.enterBlock(render)
const exitBlock = context.enterBlock(render, true)
context.reference()
return () => {

View File

@ -1,7 +1,19 @@
import { type Ref, type SchedulerJob, isRef } from '@vue/reactivity'
import {
type Ref,
type SchedulerJob,
isRef,
onScopeDispose,
} from '@vue/reactivity'
import { currentInstance } from '../component'
import { VaporErrorCodes, callWithErrorHandling } from '../errorHandling'
import { EMPTY_OBJ, hasOwn, isFunction, isString } from '@vue/shared'
import {
EMPTY_OBJ,
hasOwn,
isArray,
isFunction,
isString,
remove,
} from '@vue/shared'
import { warn } from '../warning'
import { queuePostRenderEffect } from '../scheduler'
@ -10,48 +22,90 @@ export type NodeRef = string | Ref | ((ref: Element) => void)
/**
* Function for handling a template ref
*/
export function setRef(el: Element, ref: NodeRef) {
export function setRef(el: Element, ref: NodeRef, refFor = false) {
if (!currentInstance) return
const { setupState, isUnmounted } = currentInstance
const value = isUnmounted ? null : el
if (isUnmounted) {
return
}
const refs =
currentInstance.refs === EMPTY_OBJ
? (currentInstance.refs = {})
: currentInstance.refs
if (isFunction(ref)) {
callWithErrorHandling(ref, currentInstance, VaporErrorCodes.FUNCTION_REF, [
value,
refs,
])
const invokeRefSetter = (value: Element | null) => {
callWithErrorHandling(
ref,
currentInstance,
VaporErrorCodes.FUNCTION_REF,
[value, refs],
)
}
invokeRefSetter(el)
onScopeDispose(() => invokeRefSetter(null))
} else {
const _isString = isString(ref)
const _isRef = isRef(ref)
let existing: unknown
if (_isString || _isRef) {
const doSet = () => {
if (_isString) {
refs[ref] = value
const doSet: SchedulerJob = () => {
if (refFor) {
existing = _isString
? hasOwn(setupState, ref)
? setupState[ref]
: refs[ref]
: ref.value
if (!isArray(existing)) {
existing = [el]
if (_isString) {
refs[ref] = existing
if (hasOwn(setupState, ref)) {
setupState[ref] = refs[ref]
// if setupState[ref] is a reactivity ref,
// the existing will also become reactivity too
// need to get the Proxy object by resetting
existing = setupState[ref]
}
} else {
ref.value = existing
}
} else if (!existing.includes(el)) {
existing.push(el)
}
} else if (_isString) {
refs[ref] = el
if (hasOwn(setupState, ref)) {
setupState[ref] = value
setupState[ref] = el
}
} else if (_isRef) {
ref.value = value
ref.value = el
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}
}
// #9908 ref on v-for mutates the same array for both mount and unmount
// and should be done together
if (isUnmounted /* || isVFor */) {
doSet()
} else {
// #1789: set new refs in a post job so that they don't get overwritten
// by unmounting ones.
;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet)
}
doSet.id = -1
queuePostRenderEffect(doSet)
onScopeDispose(() => {
queuePostRenderEffect(() => {
if (isArray(existing)) {
remove(existing, el)
} else if (_isString) {
refs[ref] = null
if (hasOwn(setupState, ref)) {
setupState[ref] = null
}
} else if (_isRef) {
ref.value = null
}
})
})
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}