Skip to content

Commit 52e45a9

Browse files
committed
fix(v-once): fix v-once usage with v-if and v-for
fix #2035
1 parent ad93fa4 commit 52e45a9

File tree

4 files changed

+77
-13
lines changed

4 files changed

+77
-13
lines changed

packages/compiler-core/__tests__/transforms/vOnce.spec.ts

+31-9
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,17 @@ import {
33
transform,
44
NodeTypes,
55
generate,
6-
CompilerOptions
6+
CompilerOptions,
7+
getBaseTransformPreset
78
} from '../../src'
8-
import { transformOnce } from '../../src/transforms/vOnce'
9-
import { transformElement } from '../../src/transforms/transformElement'
109
import { RENDER_SLOT, SET_BLOCK_TRACKING } from '../../src/runtimeHelpers'
11-
import { transformBind } from '../../src/transforms/vBind'
12-
import { transformSlotOutlet } from '../../src/transforms/transformSlotOutlet'
1310

1411
function transformWithOnce(template: string, options: CompilerOptions = {}) {
1512
const ast = parse(template)
13+
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
1614
transform(ast, {
17-
nodeTransforms: [transformOnce, transformElement, transformSlotOutlet],
18-
directiveTransforms: {
19-
bind: transformBind
20-
},
15+
nodeTransforms,
16+
directiveTransforms,
2117
...options
2218
})
2319
return ast
@@ -102,4 +98,30 @@ describe('compiler: v-once transform', () => {
10298
})
10399
expect(generate(root).code).toMatchSnapshot()
104100
})
101+
102+
test('with v-if', () => {
103+
const root = transformWithOnce(`<div v-if="true" v-once />`)
104+
expect(root.cached).toBe(1)
105+
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
106+
expect(root.children[0]).toMatchObject({
107+
type: NodeTypes.IF,
108+
// should cache the entire v-if expression, not just a single branch
109+
codegenNode: {
110+
type: NodeTypes.JS_CACHE_EXPRESSION
111+
}
112+
})
113+
})
114+
115+
test('with v-for', () => {
116+
const root = transformWithOnce(`<div v-for="i in list" v-once />`)
117+
expect(root.cached).toBe(1)
118+
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
119+
expect(root.children[0]).toMatchObject({
120+
type: NodeTypes.FOR,
121+
// should cache the entire v-for expression, not just a single branch
122+
codegenNode: {
123+
type: NodeTypes.JS_CACHE_EXPRESSION
124+
}
125+
})
126+
})
105127
})

packages/compiler-core/src/transform.ts

+1
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ export function traverseNode(
399399
}
400400

401401
// exit transforms
402+
context.currentNode = node
402403
let i = exitFns.length
403404
while (i--) {
404405
exitFns[i]()

packages/compiler-core/src/transforms/vOnce.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import { NodeTransform } from '../transform'
22
import { findDir } from '../utils'
3-
import { NodeTypes } from '../ast'
3+
import { ElementNode, ForNode, IfNode, NodeTypes } from '../ast'
44
import { SET_BLOCK_TRACKING } from '../runtimeHelpers'
55

6+
const seen = new WeakSet()
7+
68
export const transformOnce: NodeTransform = (node, context) => {
79
if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
10+
if (seen.has(node)) {
11+
return
12+
}
13+
seen.add(node)
814
context.helper(SET_BLOCK_TRACKING)
915
return () => {
10-
if (node.codegenNode) {
11-
node.codegenNode = context.cache(node.codegenNode, true /* isVNode */)
16+
const cur = context.currentNode as ElementNode | IfNode | ForNode
17+
if (cur.codegenNode) {
18+
cur.codegenNode = context.cache(cur.codegenNode, true /* isVNode */)
1219
}
1320
}
1421
}

packages/vue/__tests__/index.spec.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createApp, ref, nextTick } from '../src'
1+
import { createApp, ref, nextTick, reactive } from '../src'
22

33
describe('compiler + runtime integration', () => {
44
it('should support runtime template compilation', () => {
@@ -247,4 +247,38 @@ describe('compiler + runtime integration', () => {
247247

248248
document.querySelector = origin
249249
})
250+
251+
test('v-if + v-once', async () => {
252+
const ok = ref(true)
253+
const App = {
254+
setup() {
255+
return { ok }
256+
},
257+
template: `<div>{{ ok }}<div v-if="ok" v-once>{{ ok }}</div></div>`
258+
}
259+
const container = document.createElement('div')
260+
createApp(App).mount(container)
261+
262+
expect(container.innerHTML).toBe(`<div>true<div>true</div></div>`)
263+
ok.value = false
264+
await nextTick()
265+
expect(container.innerHTML).toBe(`<div>false<div>true</div></div>`)
266+
})
267+
268+
test('v-for + v-once', async () => {
269+
const list = reactive([1])
270+
const App = {
271+
setup() {
272+
return { list }
273+
},
274+
template: `<div>{{ list.length }}<div v-for="i in list" v-once>{{ i }}</div></div>`
275+
}
276+
const container = document.createElement('div')
277+
createApp(App).mount(container)
278+
279+
expect(container.innerHTML).toBe(`<div>1<div>1</div></div>`)
280+
list.push(2)
281+
await nextTick()
282+
expect(container.innerHTML).toBe(`<div>2<div>1</div></div>`)
283+
})
250284
})

0 commit comments

Comments
 (0)