本篇文章是細談 vue 系列的第三篇,這篇文章主要會介紹一下 vue
的內建元件 transition
。前幾篇連結如下
開始之前,我們先看下官方對 transition
的定義
- 自動嗅探目標元素是否使用了 CSS 過渡或動畫,如果使用,會在合適的時機新增/移除 CSS 過渡 class。
- 如果過渡元件設定了 JavaScript 鉤子函式,這些鉤子函式將在合適的時機呼叫。
- 如果沒有檢測到 CSS 過渡/動畫,並且也沒有設定 JavaScript 鉤子函式,插入和/或刪除 DOM 的操作會在下一幀中立即執行
一、抽象元件
在 vue
中,對於 options
有一條屬性叫 abstract
,型別為 boolean
,他代表元件是否為抽象元件。但找尋了有關 options
的型別定義,卻只找到這麼一條
static options: Object;
複製程式碼
但實際上,vue
還是有對 abstract
屬性進行處理的,在 lifecycle.js
的 initLifecycle()
方法中有這麼一段邏輯
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
複製程式碼
從這段邏輯我們能看出,vue
會一層一層往上找父節點是否擁有 abstract
屬性,找到之後則直接將 vm
丟到其父節點的 $children
中,其本身的父子關係是直接被忽略的
然後在create-component.js
的 createComponent()
方法中對其處理如下
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
複製程式碼
二、舉個例子
分析之前,先看個官方 transition
的例子
<template>
<div id="example">
<button @click="show = !show">
Toggle render
</button>
<transition name="slide-fade">
<p v-if="show">hello</p>
</transition>
</div>
</template>
<script>
export default {
data () {
return {
show: true
}
}
}
</script>
<style lang="scss">
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to {
transform: translateX(10px);
opacity: 0;
}
</style>
複製程式碼
當點選按鈕切換顯示狀態時,被 transition
包裹的元素會有一個 css 過渡效果。接下來,我們將分析具體它是如何做到這種效果的。
三、transition 實現
從上面的用法中,我們應該能感知出,對於 <transition>
,vue
給我們提供了一整套 css 以及 js 的鉤子,這些鉤子本身都已經幫我定義好並繫結在例項上面,剩下的就是需要我們自己去重寫 css 或者 js 鉤子函式。其實這和 vue
本身的 hooks
實現非常類似,都會在不同的鉤子幫我們做好對應的不同的事情。
這種程式設計思維非常像函數語言程式設計中的思想,或者說它就是。
函數語言程式設計關心資料的對映,函數語言程式設計中的 lambda 可以看成是兩個型別之間的關係,一個輸入型別和一個輸出型別。你給它一個輸入型別的值,則可以得到一個輸出型別的值。
其實這個反映到這塊,在其不同的鉤子裡面進行不同程度的重寫,即可得到不同的輸出結果,當然這裡的輸出其實就是過渡效果,而這種方式不恰好是 輸入 => 輸出
的方式嗎?接下來我們就講一下這塊的具體實現。
<transtion>
元件的實現是 export
出 一個物件,從這裡我們能看出它會將預先設定好的 props
繫結到 transition
上,而後我們只需對其中的點進行輸入即可得到對應的樣式輸出,具體如何進行的過程,就不需要我們去考慮了
export default {
name: 'transition',
props: transitionProps,
abstract: true,
render (h: Function) {
// 具體 render 等會再看
}
}
複製程式碼
其中支援的 props
如下,你可以對 enter
和 leave
的樣式進行任意形式的重寫
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]
}
複製程式碼
接下來我們來看看 render
裡面的內容是如何對我們預先繫結好的東西進行 輸入 => 輸出
的一個轉換的
children
處理邏輯:首先從預設插槽中獲取到<transition>
包裹的子節點,隨後對其進行filter
過濾,將文字節點以及空格給過濾掉。如果不存在子節點則直接return
,如果子節點為多個,則報錯,因為<transition>
元件只能有一個子節點
let children: any = this.$slots.default
if (!children) {
return
}
children = children.filter(isNotTextNode)
if (!children.length) {
return
}
if (process.env.NODE_ENV !== 'production' && children.length > 1) {
warn(
'<transition> can only be used on a single element. Use ' +
'<transition-group> for lists.',
this.$parent
)
}
複製程式碼
model
處理邏輯:判斷mode
是否為in-out
和out-in
兩種模式,如果不是,直接報錯
const mode: string = this.mode
if (
process.env.NODE_ENV !== 'production' &&
mode && mode !== 'in-out' && mode !== 'out-in'
) {
warn(
'invalid <transition> mode: ' + mode,
this.$parent
)
}
複製程式碼
rawChild & child
處理邏輯:rawChild
為<transition>
元件包裹的第一個vnode
子節點,緊接著判斷其父容器是否也為transition
,如果是則直接返回rawChild
;隨後執行getRealChild()
方法獲取元件的真實子節點,不存在則返回rawChild
const rawChild: VNode = children[0]
if (hasParentTransition(this.$vnode)) {
return rawChild
}
const child: ?VNode = getRealChild(rawChild)
if (!child) {
return rawChild
}
複製程式碼
hasParentTransition()
: 往上一層一層尋找父節點是否也擁有transition
屬性
function hasParentTransition (vnode: VNode): ?boolean {
while ((vnode = vnode.parent)) {
if (vnode.data.transition) {
return true
}
}
}
複製程式碼
getRealChild()
:遞迴獲取第一個非抽象子節點並返回
function getRealChild (vnode: ?VNode): ?VNode {
const compOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (compOptions && compOptions.Ctor.options.abstract) {
return getRealChild(getFirstComponentChild(compOptions.children))
} else {
return vnode
}
}
複製程式碼
- 緊接著的就是對
child.key
和data
的處理了。首先通過id
和一系列條件對child.key
進行賦值操作,然後使用extractTransitionData
從元件例項上獲取過渡需要的data
資料
const id: string = `__transition-${this._uid}-` // 使用 this._uid 進行拼接
child.key = child.key == null // 若 child.key 為 null
? child.isComment // child 為註釋節點
? id + 'comment'
: id + child.tag
: isPrimitive(child.key) // 若 child.key 為 原始值
? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
: child.key
// 獲取過渡需要的資料
const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
if (child.data.directives && child.data.directives.some(isVShowDirective)) {
child.data.show = true
}
複製程式碼
extractTransitionData()
:這塊整體就是對輸入值props
以及內部events
的一個轉換處理,該方法引數為this
,即當前元件。首先遍歷當前元件的options.propsData
並賦值給data
,然後遍歷父元件的事件並將其賦值給data
。
export function extractTransitionData (comp: Component): Object {
const data = {}
const options: ComponentOptions = comp.$options
// props
for (const key in options.propsData) {
data[key] = comp[key]
}
// events.
// extract listeners and pass them directly to the transition methods
const listeners: ?Object = options._parentListeners
for (const key in listeners) {
data[camelize(key)] = listeners[key]
}
return data
}
複製程式碼
根據目前邏輯,我上面所舉的例子,獲取到的 child.data
如下
{
transition: {
name: 'slide-fade'
}
}
複製程式碼
- 最後則是對新舊
child
進行比較,並對部分鉤子函式進行hook merge
等操作
const oldRawChild: VNode = this._vnode
const oldChild: VNode = getRealChild(oldRawChild)
if (
oldChild &&
oldChild.data &&
!isSameChild(child, oldChild) &&
!isAsyncPlaceholder(oldChild) &&
!(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)
) {
// 將 oldData 更為為當前的 data
const oldData: Object = oldChild.data.transition = extend({}, data)
// 當 transition mode 為 out-in 的情況,返回一個 vnode 佔位,執行完 watch 進行更新
if (mode === 'out-in') {
this._leaving = true
mergeVNodeHook(oldData, 'afterLeave', () => {
this._leaving = false
this.$forceUpdate()
})
return placeholder(h, rawChild)
// 當 transition mode 為 in-out 的情況
} 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 })
}
}
複製程式碼
這裡就看下mergeVNodeHook()
、 placeholder()
和 $forceUpdate()
的邏輯,這裡就提一下 mergeVNodeHook
的邏輯:它會將 hook
函式合併到 def.data.hook[hookKey]
中,生成一個新的 invoker
// mergeVNodeHook
export function mergeVNodeHook (def: Object, hookKey: string, hook: Function) {
if (def instanceof VNode) {
def = def.data.hook || (def.data.hook = {})
}
let invoker
// 獲取已有 hook 賦值給 oldHook
const oldHook = def[hookKey]
function wrappedHook () {
hook.apply(this, arguments)
// 刪除合併的鉤子確保其只被呼叫一次,這樣能防止記憶體洩漏
remove(invoker.fns, wrappedHook)
}
// 如果 oldHook 不存在,則直接建立一個 invoker
if (isUndef(oldHook)) {
invoker = createFnInvoker([wrappedHook])
} else {
// oldHook 已經存在,則將 invoker 賦值為 oldHook
if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
invoker = oldHook
invoker.fns.push(wrappedHook)
} else {
// 現有的普通鉤子
invoker = createFnInvoker([oldHook, wrappedHook])
}
}
invoker.merged = true
def[hookKey] = invoker
}
// placeholder
function placeholder (h: Function, rawChild: VNode): ?VNode {
if (/\d-keep-alive$/.test(rawChild.tag)) {
return h('keep-alive', {
props: rawChild.componentOptions.propsData
})
}
}
// $forceUpdate
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
複製程式碼
四、enter & leave
介紹完 <transition>
元件的實現,我們瞭解到其在 render
階段對於 輸入值
props
以及部分 js 鉤子進行輸出
處理的過程。
或者我們可以這麼理解,<transition>
在 render
階段會獲取節點上的資料、在不同的 mode
下繫結了對應的鉤子函式以及其每個鉤子需要用到的 data
資料、且同時也會返回了 rawChild
vnode
節點。
但是,目前為止它卻並沒有在動畫這塊進行任何設計。so,接下來,我們將詳細談一下 transition
在 enter
以及 leave
這塊是如何進行 輸入 => 輸出
的。
首先在 src/platforms/web/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) {
if (vnode.data.show !== true) {
leave(vnode, rm)
} else {
rm()
}
}
} : {}
複製程式碼
從上面的程式碼我們能看出,在動畫處理這塊,它設定了兩個時機,分別是:
- 在
create
和activate
的時候執行enter()
remove
的時候執行leave()
1、enter
分析前,我們看下 enter()
設計脈絡
export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
const el: any = vnode.elm
// 如果 el 中存在 _leaveCb,則立即執行 _leaveCb()
if (isDef(el._leaveCb)) {
el._leaveCb.cancelled = true
el._leaveCb()
}
// 一系列處理,這裡忽略,後面會具體分析
}
複製程式碼
- 首先我們看下
enter
對於過渡資料是如何處理的:它會將vnode.data.transition
傳入給resolveTransition()
當引數,並對其進行解析進而拿到data
資料
const data = resolveTransition(vnode.data.transition)
// 如果 data 不存在,則直接返回
if (isUndef(data)) {
return
}
// 如果 el 中存在 _enterCb 或者 el 不是元素節點,則直接返回
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
複製程式碼
然後我們在看看 resolveTransition()
:該方法有一個引數為 vnode.data.transition
,它通過 autoCssTransition()
方法處理 name
屬性,並擴充到 vnode.data.transition
上並進行返回
export function resolveTransition (def?: string | Object): ?Object {
if (!def) {
return
}
if (typeof def === 'object') {
const res = {}
if (def.css !== false) {
extend(res, autoCssTransition(def.name || 'v'))
}
extend(res, def)
return res
} else if (typeof def === 'string') {
return autoCssTransition(def)
}
}
複製程式碼
其中 autoCssTransition()
具體邏輯如下:獲取到引數 name
後返回與 name
相關的 css class
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`
}
})
複製程式碼
- 隨即,對
<transition>
是子元件的根節點的邊界情況進行處理,我們需要對其父元件進行appear check
let context = activeInstance
let transitionNode = activeInstance.$vnode
// 往上查詢出 <transition> 是子元件的根節點的邊界情況,進行賦值
while (transitionNode && transitionNode.parent) {
context = transitionNode.context
transitionNode = transitionNode.parent
}
// 上下文例項沒有 mounted 或者 vnode 不是根節點插入的
const isAppear = !context._isMounted || !vnode.isRootInsert
if (isAppear && !appear && appear !== '') {
return
}
複製程式碼
- 對過渡的
class
、鉤子函式進行處理
// 過渡 class 處理
const startClass = isAppear && appearClass
? appearClass
: enterClass
const activeClass = isAppear && appearActiveClass
? appearActiveClass
: enterActiveClass
const toClass = isAppear && appearToClass
? appearToClass
: enterToClass
// 鉤子函式處理
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
複製程式碼
- 獲取其他配置
const explicitEnterDuration: any = toNumber(
isObject(duration)
? duration.enter
: duration
) // 獲取 enter 動畫執行時間
if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
checkDuration(explicitEnterDuration, 'enter', vnode)
}
// 過渡動畫是否受 css 影響
const expectsCSS = css !== false && !isIE9
// 使用者是否想介入控制 css 動畫
const userWantsControl = getHookArgumentsLength(enterHook)
複製程式碼
- 對
insert
鉤子函式進行合併
if (!vnode.data.show) {
// remove pending leave element on enter by injecting an insert hook
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)
})
}
複製程式碼
- 過渡動畫鉤子函式執行時機:先執行
beforeEnterHook
鉤子,並將 DOM 節點el
傳入,隨即判斷是否希望通過 css 來控制動畫,如果是true
則,執行addTransitionClass()
方法為節點加上startClass
和activeClass
。 - 然後執行
nextFrame()
進入下一幀,下一幀主要是移除掉上一幀增加好的class
;隨即判斷過渡是否取消,如未取消,則加上toClass
過渡類;之後如果使用者沒有通過enterHook
鉤子函式來控制動畫,此時若使用者指定了duration
時間,則執行setTimeout
進行duration
時長的延時,否則執行whenTransitionEnds
決定cb
的執行時機
beforeEnterHook && beforeEnterHook(el)
if (expectsCSS) {
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
nextFrame(() => {
removeTransitionClass(el, startClass)
if (!cb.cancelled) {
addTransitionClass(el, toClass)
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
}
})
}
複製程式碼
- 最後執行事先定義好
cb
函式:若動畫受css
控制,則移除掉toClass
和activeClass
;隨即判定過渡是否取消,若取消了,直接移除startClass
並執行enterCancelledHook
,否則繼續執行afterEnterHook
// enter 執行後不同動機對應不同的 cb 回撥處理
const cb = el._enterCb = once(() => {
if (expectsCSS) {
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass)
}
enterCancelledHook && enterCancelledHook(el)
} else {
afterEnterHook && afterEnterHook(el)
}
el._enterCb = null
})
複製程式碼
上面牽扯到的函式的程式碼具體如下:
nextFrame
:簡易版requestAnimationFrame
,引數為fn
,即下一幀需要執行的方法addTransitionClass
:為當前元素el
增加指定的class
removeTransitionClass
:移除當前元素el
指定的class
whenTransitionEnds
:通過getTransitionInfo
獲取到transition
的一些資訊,如type
、timeout
、propCount
,併為el
元素繫結上onEnd
。隨後不停執行下一幀,更新ended
值,直到動畫結束。則將el
元素的onEnd
移除,並執行cb
函式
// nextFrame
const raf = inBrowser
? window.requestAnimationFrame
? window.requestAnimationFrame.bind(window)
: setTimeout
: fn => fn()
export function nextFrame (fn: Function) {
raf(() => {
raf(fn)
})
}
// addTransitionClass
export function addTransitionClass (el: any, cls: string) {
const transitionClasses = el._transitionClasses || (el._transitionClasses = [])
if (transitionClasses.indexOf(cls) < 0) {
transitionClasses.push(cls)
addClass(el, cls)
}
}
// removeTransitionClass
export function removeTransitionClass (el: any, cls: string) {
if (el._transitionClasses) {
remove(el._transitionClasses, cls)
}
removeClass(el, cls)
}
// whenTransitionEnds
export function whenTransitionEnds (
el: Element,
expectedType: ?string,
cb: Function
) {
const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
if (!type) return cb()
const event: string = type === TRANSITION ? transitionEndEvent : animationEndEvent
let ended = 0
const end = () => {
el.removeEventListener(event, onEnd)
cb()
}
const onEnd = e => {
if (e.target === el) {
if (++ended >= propCount) {
end()
}
}
}
setTimeout(() => {
if (ended < propCount) {
end()
}
}, timeout + 1)
el.addEventListener(event, onEnd)
}
複製程式碼
2、leave
上面我們講完了 enter
這塊 輸入 => 輸出
的處理,它主要發生在元件插入後。接下來就是與之對應 leave
階段的 輸入 => 輸出
處理了,它主要發生在元件銷燬前。leave
和 enter
階段的處理非常類似,所以這裡我就不再贅述,大家自行閱讀即可。下面我稍微談下其中的 delayLeave
對於延時執行 leave
過渡動畫是如何設計的
export function leave (vnode: VNodeWithData, rm: Function) {
const el: any = vnode.elm
// call enter callback now
if (isDef(el._enterCb)) {
el._enterCb.cancelled = true
el._enterCb()
}
const data = resolveTransition(vnode.data.transition)
if (isUndef(data) || el.nodeType !== 1) {
return rm()
}
if (isDef(el._leaveCb)) {
return
}
const {
css,
type,
leaveClass,
leaveToClass,
leaveActiveClass,
beforeLeave,
leave,
afterLeave,
leaveCancelled,
delayLeave,
duration
} = data
const expectsCSS = css !== false && !isIE9
const userWantsControl = getHookArgumentsLength(leave)
const explicitLeaveDuration: any = toNumber(
isObject(duration)
? duration.leave
: duration
)
if (process.env.NODE_ENV !== 'production' && isDef(explicitLeaveDuration)) {
checkDuration(explicitLeaveDuration, 'leave', vnode)
}
const cb = el._leaveCb = once(() => {
if (el.parentNode && el.parentNode._pending) {
el.parentNode._pending[vnode.key] = null
}
if (expectsCSS) {
removeTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveActiveClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, leaveClass)
}
leaveCancelled && leaveCancelled(el)
} else {
rm()
afterLeave && afterLeave(el)
}
el._leaveCb = null
})
if (delayLeave) {
delayLeave(performLeave)
} else {
performLeave()
}
function performLeave () {
// the delayed leave may have already been cancelled
if (cb.cancelled) {
return
}
// record leaving element
if (!vnode.data.show && el.parentNode) {
(el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key: any)] = vnode
}
beforeLeave && beforeLeave(el)
if (expectsCSS) {
addTransitionClass(el, leaveClass)
addTransitionClass(el, leaveActiveClass)
nextFrame(() => {
removeTransitionClass(el, leaveClass)
if (!cb.cancelled) {
addTransitionClass(el, leaveToClass)
if (!userWantsControl) {
if (isValidDuration(explicitLeaveDuration)) {
setTimeout(cb, explicitLeaveDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
}
})
}
leave && leave(el, cb)
if (!expectsCSS && !userWantsControl) {
cb()
}
}
}
複製程式碼
delayLeave
是通過 resolveTransition(vnode.data.transition)
獲取到的函式,如果存在,則執行 delayLeave
,否則直接執行 performLeave
if (delayLeave) {
delayLeave(performLeave)
} else {
performLeave()
}
複製程式碼
能看出來 delayLeave
是一個函式,它本身是不做任何操作的,唯一要做的事情就是將 performLeave
作為回撥引數暴露給使用者去自行呼叫。
根據這個思路,我們簡單改造一下上面官方的例子,具體如下
<template>
<div id="example">
<button @click="show = !show">
Toggle render
</button>
<transition name="slide-fade" @delay-leave="handleDelay">
<p v-if="show">hello</p>
</transition>
</div>
</template>
<script>
export default {
data () {
return {
show: true
}
},
methods: {
handleDelay (done) {
setTimeout(() => {
done()
}, 2000)
}
}
}
</script>
<style lang="scss">
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to {
transform: translateX(10px);
opacity: 0;
}
</style>
複製程式碼
最終實現的效果就是,我的 leave
過渡效果將在 2S 後執行
總結
文章到這,對於內建 <transition>
元件的分析就介紹了。文章開篇到結尾,我都穿插了一個 輸入 => 輸出
的概念進去。<transition>
元件或者說 vue
本身的設計也正是如此,它在內部幫我們做好了 輸入 => 輸出
的處理,讓我們本身可以不用去管其內部,只需關注本身業務邏輯即可。
所以,目前看來,要把 <transition>
用的溜,還需要你自己 css
溜起來先
最後的最後,鄙人建了一個前端交流群:731175396
歡迎各位妹紙(算了漢紙也行吧 ~)加入,大家一起吹牛逼,聊人生,聊技術
個人準備重新撿回自己的公眾號了,之後每週保證一篇高質量好文,感興趣的小夥伴可以關注一波。