Evan You 9 лет назад
Родитель
Сommit
ec824be88e

+ 1 - 0
flow/compiler.js

@@ -117,6 +117,7 @@ declare type ASTElement = {
 
   forbidden?: true;
   once?: true;
+  onceProcessed?: boolean;
   wrapData?: (code: string) => string;
 }
 

+ 2 - 0
flow/component.js

@@ -83,6 +83,8 @@ declare interface Component {
   _h: (vnode?: VNode, data?: VNodeData, children?: VNodeChildren) => VNode | void;
   // renderStatic
   _m: (index: number, isInFor?: boolean) => VNode | VNodeChildren;
+  // markOnce
+  _o: (vnode: VNode | Array<VNode>, index: number, key: string) => VNode | VNodeChildren;
   // toString
   _s: (value: any) => string;
   // toNumber

+ 39 - 4
src/compiler/codegen/index.js

@@ -10,6 +10,7 @@ let transforms
 let dataGenFns
 let platformDirectives
 let staticRenderFns
+let onceCount
 let currentOptions
 
 export function generate (
@@ -22,6 +23,8 @@ export function generate (
   // save previous staticRenderFns so generate calls can be nested
   const prevStaticRenderFns: Array<string> = staticRenderFns
   const currentStaticRenderFns: Array<string> = staticRenderFns = []
+  const prevOnceCount = onceCount
+  onceCount = 0
   currentOptions = options
   warn = options.warn || baseWarn
   transforms = pluckModuleFunction(options.modules, 'transformCode')
@@ -29,6 +32,7 @@ export function generate (
   platformDirectives = options.directives || {}
   const code = ast ? genElement(ast) : '_h("div")'
   staticRenderFns = prevStaticRenderFns
+  onceCount = prevOnceCount
   return {
     render: `with(this){return ${code}}`,
     staticRenderFns: currentStaticRenderFns
@@ -37,10 +41,9 @@ export function generate (
 
 function genElement (el: ASTElement): string {
   if (el.staticRoot && !el.staticProcessed) {
-    // hoist static sub-trees out
-    el.staticProcessed = true
-    staticRenderFns.push(`with(this){return ${genElement(el)}}`)
-    return `_m(${staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`
+    return genStatic(el)
+  } else if (el.once && !el.onceProcessed) {
+    return genOnce(el)
   } else if (el.for && !el.forProcessed) {
     return genFor(el)
   } else if (el.if && !el.ifProcessed) {
@@ -72,6 +75,38 @@ function genElement (el: ASTElement): string {
   }
 }
 
+// hoist static sub-trees out
+function genStatic (el: ASTElement): string {
+  el.staticProcessed = true
+  staticRenderFns.push(`with(this){return ${genElement(el)}}`)
+  return `_m(${staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`
+}
+
+// v-once
+function genOnce (el: ASTElement): string {
+  el.onceProcessed = true
+  if (el.staticInFor) {
+    let key = ''
+    let parent = el.parent
+    while (parent) {
+      if (parent.for) {
+        key = parent.key
+        break
+      }
+      parent = parent.parent
+    }
+    if (!key) {
+      process.env.NODE_ENV !== 'production' && warn(
+        `v-once can only be used inside v-for that is keyed. `
+      )
+      return genElement(el)
+    }
+    return `_o(${genElement(el)},${onceCount++}${key ? `,${key}` : ``})`
+  } else {
+    return genStatic(el)
+  }
+}
+
 function genIf (el: any): string {
   const exp = el.if
   el.ifProcessed = true // avoid recursion

+ 4 - 2
src/compiler/optimizer.js

@@ -50,9 +50,11 @@ function markStatic (node: ASTNode) {
 
 function markStaticRoots (node: ASTNode, isInFor: boolean) {
   if (node.type === 1) {
-    if (node.once || node.static) {
-      node.staticRoot = true
+    if (node.static || node.once) {
       node.staticInFor = isInFor
+    }
+    if (node.static) {
+      node.staticRoot = true
       return
     }
     if (node.children) {

+ 24 - 6
src/core/instance/render.js

@@ -115,18 +115,36 @@ export function renderMixin (Vue: Class<Component>) {
     }
     // otherwise, render a fresh tree.
     tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy)
+    markStatic(tree, `__static__${index}`, false)
+    return tree
+  }
+
+  // mark node as static (v-once)
+  Vue.prototype._o = function markOnce (
+    tree: VNode | Array<VNode>,
+    index: number,
+    key: string
+  ) {
+    markStatic(tree, `__once__${index}${key ? `_${key}` : ``}`, true)
+    return tree
+  }
+
+  function markStatic (tree, key, isOnce) {
     if (Array.isArray(tree)) {
       for (let i = 0; i < tree.length; i++) {
-        if (typeof tree[i] !== 'string') {
-          tree[i].isStatic = true
-          tree[i].key = `__static__${index}_${i}`
+        if (tree[i] && typeof tree[i] !== 'string') {
+          markStaticNode(tree[i], `${key}_${i}`, isOnce)
         }
       }
     } else {
-      tree.isStatic = true
-      tree.key = `__static__${index}`
+      markStaticNode(tree, key, isOnce)
     }
-    return tree
+  }
+
+  function markStaticNode (node, key, isOnce) {
+    node.isStatic = true
+    node.key = key
+    node.isOnce = isOnce
   }
 
   // filter resolution helper

+ 1 - 1
src/core/vdom/patch.js

@@ -337,7 +337,7 @@ export function createPatchFunction (backend) {
     if (vnode.isStatic &&
         oldVnode.isStatic &&
         vnode.key === oldVnode.key &&
-        vnode.isCloned) {
+        (vnode.isCloned || vnode.isOnce)) {
       vnode.elm = oldVnode.elm
       return
     }

+ 2 - 0
src/core/vdom/vnode.js

@@ -18,6 +18,7 @@ export default class VNode {
   isRootInsert: boolean; // necessary for enter transition check
   isComment: boolean; // empty comment placeholder?
   isCloned: boolean; // is a cloned node?
+  isOnce: boolean; // is a v-once node?
 
   constructor (
     tag?: string,
@@ -46,6 +47,7 @@ export default class VNode {
     this.isRootInsert = true
     this.isComment = false
     this.isCloned = false
+    this.isOnce = false
   }
 }
 

+ 56 - 0
test/unit/features/directives/once.spec.js

@@ -105,4 +105,60 @@ describe('Directive v-once', () => {
       expect(vm.$el.textContent).toBe('123')
     }).then(done)
   })
+
+  it('should work inside v-for', done => {
+    const vm = new Vue({
+      data: {
+        list: [
+          { id: 0, text: 'a' },
+          { id: 1, text: 'b' },
+          { id: 2, text: 'c' }
+        ]
+      },
+      template: `
+        <div>
+          <div v-for="i in list" :key="i.id">
+            <div>
+              <span v-once>{{ i.text }}</span><span>{{ i.text }}</span>
+            </div>
+          </div>
+        </div>
+      `
+    }).$mount()
+
+    expect(vm.$el.textContent).toBe('aabbcc')
+
+    vm.list[0].text = 'd'
+    waitForUpdate(() => {
+      expect(vm.$el.textContent).toBe('adbbcc')
+      vm.list[1].text = 'e'
+    }).then(() => {
+      expect(vm.$el.textContent).toBe('adbecc')
+      vm.list.reverse()
+    }).then(() => {
+      expect(vm.$el.textContent).toBe('ccbead')
+    }).then(done)
+  })
+
+  it('should warn inside non-keyed v-for', () => {
+    const vm = new Vue({
+      data: {
+        list: [
+          { id: 0, text: 'a' },
+          { id: 1, text: 'b' },
+          { id: 2, text: 'c' }
+        ]
+      },
+      template: `
+        <div>
+          <div v-for="i in list">
+            <span v-once>{{ i.text }}</span><span>{{ i.text }}</span>
+          </div>
+        </div>
+      `
+    }).$mount()
+
+    expect(vm.$el.textContent).toBe('aabbcc')
+    expect(`v-once can only be used inside v-for that is keyed.`).toHaveBeenWarned()
+  })
 })