Skip to content

Commit 956756b

Browse files
committed
refactor: use more efficient on-demand clone to handle reused node edge cases
removes unnecessary slot/static node clones, fix #7292
1 parent 8335217 commit 956756b

File tree

8 files changed

+107
-55
lines changed

8 files changed

+107
-55
lines changed

src/core/instance/lifecycle.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ export function updateChildComponent (
239239
// update $attrs and $listeners hash
240240
// these are also reactive so they may trigger child update if the child
241241
// used them during render
242-
vm.$attrs = (parentVnode.data && parentVnode.data.attrs) || emptyObject
242+
vm.$attrs = parentVnode.data.attrs || emptyObject
243243
vm.$listeners = listeners || emptyObject
244244

245245
// update props
@@ -262,6 +262,7 @@ export function updateChildComponent (
262262
vm.$options._parentListeners = listeners
263263
updateComponentListeners(vm, listeners, oldListeners)
264264
}
265+
265266
// resolve slots + force update if has children
266267
if (hasChildren) {
267268
vm.$slots = resolveSlots(renderChildren, parentVnode.context)

src/core/instance/render-helpers/render-static.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
/* @flow */
22

3-
import { cloneVNode, cloneVNodes } from 'core/vdom/vnode'
4-
53
/**
64
* Runtime helper for rendering static trees.
75
*/
@@ -12,11 +10,9 @@ export function renderStatic (
1210
const cached = this._staticTrees || (this._staticTrees = [])
1311
let tree = cached[index]
1412
// if has already-rendered static tree and not inside v-for,
15-
// we can reuse the same tree by doing a shallow clone.
13+
// we can reuse the same tree.
1614
if (tree && !isInFor) {
17-
return Array.isArray(tree)
18-
? cloneVNodes(tree)
19-
: cloneVNode(tree)
15+
return tree
2016
}
2117
// otherwise, render a fresh tree.
2218
tree = cached[index] = this.$options.staticRenderFns[index].call(

src/core/instance/render.js

+8-11
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import { createElement } from '../vdom/create-element'
1212
import { installRenderHelpers } from './render-helpers/index'
1313
import { resolveSlots } from './render-helpers/resolve-slots'
14-
import VNode, { cloneVNodes, createEmptyVNode } from '../vdom/vnode'
14+
import VNode, { createEmptyVNode } from '../vdom/vnode'
1515

1616
import { isUpdatingChildComponent } from './lifecycle'
1717

@@ -62,20 +62,17 @@ export function renderMixin (Vue: Class<Component>) {
6262
const vm: Component = this
6363
const { render, _parentVnode } = vm.$options
6464

65-
if (vm._isMounted) {
66-
// if the parent didn't update, the slot nodes will be the ones from
67-
// last render. They need to be cloned to ensure "freshness" for this render.
65+
// reset _rendered flag on slots for duplicate slot check
66+
if (process.env.NODE_ENV !== 'production') {
6867
for (const key in vm.$slots) {
69-
const slot = vm.$slots[key]
70-
// _rendered is a flag added by renderSlot, but may not be present
71-
// if the slot is passed from manually written render functions
72-
if (slot._rendered || (slot[0] && slot[0].elm)) {
73-
vm.$slots[key] = cloneVNodes(slot, true /* deep */)
74-
}
68+
// $flow-disable-line
69+
vm.$slots[key]._rendered = false
7570
}
7671
}
7772

78-
vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject
73+
if (_parentVnode) {
74+
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
75+
}
7976

8077
// set parent vnode. this allows render functions to have access
8178
// to the data on the placeholder node.

src/core/vdom/create-component.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,22 @@ const componentVNodeHooks = {
4040
parentElm: ?Node,
4141
refElm: ?Node
4242
): ?boolean {
43-
if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
43+
if (
44+
vnode.componentInstance &&
45+
!vnode.componentInstance._isDestroyed &&
46+
vnode.data.keepAlive
47+
) {
48+
// kept-alive components, treat as a patch
49+
const mountedNode: any = vnode // work around flow
50+
componentVNodeHooks.prepatch(mountedNode, mountedNode)
51+
} else {
4452
const child = vnode.componentInstance = createComponentInstanceForVnode(
4553
vnode,
4654
activeInstance,
4755
parentElm,
4856
refElm
4957
)
5058
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
51-
} else if (vnode.data.keepAlive) {
52-
// kept-alive components, treat as a patch
53-
const mountedNode: any = vnode // work around flow
54-
componentVNodeHooks.prepatch(mountedNode, mountedNode)
5559
}
5660
},
5761

src/core/vdom/create-element.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import config from '../config'
44
import VNode, { createEmptyVNode } from './vnode'
55
import { createComponent } from './create-component'
6+
import { traverse } from '../observer/traverse'
67

78
import {
89
warn,
910
isDef,
1011
isUndef,
1112
isTrue,
13+
isObject,
1214
isPrimitive,
1315
resolveAsset
1416
} from '../util/index'
@@ -116,10 +118,11 @@ export function _createElement (
116118
// direct component options / constructor
117119
vnode = createComponent(tag, data, context, children)
118120
}
119-
if (isDef(vnode)) {
120-
if (ns && !Array.isArray(vnode)) {
121-
applyNS(vnode, ns)
122-
}
121+
if (Array.isArray(vnode)) {
122+
return vnode
123+
} else if (isDef(vnode)) {
124+
if (isDef(ns)) applyNS(vnode, ns)
125+
if (isDef(data)) registerDeepBindings(data)
123126
return vnode
124127
} else {
125128
return createEmptyVNode()
@@ -142,3 +145,15 @@ function applyNS (vnode, ns, force) {
142145
}
143146
}
144147
}
148+
149+
// ref #5318
150+
// necessary to ensure parent re-render when deep bindings like :style and
151+
// :class are used on slot nodes
152+
function registerDeepBindings (data) {
153+
if (isObject(data.style)) {
154+
traverse(data.style)
155+
}
156+
if (isObject(data.class)) {
157+
traverse(data.class)
158+
}
159+
}

src/core/vdom/patch.js

+25-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* of making flow understand it is not worth it.
1111
*/
1212

13-
import VNode from './vnode'
13+
import VNode, { cloneVNode } from './vnode'
1414
import config from '../config'
1515
import { SSR_ATTR } from 'shared/constants'
1616
import { registerRef } from './modules/ref'
@@ -121,7 +121,25 @@ export function createPatchFunction (backend) {
121121
}
122122

123123
let creatingElmInVPre = 0
124-
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
124+
125+
function createElm (
126+
vnode,
127+
insertedVnodeQueue,
128+
parentElm,
129+
refElm,
130+
nested,
131+
ownerArray,
132+
index
133+
) {
134+
if (isDef(vnode.elm) && isDef(ownerArray)) {
135+
// This vnode was used in a previous render!
136+
// now it's used as a new node, overwriting its elm would cause
137+
// potential patch errors down the road when it's used as an insertion
138+
// reference node. Instead, we clone the node on-demand before creating
139+
// associated DOM element for it.
140+
vnode = ownerArray[index] = cloneVNode(vnode)
141+
}
142+
125143
vnode.isRootInsert = !nested // for transition enter check
126144
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
127145
return
@@ -144,6 +162,7 @@ export function createPatchFunction (backend) {
144162
)
145163
}
146164
}
165+
147166
vnode.elm = vnode.ns
148167
? nodeOps.createElementNS(vnode.ns, tag)
149168
: nodeOps.createElement(tag, vnode)
@@ -267,7 +286,7 @@ export function createPatchFunction (backend) {
267286
checkDuplicateKeys(children)
268287
}
269288
for (let i = 0; i < children.length; ++i) {
270-
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true)
289+
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
271290
}
272291
} else if (isPrimitive(vnode.text)) {
273292
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
@@ -320,7 +339,7 @@ export function createPatchFunction (backend) {
320339

321340
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
322341
for (; startIdx <= endIdx; ++startIdx) {
323-
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm)
342+
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
324343
}
325344
}
326345

@@ -430,7 +449,7 @@ export function createPatchFunction (backend) {
430449
? oldKeyToIdx[newStartVnode.key]
431450
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
432451
if (isUndef(idxInOld)) { // New element
433-
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
452+
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
434453
} else {
435454
vnodeToMove = oldCh[idxInOld]
436455
if (sameVnode(vnodeToMove, newStartVnode)) {
@@ -439,7 +458,7 @@ export function createPatchFunction (backend) {
439458
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
440459
} else {
441460
// same key but different element. treat as new element
442-
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
461+
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
443462
}
444463
}
445464
newStartVnode = newCh[++newStartIdx]

src/core/vdom/vnode.js

+2-20
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,15 @@ export function createTextVNode (val: string | number) {
8585
// used for static nodes and slot nodes because they may be reused across
8686
// multiple renders, cloning them avoids errors when DOM manipulations rely
8787
// on their elm reference.
88-
export function cloneVNode (vnode: VNode, deep?: boolean): VNode {
89-
const componentOptions = vnode.componentOptions
88+
export function cloneVNode (vnode: VNode): VNode {
9089
const cloned = new VNode(
9190
vnode.tag,
9291
vnode.data,
9392
vnode.children,
9493
vnode.text,
9594
vnode.elm,
9695
vnode.context,
97-
componentOptions,
96+
vnode.componentOptions,
9897
vnode.asyncFactory
9998
)
10099
cloned.ns = vnode.ns
@@ -105,22 +104,5 @@ export function cloneVNode (vnode: VNode, deep?: boolean): VNode {
105104
cloned.fnOptions = vnode.fnOptions
106105
cloned.fnScopeId = vnode.fnScopeId
107106
cloned.isCloned = true
108-
if (deep) {
109-
if (vnode.children) {
110-
cloned.children = cloneVNodes(vnode.children, true)
111-
}
112-
if (componentOptions && componentOptions.children) {
113-
componentOptions.children = cloneVNodes(componentOptions.children, true)
114-
}
115-
}
116107
return cloned
117108
}
118-
119-
export function cloneVNodes (vnodes: Array<VNode>, deep?: boolean): Array<VNode> {
120-
const len = vnodes.length
121-
const res = new Array(len)
122-
for (let i = 0; i < len; i++) {
123-
res[i] = cloneVNode(vnodes[i], deep)
124-
}
125-
return res
126-
}

test/unit/modules/vdom/patch/edge-cases.spec.js

+40-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('vdom patch: edge cases', () => {
2626
})
2727

2828
// #3533
29-
// a static node (<br>) is reused in createElm, which changes its elm reference
29+
// a static node is reused in createElm, which changes its elm reference
3030
// and is inserted into a different parent.
3131
// later when patching the next element a DOM insertion uses it as the
3232
// reference node, causing a parent mismatch.
@@ -40,11 +40,12 @@ describe('vdom patch: edge cases', () => {
4040
<button @click="ok = !ok">toggle</button>
4141
<div class="b" v-if="ok">123</div>
4242
<div class="c">
43-
<br><p>{{ 1 }}</p>
43+
<div><span/></div><p>{{ 1 }}</p>
4444
</div>
4545
<div class="d">
4646
<label>{{ 2 }}</label>
4747
</div>
48+
<div class="b" v-if="ok">123</div>
4849
</div>
4950
`
5051
}).$mount()
@@ -58,6 +59,43 @@ describe('vdom patch: edge cases', () => {
5859
}).then(done)
5960
})
6061

62+
it('should handle slot nodes being reused across render', done => {
63+
const vm = new Vue({
64+
template: `
65+
<foo ref="foo">
66+
<div>slot</div>
67+
</foo>
68+
`,
69+
components: {
70+
foo: {
71+
data () {
72+
return { ok: true }
73+
},
74+
render (h) {
75+
const children = [
76+
this.ok ? h('div', 'toggler ') : null,
77+
h('div', [this.$slots.default, h('span', ' 1')]),
78+
h('div', [h('label', ' 2')])
79+
]
80+
return h('div', children)
81+
}
82+
}
83+
}
84+
}).$mount()
85+
expect(vm.$el.textContent).toContain('toggler slot 1 2')
86+
vm.$refs.foo.ok = false
87+
waitForUpdate(() => {
88+
expect(vm.$el.textContent).toContain('slot 1 2')
89+
vm.$refs.foo.ok = true
90+
}).then(() => {
91+
expect(vm.$el.textContent).toContain('toggler slot 1 2')
92+
vm.$refs.foo.ok = false
93+
}).then(() => {
94+
expect(vm.$el.textContent).toContain('slot 1 2')
95+
vm.$refs.foo.ok = true
96+
}).then(done)
97+
})
98+
6199
it('should synchronize vm\' vnode', done => {
62100
const comp = {
63101
data: () => ({ swap: true }),

0 commit comments

Comments
 (0)