Skip to content

Commit 16a6b49

Browse files
committed
feat(compiler): support v-once directive
1 parent 06f64ea commit 16a6b49

File tree

7 files changed

+323
-3
lines changed

7 files changed

+323
-3
lines changed

packages/compiler/src/compile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
type RootNode,
2929
} from './ir'
3030
import { transformVFor } from './transforms/vFor'
31+
import { transformVOnce } from './transforms/vOnce'
3132
import type { CompilerOptions as BaseCompilerOptions } from '@vue/compiler-dom'
3233
import type { ExpressionStatement, JSXElement, JSXFragment } from '@babel/types'
3334

@@ -110,6 +111,7 @@ export type TransformPreset = [
110111
export function getBaseTransformPreset(): TransformPreset {
111112
return [
112113
[
114+
transformVOnce,
113115
transformVFor,
114116
transformTemplateRef,
115117
transformText,

packages/compiler/src/transforms/transformText.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,15 @@ function processTextLike(context: TransformContext<JSXExpressionContainer>) {
6767
const idx = nexts.findIndex((n) => !isTextLike(n))
6868
const nodes = (idx > -1 ? nexts.slice(0, idx) : nexts) as Array<TextLike>
6969

70+
const id = context.reference()
71+
const values = createTextLikeExpressions(nodes, context)
72+
7073
context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
74+
7175
context.registerOperation({
7276
type: IRNodeTypes.CREATE_TEXT_NODE,
73-
id: context.reference(),
74-
values: createTextLikeExpressions(nodes, context),
77+
id,
78+
values,
7579
jsx: true,
7680
})
7781
}
@@ -102,7 +106,7 @@ function createTextLikeExpressions(
102106
for (const node of nodes) {
103107
seen.get(context.root)!.add(node)
104108
if (isEmptyText(node)) continue
105-
values.push(resolveExpression(node, context, true))
109+
values.push(resolveExpression(node, context, !context.inVOnce))
106110
}
107111
return values
108112
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NodeTypes, findDir } from '@vue/compiler-dom'
2+
import { findProp } from '../utils'
3+
import type { NodeTransform } from '../transform'
4+
5+
export const transformVOnce: NodeTransform = (node, context) => {
6+
if (
7+
// !context.inSSR &&
8+
node.type === 'JSXElement' &&
9+
findProp(node, 'v-once')
10+
) {
11+
context.inVOnce = true
12+
}
13+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`compiler: v-once > as root node 1`] = `
4+
"
5+
const n0 = t0()
6+
_setProp(n0, "id", foo)
7+
return n0
8+
"
9+
`;
10+
11+
exports[`compiler: v-once > basic 1`] = `
12+
"
13+
const n2 = t0()
14+
const n1 = _child(n2)
15+
const n0 = _createTextNode(msg.value)
16+
_setClass(n1, clz.value)
17+
_prepend(n2, n0)
18+
return n2
19+
"
20+
`;
21+
22+
exports[`compiler: v-once > inside v-once 1`] = `
23+
"
24+
const n0 = t0()
25+
return n0
26+
"
27+
`;
28+
29+
exports[`compiler: v-once > on component 1`] = `
30+
"
31+
const n1 = t0()
32+
const n0 = _createComponent(Comp, { id: () => (foo) }, null, null, true)
33+
_insert(n0, n1)
34+
return n1
35+
"
36+
`;
37+
38+
exports[`compiler: v-once > on nested plain element 1`] = `
39+
"
40+
const n1 = t0()
41+
const n0 = _child(n1)
42+
_setProp(n0, "id", foo)
43+
return n1
44+
"
45+
`;
46+
47+
exports[`compiler: v-once > with v-for 1`] = `
48+
"
49+
const n0 = _createFor(() => (list), (_for_item0) => {
50+
const n2 = t0()
51+
return n2
52+
}, null, 4)
53+
return n0
54+
"
55+
`;
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { BindingTypes, NodeTypes } from '@vue/compiler-dom'
3+
import { IRNodeTypes } from '../../src'
4+
import { getBaseTransformPreset } from '../../src/compile'
5+
import { makeCompile } from './_utils'
6+
7+
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
8+
const compileWithOnce = makeCompile({
9+
nodeTransforms,
10+
directiveTransforms,
11+
})
12+
13+
describe('compiler: v-once', () => {
14+
test('basic', () => {
15+
const { ir, code } = compileWithOnce(
16+
`<div v-once>
17+
{ msg }
18+
<span class={clz} />
19+
</div>`,
20+
{
21+
bindingMetadata: {
22+
msg: BindingTypes.SETUP_REF,
23+
clz: BindingTypes.SETUP_REF,
24+
},
25+
},
26+
)
27+
28+
expect(code).toMatchSnapshot()
29+
expect(ir.block.effect).lengthOf(0)
30+
expect(ir.block.operation).toMatchObject([
31+
{
32+
type: IRNodeTypes.CREATE_TEXT_NODE,
33+
id: 0,
34+
values: [
35+
{
36+
type: NodeTypes.SIMPLE_EXPRESSION,
37+
content: 'msg',
38+
isStatic: false,
39+
},
40+
],
41+
},
42+
{
43+
element: 1,
44+
type: IRNodeTypes.SET_PROP,
45+
prop: {
46+
key: {
47+
type: NodeTypes.SIMPLE_EXPRESSION,
48+
content: 'class',
49+
isStatic: true,
50+
},
51+
values: [
52+
{
53+
type: NodeTypes.SIMPLE_EXPRESSION,
54+
content: 'clz',
55+
isStatic: false,
56+
},
57+
],
58+
},
59+
},
60+
{
61+
type: IRNodeTypes.PREPEND_NODE,
62+
elements: [0],
63+
parent: 2,
64+
},
65+
])
66+
})
67+
68+
test('as root node', () => {
69+
const { ir, code } = compileWithOnce(`<div id={foo} v-once />`)
70+
71+
expect(code).toMatchSnapshot()
72+
73+
expect(ir.block.effect).lengthOf(0)
74+
expect(ir.block.operation).toMatchObject([
75+
{
76+
type: IRNodeTypes.SET_PROP,
77+
element: 0,
78+
prop: {
79+
key: {
80+
type: NodeTypes.SIMPLE_EXPRESSION,
81+
content: 'id',
82+
isStatic: true,
83+
},
84+
values: [
85+
{
86+
type: NodeTypes.SIMPLE_EXPRESSION,
87+
content: 'foo',
88+
isStatic: false,
89+
},
90+
],
91+
},
92+
},
93+
])
94+
expect(code).not.contains('effect')
95+
})
96+
97+
test('on nested plain element', () => {
98+
const { ir, code } = compileWithOnce(`<div><div id={foo} v-once /></div>`)
99+
100+
expect(code).toMatchSnapshot()
101+
102+
expect(ir.block.effect).lengthOf(0)
103+
expect(ir.block.operation).toMatchObject([
104+
{
105+
type: IRNodeTypes.SET_PROP,
106+
element: 0,
107+
prop: {
108+
runtimeCamelize: false,
109+
key: {
110+
type: NodeTypes.SIMPLE_EXPRESSION,
111+
content: 'id',
112+
isStatic: true,
113+
},
114+
values: [
115+
{
116+
type: NodeTypes.SIMPLE_EXPRESSION,
117+
content: 'foo',
118+
isStatic: false,
119+
},
120+
],
121+
},
122+
},
123+
])
124+
})
125+
126+
test('on component', () => {
127+
const { ir, code } = compileWithOnce(`<div><Comp id={foo} v-once /></div>`)
128+
expect(code).toMatchSnapshot()
129+
expect(ir.block.effect).lengthOf(0)
130+
expect(ir.block.operation).toMatchObject([
131+
{
132+
type: IRNodeTypes.CREATE_COMPONENT_NODE,
133+
id: 0,
134+
tag: 'Comp',
135+
once: true,
136+
},
137+
{
138+
type: IRNodeTypes.INSERT_NODE,
139+
elements: [0],
140+
parent: 1,
141+
},
142+
])
143+
})
144+
145+
test.todo('on slot outlet')
146+
147+
test('inside v-once', () => {
148+
const { ir, code } = compileWithOnce(`<div v-once><div v-once/></div>`)
149+
150+
expect(code).toMatchSnapshot()
151+
152+
expect(ir.block.effect).lengthOf(0)
153+
expect(ir.block.operation).lengthOf(0)
154+
})
155+
156+
test.todo('with hoistStatic: true')
157+
158+
// test('with v-if', () => {
159+
// const { ir, code } = compileWithOnce(`<div v-if="expr" v-once />`)
160+
// expect(code).toMatchSnapshot()
161+
162+
// expect(ir.block.effect).lengthOf(0)
163+
// expect(ir.block.operation).toMatchObject([
164+
// {
165+
// type: IRNodeTypes.IF,
166+
// id: 0,
167+
// once: true,
168+
// condition: {
169+
// type: NodeTypes.SIMPLE_EXPRESSION,
170+
// content: 'expr',
171+
// isStatic: false,
172+
// },
173+
// positive: {
174+
// type: IRNodeTypes.BLOCK,
175+
// dynamic: {
176+
// children: [{ template: 0 }],
177+
// },
178+
// },
179+
// },
180+
// ])
181+
// })
182+
183+
// test('with v-if/else', () => {
184+
// const { ir, code } = compileWithOnce(
185+
// `<div v-if="expr" v-once /><p v-else/>`,
186+
// )
187+
// expect(code).toMatchSnapshot()
188+
189+
// expect(ir.block.effect).lengthOf(0)
190+
// expect(ir.block.operation).toMatchObject([
191+
// {
192+
// type: IRNodeTypes.IF,
193+
// id: 0,
194+
// once: true,
195+
// condition: {
196+
// type: NodeTypes.SIMPLE_EXPRESSION,
197+
// content: 'expr',
198+
// isStatic: false,
199+
// },
200+
// positive: {
201+
// type: IRNodeTypes.BLOCK,
202+
// dynamic: {
203+
// children: [{ template: 0 }],
204+
// },
205+
// },
206+
// negative: {
207+
// type: IRNodeTypes.BLOCK,
208+
// dynamic: {
209+
// children: [{ template: 1 }],
210+
// },
211+
// },
212+
// },
213+
// ])
214+
// })
215+
216+
test('with v-for', () => {
217+
const { ir, code } = compileWithOnce(`<div v-for={i in list} v-once />`)
218+
expect(code).toMatchSnapshot()
219+
expect(ir.block.effect).lengthOf(0)
220+
expect(ir.block.operation).toMatchObject([
221+
{
222+
type: IRNodeTypes.FOR,
223+
id: 0,
224+
once: true,
225+
},
226+
])
227+
})
228+
})

playground/src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Slot from './slot'
66
import Model from './model'
77
import Show from './show'
88
import Html from './html'
9+
import Once from './once'
910

1011
export default () => {
1112
const count = ref('1')
@@ -61,6 +62,11 @@ export default () => {
6162
<legend>v-html</legend>
6263
<Html />
6364
</fieldset>
65+
66+
<fieldset>
67+
<legend>v-once</legend>
68+
<Once />
69+
</fieldset>
6470
</>
6571
)
6672
}

playground/src/once.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ref } from 'vue'
2+
3+
export default () => {
4+
const count = ref(3)
5+
6+
return (
7+
<>
8+
<input v-model_number={count.value} />
9+
<div v-once>{count.value}</div>
10+
</>
11+
)
12+
}

0 commit comments

Comments
 (0)