細談 vue - transition 篇

本篇文章是細談 vue 系列的第三篇,這篇文章主要會介紹一下 vue 的內置組件 transition 。前幾篇連接以下javascript

開始以前,咱們先看下官方對 transition 的定義前端

  1. 自動嗅探目標元素是否使用了 CSS 過渡或動畫,若是使用,會在合適的時機添加/移除 CSS 過渡 class。
  2. 若是過渡組件設置了 JavaScript 鉤子函數,這些鉤子函數將在合適的時機調用。
  3. 若是沒有檢測到 CSS 過渡/動畫,而且也沒有設置 JavaScript 鉤子函數,插入和/或刪除 DOM 的操做會在下一幀中當即執行

1、抽象組件

vue 中,對於 options 有一條屬性叫 abstract ,類型爲 boolean ,他表明組件是否爲抽象組件。但找尋了有關 options 的類型定義,卻只找到這麼一條vue

static options: Object;
複製代碼

但實際上,vue 仍是有對 abstract 屬性進行處理的,在 lifecycle.jsinitLifecycle() 方法中有這麼一段邏輯java

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 中,其自己的父子關係是直接被忽略的node

而後在create-component.jscreateComponent() 方法中對其處理以下web

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
  }
}
複製代碼

2、舉個例子

分析以前,先看個官方 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 過渡效果。接下來,咱們將分析具體它是如何作到這種效果的。app

3、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 以下,你能夠對 enterleave 的樣式進行任意形式的重寫

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-outout-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.keydata 的處理了。首先經過 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()
  }
}

複製代碼

4、enter & leave

介紹完 <transition> 組件的實現,咱們瞭解到其在 render 階段對於 輸入值 props 以及部分 js 鉤子進行輸出 處理的過程。

或者咱們能夠這麼理解,<transition>render 階段會獲取節點上的數據、在不一樣的 mode 下綁定了對應的鉤子函數以及其每一個鉤子須要用到的 data 數據、且同時也會返回了 rawChild vnode 節點。

可是,目前爲止它卻並無在動畫這塊進行任何設計。so,接下來,咱們將詳細談一下 transitionenter 以及 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()
    }
  }
} : {}

複製代碼

從上面的代碼咱們能看出,在動畫處理這塊,它設定了兩個時機,分別是:

  1. createactivate 的時候執行 enter()
  2. remove 的時候執行 leave()

一、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() 方法爲節點加上 startClassactiveClass
  • 而後執行 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 控制,則移除掉 toClassactiveClass;隨即斷定過渡是否取消,若取消了,直接移除 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
})

複製代碼

上面牽扯到的函數的代碼具體以下:

  1. nextFrame:簡易版 requestAnimationFrame,參數爲 fn,即下一幀須要執行的方法
  2. addTransitionClass:爲當前元素 el 增長指定的 class
  3. removeTransitionClass:移除當前元素 el 指定的 class
  4. whenTransitionEnds:經過 getTransitionInfo 獲取到 transition 的一些信息,如 typetimeoutpropCount,併爲 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)
}

複製代碼

二、leave

上面咱們講完了 enter 這塊 輸入 => 輸出 的處理,它主要發生在組件插入後。接下來就是與之對應 leave 階段的 輸入 => 輸出 處理了,它主要發生在組件銷燬前。leaveenter 階段的處理很是相似,因此這裏我就再也不贅述,你們自行閱讀便可。下面我稍微談下其中的 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

歡迎各位妹紙(算了漢紙也行吧 ~)加入,你們一塊兒吹牛逼,聊人生,聊技術

我的準備從新撿回本身的公衆號了,以後每週保證一篇高質量好文,感興趣的小夥伴能夠關注一波。

相關文章
相關標籤/搜索