本篇文章是細談 vue 系列的第四篇,按理說這篇文章是上篇 《細談 vue - transition 篇》中的一個單獨的大章節。然鵝,上篇文章篇幅過長,因此不得已將其單獨拎出來寫成一篇了。對該系列之前的文章感興趣的能夠點擊如下連接進行傳送javascript
書接上文,上篇文章咱們主要介紹了 <transition>
組件對 props
和 vnode hooks
的 輸入 => 輸出
處理設計,它針對單一元素的 enter
以及 leave
階段進行了過渡效果的封裝處理,使得咱們只需關注 css
和 js
鉤子函數的業務實現便可。css
可是咱們在實際開發中,卻終究難逃多個元素都須要進行使用過渡效果進行展現,很顯然,<transition>
組件並不能實現個人業務需求。這個時候,vue
內部封裝了 <transition-group>
這麼一個內置組件來知足咱們的須要,它很好的幫助咱們實現了列表的過渡效果。html
老樣子,直接先上一個官方的例子前端
<template>
<div id="list-demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
</template>
<script> export default { name: 'home', data () { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 } }, methods: { randomIndex: function () { return Math.floor(Math.random() * this.items.length) }, add: function () { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove: function () { this.items.splice(this.randomIndex(), 1) } } } </script>
<style lang="scss"> .list-item { display: inline-block; margin-right: 10px; } .list-enter-active, .list-leave-active { transition: all 1s; } .list-enter, .list-leave-to { opacity: 0; transform: translateY(30px); } </style>
複製代碼
效果以下圖vue
接下來,我將帶着你們一塊兒探究一下 <transition-group>
組件的設計java
和 <transition>
組件相比,<transition>
是一個抽象組件,且只對單個元素生效。而 <transition-group>
組件實現了列表的過渡,而且它會渲染一個真實的元素節點。node
但他們的設計理念倒是一致的,一樣會給咱們提供一個 props
和一系列鉤子函數給咱們當作 輸入
的接口,內部進行 輸入 => 輸出
的轉換或者說綁定處理瀏覽器
export default {
props,
beforeMount () {
// ...
},
render (h: Function) {
// ...
},
updated () {
// ...
},
methods: {
// ...
}
}
複製代碼
<transition-group>
的 props
和 <transition>
的props
基本一致,只是多了一個 tag
和 moveClass
屬性,刪除了 mode
屬性緩存
// props
import { transitionProps, extractTransitionData } from './transition'
const props = extend({
tag: String,
moveClass: String
}, transitionProps)
delete props.mode
// other import
import { warn, extend } from 'core/util/index'
import { addClass, removeClass } from '../class-util'
import { setActiveInstance } from 'core/instance/lifecycle'
import {
hasTransition,
getTransitionInfo,
transitionEndEvent,
addTransitionClass,
removeTransitionClass
} from '../transition-util'
複製代碼
首先,咱們須要定義一系列變量,方便後續的操做markdown
tag
:從上面設計的總體脈絡咱們能看到,<transition-group>
並無 abstract
屬性,即它將渲染一個真實節點,那麼節點 tag
則是必須的,其默認值爲 span
。map
:建立一個空對象prevChildren
:用來存儲上一次的子節點rawChildren
:獲取 <transition-group>
包裹的子節點children
:用來存儲當前的子節點transitionData
:獲取組件上的渲染數據const tag: string = this.tag || this.$vnode.data.tag || 'span'
const map: Object = Object.create(null)
const prevChildren: Array<VNode> = this.prevChildren = this.children
const rawChildren: Array<VNode> = this.$slots.default || []
const children: Array<VNode> = this.children = []
const transitionData: Object = extractTransitionData(this)
複製代碼
緊接着是對節點遍歷的操做,這裏主要對列表中每一個節點進行過渡動畫的綁定
rawChildren
進行遍歷,並將每一個 vnode
節點取出;key
,則將 vnode
丟到 children
中;transitionData
添加到 vnode.data.transition
上,這樣便能實現列表中單個元素的過渡動畫for (let i = 0; i < rawChildren.length; i++) {
const c: VNode = rawChildren[i]
if (c.tag) {
if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
children.push(c)
map[c.key] = c
;(c.data || (c.data = {})).transition = transitionData
} else if (process.env.NODE_ENV !== 'production') {
const opts: ?VNodeComponentOptions = c.componentOptions
const name: string = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag
warn(`<transition-group> children must be keyed: <${name}>`)
}
}
}
複製代碼
隨後對 prevChildren
進行處理
prevChildren
存在,則對其進行遍歷,將 transitionData
賦值給 vnode.data.transition
,如此以後,當 vnode
子節點 enter
和 leave
階段存在過渡動畫的時候,則會執行對應的過渡動畫getBoundingClientRect
獲取元素的位置信息,將其記錄到 vnode.data.pos
中map
中是否存在 vnode.key
,若存在,則將 vnode
放到 kept
中,不然丟到 removed
隊列中this.kept
中,this.removed
則用來記錄被移除掉的節點if (prevChildren) {
const kept: Array<VNode> = []
const removed: Array<VNode> = []
for (let i = 0; i < prevChildren.length; i++) {
const c: VNode = prevChildren[i]
c.data.transition = transitionData
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
}
複製代碼
最後 <transition-group>
進行渲染
return h(tag, null, children)
複製代碼
上面咱們已經在 render
階段對列表中的每一個元素綁定好了 transition
相關的過渡效果,接下來就是每一個元素動態變動時,整個列表進行 update
時候的動態過渡了。那具體這塊又是如何操做的呢?接下來咱們就捋捋這塊的邏輯
update
鉤子函數裏面,會先獲取上一次的子節點 prevChildren
和 moveClass
;隨後判斷 children
是否存在以及 children
是否 has move ,若 children
不存在,或者 children
沒有 move
狀態,那麼也沒有必要繼續進行 update
的 move
過渡了,直接 return
便可const children: Array<VNode> = this.prevChildren
const moveClass: string = this.moveClass || ((this.name || 'v') + '-move')
if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
return
}
複製代碼
hasMove()
:該方法主要用來判斷 el
節點是否有 move
的狀態。return
條件不符合的狀況下,它會先克隆一個 DOM 節點,而後爲了不元素內部已經有了 css 過渡,因此會移除掉克隆節點上的全部的 transitionClasses
moveClass
,並將其 display
設爲 none
,而後添加到 this.$el
上getTransitionInfo
獲取它的 transition
相關的信息,而後從 this.$el
上將其移除。這個時候咱們已經獲取到了節點是否有 transform
的信息了export const hasTransition = inBrowser && !isIE9
hasMove (el: any, moveClass: string): boolean {
// 若不在瀏覽器中,或者瀏覽器不支持 transition,直接返回 false 便可
if (!hasTransition) {
return false
}
// 若當前實例上下文的有 _hasMove,直接返回 _hasMove 的值便可
if (this._hasMove) {
return this._hasMove
}
const clone: HTMLElement = el.cloneNode()
if (el._transitionClasses) {
el._transitionClasses.forEach((cls: string) => { removeClass(clone, cls) })
}
addClass(clone, moveClass)
clone.style.display = 'none'
this.$el.appendChild(clone)
const info: Object = getTransitionInfo(clone)
this.$el.removeChild(clone)
return (this._hasMove = info.hasTransform)
}
複製代碼
children.forEach(callPendingCbs)
children.forEach(recordPosition)
children.forEach(applyTranslation)
複製代碼
三個函數的處理分別以下
callPendingCbs()
:判斷每一個節點前一幀的過渡動畫是否執行完畢,若是沒有執行完,則提早執行 _moveCb()
和 _enterCb()
recordPosition()
:記錄每一個節點的新位置applyTranslation()
:分別獲取節點新舊位置,並計算差值,若存在差值,則經過設置節點的 transform
屬性將須要移動的節點位置偏移到以前的位置,爲列表 move
作準備function callPendingCbs (c: VNode) {
if (c.elm._moveCb) {
c.elm._moveCb()
}
if (c.elm._enterCb) {
c.elm._enterCb()
}
}
function recordPosition (c: VNode) {
c.data.newPos = c.elm.getBoundingClientRect()
}
function applyTranslation (c: VNode) {
const oldPos = c.data.pos
const newPos = c.data.newPos
const dx = oldPos.left - newPos.left
const dy = oldPos.top - newPos.top
if (dx || dy) {
c.data.moved = true
const s = c.elm.style
s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
s.transitionDuration = '0s'
}
}
複製代碼
move
過渡。遍歷前會經過獲取 document.body.offsetHeight
,從而發生計算,觸發迴流,讓瀏覽器進行重繪children
進行遍歷,期間若 vnode.data.moved
爲 true
,則執行 addTransitionClass
爲子節點加上 moveClass
,並將其 style.transform
屬性清空,因爲咱們在子節點預處理中已經將子節點偏移到了以前的舊位置,因此此時它會從舊位置過渡偏移到當前位置,這就是咱們要的 move
過渡的效果transitionend
過渡結束的監聽事件,在事件裏作一些清理的操做this._reflow = document.body.offsetHeight
children.forEach((c: VNode) => {
if (c.data.moved) {
const el: any = c.elm
const s: any = el.style
addTransitionClass(el, moveClass)
s.transform = s.WebkitTransform = s.transitionDuration = ''
el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
if (e && e.target !== el) {
return
}
if (!e || /transform$/.test(e.propertyName)) {
el.removeEventListener(transitionEndEvent, cb)
el._moveCb = null
removeTransitionClass(el, moveClass)
}
})
}
})
複製代碼
注:瀏覽器迴流觸發條件我稍微作個總結,好比瀏覽器窗口改變、計算樣式、對 DOM 進行元素的添加或者刪除、改變元素 class 等
- 添加或者刪除可見的DOM元素
- 元素位置改變
- 元素尺寸改變 —— 邊距、填充、邊框、寬度和高度
- 內容變化,好比用戶在 input 框中輸入文字,文本或者圖片大小改變而引發的計算值寬度和高度改變
- 頁面渲染初始化
- 瀏覽器窗口尺寸改變 —— resize 事件發生時
- 計算 offsetWidth 和 offsetHeight 屬性
- 設置 style 屬性的值
因爲 VDOM
在節點 diff
更新的時候是不能保證被移除元素它的一個相對位置。因此這裏須要在 beforeMount
鉤子函數裏面對 update
渲染邏輯重寫,來達到咱們想要的效果
update
方法,進行緩存this.kept
是緩存的上次的節點,而且裏面的節點增長了一些 transition
過渡屬性。這裏首先經過 setActiveInstance
緩存好當前實例,隨即對 vnode
進行 __patch__
操做並移除須要被移除掉的 vnode
,而後執行 restoreActiveInstance
將其實例指向恢復this.kept
賦值給 this._vnode
,使其觸發過渡update
渲染節點beforeMount () {
const update = this._update
this._update = (vnode, hydrating) => {
const restoreActiveInstance = setActiveInstance(this)
// force removing pass
this.__patch__(
this._vnode,
this.kept,
false, // hydrating
true // removeOnly (!important, avoids unnecessary moves)
)
this._vnode = this.kept
restoreActiveInstance()
update.call(this, vnode, hydrating)
}
}
複製代碼
setActiveInstance
export let activeInstance: any = null
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
複製代碼
文章到這就已經差很少了,對 transition
相關的內置組件 <transition>
以及 <transition-group>
的解析也已是結束了。不一樣的組件類型,一個抽象組件、一個則會渲染實際節點元素,想要作的事情倒是同樣的,初始化給用戶的 輸入
接口,輸入
後便可獲得 輸出
的過渡效果。
前端交流羣:731175396,熱烈歡迎各位妹紙,漢紙踊躍加入