mirror of
https://github.com/vuejs/vue.git
synced 2024-11-22 04:39:46 +00:00
refactor: use more efficient on-demand clone to handle reused node edge cases
removes unnecessary slot/static node clones, fix #7292
This commit is contained in:
parent
8335217cb4
commit
956756b1be
@ -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
|
||||
@ -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)
|
||||
|
@ -1,7 +1,5 @@
|
||||
/* @flow */
|
||||
|
||||
import { cloneVNode, cloneVNodes } from 'core/vdom/vnode'
|
||||
|
||||
/**
|
||||
* Runtime helper for rendering static trees.
|
||||
*/
|
||||
@ -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(
|
||||
|
@ -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'
|
||||
|
||||
@ -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.
|
||||
|
@ -40,7 +40,15 @@ 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,
|
||||
@ -48,10 +56,6 @@ const componentVNodeHooks = {
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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'
|
||||
@ -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()
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
@ -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
|
||||
@ -144,6 +162,7 @@ export function createPatchFunction (backend) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
vnode.elm = vnode.ns
|
||||
? nodeOps.createElementNS(vnode.ns, tag)
|
||||
: nodeOps.createElement(tag, vnode)
|
||||
@ -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)))
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)) {
|
||||
@ -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]
|
||||
|
@ -85,8 +85,7 @@ 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,
|
||||
@ -94,7 +93,7 @@ export function cloneVNode (vnode: VNode, deep?: boolean): VNode {
|
||||
vnode.text,
|
||||
vnode.elm,
|
||||
vnode.context,
|
||||
componentOptions,
|
||||
vnode.componentOptions,
|
||||
vnode.asyncFactory
|
||||
)
|
||||
cloned.ns = vnode.ns
|
||||
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
@ -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()
|
||||
@ -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 }),
|
||||
|
Loading…
Reference in New Issue
Block a user