Vue - The Good Parts: transition

前言

隨着你們對於交互更高的要求,在不少場景下咱們的交互設計是都會要求在頁面中適當的加入一些動畫來加強用戶的感知,或者有一些過渡效果來提高連貫性。css

在 Vue 中提供了十分友好、極其好用的過渡組件,能夠幫助咱們很容易的實現過渡動畫需求。So easy!html

那裏邊是如何實現的,有哪些是很值得咱們學習的?前端

正文分析

What

Vue 中 transition 相關的有兩個組件:單個過渡的 transition 組件以及列表過渡 transition-group 組件。vue

和過渡相關的工具大概:node

image2021-7-6_15-27-24.png

功能仍是不少,開發者徹底能夠根據本身的場景來決定使用什麼樣的工具去完成須要的過渡動效。git

典型的場景:github

image2021-7-6_15-33-3.png

當點擊 Toggle 的時候,下放的 hello 文本就會有一個透明度的過渡動效。web

整個過渡的過程能夠詳細的描述爲:瀏覽器

transition (1).png

還有列表過渡的示例:緩存

image2021-7-6_15-38-44.png

點擊 Shuffle 按鈕,會打散數的排列,即對 items 進行洗牌,會出現神奇的動畫過渡效果。

這些就是 Vue 中 transition 和 transition-group 組件提供的強大能力。

How

那如此神奇的組件究竟是怎麼實現的呢?咱們一塊兒來看下。

transition

先來看 transition,在 github.com/vuejs/vue/b…

// 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]
}
 
// in case the child is also an abstract component, e.g. <keep-alive>
// we want to recursively retrieve the real component to be rendered
// 獲得真實的 child,拋掉 abstract 的
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
  }
}
 
// 提取 transition 須要的數據 data,包括了 props 和 events
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
}
 
function placeholder (h: Function, rawChild: VNode): ?VNode {
  if (/\d-keep-alive$/.test(rawChild.tag)) {
    return h('keep-alive', {
      props: rawChild.componentOptions.propsData
    })
  }
}
 
function hasParentTransition (vnode: VNode): ?boolean {
  // 一直查找 vnode 的parent 直至沒有,只要有一層出現了 transition 則表明父級有 transition
  while ((vnode = vnode.parent)) {
    if (vnode.data.transition) {
      return true
    }
  }
}
 
function isSameChild (child: VNode, oldChild: VNode): boolean {
  return oldChild.key === child.key && oldChild.tag === child.tag
}
 
const isNotTextNode = (c: VNode) => c.tag || isAsyncPlaceholder(c)
 
const isVShowDirective = d => d.name === 'show'
 
export default {
  name: 'transition',
  props: transitionProps,
  // 抽象組件
  abstract: true,
 
  render (h: Function) {
    let children: any = this.$slots.default
    if (!children) {
      return
    }
 
    // filter out text nodes (possible whitespaces)
    children = children.filter(isNotTextNode)
    /* istanbul ignore if */
    if (!children.length) {
      return
    }
 
    // warn multiple elements
    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
      )
    }
 
    const mode: string = this.mode
 
    // warn invalid mode
    if (process.env.NODE_ENV !== 'production' &&
      mode && mode !== 'in-out' && mode !== 'out-in'
    ) {
      warn(
        'invalid <transition> mode: ' + mode,
        this.$parent
      )
    }
 
    const rawChild: VNode = children[0]
 
    // if this is a component root node and the component's
    // parent container node also has transition, skip.
    // 注意這裏用的是 $vnode
    if (hasParentTransition(this.$vnode)) {
      return rawChild
    }
 
    // apply transition data to child
    // use getRealChild() to ignore abstract components e.g. keep-alive
    const child: ?VNode = getRealChild(rawChild)
    /* istanbul ignore if */
    if (!child) {
      return rawChild
    }
 
    if (this._leaving) {
      return placeholder(h, rawChild)
    }
 
    // ensure a key that is unique to the vnode type and to this transition
    // component instance. This key will be used to remove pending leaving nodes
    // during entering.
    const id: string = `__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
 
    const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
    // 這裏用的是 _vnode
    const oldRawChild: VNode = this._vnode
    const oldChild: VNode = getRealChild(oldRawChild)
 
    // mark v-show
    // so that the transition module can hand over the control to the directive
    if (child.data.directives && child.data.directives.some(isVShowDirective)) {
      child.data.show = true
    }
    // 判斷 oldChild 以及 oldChild 和 新的是否是相同的
    // 正常狀況下,單個元素的狀況下,是不會進入的,由於 若是是從隱藏到顯示,old就是comment 若是是從顯示到隱藏 child 就沒有
    if (
      oldChild &&
      oldChild.data &&
      !isSameChild(child, oldChild) &&
      !isAsyncPlaceholder(oldChild) &&
      // #6687 component root is a comment node
      !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)
    ) {
      // replace old child transition data with fresh one
      // important for dynamic transitions!
      // 更新 vnode data transition 值
      const oldData: Object = oldChild.data.transition = extend({}, data)
      // handle transition mode
      // 多個元素,當前元素和新元素之間的動畫過渡模式 兩個 mode
      // 模式 out-in:當前元素先進行過渡,完成以後新元素過渡進入。
      if (mode === 'out-in') {
        // return placeholder node and queue update when leave finishes
        // 標記 _leaving,等待
        this._leaving = true
        // 監控 afterLeave
        mergeVNodeHook(oldData, 'afterLeave', () => {
          // reset & 更新
          this._leaving = false
          // 此時已經 out 完畢,執行 forceUpdate 走 show 而後 in 的邏輯了
          this.$forceUpdate()
        })
        // 考慮 keep-alive 場景,通常就會返回 undefined 了,也就是會觸發 patch 而後把元素 remove 掉,即 out 邏輯
        return placeholder(h, rawChild)
      } else if (mode === 'in-out') {
        // 模式 in-out:新元素先進行過渡,完成以後當前元素過渡離開。
        if (isAsyncPlaceholder(child)) {
          // 異步 先返回舊的
          return oldRawChild
        }
        let delayedLeave
        const performLeave = () => { delayedLeave() }
        // 監聽 afterEnter 鉤子 執行以前保留下來的 leave 回調邏輯
        // 這樣實現了先 in 後 out 的效果,由於是等到 afterEnter 以後才走的 leave 邏輯
        mergeVNodeHook(data, 'afterEnter', performLeave)
        mergeVNodeHook(data, 'enterCancelled', performLeave)
        // 回調設置,把 leave 執行的執行邏輯保留下來
        mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
      }
    }
 
    return rawChild
  }
}
複製代碼

能夠看出單純從組件定義上講,組件的 render() 主要作了這幾件事:

  • 獲得真實 children,獲得第一個 child,由於只容許有一個 child 因此這裏取第一個便可
  • 設置 id 和 key,確保惟一
  • 若是多個元素,根據 mode,決定監聽不一樣的 hook 進行處理

可是,從這些咱們是不可以理解咋執行的,那是由於還缺乏一部分核心的邏輯,在 github.com/vuejs/vue/b… 中實現的:

PS:須要依賴咱們在Vue - The Good Parts: 組件中講述的 modules 知識,也就是在 patch 的過程當中,會調用各個 module 的各個鉤子 'create', 'activate', 'update', 'remove', 'destroy',對應的就是 vnode 自己的一些更新。這裏最核心的就是利用了 create 和 remove 鉤子。

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 的邏輯:

export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
  const el: any = vnode.elm
 
  // call leave callback now
  // 若是 還沒等到 leave 就又更新了 那麼直接結束上次的 leave 至關於取消了 leave
  if (isDef(el._leaveCb)) {
    el._leaveCb.cancelled = true
    el._leaveCb()
  }
 
  const data = resolveTransition(vnode.data.transition)
  if (isUndef(data)) {
    return
  }
 
  /* istanbul ignore if */
  if (isDef(el._enterCb) || el.nodeType !== 1) {
    return
  }
  // 從 transition data 中直接獲取配置的一些 props 以及綁定的事件們
  const {
    css,
    type,
    enterClass,
    enterToClass,
    enterActiveClass,
    appearClass,
    appearToClass,
    appearActiveClass,
    beforeEnter,
    enter,
    afterEnter,
    enterCancelled,
    beforeAppear,
    appear,
    afterAppear,
    appearCancelled,
    duration
  } = data
 
  // activeInstance will always be the <transition> component managing this
  // transition. One edge case to check is when the <transition> is placed
  // as the root node of a child component. In that case we need to check
  // <transition>'s parent for appear check.
  // context 就是當前的 transition 組件實例
  let context = activeInstance
  let transitionNode = activeInstance.$vnode
  while (transitionNode && transitionNode.parent) {
    context = transitionNode.context
    transitionNode = transitionNode.parent
  }
  // 是否可見了已經
  const isAppear = !context._isMounted || !vnode.isRootInsert
  // 固然這裏依舊能夠經過 appear prop 屬性來強制改變狀態
  if (isAppear && !appear && appear !== '') {
    return
  }
  // 對 class 名字的一對處理
  const startClass = isAppear && appearClass
    ? appearClass
    : enterClass
  const activeClass = isAppear && appearActiveClass
    ? appearActiveClass
    : enterActiveClass
  const toClass = isAppear && appearToClass
    ? appearToClass
    : enterToClass
 
  // hook 各類處理
  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
  )
 
  if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
    checkDuration(explicitEnterDuration, 'enter', vnode)
  }
 
  const expectsCSS = css !== false && !isIE9
  const userWantsControl = getHookArgumentsLength(enterHook)
  // enter 回調,once 確保執行一次
  const cb = el._enterCb = once(() => {
    if (expectsCSS) {
      removeTransitionClass(el, toClass)
      removeTransitionClass(el, activeClass)
    }
    // 一樣會存在 cancel 狀況
    if (cb.cancelled) {
      if (expectsCSS) {
        removeTransitionClass(el, startClass)
      }
      enterCancelledHook && enterCancelledHook(el)
    } else {
      // 調用 afterEnter 的 hook
      afterEnterHook && afterEnterHook(el)
    }
    el._enterCb = null
  })
 
  if (!vnode.data.show) {
    // remove pending leave element on enter by injecting an insert hook
    mergeVNodeHook(vnode, 'insert', () => {
      const parent = el.parentNode
      // 動畫過程當中的當前元素,若是被插入了,那麼應該直接結束以前的 leave 動畫
      // 數據是掛載到 父元素的 _pending 屬性 prop 上的
      const pendingNode = parent && parent._pending && parent._pending[vnode.key]
      if (pendingNode &&
        pendingNode.tag === vnode.tag &&
        pendingNode.elm._leaveCb
      ) {
        pendingNode.elm._leaveCb()
      }
      // 調用 enter 的鉤子
      enterHook && enterHook(el, cb)
    })
  }
 
  // start enter transition
  // 調用 beforeEnter 鉤子
  beforeEnterHook && beforeEnterHook(el)
  if (expectsCSS) {
    // 增長 css 過渡的 class
    addTransitionClass(el, startClass)
    addTransitionClass(el, activeClass)
    // 下一幀即 requestAnimationFrame 簡稱 raf
    nextFrame(() => {
      // 移除掉 startClass
      removeTransitionClass(el, startClass)
      if (!cb.cancelled) {
        addTransitionClass(el, toClass)
        if (!userWantsControl) {
          // 利用 timeout 或者監聽 transitionend/animationend 結束事件 調用 cb
          if (isValidDuration(explicitEnterDuration)) {
            setTimeout(cb, explicitEnterDuration)
          } else {
            whenTransitionEnds(el, type, cb)
          }
        }
      }
    })
  }
 
  if (vnode.data.show) {
    toggleDisplay && toggleDisplay()
    enterHook && enterHook(el, cb)
  }
 
  if (!expectsCSS && !userWantsControl) {
    cb()
  }
}
複製代碼

enter 的時候最核心的就是在 nextFrame 中移除了 startClass,使得能夠有一幀去渲染且立刻移除進而作動畫。

再來看下 leave 的邏輯:

export function leave (vnode: VNodeWithData, rm: Function) {
  // rm 就是真正的 操做 DOM 移除元素的函數
  const el: any = vnode.elm
 
  // call enter callback now
  // 相對等的邏輯 還沒等到 enter 完成 就已經 leave 了
  if (isDef(el._enterCb)) {
    el._enterCb.cancelled = true
    el._enterCb()
  }
 
  const data = resolveTransition(vnode.data.transition)
  if (isUndef(data) || el.nodeType !== 1) {
    return rm()
  }
 
  /* istanbul ignore if */
  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)
  }
  // leave 完成的 cb
  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 && afterLeave(el)
    }
    el._leaveCb = null
  })
  // 若是是 delayLeave 即 in-out 模式
  if (delayLeave) {
    delayLeave(performLeave)
  } else {
    performLeave()
  }
  // 真正執行 leave 動畫邏輯
  function performLeave () {
    // the delayed leave may have already been cancelled
    if (cb.cancelled) {
      return
    }
    // record leaving element
    // 記錄下 移除中 的元素,注意和 enter 中對應,掛載到 父元素上
    if (!vnode.data.show && el.parentNode) {
      (el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key: any)] = vnode
    }
    // beforeLeave 鉤子
    beforeLeave && beforeLeave(el)
    if (expectsCSS) {
      // 增長 class
      addTransitionClass(el, leaveClass)
      addTransitionClass(el, leaveActiveClass)
      // 類似的邏輯 下一幀 移除 class
      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()
    }
  }
}
複製代碼

對比下 enter 的部分,發現基本上的大概邏輯仍是一致的。

總體仍是利用 transition data 傳遞了來自 transition 的 prop 值以及對應的監聽的事件鉤子函數,將他們掛載在 vnode.data 上,這樣在 transition module 中能夠直接根據 vnode 上掛載的相關數據直接進行操做。

大概總結總體的邏輯關係就是:

  • render() 根據新舊 children 的 vnode 信息決定返回的內容,以此決定了後續 patch 過程當中是走 create 仍是 remove 的鉤子;這個過程當中一樣會把 transition 組件上傳入的 props 以及相關事件監聽附在 vnode.data 上
  • 在 transition module 中,註冊了 create 和 remove 的鉤子,而後結合 vnode.data 中的 transition data 進行 leave 和 enter 相關的過渡動畫處理

transition-group

除了上邊分析的 transition 組件,Vue 還提供了在列表場景下的過渡組件 transition-group cn.vuejs.org/v2/guide/tr…

image2021-7-9_21-28-58.png

講述了 transition-group 組件的幾個特色以及相關的注意事項。

那麼這個組件詳細背後的邏輯是啥,一塊兒看看 github.com/vuejs/vue/b…

// extend 了 transition 組件的 props
const props = extend({
  tag: String,
  moveClass: String
}, transitionProps)
// 不支持 mode 了
delete props.mode
 
export default {
  props,
 
  beforeMount () {
    // beforeMount 鉤子中所作的事情
    // 劫持(代理了) _update 函數,咱們知道這個函數的核心是 執行 patch 以及 更新當前活動的組件實例 active instance
    const update = this._update
    this._update = (vnode, hydrating) => {
      // 當 _update 被調用的時候 傳入的 vnode 就是調用 render() 獲得的
      const restoreActiveInstance = setActiveInstance(this)
      // force removing pass
      // 此時是將上次的 vnode 和 kept(列表中須要保留的)vnode 進行 patch
      // 進而將那些須要移除的節點刪除
      this.__patch__(
        this._vnode,
        this.kept,
        false, // hydrating
        true // removeOnly (!important, avoids unnecessary moves)
      )
      // 更新此時的新的 _vnode 爲 kept 中的
      this._vnode = this.kept
      // 恢復 active instance
      restoreActiveInstance()
      // 執行本來的 patch 正常邏輯
      update.call(this, vnode, hydrating)
    }
  },
 
  render (h: Function) {
    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)
    // 首先遍歷如今的 children
    for (let i = 0; i < rawChildren.length; i++) {
      const c: VNode = rawChildren[i]
      if (c.tag) {
        // 記錄 tag 元素相關的
        // 必定要有 key,放入 children 中,且這個 key 不能是 自動生成的 key
        // 同時利用 map 作了一個按 key 緩存
        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}>`)
        }
      }
    }
    if (prevChildren) {
      const kept: Array<VNode> = []
      const removed: Array<VNode> = []
      // 遍歷以前的 children
      for (let i = 0; i < prevChildren.length; i++) {
        const c: VNode = prevChildren[i]
        // 保存好以前的 transition data
        c.data.transition = transitionData
        // 增長位置信息
        c.data.pos = c.elm.getBoundingClientRect()
        // 若是這個 key 的元素在新的裏邊也存在 那麼就放入 kept 中
        if (map[c.key]) {
          kept.push(c)
        } else {
          removed.push(c)
        }
      }
      // 保存須要保留的元素
      this.kept = h(tag, null, kept)
      // 保存須要刪除的元素 其實這個是沒有用的
      this.removed = removed
    }
    // 返回包含 children 的指定 tag 的 vnode 元素
    return h(tag, null, children)
  },
  updated () {
    // 以前的 children
    const children: Array<VNode> = this.prevChildren
    // move 的 class
    const moveClass: string = this.moveClass || ((this.name || 'v') + '-move')
    if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
      return
    }
    // 下面的就是實現和 https://cn.vuejs.org/v2/guide/transitions.html#%E5%88%97%E8%A1%A8%E7%9A%84%E6%8E%92%E5%BA%8F%E8%BF%87%E6%B8%A1 FLIP 相關動畫
    // we divide the work into three loops to avoid mixing DOM reads and writes
    // in each iteration - which helps prevent layout thrashing.
    // 全部的 children 一塊兒:
    // 1. 調用每一個 child 的 _moveCb 和 _enterCb,上一次還沒完成的,因此是 pending 的命名
    // 2. 記錄每一個 child 的位置信息
    // 3. 給每個應用 0s 的 位置差 transform 動畫 讓元素」恢復「在原位(位置差)
    children.forEach(callPendingCbs)
    children.forEach(recordPosition)
    children.forEach(applyTranslation)
 
    // force reflow to put everything in position
    // assign to this to avoid being removed in tree-shaking
    // $flow-disable-line
    // 強制 reflow 確保瀏覽器從新繪製到指定位置
    this._reflow = document.body.offsetHeight
 
    children.forEach((c: VNode) => {
      if (c.data.moved) {
        // 若是有移動
        const el: any = c.elm
        const s: any = el.style
        // 增長 move class
        addTransitionClass(el, moveClass)
        // 重置 transform & transitionDuration
        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
            // 移除 class
            removeTransitionClass(el, moveClass)
          }
        })
      }
    })
  },
 
  methods: {
    // 判斷是否應該應用移動 feat
    hasMove (el: any, moveClass: string): boolean {
      /* istanbul ignore if */
      if (!hasTransition) {
        return false
      }
      /* istanbul ignore if */
      // 緩存
      if (this._hasMove) {
        return this._hasMove
      }
      // Detect whether an element with the move class applied has
      // CSS transitions. Since the element may be inside an entering
      // transition at this very moment, we make a clone of it and remove
      // all other transition classes applied to ensure only the move class
      // is applied.
      const clone: HTMLElement = el.cloneNode()
      // clone 一個 將以前的 _transitionClasses 移除
      if (el._transitionClasses) {
        el._transitionClasses.forEach((cls: string) => { removeClass(clone, cls) })
      }
      // 只加上 move 的 class
      addClass(clone, moveClass)
      clone.style.display = 'none'
      this.$el.appendChild(clone)
      // 獲得新的 transition 信息
      const info: Object = getTransitionInfo(clone)
      // 移除這個 clone 的元素
      this.$el.removeChild(clone)
      // 判斷 動畫中有沒有作 transform 相關的動畫
      return (this._hasMove = info.hasTransform)
    }
  }
}
 
function callPendingCbs (c: VNode) {
  /* istanbul ignore if */
  if (c.elm._moveCb) {
    c.elm._moveCb()
  }
  /* istanbul ignore if */
  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'
  }
}
複製代碼

如今讓咱們梳理下上邊的邏輯搭配 Vue 的渲染邏輯的最終樣子:

  • beforeMount
    • 劫持了 _update
  • 第一次 render()
    • children 節點設置 transition data,經過上邊 transition 的分析咱們知道 只要節點上保存了 transition data 信息,就能夠執行對應的 transition 相關過渡動畫邏輯
  • 調用 _update
    • 約等於作了一次空 patch
    • 執行真正的原始 patch 邏輯
  • 當有更新的時候,第二次 render()
    • 核心對比,新舊 children,找出 kept 的節點數據
    • 返回新的 vnode 數據
  • 再次 _update
    • 進行一次 kept 和 現有的 vnode 數據的 patch,結論就是把須要刪除的元素移除掉
    • 而後進行正常的原始的 patch 邏輯,這個時候 patch 進行比較的是 kept 和 新獲得的 vnode 數據
  • updated
    • 根據新的 DOM 獲得最新的列表節點的位置信息
    • 利用 transform 根據元素位置差 將元素「恢復」到原位
    • 給元素增長 move 的 class(作過渡動畫)

代理 _update 實現了列表項的增長、刪除動畫邏輯,updated 鉤子中則完成了 move 這個 feature 的(如應用 FLIP 動畫)邏輯。

Why

我的認爲,大概的緣由,最核心的一個點,Vue 是一個框架,須要根據開發者的平常場景提供功能 feature(固然,選擇權在 Vue 團隊),考慮到在 Web 中過渡動畫類需求如此場景,尤爲是有了 CSS 相關的過渡動畫規範以後,並且基本沒啥兼容性了,固然考慮到利用 JS 精細控制過渡動畫,還提供了友好的鉤子,開發者能夠自行選擇。

並且,Vue 不但要作,仍是作到很好,經過上邊的分析,咱們知道了過渡相關組件提供的能力,基本能夠和官網說的同樣:惟一的限制是你的想象力。Vue 已經提供瞭如此靈活強大的組件,你的場景均可以知足!

總結

此次咱們基本分析了 Vue 中提供的兩個強大的過渡組件 transition 和 transition-group,對應的代碼雖然不是不少,可是給開發者提供的功能卻有不少,基本上涵蓋了平常開發過渡動畫的大多數場景,甚至藉助於其動態過渡能力,能夠很方便的自定義出知足複雜場景的過渡動畫效果。

那從 transition 以及 transition-group 的分析中,咱們能夠學到什麼呢?

過渡動畫

從上邊分析咱們知道,Vue 中實現過渡動畫分爲了三種過渡類名:

  • 過渡生效狀態——定義整個過渡階段,通常用於設置過渡時間、延遲等,會在整個動畫結束後移除
  • 過渡開始狀態——元素插入後下一幀就會移除
  • 過渡結束狀態——開始狀態移除後,就設置告終束狀態,會在動畫結束後移除

以及提供了鉤子(以Enter舉例):

  • before-enter 未進入以前,元素還沒插入
  • enter 元素插入以後
  • after-enter 過渡動畫結束後
  • enter-cancelled 過渡動畫被取消

不只僅知足了你們使用 CSS 作過渡動畫的場景,同時也知足了利用 JS 作動畫的訴求。API 或者類名切換的理解成本很低,這個是咱們能夠在本身抽象和設計過渡動畫相關的 API 的時候,能夠學習參考的。

代理模式

在 transition-group 中,利用對 _update 的代理,實現了一次更新,兩次 patch 的目標。代理模式是一種頗有用的模式,在不少場景中咱們都是可使用,這種方式意味着咱們能夠在不修改原有代碼的基礎上就能夠實現功能擴展,很符合開閉原則。

異步回調

在分析中,咱們看到了關於異步回調的處理,如 _enterCb、_leaveCb、_moveCb 這種處理,這些回調函數在正常流程中調用的話沒問題,可是會存在一些異常狀況,這裏的典型就是數據再次更新,再次須要 enter 或者 leave 怎麼辦,由於處理過程是一個異步的,這個時候須要清除上一次更新的影響,Vue 中此時的處理是統一都會調用這個回調,回調內部根據這個 cb 的 cancelled 屬性決定是取消模式仍是支持回調的。

同時這裏邊也能夠看到及時的消除引用關係,釋放內存。

模塊解耦

咱們此時已經知道了,transition 的功能實現依賴兩個:一個是 transition 組件的定義,一個是 transition module 模塊的鉤子處理。原本這部分應該是一個強耦合的邏輯,在 Vue 中,由於有了 vnode 的存在,他們能夠彼此解耦。

transition 過渡動畫的核心邏輯都在 transition module 中,也正是由於這個,很容易實現了 transition-group 組件。

這裏邊的另外一個解耦,就是利用鉤子的處理,咱們已經明顯感知到了他們存在。

延遲設值

在 transition 組件的 render 中存在這樣一段邏輯:

// mode === 'in-out'
let delayedLeave
const performLeave = () => { delayedLeave() }
// 監聽 afterEnter 鉤子 執行以前保留下來的 leave 回調邏輯
// 這樣實現了先 in 後 out 的效果,由於是等到 afterEnter 以後才走的 leave 邏輯
mergeVNodeHook(data, 'afterEnter', performLeave)
mergeVNodeHook(data, 'enterCancelled', performLeave)
// 回調設置,把 leave 執行的執行邏輯保留下來
mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
複製代碼

這是一段異步加異步的狀況,經過在 delayLeave 鉤子中,設置了 delayedLeave 這個回調的值,而後在其餘場景(時機)去調用這個 delayedLeave 回調。

同時這裏爲了確保 afterEnter 的鉤子函數是必定存在的,因此新增長了一個函數 performLeave,巧用了閉包的技巧實現了訪問後設置的 delayedLeave。

其餘小Tips

  • 在組件中對 vnode 的各類使用,例如如何獲取上一次的 children 信息,若是獲取當前 children 信息
  • mergeVNodeHook 是如何實現的 github.com/vuejs/vue/b…
  • FLIP 動畫 aerotwist.com/blog/flip-y…
  • 強制reflow
  • once 的實現以及應用
  • $forceUpdate API 以及其應用

滴滴前端技術團隊的團隊號已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。

相關文章
相關標籤/搜索