关于 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() }