在學習 element ui 時辱士,發(fā)現(xiàn)組件的過渡用的是 Vue.js 提供的 <transition> 標簽锭沟。這里來好好認識下 vue 的過渡到底是如何工作的。
簡介
廢話不多說识补,詳細的內(nèi)容請看官方文檔族淮,里面有詳細的分析和例子夠你看懂了(就是費時間~)。簡單說說我對 vue 過渡的理解凭涂。經(jīng)過一下午的折騰祝辣,總結(jié)出以下幾點:
-
有四種情況會觸發(fā)過渡效果:
1 v-if
2 v-show
3 動態(tài)組件(如 component 的 is 屬性)
4 組件根節(jié)點發(fā)生變化(如 v-if v-else 切換根節(jié)點) - 過渡效果 CSS 命名規(guī)律:(name 屬性,默認為 v)-(行為:enter切油、leave蝙斜、appear、move)-(階段:無澎胡、active孕荠、to)
-
有三種方式來設(shè)置過渡樣式:
1 為 <transition> 標簽設(shè)定 name 屬性娩鹉。
2 在 <transition> 標簽中插入enter-active-class
等設(shè)置自定義過渡類名。
3 使用 JavaScript 在過渡的鉤子處修改過渡樣式稚伍。 - 個人理解:
<transition>
標簽用于單個元素的進入和離開效果弯予。<transition-group>
標簽用于處理如v-for
遍歷這樣多個元素的過渡動畫。
自己實現(xiàn)個過渡方法
先來兩個簡單例子理解下 transition(為了節(jié)省篇幅和便于查看寫在 JSFiddle 中)有興趣的朋友可以看下~
例1:v-enter 和 v-leave 簡單實現(xiàn)
例2:v-move 簡單實現(xiàn)
transition 學習
1. 基本原理是什么个曙?
基本原理還是 CSS3 的 transition
锈嫩、transform
、animation
這幾個屬性垦搬。用戶定義過渡效果呼寸,Vue.js 進行處理。下面我們通過 <transition> 過渡的進入的過程看一下:
- 插入元素
- 解析 <transition> 標簽猴贰,獲取對應(yīng)的過渡類名对雪。這里默認就
v-
開頭了。 - 為元素定義 v-enter 和 v-enter-active 兩個類米绕。
class="v-enter v-enter-active"
慌植。 - 下一幀移除 v-enter,添加 v-enter-to义郑。
class="v-enter-active v-enter-to"
蝶柿。 - 獲取過渡時間,延時執(zhí)行回調(diào)函數(shù)非驮。
- 回調(diào)函數(shù)中移除 v-enter交汤、v-enter-active 和 v-enter-to 的這些過渡類名,完成過渡劫笙。
- 在整個過程中調(diào)用了
beforeEnterHook
芙扎、enterHook
、afterEnterHook
填大、enterCancelledHook
四個函數(shù)戒洼,執(zhí)行相應(yīng)的 JavaScript 鉤子。
下面是 enter
函數(shù)的代碼及注釋:
// 進入過渡效果
export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
const el: any = vnode.elm
// call leave callback now 執(zhí)行 leave 回調(diào)函數(shù)
if (isDef(el._leaveCb)) {
el._leaveCb.cancelled = true
el._leaveCb()
}
// 解析 transition 的數(shù)據(jù)(class允华、tag圈浇、name等)
const data = resolveTransition(vnode.data.transition)
if (isUndef(data)) {
return
}
/* istanbul ignore if */
if (isDef(el._enterCb) || el.nodeType !== 1) {
return
}
const {
css,
type,
enterClass,
enterToClass,
enterActiveClass,
appearClass,
appearToClass,
appearActiveClass,
beforeEnter,
enter,
afterEnter,
enterCancelled,
beforeAppear,
appear,
afterAppear,
appearCancelled,
duration
} = data
// 將作為子組件的根節(jié)點放置時,我們需要檢查 <transition> 的父元素是否出現(xiàn)檢查靴寂。
let context = activeInstance
let transitionNode = activeInstance.$vnode
while (transitionNode && transitionNode.parent) {
transitionNode = transitionNode.parent
context = transitionNode.context
}
const isAppear = !context._isMounted || !vnode.isRootInsert
if (isAppear && !appear && appear !== '') {
return
}
// 獲取進入的 class
// v-enter
const startClass = isAppear && appearClass
? appearClass
: enterClass
// v-enter-active
const activeClass = isAppear && appearActiveClass
? appearActiveClass
: enterActiveClass
// v-enter-to
const toClass = isAppear && appearToClass
? appearToClass
: enterToClass
// 4個生命周期鉤子函數(shù)
const beforeEnterHook = isAppear
? (beforeAppear || beforeEnter)
: beforeEnter
const enterHook = isAppear
? (typeof appear === 'function' ? appear : enter)
: enter
const afterEnterHook = isAppear
? (afterAppear || afterEnter)
: afterEnter
const enterCancelledHook = isAppear
? (appearCancelled || enterCancelled)
: enterCancelled
// https://cn.vuejs.org/v2/guide/transitions.html#顯性的過渡持續(xù)時間
const explicitEnterDuration: any = toNumber(
isObject(duration)
? duration.enter
: duration
)
if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
checkDuration(explicitEnterDuration, 'enter', vnode)
}
const expectsCSS = css !== false && !isIE9
const userWantsControl = getHookArgumentsLength(enterHook)
// 完成進入過渡后的回調(diào)函數(shù)
const cb = el._enterCb = once(() => {
if (expectsCSS) {
// 移除 v-enter-to 和 v-enter-active
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
if (cb.cancelled) {
if (expectsCSS) {
// 移除 v-enter
removeTransitionClass(el, startClass)
}
// 調(diào)用 enter-cancelled
enterCancelledHook && enterCancelledHook(el)
} else {
afterEnterHook && afterEnterHook(el)
}
el._enterCb = null
})
if (!vnode.data.show) {
// 通過注入一個 insert 鉤子磷蜀,將待處理的 leave 元素移除。
mergeVNodeHook(vnode, 'insert', () => {
const parent = el.parentNode
const pendingNode = parent && parent._pending && parent._pending[vnode.key]
if (pendingNode &&
pendingNode.tag === vnode.tag &&
pendingNode.elm._leaveCb
) {
pendingNode.elm._leaveCb()
}
enterHook && enterHook(el, cb)
})
}
// start enter transition
beforeEnterHook && beforeEnterHook(el)
// 預(yù)期 CSS
if (expectsCSS) {
// 添加 v-enter v-enter-active
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
// 下一幀
nextFrame(() => {
// 移除 v-enter
removeTransitionClass(el, startClass)
if (!cb.cancelled) {
// 添加 v-enter-to
addTransitionClass(el, toClass)
if (!userWantsControl) {
// 預(yù)期進入時間
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
// 當 transition 結(jié)束
whenTransitionEnds(el, type, cb)
}
}
}
})
}
if (vnode.data.show) {
toggleDisplay && toggleDisplay()
enterHook && enterHook(el, cb)
}
if (!expectsCSS && !userWantsControl) {
cb()
}
}
2. 過渡的類名和自定義過渡的類名如何用于 <transition> 中百炬?
在 <transition> 中一共有如下屬性(props):
export const transitionProps = {
name: String,
appear: Boolean,
css: Boolean,
mode: String,
type: String,
enterClass: String,
leaveClass: String,
enterToClass: String,
leaveToClass: String,
enterActiveClass: String,
leaveActiveClass: String,
appearClass: String,
appearActiveClass: String,
appearToClass: String,
duration: [Number, String, Object]
}
可以看到其中就有這些自定義過渡類名褐隆,如 enterClass。這些屬性如被傳入到 <transition> 子組件的 data.transition 對象中剖踊。
// extractTransitionData 函數(shù)返回組件的所有 propsData 和 listener
const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
而這個 data.transition 對象在 enter 函數(shù)中用到:
const data = resolveTransition(vnode.data.transition)
resolveTransition
函數(shù):
// 解析 transition 過渡 CSS
export function resolveTransition (def?: string | Object): ?Object {
if (!def) {
return
}
// 合并過渡類名和自定義過渡類名
if (typeof def === 'object') {
const res = {}
if (def.css !== false) {
// 使用 name庶弃,默認為 v
extend(res, autoCssTransition(def.name || 'v'))
}
extend(res, def)
return res
} else if (typeof def === 'string') {
return autoCssTransition(def)
}
}
// 通過 name 屬性獲取過渡 CSS 類名
const autoCssTransition: (name: string) => Object = cached(name => {
return {
enterClass: `${name}-enter`,
enterToClass: `${name}-enter-to`,
enterActiveClass: `${name}-enter-active`,
leaveClass: `${name}-leave`,
leaveToClass: `${name}-leave-to`,
leaveActiveClass: `${name}-leave-active`
}
})
resolveTransition 函數(shù)合并了過渡類名和自定義過渡類名衫贬,返回最終的過渡類名。之后就是使用這些類名來實現(xiàn)過渡動畫歇攻。
PS:從源碼中可以知道自定義過渡類名要優(yōu)先于 name 定義的過渡類名固惯。
小結(jié)一下就是:Vue.js 通過 <transition> 的 props 獲取自定義過渡類名,通過 <transition> 的 name 屬性解析獲取過渡類名掉伏,兩者合并成為最終過渡類名,用以實現(xiàn)過渡效果澳窑。
3. JavaScript 鉤子如何實現(xiàn)斧散?
從 enter
函數(shù)中可以知道,在特定時間點會調(diào)用指定 JavaScript 鉤子函數(shù)摊聋,所以我們只需綁定好函數(shù)即可按時間點觸發(fā)鸡捐。像這樣:
enterHook && enterHook(el, cb)
4. transition 組件和 transition-group 標簽的基本原理是什么?
其實就是 Vue.js 的組件麻裁,在其中實現(xiàn)了過渡效果而已箍镜。
transition 中只能包含一個子元素,標簽通過 render 函數(shù)來渲染子元素(不渲染自身煎源,所以我們在 DOM 中看不到 transition 節(jié)點)色迂。主要用于控制元素的進入和離開,當元素離開后元素就從 DOM 中移除了手销。
transition-group 可以包含多個子元素歇僧,也是用 render 函數(shù),渲染為指定標簽名的元素锋拖。相比 transition 多了一個 v-move 屬性用于控制多個組件間的移動速度诈悍。
5. v-if、v-show兽埃、component 等組件變化如何監(jiān)聽侥钳?
在使用 v-if、v-else 和 component 切換組件的時候柄错,v-if舷夺、v-else 需要傳入 key 以區(qū)分相同標簽的不同元素。而 component 標簽不需要售貌。在代碼中會解析 key 和 component 名組成新的 key冕房,所以兩個不同的 component 也會擁有不同的 key 實現(xiàn)切換效果。
var id = "__transition-" + (this._uid) + "-";
child.key = child.key == null
? child.isComment
? id + 'comment'
: id + child.tag
: isPrimitive(child.key)
? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
: child.key;
而對于 v-show趁矾,做了特殊標記 —— 當有 v-show 指令時標記 child.data.show 為 true:
if (child.data.directives && child.data.directives.some(d => d.name === 'show')) {
child.data.show = true
}
之后再過渡的邏輯中對 v-show 做了些處理耙册,實現(xiàn)過渡效果。
同時毫捣,在 v-show
的源碼 src/platforms/web/runtime/directives/show.js
中對于 transition 也做了一些處理详拙。比如在 update 方法中獲取 transition帝际,如果有過渡則 v-show 使用過渡效果,否則使用 style.display
來隱藏元素饶辙。
update (el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {
if (value === oldValue) return
vnode = locateNode(vnode)
// 過渡效果
const transition = vnode.data && vnode.data.transition
if (transition) {
vnode.data.show = true
if (value) {
enter(vnode, () => {
el.style.display = el.__vOriginalDisplay
})
} else {
leave(vnode, () => {
el.style.display = 'none'
})
}
} else {
// 隱藏
el.style.display = value ? el.__vOriginalDisplay : 'none'
}
},
6. transition 中兩個相同標簽的組件為何要用 key 分開蹲诀?
使用 key 和 tagName 來判斷是否為同一個節(jié)點。
function isSameChild (child: VNode, oldChild: VNode): boolean {
return oldChild.key === child.key && oldChild.tag === child.tag
}
8. 過渡邏輯和過渡組件如何作用于一起
在源碼中有四個過渡相關(guān)的源碼:
-
src/platforms/web/runtime/components/transition.js
<transition> 組件源碼弃揽。 -
src/platforms/web/runtime/components/transition-group.js
<transition-group> 組件源碼 -
src/platforms/web/runtime/transition-util.js
過渡工具代碼脯爪。 -
src/platforms/web/runtime/modules/transition.js
過渡邏輯代碼。
前三個很好理解矿微,最后一個 transition.js 其實是在 patch 方法中和 v-show 中使用的~
// src/platforms/web/runtime/directives/show.js
import { enter, leave } from '../modules/transition'
v-show 中調(diào)用了 transition 的 enter 和 leave 函數(shù)痕慢,在 v-show 作用于過渡效果時調(diào)用。
另外一個使用的地方比較隱蔽涌矢,先來看看 transition.js 導(dǎo)出的內(nèi)容:
// src/platforms/web/runtime/modules/transition.js
function _enter (_: any, vnode: VNodeWithData) {
if (vnode.data.show !== true) {
enter(vnode)
}
}
export default inBrowser ? {
create: _enter,
activate: _enter,
remove (vnode: VNode, rm: Function) {
/* istanbul ignore else */
if (vnode.data.show !== true) {
leave(vnode, rm)
} else {
rm()
}
}
} : {}
這里講 enter 和 leave 函數(shù)在方法中使用并導(dǎo)出(如果是瀏覽器的話)掖举。繼續(xù)往下找:
// src/platforms/web/runtime/modules/index.js
import transition from './transition'
導(dǎo)入到 modules 文件夾 index.js,index.js 在 patch.js 中使用了娜庇。
// src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
在此處合并 modules塔次,并且創(chuàng)建了 patch 方法。這個 patch 方法在之前寫的Vue.js 源碼學習六 —— VNode虛擬DOM學習中提到過名秀,用于對比虛擬 DOM励负,實現(xiàn)差異化更新。
可以看下 modules 在 createPatchFunction 方法中做了些什么匕得?
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
...
}
這里可以發(fā)現(xiàn) transition.js 中導(dǎo)出的 create熄守、activate 和 remove 方法都是 patch 的生命周期函數(shù)。也就是說當元素創(chuàng)建耗跛、激活裕照、移除行為時就會執(zhí)行 transition.js 中的邏輯,而 <transition> 和 <transition-group> 組件都會有組件的這些行為调塌。Vue.js 很巧妙的將組件相關(guān)行為都交給了 patch 的生命周期去處理晋南,學習了!
8. 過渡模式 mode 的實現(xiàn)原理是啥
在 <transition> 組件的 render 函數(shù)中有這么一段:
// 控制離開/進入的過渡時間序列羔砾。有效的模式有 "out-in" 和 "in-out"负间;默認同時生效。
if (mode === 'out-in') {
// return placeholder node and queue update when leave finishes
this._leaving = true
mergeVNodeHook(oldData, 'afterLeave', () => {
this._leaving = false
this.$forceUpdate()
})
return placeholder(h, rawChild)
} else if (mode === 'in-out') {
if (isAsyncPlaceholder(child)) {
return oldRawChild
}
let delayedLeave
const performLeave = () => { delayedLeave() }
mergeVNodeHook(data, 'afterEnter', performLeave)
mergeVNodeHook(data, 'enterCancelled', performLeave)
mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
}
這里就是 mode 的實現(xiàn)代碼了姜凄,先看看兩種 mode 的用法
- in-out:新元素先進行過渡政溃,完成之后當前元素過渡離開。
- out-in:當前元素先進行過渡态秧,完成之后新元素過渡進入董虱。
可以看到,在 out-in 邏輯中,當切換元素時愤诱,先不渲染第二個組件而是返回云头,之后才會返回 placeholder 函數(shù)結(jié)果,當?shù)谝粋€元素完全 leave 后加載第二個元素淫半。而在 in-out 元素中做的是將第一個元素延時到第二個元素 enter 后再 leave溃槐。
9. <transition-group> 的 v-move 重新排序一組內(nèi)容,如何實現(xiàn)的移動變化科吭?
比如我們有一個 1-5 的數(shù)組使用 v-for 遍歷顯示到 transition-group 中昏滴,當數(shù)組發(fā)生變化時,會做如下操作:
- 初始數(shù)組
[ 1, 2, 3, 4, 5 ]
- 數(shù)組發(fā)生變化
[ 1, 4, 3, 2, 5 ]
- 在 render 函數(shù)中記錄變化前后額數(shù)組 preChildren 和 children 兩個 VNode 數(shù)組对人。
- 在 render 函數(shù)中使用 getBoundingClientRect() 方法記錄變化前每個元素的位置 oldPos谣殊。
- 獲取要保留和移除的元素數(shù)組。
- 渲染變化后的數(shù)組元素规伐。
- 在 beforeUpdate 方法中使用 patch 方法移除要移除的元素蟹倾。
- 進入 Updated 方法中匣缘,注意此時渲染結(jié)果已經(jīng)是新數(shù)組
[ 1, 4, 3, 2, 5 ]
了猖闪。 - 獲取過渡類名和子元素數(shù)組 children。
- 遍歷調(diào)用
- 執(zhí)行回調(diào)函數(shù)
- 計算當前各元素位置 newPos
- 根據(jù) oldPos 和 newPos肌厨,使用內(nèi)聯(lián)樣式
translate(${dx}px,${dy}px)
將元素移動到之前的位置培慌,看著就像是[ 1, 2, 3, 4, 5 ]
- 最后遍歷元素,添加 moveClass 類名柑爸,移除
translate(${dx}px,${dy}px)
內(nèi)聯(lián)樣式吵护。綁定 transitionend 事件。
代碼太長表鳍,就不多貼了~可以點擊這里跳轉(zhuǎn)查看馅而。總結(jié)下來就是先改變元素,然后把元素移動成之前的樣子譬圣,然后使用過渡類名定義過渡時間實現(xiàn)過渡效果瓮恭。
v-move 的關(guān)鍵就是“假裝元素位置沒變”的行為。讓我們看上去像是慢慢移動的厘熟。
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) {
// 定義 0 秒的 translate 內(nèi)聯(lián)樣式把元素移動到原來的樣子
c.data.moved = true
const s = c.elm.style
s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
s.transitionDuration = '0s'
}
}
10. vue 的 transition 和 CSS3 的 transition 有何不同屯蹦?
基本原理都是使用了 CSS3 的 transition,但是 Vue 的 transition 組件是配合著 VDOM 來寫的绳姨、同時提供了過渡各階段效果的 CSS 和 JS 控制登澜,便于我們快捷、精確飘庄、安全地實現(xiàn)一些簡單或復(fù)雜的過渡效果脑蠕。
最后
原本只是想看看 transition 如何實現(xiàn),卻扯出這么一堆問題跪削。其中關(guān)于 transition 和 transition-group 組件講的有點草率空郊,有興趣可以再深入學習下~
從本次學習中我學到了:
- 更加優(yōu)雅高效的 JS 邏輯寫法(patch 中的生命周期統(tǒng)一處理 DOM 操作中的邏輯)
- 更加熟悉 CSS3 的 transition 過渡屬性份招。
- 解決了我對 transition 的各種疑問。
OK狞甚,關(guān)于 Vue 的過渡效果就聊到這兒了锁摔,寫了三天……我得去休息休息了 0.0