Skip to content

Commit

Permalink
refactor: use more efficient on-demand clone to handle reused node ed…
Browse files Browse the repository at this point in the history
…ge cases

removes unnecessary slot/static node clones, fix vuejs#7292
  • Loading branch information
yyx990803 committed Dec 21, 2017
1 parent 8335217 commit 956756b
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 55 deletions.
3 changes: 2 additions & 1 deletion src/core/instance/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export function updateChildComponent (
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = (parentVnode.data && parentVnode.data.attrs) || emptyObject
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject

// update props
Expand All @@ -262,6 +262,7 @@ export function updateChildComponent (
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
}

// resolve slots + force update if has children
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
Expand Down
8 changes: 2 additions & 6 deletions src/core/instance/render-helpers/render-static.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
/* @flow */

import { cloneVNode, cloneVNodes } from 'core/vdom/vnode'

/**
* Runtime helper for rendering static trees.
*/
Expand All @@ -12,11 +10,9 @@ export function renderStatic (
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree by doing a shallow clone.
// we can reuse the same tree.
if (tree && !isInFor) {
return Array.isArray(tree)
? cloneVNodes(tree)
: cloneVNode(tree)
return tree
}
// otherwise, render a fresh tree.
tree = cached[index] = this.$options.staticRenderFns[index].call(
Expand Down
19 changes: 8 additions & 11 deletions src/core/instance/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { createElement } from '../vdom/create-element'
import { installRenderHelpers } from './render-helpers/index'
import { resolveSlots } from './render-helpers/resolve-slots'
import VNode, { cloneVNodes, createEmptyVNode } from '../vdom/vnode'
import VNode, { createEmptyVNode } from '../vdom/vnode'

import { isUpdatingChildComponent } from './lifecycle'

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

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

vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}

// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
Expand Down
14 changes: 9 additions & 5 deletions src/core/vdom/create-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,22 @@ const componentVNodeHooks = {
parentElm: ?Node,
refElm: ?Node
): ?boolean {
if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance,
parentElm,
refElm
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
} else if (vnode.data.keepAlive) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
}
},

Expand Down
23 changes: 19 additions & 4 deletions src/core/vdom/create-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import config from '../config'
import VNode, { createEmptyVNode } from './vnode'
import { createComponent } from './create-component'
import { traverse } from '../observer/traverse'

import {
warn,
isDef,
isUndef,
isTrue,
isObject,
isPrimitive,
resolveAsset
} from '../util/index'
Expand Down Expand Up @@ -116,10 +118,11 @@ export function _createElement (
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (isDef(vnode)) {
if (ns && !Array.isArray(vnode)) {
applyNS(vnode, ns)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
Expand All @@ -142,3 +145,15 @@ function applyNS (vnode, ns, force) {
}
}
}

// ref #5318
// necessary to ensure parent re-render when deep bindings like :style and
// :class are used on slot nodes
function registerDeepBindings (data) {
if (isObject(data.style)) {
traverse(data.style)
}
if (isObject(data.class)) {
traverse(data.class)
}
}
31 changes: 25 additions & 6 deletions src/core/vdom/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* of making flow understand it is not worth it.
*/

import VNode from './vnode'
import VNode, { cloneVNode } from './vnode'
import config from '../config'
import { SSR_ATTR } from 'shared/constants'
import { registerRef } from './modules/ref'
Expand Down Expand Up @@ -121,7 +121,25 @@ export function createPatchFunction (backend) {
}

let creatingElmInVPre = 0
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {

function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}

vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
Expand All @@ -144,6 +162,7 @@ export function createPatchFunction (backend) {
)
}
}

vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
Expand Down Expand Up @@ -267,7 +286,7 @@ export function createPatchFunction (backend) {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true)
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
Expand Down Expand Up @@ -320,7 +339,7 @@ export function createPatchFunction (backend) {

function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm)
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
}
}

Expand Down Expand Up @@ -430,7 +449,7 @@ export function createPatchFunction (backend) {
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
Expand All @@ -439,7 +458,7 @@ export function createPatchFunction (backend) {
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
Expand Down
22 changes: 2 additions & 20 deletions src/core/vdom/vnode.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,15 @@ export function createTextVNode (val: string | number) {
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode (vnode: VNode, deep?: boolean): VNode {
const componentOptions = vnode.componentOptions
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text,
vnode.elm,
vnode.context,
componentOptions,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
Expand All @@ -105,22 +104,5 @@ export function cloneVNode (vnode: VNode, deep?: boolean): VNode {
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.isCloned = true
if (deep) {
if (vnode.children) {
cloned.children = cloneVNodes(vnode.children, true)
}
if (componentOptions && componentOptions.children) {
componentOptions.children = cloneVNodes(componentOptions.children, true)
}
}
return cloned
}

export function cloneVNodes (vnodes: Array<VNode>, deep?: boolean): Array<VNode> {
const len = vnodes.length
const res = new Array(len)
for (let i = 0; i < len; i++) {
res[i] = cloneVNode(vnodes[i], deep)
}
return res
}
42 changes: 40 additions & 2 deletions test/unit/modules/vdom/patch/edge-cases.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('vdom patch: edge cases', () => {
})

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

it('should handle slot nodes being reused across render', done => {
const vm = new Vue({
template: `
<foo ref="foo">
<div>slot</div>
</foo>
`,
components: {
foo: {
data () {
return { ok: true }
},
render (h) {
const children = [
this.ok ? h('div', 'toggler ') : null,
h('div', [this.$slots.default, h('span', ' 1')]),
h('div', [h('label', ' 2')])
]
return h('div', children)
}
}
}
}).$mount()
expect(vm.$el.textContent).toContain('toggler slot 1 2')
vm.$refs.foo.ok = false
waitForUpdate(() => {
expect(vm.$el.textContent).toContain('slot 1 2')
vm.$refs.foo.ok = true
}).then(() => {
expect(vm.$el.textContent).toContain('toggler slot 1 2')
vm.$refs.foo.ok = false
}).then(() => {
expect(vm.$el.textContent).toContain('slot 1 2')
vm.$refs.foo.ok = true
}).then(done)
})

it('should synchronize vm\' vnode', done => {
const comp = {
data: () => ({ swap: true }),
Expand Down

0 comments on commit 956756b

Please sign in to comment.