關於 vue 中使用 transition 效果,官網上的解釋以下:vue
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.git
當元素插入到 DOM 樹或者從 DOM 樹中移除的時候, transition 屬性提供變換的效果,可使用 css 來定義變化效果,也可使用 JS 來定義github
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 是回調函數app
vue 將解析後的transition做爲 DOM 元素的屬性 __v_trans ,這樣每次操做 DOM 的時候都會作如下判斷:異步
若是元素沒有被定義了 transitionasync
若是元素沒有 jshook 且 css transition 的定義不支持ide
若是元素尚未編譯完成函數
若是元素有父元素且父元素沒有編譯完成
存在以上其中一種狀況的話則直接執行操做方法 op 而不作變化,不然執行:
var action = direction > 0 ? 'enter' : 'leave' transition[action](op, cb)
除了添加,還有插入和刪除兩個操做方法:
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 是怎麼實現的,這個還得繼續深挖
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 三個文件中的第二個,從字面量上理解是一個隊列,從代碼上看實現的是一個任務隊列,每當調用 pushJob 的時候,都會往任務隊列 queue 裏面推一個任務,而且有一個標識queued, 若是爲 false 則會在 nextTick 的時候將 queued 置爲 true同時調用 flush 方法,這個方法會執行全部在任務隊列 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的解釋: MutaitionObserver
& CharacterData
flush 函數聲明變量f: var f = document.documentElement.offsetHeight
從註釋上看應該是強制DOM更新,由於調用offsetHeight的時候會讓瀏覽器從新計算出文檔的滾動高度的緣故吧
transition 實現了元素過渡變換的邏輯和狀態,Transition 的原型包含了 enter, enterNextTick, enterDone, leave, leaveNextTick, leaveDone
這幾個狀態,以 enter 爲例子:
/** * 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() }