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

+ 13 - 8
src/core/vdom/patch.js

@@ -218,7 +218,7 @@ export function createPatchFunction (backend) {
     }
   }
 
-  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue) {
+  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
     let oldStartIdx = 0
     let newStartIdx = 0
     let oldEndIdx = oldCh.length - 1
@@ -229,6 +229,11 @@ export function createPatchFunction (backend) {
     let newEndVnode = newCh[newEndIdx]
     let oldKeyToIdx, idxInOld, elmToMove, before
 
+    // removeOnly is a special flag used only by <transition-group>
+    // to ensure removed elements stay in correct relative positions
+    // during leaving transitions
+    const canMove = !removeOnly
+
     while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
       if (isUndef(oldStartVnode)) {
         oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
@@ -244,12 +249,12 @@ export function createPatchFunction (backend) {
         newEndVnode = newCh[--newEndIdx]
       } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
         patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
-        nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
+        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
         oldStartVnode = oldCh[++oldStartIdx]
         newEndVnode = newCh[--newEndIdx]
       } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
         patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
-        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
+        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
         oldEndVnode = oldCh[--oldEndIdx]
         newStartVnode = newCh[++newStartIdx]
       } else {
@@ -274,7 +279,7 @@ export function createPatchFunction (backend) {
           } else {
             patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
             oldCh[idxInOld] = undefined
-            nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
+            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
             newStartVnode = newCh[++newStartIdx]
           }
         }
@@ -288,7 +293,7 @@ export function createPatchFunction (backend) {
     }
   }
 
-  function patchVnode (oldVnode, vnode, insertedVnodeQueue) {
+  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
     if (oldVnode === vnode) return
     let i, hook
     const hasData = isDef(i = vnode.data)
@@ -304,7 +309,7 @@ export function createPatchFunction (backend) {
     }
     if (isUndef(vnode.text)) {
       if (isDef(oldCh) && isDef(ch)) {
-        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
+        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
       } else if (isDef(ch)) {
         if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
         addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
@@ -384,7 +389,7 @@ export function createPatchFunction (backend) {
     }
   }
 
-  return function patch (oldVnode, vnode, hydrating) {
+  return function patch (oldVnode, vnode, hydrating, removeOnly) {
     let elm, parent
     let isInitialPatch = false
     const insertedVnodeQueue = []
@@ -396,7 +401,7 @@ export function createPatchFunction (backend) {
     } else {
       const isRealElement = isDef(oldVnode.nodeType)
       if (!isRealElement && sameVnode(oldVnode, vnode)) {
-        patchVnode(oldVnode, vnode, insertedVnodeQueue)
+        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
       } else {
         if (isRealElement) {
           // mounting to a real element

+ 28 - 24
src/platforms/web/runtime/components/transition-group.js

@@ -30,11 +30,11 @@ export default {
   props,
 
   render (h) {
-    const prevMap = this.map
-    const map = this.map = {}
+    const tag = this.tag || this.$vnode.data.tag || 'span'
+    const map = Object.create(null)
+    const prevChildren = this.prevChildren = this.children
     const rawChildren = this.$slots.default || []
-    const children = []
-    const kept = []
+    const children = this.children = []
     const transitionData = extractTransitionData(this)
 
     for (let i = 0; i < rawChildren.length; i++) {
@@ -44,12 +44,6 @@ export default {
           children.push(c)
           map[c.key] = c
           ;(c.data || (c.data = {})).transition = transitionData
-          const prev = prevMap && prevMap[c.key]
-          if (prev) {
-            prev.data.kept = true
-            c.data.pos = prev.elm.getBoundingClientRect()
-            kept.push(c)
-          }
         } else if (process.env.NODE_ENV !== 'production') {
           const opts = c.componentOptions
           const name = opts
@@ -60,17 +54,20 @@ export default {
       }
     }
 
-    const tag = this.tag || this.$vnode.data.tag || 'span'
-    if (prevMap) {
-      this.kept = h(tag, null, kept)
-      this.removed = []
-      for (const key in prevMap) {
-        const c = prevMap[key]
-        if (!c.data.kept) {
-          c.data.pos = c.elm.getBoundingClientRect()
-          this.removed.push(c)
+    if (prevChildren) {
+      const kept = []
+      const removed = []
+      for (let i = 0; i < prevChildren.length; i++) {
+        const c = prevChildren[i]
+        c.data.pos = c.elm.getBoundingClientRect()
+        if (map[c.key]) {
+          kept.push(c)
+        } else {
+          removed.push(c)
         }
       }
+      this.kept = h(tag, null, kept)
+      this.removed = removed
     }
 
     return h(tag, null, children)
@@ -78,12 +75,17 @@ export default {
 
   beforeUpdate () {
     // force removing pass
-    this.__patch__(this._vnode, this.kept)
+    this.__patch__(
+      this._vnode,
+      this.kept,
+      false, // hydrating
+      true // removeOnly (!important, avoids unnecessary moves)
+    )
     this._vnode = this.kept
   },
 
   updated () {
-    const children = this.kept.children.concat(this.removed)
+    const children = this.prevChildren
     const moveClass = this.moveClass || (this.name + '-move')
     if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
       return
@@ -108,12 +110,13 @@ export default {
     children.forEach(c => {
       if (c.data.moved) {
         const el = c.elm
+        /* istanbul ignore if */
+        if (el._pendingMoveCb) {
+          el._pendingMoveCb()
+        }
         const s = el.style
         addTransitionClass(el, moveClass)
         s.transform = s.WebkitTransform = s.transitionDuration = ''
-        if (el._pendingMoveCb) {
-          el.removeEventListener(transitionEndEvent, el._pendingMoveCb)
-        }
         el.addEventListener(transitionEndEvent, el._pendingMoveCb = function cb () {
           el.removeEventListener(transitionEndEvent, cb)
           el._pendingMoveCb = null
@@ -125,6 +128,7 @@ export default {
 
   methods: {
     hasMove (el, moveClass) {
+      /* istanbul ignore if */
       if (!hasTransition) {
         return false
       }

+ 3 - 2
src/platforms/web/runtime/modules/transition.js

@@ -1,6 +1,7 @@
 /* @flow */
 
 import { inBrowser } from 'core/util/index'
+import { isIE9 } from 'web/util/index'
 import { cached, extend } from 'shared/util'
 import { mergeVNodeHook } from 'core/vdom/helpers'
 import {
@@ -52,7 +53,7 @@ export function enter (vnode: VNodeWithData) {
   const afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter
   const enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled
 
-  const expectsCSS = css !== false
+  const expectsCSS = css !== false && !isIE9
   const userWantsControl =
     enterHook &&
     // enterHook may be a bound method which exposes
@@ -127,7 +128,7 @@ export function leave (vnode: VNodeWithData, rm: Function) {
     delayLeave
   } = data
 
-  const expectsCSS = css !== false
+  const expectsCSS = css !== false && !isIE9
   const userWantsControl =
     leave &&
     // leave hook may be a bound method which exposes

+ 4 - 0
test/unit/features/transition/inject-styles.js

@@ -15,6 +15,10 @@ export default function injectStyles () {
       -webkit-transition: opacity ${duration}ms ease;
       transition: opacity ${duration}ms ease;
     }
+    .group-move {
+      -webkit-transition: -webkit-transform ${duration}ms ease;
+      transition: transform ${duration}ms ease;
+    }
     .v-appear, .v-enter, .v-leave-active,
     .test-appear, .test-enter, .test-leave-active,
     .hello, .bye.active,

+ 297 - 0
test/unit/features/transition/transition-group.spec.js

@@ -0,0 +1,297 @@
+import Vue from 'vue'
+import injectStyles from './inject-styles'
+import { isIE9 } from 'web/util/index'
+import { nextFrame } from 'web/runtime/transition-util'
+
+if (!isIE9) {
+  describe('Transition group', () => {
+    const duration = injectStyles()
+
+    let el
+    beforeEach(() => {
+      el = document.createElement('div')
+      document.body.appendChild(el)
+    })
+
+    function createBasicVM (useIs, appear) {
+      const vm = new Vue({
+        template: `
+          <div>
+            ${useIs ? `<span is="transition-group">` : `<transition-group${appear ? ` appear` : ``}>`}
+              <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+            ${useIs ? `</span>` : `</transition-group>`}
+          </div>
+        `,
+        data: {
+          items: ['a', 'b', 'c']
+        }
+      }).$mount(el)
+      if (!appear) {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            vm.items.map(i => `<div class="test">${i}</div>`).join('') +
+          `</span>`
+        )
+      }
+      return vm
+    }
+
+    it('enter', done => {
+      const vm = createBasicVM()
+      vm.items.push('d', 'e')
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            ['a', 'b', 'c'].map(i => `<div class="test">${i}</div>`).join('') +
+            `<div class="test v-enter v-enter-active">d</div>` +
+            `<div class="test v-enter v-enter-active">e</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            ['a', 'b', 'c'].map(i => `<div class="test">${i}</div>`).join('') +
+            `<div class="test v-enter-active">d</div>` +
+            `<div class="test v-enter-active">e</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(duration + 10).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            vm.items.map(i => `<div class="test">${i}</div>`).join('') +
+          `</span>`
+        )
+      }).then(done)
+    })
+
+    it('leave', done => {
+      const vm = createBasicVM()
+      vm.items = ['b']
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test v-leave v-leave-active">a</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test v-leave v-leave-active">c</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test v-leave-active">a</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test v-leave-active">c</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(duration + 10).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            vm.items.map(i => `<div class="test">${i}</div>`).join('') +
+          `</span>`
+        )
+      }).then(done)
+    })
+
+    it('enter + leave', done => {
+      const vm = createBasicVM()
+      vm.items = ['b', 'c', 'd']
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test v-leave v-leave-active">a</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test">c</div>` +
+            `<div class="test v-enter v-enter-active">d</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test v-leave-active">a</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test">c</div>` +
+            `<div class="test v-enter-active">d</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(duration + 10).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            vm.items.map(i => `<div class="test">${i}</div>`).join('') +
+          `</span>`
+        )
+      }).then(done)
+    })
+
+    it('use with "is" attribute', done => {
+      const vm = createBasicVM(true)
+      vm.items = ['b', 'c', 'd']
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test v-leave v-leave-active">a</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test">c</div>` +
+            `<div class="test v-enter v-enter-active">d</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test v-leave-active">a</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test">c</div>` +
+            `<div class="test v-enter-active">d</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(duration + 10).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            vm.items.map(i => `<div class="test">${i}</div>`).join('') +
+          `</span>`
+        )
+      }).then(done)
+    })
+
+    it('appear', done => {
+      const vm = createBasicVM(false, true /* appear */)
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            vm.items.map(i => `<div class="test v-enter v-enter-active">${i}</div>`).join('') +
+          `</span>`
+        )
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            vm.items.map(i => `<div class="test v-enter-active">${i}</div>`).join('') +
+          `</span>`
+        )
+      }).thenWaitFor(duration + 10).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            vm.items.map(i => `<div class="test">${i}</div>`).join('') +
+          `</span>`
+        )
+      }).then(done)
+    })
+
+    it('events', done => {
+      let next
+      const beforeEnterSpy = jasmine.createSpy()
+      const afterEnterSpy = jasmine.createSpy()
+      const afterLeaveSpy = jasmine.createSpy()
+      const vm = new Vue({
+        template: `
+          <div>
+            <transition-group @before-enter="beforeEnter" @after-enter="afterEnter" @after-leave="afterLeave">
+              <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+            </transition-group>
+          </div>
+        `,
+        data: {
+          items: ['a', 'b', 'c']
+        },
+        methods: {
+          beforeEnter (el) {
+            expect(el.textContent).toBe('d')
+            beforeEnterSpy()
+          },
+          afterEnter (el) {
+            expect(el.textContent).toBe('d')
+            afterEnterSpy()
+            next()
+          },
+          afterLeave (el) {
+            expect(el.textContent).toBe('a')
+            afterLeaveSpy()
+            next()
+          }
+        }
+      }).$mount(el)
+
+      vm.items.push('d')
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test">a</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test">c</div>` +
+            `<div class="test v-enter v-enter-active">d</div>` +
+          `</span>`
+        )
+        expect(beforeEnterSpy.calls.count()).toBe(1)
+      }).thenWaitFor(_next => { next = _next }).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test">a</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test">c</div>` +
+            `<div class="test">d</div>` +
+          `</span>`
+        )
+        expect(afterEnterSpy.calls.count()).toBe(1)
+        vm.items.shift()
+      }).thenWaitFor(_next => { next = _next }).then(() => {
+        expect(vm.$el.innerHTML).toBe(
+          `<span>` +
+            `<div class="test">b</div>` +
+            `<div class="test">c</div>` +
+            `<div class="test">d</div>` +
+          `</span>`
+        )
+        expect(afterLeaveSpy.calls.count()).toBe(1)
+      }).then(done)
+    })
+
+    it('move', done => {
+      const vm = new Vue({
+        template: `
+          <div>
+            <transition-group name="group">
+              <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+            </transition-group>
+          </div>
+        `,
+        data: {
+          items: ['a', 'b', 'c']
+        }
+      }).$mount(el)
+
+      vm.items = ['d', 'b', 'a']
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe(
+          `<span>` +
+            `<div class="test group-enter group-enter-active">d</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test group-move">a</div>` +
+            `<div class="test group-leave group-leave-active group-move">c</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe(
+          `<span>` +
+            `<div class="test group-enter-active">d</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test group-move">a</div>` +
+            `<div class="test group-leave-active group-move">c</div>` +
+          `</span>`
+        )
+      }).thenWaitFor(duration + 10).then(() => {
+        expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe(
+          `<span>` +
+            `<div class="test">d</div>` +
+            `<div class="test">b</div>` +
+            `<div class="test">a</div>` +
+          `</span>`
+        )
+      }).then(done)
+    })
+
+    it('warn unkeyed children', () => {
+      new Vue({
+        template: `<div><transition-group><div v-for="i in 3"></div></transition-group></div>`
+      }).$mount()
+      expect('<transition-group> children must be keyed: <div>').toHaveBeenWarned()
+    })
+  })
+}

+ 1 - 1
test/unit/features/transition/transition.spec.js

@@ -4,7 +4,7 @@ import { isIE9 } from 'web/util/index'
 import { nextFrame } from 'web/runtime/transition-util'
 
 if (!isIE9) {
-  describe('Transition system', () => {
+  describe('Transition basic', () => {
     const duration = injectStyles()
 
     let el