Repository: vuejs/vue
Pull request: fix v-once inside v-for and v-once with v-if (fix #4182)

fix #4182

This PR also fix a bug when v-if works with v-once. The generated code is like *_m(1)*, which is cached by the static tree.  I created a [jsfiddle](https://jsfiddle.net/defcc/2wzdcon5/5/) here



Changed files:
filename: src/compiler/codegen/index.js
patch: @@ -85,7 +85,9 @@ function genStatic (el: ASTElement): string {
 // v-once
 function genOnce (el: ASTElement): string {
   el.onceProcessed = true
-  if (el.staticInFor) {
+  if (el.if && !el.ifProcessed) {
+    return genIf(el)
+  } else if (el.staticInFor) {
     let key = ''
     let parent = el.parent
     while (parent) {
@@ -107,10 +109,11 @@ function genOnce (el: ASTElement): string {
   }
 }
 
+// v-if with v-once shuold generate code like (a)?_m(0):_m(1)
 function genIf (el: any): string {
   const exp = el.if
   el.ifProcessed = true // avoid recursion
-  return `(${exp})?${genElement(el)}:${genElse(el)}`
+  return `(${exp})?${el.once ? genOnce(el) : genElement(el)}:${genElse(el)}`
 }
 
 function genElse (el: ASTElement): string {

filename: src/compiler/optimizer.js
patch: @@ -69,7 +69,12 @@ function markStaticRoots (node: ASTNode, isInFor: boolean) {
     }
     if (node.children) {
       for (let i = 0, l = node.children.length; i < l; i++) {
-        markStaticRoots(node.children[i], isInFor || !!node.for)
+        const child = node.children[i]
+        isInFor = isInFor || !!node.for
+        markStaticRoots(child, isInFor)
+        if (child.type === 1 && child.elseBlock) {
+          markStaticRoots(child.elseBlock, isInFor)
+        }
       }
     }
   }

filename: test/unit/features/directives/once.spec.js
patch: @@ -15,10 +15,11 @@ describe('Directive v-once', () => {
 
   it('should not rerender self and child component', done => {
     const vm = new Vue({
-      template: `<div v-once>
-                  <span>{{ a }}</span>
-                  <item :b="a"></item>
-                </div>`,
+      template: `
+        <div v-once>
+          <span>{{ a }}</span>
+          <item :b="a"></item>
+        </div>`,
       data: { a: 'hello' },
       components: {
         item: {
@@ -39,10 +40,11 @@ describe('Directive v-once', () => {
 
   it('should rerender parent but not self', done => {
     const vm = new Vue({
-      template: `<div>
-                  <span>{{ a }}</span>
-                  <item v-once :b="a"></item>
-                </div>`,
+      template: `
+        <div>
+          <span>{{ a }}</span>
+          <item v-once :b="a"></item>
+        </div>`,
       data: { a: 'hello' },
       components: {
         item: {
@@ -63,11 +65,12 @@ describe('Directive v-once', () => {
 
   it('should not rerender static sub nodes', done => {
     const vm = new Vue({
-      template: `<div>
-                  <span v-once>{{ a }}</span>
-                  <item :b="a"></item>
-                  <span>{{ suffix }}</span>
-                </div>`,
+      template: `
+        <div>
+          <span v-once>{{ a }}</span>
+          <item :b="a"></item>
+          <span>{{ suffix }}</span>
+        </div>`,
       data: {
         a: 'hello',
         suffix: '?'
@@ -92,6 +95,39 @@ describe('Directive v-once', () => {
     }).then(done)
   })
 
+  it('should work with v-if', done => {
+    const vm = new Vue({
+      data: {
+        tester: true,
+        yes: 'y',
+        no: 'n'
+      },
+      template: `
+        <div>
+          <div v-if="tester">{{ yes }}</div>
+          <div v-else>{{ no }}</div>
+          <div v-if="tester" v-once>{{ yes }}</div>
+          <div v-else>{{ no }}</div>
+          <div v-if="tester">{{ yes }}</div>
+          <div v-else v-once>{{ no }}</div>
+          <div v-if="tester" v-once>{{ yes }}</div>
+          <div v-else v-once>{{ no }}</div>
+        </div>
+      `
+    }).$mount()
+    expectTextContent(vm, 'yyyy')
+    vm.yes = 'yes'
+    waitForUpdate(() => {
+      expectTextContent(vm, 'yesyyesy')
+      vm.tester = false
+    }).then(() => {
+      expectTextContent(vm, 'nnnn')
+      vm.no = 'no'
+    }).then(() => {
+      expectTextContent(vm, 'nononn')
+    }).then(done)
+  })
+
   it('should work with v-for', done => {
     const vm = new Vue({
       data: {
@@ -140,6 +176,43 @@ describe('Directive v-once', () => {
     }).then(done)
   })
 
+  it('should work inside v-for with v-if', done => {
+    const vm = new Vue({
+      data: {
+        list: [
+          { id: 0, text: 'a', tester: true, truthy: 'y' }
+        ]
+      },
+      template: `
+        <div>
+          <div v-for="i in list" :key="i.id">
+              <span v-if="i.tester" v-once>{{ i.truthy }}</span>
+              <span v-else v-once>{{ i.text }}</span>
+              <span v-if="i.tester" v-once>{{ i.truthy }}</span>
+              <span v-else>{{ i.text }}</span>
+              <span v-if="i.tester">{{ i.truthy }}</span>
+              <span v-else v-once>{{ i.text }}</span>
+              <span v-if="i.tester">{{ i.truthy }}</span>
+              <span v-else>{{ i.text }}</span>
+          </div>
+        </div>
+      `
+    }).$mount()
+
+    expectTextContent(vm, 'yyyy')
+
+    vm.list[0].truthy = 'yy'
+    waitForUpdate(() => {
+      expectTextContent(vm, 'yyyyyy')
+      vm.list[0].tester = false
+    }).then(() => {
+      expectTextContent(vm, 'aaaa')
+      vm.list[0].text = 'nn'
+    }).then(() => {
+      expectTextContent(vm, 'annann')
+    }).then(done)
+  })
+
   it('should warn inside non-keyed v-for', () => {
     const vm = new Vue({
       data: {
@@ -162,3 +235,7 @@ describe('Directive v-once', () => {
     expect(`v-once can only be used inside v-for that is keyed.`).toHaveBeenWarned()
   })
 })
+
+function expectTextContent (vm, text) {
+  expect(vm.$el.textContent.replace(/\r?\n|\r|\s/g, '')).toBe(text)
+}