Skip to content

Commit 014d2f8

Browse files
committed
fix vdom patch edge case for static nodes being reused and as insertion reference node (fix #3533)
1 parent ed20859 commit 014d2f8

File tree

7 files changed

+105
-33
lines changed

7 files changed

+105
-33
lines changed

src/core/instance/render.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* @flow */
22

33
import config from '../config'
4-
import VNode, { emptyVNode } from '../vdom/vnode'
4+
import VNode, { emptyVNode, cloneVNode, cloneVNodes } from '../vdom/vnode'
55
import { normalizeChildren } from '../vdom/helpers'
66
import {
77
warn, formatComponentName, bind, isObject, toObject,
@@ -36,6 +36,13 @@ export function renderMixin (Vue: Class<Component>) {
3636
_parentVnode
3737
} = vm.$options
3838

39+
if (vm._isMounted) {
40+
// clone slot nodes on re-renders
41+
for (const key in vm.$slots) {
42+
vm.$slots[key] = cloneVNodes(vm.$slots[key])
43+
}
44+
}
45+
3946
if (staticRenderFns && !vm._staticTrees) {
4047
vm._staticTrees = []
4148
}
@@ -90,12 +97,14 @@ export function renderMixin (Vue: Class<Component>) {
9097
Vue.prototype._m = function renderStatic (
9198
index: number,
9299
isInFor?: boolean
93-
): VNode | VNodeChildren {
100+
): VNode | Array<VNode> {
94101
let tree = this._staticTrees[index]
95102
// if has already-rendered static tree and not inside v-for,
96103
// we can reuse the same tree by indentity.
97104
if (tree && !isInFor) {
98-
return tree
105+
return Array.isArray(tree)
106+
? cloneVNodes(tree)
107+
: cloneVNode(tree)
99108
}
100109
// otherwise, render a fresh tree.
101110
tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy)

src/core/vdom/vnode.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default class VNode {
1212
componentOptions: VNodeComponentOptions | void;
1313
child: Component | void; // component instance
1414
parent: VNode | void; // compoennt placeholder node
15-
raw: ?boolean; // contains raw HTML
15+
raw: ?boolean; // contains raw HTML? (server only)
1616
isStatic: ?boolean; // hoisted static node
1717
isRootInsert: boolean; // necessary for enter transition check
1818
isComment: boolean;
@@ -58,3 +58,31 @@ export const emptyVNode = () => {
5858
node.isComment = true
5959
return node
6060
}
61+
62+
// optimized shallow clone
63+
// used for static nodes and slot nodes because they may be reused across
64+
// multiple renders, cloning them avoids errors when DOM manipulations rely
65+
// on their elm reference.
66+
export function cloneVNode (vnode: VNode): VNode {
67+
const cloned = new VNode(
68+
vnode.tag,
69+
vnode.data,
70+
vnode.children,
71+
vnode.text,
72+
vnode.elm,
73+
vnode.ns,
74+
vnode.context,
75+
vnode.componentOptions
76+
)
77+
cloned.isStatic = vnode.isStatic
78+
cloned.key = vnode.key
79+
return cloned
80+
}
81+
82+
export function cloneVNodes (vnodes: Array<VNode>): Array<VNode> {
83+
const res = new Array(vnodes.length)
84+
for (let i = 0; i < vnodes.length; i++) {
85+
res[i] = cloneVNode(vnodes[i])
86+
}
87+
return res
88+
}

test/unit/modules/vdom/patch/children.spec.js

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Vue from 'vue'
21
import { patch } from 'web/runtime/patch'
32
import VNode from 'core/vdom/vnode'
43

@@ -43,7 +42,7 @@ function shuffle (array) {
4342
const inner = prop('innerHTML')
4443
const tag = prop('tagName')
4544

46-
describe('children', () => {
45+
describe('vdom patch: children', () => {
4746
let vnode0
4847
beforeEach(() => {
4948
vnode0 = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
@@ -509,28 +508,4 @@ describe('children', () => {
509508
elm = patch(vnode2, vnode3)
510509
expect(elm.textContent).toBe('ABC')
511510
})
512-
513-
// exposed by #3406
514-
// When a static vnode is inside v-for, it's possible for the same vnode
515-
// to be used in multiple places, and its element will be replaced. This
516-
// causes patch errors when node ops depend on the vnode's element position.
517-
it('should handle static vnodes by key', done => {
518-
const vm = new Vue({
519-
data: {
520-
ok: true
521-
},
522-
template: `
523-
<div>
524-
<div v-for="i in 2">
525-
<div v-if="ok">a</div><div>b</div><div v-if="!ok">c</div><div>d</div>
526-
</div>
527-
</div>
528-
`
529-
}).$mount()
530-
expect(vm.$el.textContent).toBe('abdabd')
531-
vm.ok = false
532-
waitForUpdate(() => {
533-
expect(vm.$el.textContent).toBe('bcdbcd')
534-
}).then(done)
535-
})
536511
})
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Vue from 'vue'
2+
3+
describe('vdom patch: edge cases', () => {
4+
// exposed by #3406
5+
// When a static vnode is inside v-for, it's possible for the same vnode
6+
// to be used in multiple places, and its element will be replaced. This
7+
// causes patch errors when node ops depend on the vnode's element position.
8+
it('should handle static vnodes by key', done => {
9+
const vm = new Vue({
10+
data: {
11+
ok: true
12+
},
13+
template: `
14+
<div>
15+
<div v-for="i in 2">
16+
<div v-if="ok">a</div><div>b</div><div v-if="!ok">c</div><div>d</div>
17+
</div>
18+
</div>
19+
`
20+
}).$mount()
21+
expect(vm.$el.textContent).toBe('abdabd')
22+
vm.ok = false
23+
waitForUpdate(() => {
24+
expect(vm.$el.textContent).toBe('bcdbcd')
25+
}).then(done)
26+
})
27+
28+
// #3533
29+
// a static node (<br>) is reused in createElm, which changes its elm reference
30+
// and is inserted into a different parent.
31+
// later when patching the next element a DOM insertion uses it as the
32+
// reference node, causing a parent mismatch.
33+
it('should handle static node edge case when it\'s reused AND used as a reference node for insertion', done => {
34+
const vm = new Vue({
35+
data: {
36+
ok: true
37+
},
38+
template: `
39+
<div>
40+
<button @click="ok = !ok">toggle</button>
41+
<div class="b" v-if="ok">123</div>
42+
<div class="c">
43+
<br><p>{{ 1 }}</p>
44+
</div>
45+
<div class="d">
46+
<label>{{ 2 }}</label>
47+
</div>
48+
</div>
49+
`
50+
}).$mount()
51+
52+
expect(vm.$el.querySelector('.c').textContent).toBe('1')
53+
expect(vm.$el.querySelector('.d').textContent).toBe('2')
54+
vm.ok = false
55+
waitForUpdate(() => {
56+
expect(vm.$el.querySelector('.c').textContent).toBe('1')
57+
expect(vm.$el.querySelector('.d').textContent).toBe('2')
58+
}).then(done)
59+
})
60+
})

test/unit/modules/vdom/patch/element.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Vue from 'vue'
22
import { patch } from 'web/runtime/patch'
33
import VNode from 'core/vdom/vnode'
44

5-
describe('element', () => {
5+
describe('vdom patch: element', () => {
66
it('should create an element', () => {
77
const vnode = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
88
const elm = patch(null, vnode)

test/unit/modules/vdom/patch/hooks.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import VNode from 'core/vdom/vnode'
77

88
const modules = baseModules.concat(platformModules)
99

10-
describe('hooks', () => {
10+
describe('vdom patch: hooks', () => {
1111
let vnode0
1212
beforeEach(() => {
1313
vnode0 = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])

test/unit/modules/vdom/patch/hydration.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Vue from 'vue'
22
import { patch } from 'web/runtime/patch'
33
import VNode from 'core/vdom/vnode'
44

5-
describe('hydration', () => {
5+
describe('vdom patch: hydration', () => {
66
let vnode0
77
beforeEach(() => {
88
spyOn(console, 'warn')

0 commit comments

Comments
 (0)