Skip to content

Commit 18386c4

Browse files
committed
fix static nodes optimization inside v-for (fix vuejs#3406)
1 parent 75b2f52 commit 18386c4

File tree

10 files changed

+109
-45
lines changed

10 files changed

+109
-45
lines changed

flow/compiler.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ declare type ASTElement = {
7070

7171
static?: boolean;
7272
staticRoot?: boolean;
73+
staticInFor?: boolean;
7374
staticProcessed?: boolean;
7475
hasBindings?: boolean;
7576

flow/component.js

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,32 +77,18 @@ declare interface Component {
7777
// rendering
7878
_render: () => VNode;
7979
__patch__: (a: Element | VNode | void, b: VNode) => any;
80-
// renderElementWithChildren
81-
_h: (
82-
vnode?: VNode,
83-
children?: VNodeChildren
84-
) => VNode | void;
85-
// renderElement
86-
_e: (
87-
tag?: string | Component | Object,
88-
data?: Object,
89-
namespace?: string
90-
) => VNode | void;
91-
// renderStaticTree
92-
_m: (
93-
index?: number
94-
) => Object | void;
80+
// createElement
81+
_h: (vnode?: VNode, data?: VNodeData, children?: VNodeChildren) => VNode | void;
82+
// renderStatic
83+
_m: (index: number, isInFor?: boolean) => VNode | VNodeChildren;
9584
// toString
9685
_s: (value: any) => string;
9786
// toNumber
9887
_n: (value: string) => number | string;
9988
// resolveFilter
10089
_f: (id: string) => Function;
10190
// renderList
102-
_l: (
103-
val: any,
104-
render: Function
105-
) => ?Array<VNode>;
91+
_l: (val: any, render: Function) => ?Array<VNode>;
10692
// apply v-bind object
10793
_b: (vnode: VNodeWithData, value: any) => void;
10894
// retrive custom keyCode

flow/vnode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
declare type VNodeChildren = Array<any> | string
1+
declare type VNodeChildren = Array<?VNode | string | VNodeChildren> | string
22

33
declare type VNodeComponentOptions = {
44
Ctor: Class<Component>;

src/compiler/codegen/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function genElement (el: ASTElement): string {
4040
// hoist static sub-trees out
4141
el.staticProcessed = true
4242
staticRenderFns.push(`with(this){return ${genElement(el)}}`)
43-
return `_m(${staticRenderFns.length - 1})`
43+
return `_m(${staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`
4444
} else if (el.for && !el.forProcessed) {
4545
return genFor(el)
4646
} else if (el.if && !el.ifProcessed) {

src/compiler/optimizer.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function optimize (root: ?ASTElement, options: CompilerOptions) {
2525
// first pass: mark all non-static nodes.
2626
markStatic(root)
2727
// second pass: mark static roots.
28-
markStaticRoots(root)
28+
markStaticRoots(root, false)
2929
}
3030

3131
function genStaticKeys (keys: string): Function {
@@ -48,14 +48,17 @@ function markStatic (node: ASTNode) {
4848
}
4949
}
5050

51-
function markStaticRoots (node: ASTNode) {
52-
if (node.type === 1 && (node.once || node.static)) {
53-
node.staticRoot = true
54-
return
55-
}
56-
if (node.children) {
57-
for (let i = 0, l = node.children.length; i < l; i++) {
58-
markStaticRoots(node.children[i])
51+
function markStaticRoots (node: ASTNode, isInFor: boolean) {
52+
if (node.type === 1) {
53+
if (node.once || node.static) {
54+
node.staticRoot = true
55+
node.staticInFor = isInFor
56+
return
57+
}
58+
if (node.children) {
59+
for (let i = 0, l = node.children.length; i < l; i++) {
60+
markStaticRoots(node.children[i], !!node.for)
61+
}
5962
}
6063
}
6164
}

src/core/instance/render.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,26 @@ export function renderMixin (Vue: Class<Component>) {
8787
Vue.prototype._n = toNumber
8888

8989
// render static tree by index
90-
Vue.prototype._m = function renderStatic (index?: number): Object | void {
90+
Vue.prototype._m = function renderStatic (
91+
index: number,
92+
isInFor?: boolean
93+
): VNode | VNodeChildren {
9194
let tree = this._staticTrees[index]
92-
if (!tree) {
93-
tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(
94-
this._renderProxy
95-
)
95+
// if has already-rendered static tree and not inside v-for,
96+
// we can reuse the same tree by indentity.
97+
if (tree && !isInFor) {
98+
return tree
99+
}
100+
// otherwise, render a fresh tree.
101+
tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy)
102+
if (Array.isArray(tree)) {
103+
for (let i = 0; i < tree.length; i++) {
104+
tree[i].isStatic = true
105+
tree[i].key = `__static__${index}_${i}`
106+
}
107+
} else {
96108
tree.isStatic = true
109+
tree.key = `__static__${index}`
97110
}
98111
return tree
99112
}

src/core/vdom/patch.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ function isDef (s) {
2525
}
2626

2727
function sameVnode (vnode1, vnode2) {
28-
if (vnode1.isStatic || vnode2.isStatic) {
29-
return vnode1 === vnode2
30-
}
3128
return (
3229
vnode1.key === vnode2.key &&
3330
vnode1.tag === vnode2.tag &&
@@ -273,12 +270,8 @@ export function createPatchFunction (backend) {
273270
newStartVnode = newCh[++newStartIdx]
274271
} else {
275272
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
276-
idxInOld = isDef(newStartVnode.key)
277-
? oldKeyToIdx[newStartVnode.key]
278-
: newStartVnode.isStatic
279-
? oldCh.indexOf(newStartVnode)
280-
: null
281-
if (isUndef(idxInOld) || idxInOld === -1) { // New element
273+
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
274+
if (isUndef(idxInOld)) { // New element
282275
nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
283276
newStartVnode = newCh[++newStartIdx]
284277
} else {
@@ -312,7 +305,13 @@ export function createPatchFunction (backend) {
312305
}
313306

314307
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
315-
if (oldVnode === vnode) return
308+
if (oldVnode === vnode) {
309+
return
310+
}
311+
if (vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) {
312+
vnode.elm = oldVnode.elm
313+
return
314+
}
316315
let i, hook
317316
const hasData = isDef(i = vnode.data)
318317
if (hasData && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {

test/unit/modules/compiler/codegen.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,14 @@ describe('codegen', () => {
296296
expect('Inline-template components must have exactly one child element.').toHaveBeenWarned()
297297
})
298298

299+
it('generate static trees inside v-for', () => {
300+
assertCodegen(
301+
`<div><div v-for="i in 10"><span></span></div></div>`,
302+
`with(this){return _h('div',[(10)&&_l((10),function(i){return _h('div',[_m(0,true)])})])}`,
303+
[`with(this){return _h('span')}`]
304+
)
305+
})
306+
299307
it('not specified ast type', () => {
300308
const res = generate(null, baseOptions)
301309
expect(res.render).toBe(`with(this){return _h("div")}`)

test/unit/modules/compiler/optimizer.spec.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,11 @@ describe('optimizer', () => {
203203
optimize(ast, {})
204204
expect(ast.static).toBe(false)
205205
})
206+
207+
it('mark static trees inside v-for', () => {
208+
const ast = parse(`<div><div v-for="i in 10"><span>hi</span></div></div>`, baseOptions)
209+
optimize(ast, baseOptions)
210+
expect(ast.children[0].children[0].staticRoot).toBe(true)
211+
expect(ast.children[0].children[0].staticInFor).toBe(true)
212+
})
206213
})

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Vue from 'vue'
12
import { patch } from 'web/runtime/patch'
23
import VNode from 'core/vdom/vnode'
34

@@ -475,6 +476,7 @@ describe('children', () => {
475476
}
476477
const b = makeNode('B')
477478
b.isStatic = true
479+
b.key = `__static__1`
478480
const vnode1 = new VNode('div', {}, [makeNode('A'), b, makeNode('C')])
479481
const vnode2 = new VNode('div', {}, [b])
480482
const vnode3 = new VNode('div', {}, [makeNode('A'), b, makeNode('C')])
@@ -486,4 +488,49 @@ describe('children', () => {
486488
elm = patch(vnode2, vnode3)
487489
expect(elm.textContent).toBe('ABC')
488490
})
491+
492+
it('should handle static vnodes inside ', function () {
493+
function makeNode (text) {
494+
return new VNode('div', undefined, [
495+
new VNode(undefined, undefined, undefined, text)
496+
])
497+
}
498+
const b = makeNode('B')
499+
b.isStatic = true
500+
b.key = `__static__1`
501+
const vnode1 = new VNode('div', {}, [makeNode('A'), b, makeNode('C')])
502+
const vnode2 = new VNode('div', {}, [b])
503+
const vnode3 = new VNode('div', {}, [makeNode('A'), b, makeNode('C')])
504+
505+
let elm = patch(vnode0, vnode1)
506+
expect(elm.textContent).toBe('ABC')
507+
elm = patch(vnode1, vnode2)
508+
expect(elm.textContent).toBe('B')
509+
elm = patch(vnode2, vnode3)
510+
expect(elm.textContent).toBe('ABC')
511+
})
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+
})
489536
})

0 commit comments

Comments
 (0)