VueJSソース学習-domの挿入と移動時の要素の遷移論理


src/transition
原文アドレス項目アドレス
vueでのtransition効果の使用については、公式サイトでは以下のように説明されています.
With Vue.js’ transition system you can apply automatic transition effects when elements are inserted into or removed from the DOM. Vue.js will automatically add/remove CSS classes at appropriate times to trigger CSS transitions or animations for you, and you can also provide JavaScript hook functions to perform custom DOM manipulations during the transition.
要素がDOMツリーに挿入されたり、DOMツリーから削除されたりすると、transitionプロパティは変換の効果を提供し、cssを使用して変化効果を定義したり、JSを使用して定義したりすることができます.
src/transition/index.js
import {
  before,
  remove,
  transitionEndEvent
} from '../util/index'

/**
 * Append with transition.
 *
 * @param {Element} el
 * @param {Element} target
 * @param {Vue} vm
 * @param {Function} [cb]
 */

export function appendWithTransition (el, target, vm, cb) {
  applyTransition(el, 1, function () {
    target.appendChild(el)
  }, vm, cb)
}
...

まず最初の関数は要素をDOMに挿入し、関数実装はapplyTransitionを呼び出し、実装コードは以下の通りである.
/**
 * Apply transitions with an operation callback.
 *
 * @param {Element} el
 * @param {Number} direction
 *                  1: enter
 *                 -1: leave
 * @param {Function} op - the actual DOM operation
 * @param {Vue} vm
 * @param {Function} [cb]
 */

export function applyTransition (el, direction, op, vm, cb) {
  var transition = el.__v_trans
  if (
    !transition ||
    // skip if there are no js hooks and CSS transition is
    // not supported
    (!transition.hooks && !transitionEndEvent) ||
    // skip transitions for initial compile
    !vm._isCompiled ||
    // if the vm is being manipulated by a parent directive
    // during the parent's compilation phase, skip the
    // animation.
    (vm.$parent && !vm.$parent._isCompiled)
  ) {
    op()
    if (cb) cb()
    return
  }
  var action = direction > 0 ? 'enter' : 'leave'
  transition[action](op, cb)
}

良いコードを書くのはドキュメントで、注釈と命名の上でこの関数の作用をよく理解することができて、elは操作する要素で、directionは挿入するか削除するかを代表して、opは具体的な操作方法の関数を代表して、vmは前のコードあるいは公式のドキュメントからvueの実例のオブジェクトを指すことを知ることができて、cbはコールバックの関数です
vue解析後のtransitionをDOM要素の属性__とするv_transは、DOMを操作するたびに次のように判断します.
  • 要素が定義されていない場合transition
  • 要素にjshookがない場合、css transitionの定義は
  • をサポートしません.
  • 要素がまだコンパイルされていない場合
  • 要素に親要素があり、親要素がコンパイルされていない場合
  • 以上のいずれかの場合は、操作方法opを変更せずに直接実行し、そうでなければ実行する.
    var action = direction > 0 ? 'enter' : 'leave'
    transition[action](op, cb)

    追加に加えて、挿入と削除の2つの操作方法があります.
    export function beforeWithTransition (el, target, vm, cb) {
      applyTransition(el, 1, function () {
        before(el, target)
      }, vm, cb)
    }
    
    export function removeWithTransition (el, vm, cb) {
      applyTransition(el, -1, function () {
        remove(el)
      }, vm, cb)
    }

    ではtransitoinはel._v_transはどのように実現したのか、これはまだ深く掘らなければならない.
    src/transition/queue.js
    import { nextTick } from '../util/index'
    
    let queue = []
    let queued = false
    
    /**
     * Push a job into the queue.
     *
     * @param {Function} job
     */
    
    export function pushJob (job) {
      queue.push(job)
      if (!queued) {
        queued = true
        nextTick(flush)
      }
    }
    
    /**
     * Flush the queue, and do one forced reflow before
     * triggering transitions.
     */
    
    function flush () {
      // Force layout
      var f = document.documentElement.offsetHeight
      for (var i = 0; i < queue.length; i++) {
        queue[i]()
      }
      queue = []
      queued = false
      // dummy return, so js linters don't complain about
      // unused variable f
      return f
    }
    

    これはtransitionの3つのファイルの2つ目で、字面量から理解すると1つのキューであり、コードから見るとpushJobを呼び出すたびにタスクキューにタスクをプッシュし、falseであればnextTickでqueuedをtrueに設定してflushメソッドを同時に呼び出す識別queuedがあります.このメソッドは、タスクキューqueueにあるすべてのメソッドを実行し、queuedをfalseに設定します.
    nexttickの実現を覚えていますか?src/util/envで実装:
    
    /**
     * Defer a task to execute it asynchronously. Ideally this
     * should be executed as a microtask, so we leverage
     * MutationObserver if it's available, and fallback to
     * setTimeout(0).
     *
     * @param {Function} cb
     * @param {Object} ctx
     */
    
    export const nextTick = (function () {
      var callbacks = []
      var pending = false
      var timerFunc
      function nextTickHandler () {
        pending = false
        var copies = callbacks.slice(0)
        callbacks = []
        for (var i = 0; i < copies.length; i++) {
          copies[i]()
        }
      }
      /* istanbul ignore if */
      if (typeof MutationObserver !== 'undefined') {
        var counter = 1
        var observer = new MutationObserver(nextTickHandler)
        var textNode = document.createTextNode(counter)
        observer.observe(textNode, {
          characterData: true
        })
        timerFunc = function () {
          counter = (counter + 1) % 2
          textNode.data = counter
        }
      } else {
        timerFunc = setTimeout
      }
      return function (cb, ctx) {
        var func = ctx
          ? function () { cb.call(ctx) }
          : cb
        callbacks.push(func)
        if (pending) return
        pending = true
        timerFunc(nextTickHandler, 0)
      }
    })()
    

    公式サイトの解釈は以下の通りです.
    Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update.
    すなわち、次のDOM更新サイクルでコールバックを実行し、DOMノードの更新を待つ必要がある場合に実行するための簡単な方法はsettimeout関数を利用することであり、settimeoutメソッドはコールバック関数をタイムキューに入れ、カウント終了後にイベントキューに入れて実行し、非同期実行機能を実現することを知っている.もちろん尤大はこのような状況だけを代替選択として、シミュレーションDOMを用いて作成し、観察者MutationObserverを利用してその更新を傍受して実現する.
    var observer = new MutationObserver(nextTickHandler) //        
    var textNode = document.createTextNode(counter) //         
    observer.observe(textNode, { //    textNode   characterData     true
      characterData: true
    })
    timerFunc = function () { //      nextTick,    timerFunc            
      counter = (counter + 1) % 2 //     0 1   ,       
      textNode.data = counter
    }

    MutationObserverとcharacterDataを理解していない場合はMDNの説明を参照してください:MutationObserver&CharacterData
    义齿
    flush関数宣言変数f:var f = document.documentElement.offsetHeightコメントからするとDOM更新を強制するはずですoffsetHeightを呼び出すとブラウザにドキュメントのスクロール高さを再計算させるからでしょう
    src/transition/transition.js
    Transitionは要素遷移変換の論理と状態を実現し、Transitionのプロトタイプはenterを例にenter, enterNextTick, enterDone, leave, leaveNextTick, leaveDoneのいくつかの状態を含む.
    /**
     * Start an entering transition.
     *
     * 1. enter transition triggered
     * 2. call beforeEnter hook
     * 3. add enter class
     * 4. insert/show element
     * 5. call enter hook (with possible explicit js callback)
     * 6. reflow
     * 7. based on transition type:
     *    - transition:
     *        remove class now, wait for transitionend,
     *        then done if there's no explicit js callback.
     *    - animation:
     *        wait for animationend, remove class,
     *        then done if there's no explicit js callback.
     *    - no css transition:
     *        done now if there's no explicit js callback.
     * 8. wait for either done or js callback, then call
     *    afterEnter hook.
     *
     * @param {Function} op - insert/show the element
     * @param {Function} [cb]
     */
    
    p.enter = function (op, cb) {
      this.cancelPending()
      this.callHook('beforeEnter')
      this.cb = cb
      addClass(this.el, this.enterClass)
      op()
      this.entered = false
      this.callHookWithCb('enter')
      if (this.entered) {
        return // user called done synchronously.
      }
      this.cancel = this.hooks && this.hooks.enterCancelled
      pushJob(this.enterNextTick)
    }
    

    cancelPendingはenterとleaveでのみ呼び出され、以下のように実現されます.
    /**
     * Cancel any pending callbacks from a previously running
     * but not finished transition.
     */
    
    p.cancelPending = function () {
      this.op = this.cb = null
      var hasPending = false
      if (this.pendingCssCb) {
        hasPending = true
        off(this.el, this.pendingCssEvent, this.pendingCssCb)
        this.pendingCssEvent = this.pendingCssCb = null
      }
      if (this.pendingJsCb) {
        hasPending = true
        this.pendingJsCb.cancel()
        this.pendingJsCb = null
      }
      if (hasPending) {
        removeClass(this.el, this.enterClass)
        removeClass(this.el, this.leaveClass)
      }
      if (this.cancel) {
        this.cancel.call(this.vm, this.el)
        this.cancel = null
      }
    }

    cancelPendingを呼び出す前の実行中または実行待ちのjsまたはcss変換イベントとクラス名をキャンセルし、スクリプトbeforeEnterをトリガーし、enterClassクラス名を追加し、特定の要素挿入操作を実行し、enteredをfalseに設定します.挿入操作がまだ完了していないため、callHookWithCbを実行し、最後にthisを決定します.cancelの値と次の操作enterNextTickに進み、最後の操作はenterDone
    /**
     * The "cleanup" phase of an entering transition.
     */
    
    p.enterDone = function () {
      this.entered = true
      this.cancel = this.pendingJsCb = null
      removeClass(this.el, this.enterClass)
      this.callHook('afterEnter')
      if (this.cb) this.cb()
    }