Browse Source

fix static nodes optimization inside v-for (fix #3406)

Evan You 9 years ago
parent
commit
18386c4cf4

+ 1 - 0
flow/compiler.js

@@ -70,6 +70,7 @@ declare type ASTElement = {
 
   static?: boolean;
   staticRoot?: boolean;
+  staticInFor?: boolean;
   staticProcessed?: boolean;
   hasBindings?: boolean;
 

+ 5 - 19
flow/component.js

@@ -77,21 +77,10 @@ declare interface Component {
   // rendering
   _render: () => VNode;
   __patch__: (a: Element | VNode | void, b: VNode) => any;
-  // renderElementWithChildren
-  _h: (
-    vnode?: VNode,
-    children?: VNodeChildren
-  ) => VNode | void;
-  // renderElement
-  _e: (
-    tag?: string | Component | Object,
-    data?: Object,
-    namespace?: string
-  ) => VNode | void;
-  // renderStaticTree
-  _m: (
-    index?: number
-  ) => Object | void;
+  // createElement
+  _h: (vnode?: VNode, data?: VNodeData, children?: VNodeChildren) => VNode | void;
+  // renderStatic
+  _m: (index: number, isInFor?: boolean) => VNode | VNodeChildren;
   // toString
   _s: (value: any) => string;
   // toNumber
@@ -99,10 +88,7 @@ declare interface Component {
   // resolveFilter
   _f: (id: string) => Function;
   // renderList
-  _l: (
-    val: any,
-    render: Function
-  ) => ?Array<VNode>;
+  _l: (val: any, render: Function) => ?Array<VNode>;
   // apply v-bind object
   _b: (vnode: VNodeWithData, value: any) => void;
   // retrive custom keyCode

+ 1 - 1
flow/vnode.js

@@ -1,4 +1,4 @@
-declare type VNodeChildren = Array<any> | string
+declare type VNodeChildren = Array<?VNode | string | VNodeChildren> | string
 
 declare type VNodeComponentOptions = {
   Ctor: Class<Component>;

+ 1 - 1
src/compiler/codegen/index.js

@@ -40,7 +40,7 @@ function genElement (el: ASTElement): string {
     // hoist static sub-trees out
     el.staticProcessed = true
     staticRenderFns.push(`with(this){return ${genElement(el)}}`)
-    return `_m(${staticRenderFns.length - 1})`
+    return `_m(${staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`
   } else if (el.for && !el.forProcessed) {
     return genFor(el)
   } else if (el.if && !el.ifProcessed) {

+ 12 - 9
src/compiler/optimizer.js

@@ -25,7 +25,7 @@ export function optimize (root: ?ASTElement, options: CompilerOptions) {
   // first pass: mark all non-static nodes.
   markStatic(root)
   // second pass: mark static roots.
-  markStaticRoots(root)
+  markStaticRoots(root, false)
 }
 
 function genStaticKeys (keys: string): Function {
@@ -48,14 +48,17 @@ function markStatic (node: ASTNode) {
   }
 }
 
-function markStaticRoots (node: ASTNode) {
-  if (node.type === 1 && (node.once || node.static)) {
-    node.staticRoot = true
-    return
-  }
-  if (node.children) {
-    for (let i = 0, l = node.children.length; i < l; i++) {
-      markStaticRoots(node.children[i])
+function markStaticRoots (node: ASTNode, isInFor: boolean) {
+  if (node.type === 1) {
+    if (node.once || node.static) {
+      node.staticRoot = true
+      node.staticInFor = isInFor
+      return
+    }
+    if (node.children) {
+      for (let i = 0, l = node.children.length; i < l; i++) {
+        markStaticRoots(node.children[i], !!node.for)
+      }
     }
   }
 }

+ 18 - 5
src/core/instance/render.js

@@ -87,13 +87,26 @@ export function renderMixin (Vue: Class<Component>) {
   Vue.prototype._n = toNumber
 
   // render static tree by index
-  Vue.prototype._m = function renderStatic (index?: number): Object | void {
+  Vue.prototype._m = function renderStatic (
+    index: number,
+    isInFor?: boolean
+  ): VNode | VNodeChildren {
     let tree = this._staticTrees[index]
-    if (!tree) {
-      tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(
-        this._renderProxy
-      )
+    // if has already-rendered static tree and not inside v-for,
+    // we can reuse the same tree by indentity.
+    if (tree && !isInFor) {
+      return tree
+    }
+    // otherwise, render a fresh tree.
+    tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy)
+    if (Array.isArray(tree)) {
+      for (let i = 0; i < tree.length; i++) {
+        tree[i].isStatic = true
+        tree[i].key = `__static__${index}_${i}`
+      }
+    } else {
       tree.isStatic = true
+      tree.key = `__static__${index}`
     }
     return tree
   }

+ 9 - 10
src/core/vdom/patch.js

@@ -25,9 +25,6 @@ function isDef (s) {
 }
 
 function sameVnode (vnode1, vnode2) {
-  if (vnode1.isStatic || vnode2.isStatic) {
-    return vnode1 === vnode2
-  }
   return (
     vnode1.key === vnode2.key &&
     vnode1.tag === vnode2.tag &&
@@ -273,12 +270,8 @@ export function createPatchFunction (backend) {
         newStartVnode = newCh[++newStartIdx]
       } else {
         if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
-        idxInOld = isDef(newStartVnode.key)
-          ? oldKeyToIdx[newStartVnode.key]
-          : newStartVnode.isStatic
-            ? oldCh.indexOf(newStartVnode)
-            : null
-        if (isUndef(idxInOld) || idxInOld === -1) { // New element
+        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
+        if (isUndef(idxInOld)) { // New element
           nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
           newStartVnode = newCh[++newStartIdx]
         } else {
@@ -312,7 +305,13 @@ export function createPatchFunction (backend) {
   }
 
   function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
-    if (oldVnode === vnode) return
+    if (oldVnode === vnode) {
+      return
+    }
+    if (vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) {
+      vnode.elm = oldVnode.elm
+      return
+    }
     let i, hook
     const hasData = isDef(i = vnode.data)
     if (hasData && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {

+ 8 - 0
test/unit/modules/compiler/codegen.spec.js

@@ -296,6 +296,14 @@ describe('codegen', () => {
     expect('Inline-template components must have exactly one child element.').toHaveBeenWarned()
   })
 
+  it('generate static trees inside v-for', () => {
+    assertCodegen(
+      `<div><div v-for="i in 10"><span></span></div></div>`,
+      `with(this){return _h('div',[(10)&&_l((10),function(i){return _h('div',[_m(0,true)])})])}`,
+      [`with(this){return _h('span')}`]
+    )
+  })
+
   it('not specified ast type', () => {
     const res = generate(null, baseOptions)
     expect(res.render).toBe(`with(this){return _h("div")}`)

+ 7 - 0
test/unit/modules/compiler/optimizer.spec.js

@@ -203,4 +203,11 @@ describe('optimizer', () => {
     optimize(ast, {})
     expect(ast.static).toBe(false)
   })
+
+  it('mark static trees inside v-for', () => {
+    const ast = parse(`<div><div v-for="i in 10"><span>hi</span></div></div>`, baseOptions)
+    optimize(ast, baseOptions)
+    expect(ast.children[0].children[0].staticRoot).toBe(true)
+    expect(ast.children[0].children[0].staticInFor).toBe(true)
+  })
 })

+ 47 - 0
test/unit/modules/vdom/patch/children.spec.js

@@ -1,3 +1,4 @@
+import Vue from 'vue'
 import { patch } from 'web/runtime/patch'
 import VNode from 'core/vdom/vnode'
 
@@ -475,6 +476,7 @@ describe('children', () => {
     }
     const b = makeNode('B')
     b.isStatic = true
+    b.key = `__static__1`
     const vnode1 = new VNode('div', {}, [makeNode('A'), b, makeNode('C')])
     const vnode2 = new VNode('div', {}, [b])
     const vnode3 = new VNode('div', {}, [makeNode('A'), b, makeNode('C')])
@@ -486,4 +488,49 @@ describe('children', () => {
     elm = patch(vnode2, vnode3)
     expect(elm.textContent).toBe('ABC')
   })
+
+  it('should handle static vnodes inside ', function () {
+    function makeNode (text) {
+      return new VNode('div', undefined, [
+        new VNode(undefined, undefined, undefined, text)
+      ])
+    }
+    const b = makeNode('B')
+    b.isStatic = true
+    b.key = `__static__1`
+    const vnode1 = new VNode('div', {}, [makeNode('A'), b, makeNode('C')])
+    const vnode2 = new VNode('div', {}, [b])
+    const vnode3 = new VNode('div', {}, [makeNode('A'), b, makeNode('C')])
+
+    let elm = patch(vnode0, vnode1)
+    expect(elm.textContent).toBe('ABC')
+    elm = patch(vnode1, vnode2)
+    expect(elm.textContent).toBe('B')
+    elm = patch(vnode2, vnode3)
+    expect(elm.textContent).toBe('ABC')
+  })
+
+  // exposed by #3406
+  // When a static vnode is inside v-for, it's possible for the same vnode
+  // to be used in multiple places, and its element will be replaced. This
+  // causes patch errors when node ops depend on the vnode's element position.
+  it('should handle static vnodes by key', done => {
+    const vm = new Vue({
+      data: {
+        ok: true
+      },
+      template: `
+        <div>
+          <div v-for="i in 2">
+            <div v-if="ok">a</div><div>b</div><div v-if="!ok">c</div><div>d</div>
+          </div>
+        </div>
+      `
+    }).$mount()
+    expect(vm.$el.textContent).toBe('abdabd')
+    vm.ok = false
+    waitForUpdate(() => {
+      expect(vm.$el.textContent).toBe('bcdbcd')
+    }).then(done)
+  })
 })