wip(vapor): improve v-for codegen + minor optimization

This commit is contained in:
Evan You 2025-01-31 13:14:16 +08:00
parent 2b0731a43d
commit cad7f0e583
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
9 changed files with 119 additions and 111 deletions

View File

@ -19,7 +19,7 @@ const t0 = _template("<div></div>", true)
export function render(_ctx) {
const _setTemplateRef = _createTemplateRefSetter()
const n0 = _createFor(() => ([1,2,3]), (_ctx0) => {
const n0 = _createFor(() => ([1,2,3]), (_for_item0) => {
const n2 = t0()
_setTemplateRef(n2, "foo", void 0, true)
return n2

View File

@ -5,9 +5,9 @@ exports[`compiler: v-for > array de-structured value (with rest) 1`] = `
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _ctx0[0].value[0] + _ctx0[0].value.slice(1) + _ctx0[1].value))
_renderEffect(() => _setText(n2, _for_item0.value[0] + _for_item0.value.slice(1) + _for_key0.value))
return n2
}, ([id, ...other], index) => (id))
return n0
@ -19,9 +19,9 @@ exports[`compiler: v-for > array de-structured value 1`] = `
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _ctx0[0].value[0] + _ctx0[0].value[1] + _ctx0[1].value))
_renderEffect(() => _setText(n2, _for_item0.value[0] + _for_item0.value[1] + _for_key0.value))
return n2
}, ([id, other], index) => (id))
return n0
@ -34,10 +34,10 @@ const t0 = _template("<div></div>", true)
_delegateEvents("click")
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
const n2 = t0()
_delegate(n2, "click", () => $event => (_ctx.remove(_ctx0[0].value)))
_renderEffect(() => _setText(n2, _ctx0[0].value))
_delegate(n2, "click", () => $event => (_ctx.remove(_for_item0.value)))
_renderEffect(() => _setText(n2, _for_item0.value))
return n2
}, (item) => (item.id))
return n0
@ -49,11 +49,11 @@ exports[`compiler: v-for > multi effect 1`] = `
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
const n0 = _createFor(() => (_ctx.items), (_for_item0, _for_key0) => {
const n2 = t0()
_renderEffect(() => {
_setProp(n2, "item", _ctx0[0].value)
_setProp(n2, "index", _ctx0[1].value)
_setProp(n2, "item", _for_item0.value)
_setProp(n2, "index", _for_key0.value)
})
return n2
})
@ -67,11 +67,11 @@ const t0 = _template("<span></span>")
const t1 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n5 = t1()
const n2 = _createFor(() => (_ctx0[0].value), (_ctx1) => {
const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
const n4 = t0()
_renderEffect(() => _setText(n4, _ctx1[0].value+_ctx0[0].value))
_renderEffect(() => _setText(n4, _for_item1.value+_for_item0.value))
return n4
})
_insert(n2, n5)
@ -86,9 +86,9 @@ exports[`compiler: v-for > object de-structured value (with rest) 1`] = `
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _ctx0[0].value.id + _getRestElement(_ctx0[0].value, ["id"]) + _ctx0[1].value))
_renderEffect(() => _setText(n2, _for_item0.value.id + _getRestElement(_for_item0.value, ["id"]) + _for_key0.value))
return n2
}, ({ id, ...other }, index) => (id))
return n0
@ -100,9 +100,9 @@ exports[`compiler: v-for > object de-structured value 1`] = `
const t0 = _template("<span></span>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _ctx0[0].value.id, _ctx0[0].value.value))
_renderEffect(() => _setText(n2, _for_item0.value.id, _for_item0.value.value))
return n2
}, ({ id, value }) => (id))
return n0
@ -114,9 +114,9 @@ exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = `
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _getDefaultValue(_ctx._ctx0[0].value.foo, _ctx.bar) + _ctx.bar + _ctx.baz + _getDefaultValue(_ctx._ctx0[0].value.baz[0], _ctx.quux) + _ctx.quux))
_renderEffect(() => _setText(n2, _getDefaultValue(_for_item0.value.foo, _ctx.bar) + _ctx.bar + _ctx.baz + _getDefaultValue(_for_item0.value.baz[0], _ctx.quux) + _ctx.quux))
return n2
})
return n0
@ -128,7 +128,7 @@ exports[`compiler: v-for > w/o value 1`] = `
const t0 = _template("<div>item</div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
const n2 = t0()
return n2
})

View File

@ -65,7 +65,7 @@ exports[`compiler: v-once > with v-for 1`] = `
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n2 = t0()
return n2
}, null, null, true)

View File

@ -84,9 +84,11 @@ describe('compiler: v-for', () => {
`<div v-for="i in list"><span v-for="j in i">{{ j+i }}</span></div>`,
)
expect(code).matchSnapshot()
expect(code).contains(`_createFor(() => (_ctx.list), (_ctx0) => {`)
expect(code).contains(`_createFor(() => (_ctx0[0].value), (_ctx1) => {`)
expect(code).contains(`_ctx1[0].value+_ctx0[0].value`)
expect(code).contains(`_createFor(() => (_ctx.list), (_for_item0) => {`)
expect(code).contains(
`_createFor(() => (_for_item0.value), (_for_item1) => {`,
)
expect(code).contains(`_for_item1.value+_for_item0.value`)
expect(ir.template).toEqual(['<span></span>', '<div></div>'])
expect(ir.block.operation).toMatchObject([
{
@ -149,7 +151,7 @@ describe('compiler: v-for', () => {
`<div v-for="( { id, ...other }, index) in list" :key="id">{{ id + other + index }}</div>`,
)
expect(code).matchSnapshot()
expect(code).toContain('_getRestElement(_ctx0[0].value, ["id"])')
expect(code).toContain('_getRestElement(_for_item0.value, ["id"])')
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.FOR,
source: {
@ -212,7 +214,7 @@ describe('compiler: v-for', () => {
`<div v-for="([id, ...other], index) in list" :key="id">{{ id + other + index }}</div>`,
)
expect(code).matchSnapshot()
expect(code).toContain('_ctx0[0].value.slice(1)')
expect(code).toContain('_for_item0.value.slice(1)')
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.FOR,
source: {
@ -246,11 +248,9 @@ describe('compiler: v-for', () => {
</div>`,
)
expect(code).matchSnapshot()
expect(code).toContain(`_getDefaultValue(_for_item0.value.foo, _ctx.bar)`)
expect(code).toContain(
`_getDefaultValue(_ctx._ctx0[0].value.foo, _ctx.bar)`,
)
expect(code).toContain(
`_getDefaultValue(_ctx._ctx0[0].value.baz[0], _ctx.quux)`,
`_getDefaultValue(_for_item0.value.baz[0], _ctx.quux)`,
)
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.FOR,

View File

@ -27,11 +27,13 @@ export function genFor(
const idToPathMap = parseValueDestructure()
const [depth, exitScope] = context.enterScope()
const propsName = `_ctx${depth}`
const idMap: Record<string, string | SimpleExpressionNode | null> = {}
const itemVar = `_for_item${depth}`
idMap[itemVar] = null
idToPathMap.forEach((pathInfo, id) => {
let path = `${propsName}[0].value${pathInfo ? pathInfo.path : ''}`
let path = `${itemVar}.value${pathInfo ? pathInfo.path : ''}`
if (pathInfo) {
if (pathInfo.helper) {
idMap[pathInfo.helper] = null
@ -50,13 +52,22 @@ export function genFor(
idMap[id] = path
}
})
if (rawKey) idMap[rawKey] = `${propsName}[1].value`
if (rawIndex) idMap[rawIndex] = `${propsName}[2].value`
const blockFn = context.withId(
() => genBlock(render, context, [propsName]),
idMap,
)
const args = [itemVar]
if (rawKey) {
const keyVar = `_for_key${depth}`
args.push(`, ${keyVar}`)
idMap[rawKey] = `${keyVar}.value`
idMap[keyVar] = null
}
if (rawIndex) {
const indexVar = `_for_index${depth}`
args.push(`, ${indexVar}`)
idMap[rawIndex] = `${indexVar}.value`
idMap[indexVar] = null
}
const blockFn = context.withId(() => genBlock(render, context, args), idMap)
exitScope()
return [

View File

@ -18,7 +18,7 @@ describe.todo('api: createSelector', () => {
const isSleected = createSelector(index)
return createFor(
() => list.value,
([item]) => {
item => {
const span = document.createElement('li')
renderEffect(() => {
calledTimes += 1
@ -73,7 +73,7 @@ describe.todo('api: createSelector', () => {
)
return createFor(
() => list.value,
([item]) => {
item => {
const span = document.createElement('li')
renderEffect(() => {
calledTimes += 1

View File

@ -422,7 +422,7 @@ describe('api: template ref', () => {
const n1 = t0()
const n2 = createFor(
() => list,
state => {
item => {
const n1 = t1()
createTemplateRefSetter()(
n1 as Element,
@ -431,7 +431,6 @@ describe('api: template ref', () => {
true,
)
renderEffect(() => {
const [item] = state
setText(n1, item)
})
return n1
@ -485,7 +484,7 @@ describe('api: template ref', () => {
const n1 = t0()
const n2 = createFor(
() => list,
state => {
item => {
const n1 = t1()
createTemplateRefSetter()(
n1 as Element,
@ -494,7 +493,6 @@ describe('api: template ref', () => {
true,
)
renderEffect(() => {
const [item] = state
setText(n1, item)
})
return n1
@ -546,7 +544,7 @@ describe('api: template ref', () => {
const n2 = n1!.nextSibling!
const n3 = createFor(
() => list.value,
state => {
item => {
const n4 = t1()
createTemplateRefSetter()(
n4 as Element,
@ -555,7 +553,6 @@ describe('api: template ref', () => {
true,
)
renderEffect(() => {
const [item] = state
setText(n4, item)
})
return n4

View File

@ -19,14 +19,13 @@ describe('createFor', () => {
const { host } = define(() => {
const n1 = createFor(
() => list.value,
state => {
(item, key, index) => {
const span = document.createElement('li')
renderEffect(() => {
const [{ value: item }, { value: key }, { value: index }] = state
span.innerHTML = `${key}. ${item.name}`
span.innerHTML = `${key.value}. ${item.value.name}`
// index should be undefined if source is not an object
expect(index).toBe(undefined)
expect(index.value).toBe(undefined)
})
return span
},
@ -85,11 +84,10 @@ describe('createFor', () => {
const { host } = define(() => {
const n1 = createFor(
() => count.value,
state => {
(item, key, index) => {
const span = document.createElement('li')
renderEffect(() => {
const [{ value: item }, { value: key }, index] = state
span.innerHTML = `${key}. ${item}`
span.innerHTML = `${key.value}. ${item.value}`
// index should be undefined if source is not an object
expect(index.value).toBe(undefined)
@ -130,12 +128,11 @@ describe('createFor', () => {
const { host } = define(() => {
const n1 = createFor(
() => data.value,
state => {
(item, key, index) => {
const span = document.createElement('li')
renderEffect(() => {
const [{ value: item }, { value: key }, { value: index }] = state
span.innerHTML = `${key}${index}. ${item}`
expect(index).not.toBe(undefined)
span.innerHTML = `${key.value}${index.value}. ${item.value}`
expect(index.value).not.toBe(undefined)
})
return span
},
@ -197,17 +194,11 @@ describe('createFor', () => {
const { host } = define(() => {
const n1 = createFor(
() => list.value,
state => {
(item, key, index) => {
const span = document.createElement('li')
renderEffect(() => {
const [
{
value: { name },
},
{ value: key },
index,
] = state
span.innerHTML = `${key}. ${name}`
// compiler rewrites { name } destructure to inline access
span.innerHTML = `${key.value}. ${item.value.name}`
// index should be undefined if source is not an object
expect(index.value).toBe(undefined)
})
@ -275,14 +266,14 @@ describe('createFor', () => {
const { host } = define(() => {
const n1 = createFor(
() => list.value,
state => {
(item, _key, index) => {
const span = document.createElement('li')
renderEffect(() => {
span.innerHTML = JSON.stringify(
getRestElement(state[0].value, ['name']),
getRestElement(item.value, ['name']),
)
// index should be undefined if source is not an object
expect(state[2].value).toBe(undefined)
expect(index.value).toBe(undefined)
})
return span
},
@ -341,12 +332,12 @@ describe('createFor', () => {
const { host } = define(() => {
const n1 = createFor(
() => list.value,
state => {
(item, _key, index) => {
const span = document.createElement('li')
renderEffect(() => {
span.innerHTML = getDefaultValue(state[0].value.x, '0')
span.innerHTML = getDefaultValue(item.value.x, '0')
// index should be undefined if source is not an object
expect(state[2].value).toBe(undefined)
expect(index.value).toBe(undefined)
})
return span
},
@ -378,14 +369,13 @@ describe('createFor', () => {
const { host } = define(() => {
const n1 = createFor(
() => list.value,
state => {
(item, key, index) => {
const span = document.createElement('li')
renderEffect(() => {
const [{ value: item }, { value: key }, { value: index }] = state
span.innerHTML = `${key}. ${item.name}`
span.innerHTML = `${key.value}. ${item.value.name}`
// index should be undefined if source is not an object
expect(index).toBe(undefined)
expect(index.value).toBe(undefined)
})
return span
},
@ -485,7 +475,7 @@ describe('createFor', () => {
define(() => {
const n1 = createFor(
() => (++sourceCalledTimes, list.value),
([item, index]) => {
(item, index) => {
++renderCalledTimes
const span = document.createElement('li')
renderEffect(() => {
@ -604,7 +594,7 @@ describe('createFor', () => {
define(() => {
const n1 = createFor(
() => (++sourceCalledTimes, list.value),
([item, index]) => {
(item, index) => {
++renderCalledTimes
const span = document.createElement('li')
renderEffect(() => {

View File

@ -15,27 +15,28 @@ import { currentInstance, isVaporComponent } from './component'
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
type ForBlockState = [
item: ShallowRef<any>,
key: ShallowRef<any>,
index: ShallowRef<number | undefined>,
]
class ForBlock extends Fragment {
scope: EffectScope | undefined
state: ForBlockState
key: any
itemRef: ShallowRef<any>
keyRef: ShallowRef<any> | undefined
indexRef: ShallowRef<number | undefined> | undefined
constructor(
nodes: Block,
scope: EffectScope | undefined,
state: ForBlockState,
key: any,
item: ShallowRef<any>,
key: ShallowRef<any> | undefined,
index: ShallowRef<number | undefined> | undefined,
renderKey: any,
) {
super(nodes)
this.scope = scope
this.state = state
this.key = key
this.itemRef = item
this.keyRef = key
this.indexRef = index
this.key = renderKey
}
}
@ -50,7 +51,11 @@ type ResolvedSource = {
/*! #__NO_SIDE_EFFECTS__ */
export const createFor = (
src: () => Source,
renderItem: (block: ForBlock['state']) => Block,
renderItem: (
item: ShallowRef<any>,
key: ShallowRef<any>,
index: ShallowRef<number | undefined>,
) => Block,
getKey?: (item: any, key: any, index?: number) => any,
/**
* Whether this v-for is used directly on a component. If true, we can avoid
@ -261,32 +266,38 @@ export const createFor = (
}
}
const needKey = renderItem.length > 1
const needIndex = renderItem.length > 2
const mount = (
source: ResolvedSource,
idx: number,
anchor: Node | undefined = parentAnchor,
): ForBlock => {
const [item, key, index] = getItem(source, idx)
const state = [
shallowRef(item),
shallowRef(key),
shallowRef(index),
] as ForBlock['state']
const itemRef = shallowRef(item)
// avoid creating refs if the render fn doesn't need it
const keyRef = needKey ? shallowRef(key) : undefined
const indexRef = needIndex ? shallowRef(index) : undefined
let nodes: Block
let scope: EffectScope | undefined
if (isComponent) {
// component already has its own scope so no outer scope needed
nodes = renderItem(state)
nodes = renderItem(itemRef, keyRef as any, indexRef as any)
} else {
scope = new EffectScope()
nodes = scope.run(() => renderItem(state))!
nodes = scope.run(() =>
renderItem(itemRef, keyRef as any, indexRef as any),
)!
}
const block = (newBlocks[idx] = new ForBlock(
nodes,
scope,
state,
itemRef,
keyRef,
indexRef,
getKey && getKey(item, key, index),
))
@ -305,20 +316,19 @@ export const createFor = (
}
const update = (
block: ForBlock,
{ itemRef, keyRef, indexRef }: ForBlock,
newItem: any,
newKey = block.state[1].value,
newIndex = block.state[2].value,
newKey?: any,
newIndex?: any,
) => {
const [item, key, index] = block.state
if (
newItem !== item.value ||
newKey !== key.value ||
newIndex !== index.value
) {
item.value = newItem
key.value = newKey
index.value = newIndex
if (newIndex !== itemRef.value) {
itemRef.value = newItem
}
if (keyRef && newKey !== undefined && newKey !== keyRef.value) {
keyRef.value = newKey
}
if (indexRef && newIndex !== undefined && newIndex !== indexRef.value) {
indexRef.value = newIndex
}
}