资讯专栏INFORMATION COLUMN

VueJS源码学习——元素在插入和移出 dom 时的过渡逻辑

Dogee / 2133人阅读

摘要:原文地址项目地址关于中使用效果,官网上的解释如下当元素插入到树或者从树中移除的时候,属性提供变换的效果,可以使用来定义变化效果,也可以使用来定义首先第一个函数是将元素插入,函数实现调用了实现代码如下写的好的代码就是文档,从注释和命名上就

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)

除了添加,还有插入和删除两个操作方法:

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 三个文件中的第二个,从字面量上理解是一个队列,从代码上看实现的是一个任务队列,每当调用 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

mutationObserver 例子

flush 函数声明变量f: var f = document.documentElement.offsetHeight 从注释上看应该是强制DOM更新,因为调用offsetHeight的时候会让浏览器重新计算出文档的滚动高度的缘故吧

src/transition/transition.js

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 只有在 enterleave 里被调用了,实现如下:

/**
 * 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()
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88380.html

相关文章

  • CSS transition delay简介与进阶应用

    摘要:用来定义元素两种状态之间的过渡。到目前为止,我们利用完全模拟了第一部分我们使用实现的功能,而且看上去更简洁。附上利用来实现该方案的代码用于参考。由于代码效果时好时坏,猜测可能与的容器相关。 背景 在日常的项目开发中,我们会很经常的遇见如下的需求: 在浏览器页面中,当鼠标移动到某个部分后,另一个部分在延迟若干时间后出现 在鼠标移除该区域后,另一部分也在延迟若干时间后消失 我相信这是一...

    e10101 评论0 收藏0
  • Vue2 transition源码分析

    摘要:至此算是找到了源码位置。至此进入过渡的部分完毕。在动画结束后,调用了由组件生命周期传入的方法,把这个元素的副本移出了文档流。这篇并没有去分析相关的内容,推荐一篇讲非常不错的文章,对构造函数如何来的感兴趣的同学可以看这里 Vue transition源码分析 本来打算自己造一个transition的轮子,所以决定先看看源码,理清思路。Vue的transition组件提供了一系列钩子函数,...

    Genng 评论0 收藏0
  • 手把手教你用原生JavaScript造轮子(2)——轮播图(更新:ES6版本)

    摘要:绑定轮播事件然后是鼠标移入移出事件的绑定鼠标移入移出事件移入时停止轮播播放的定时器,移出后自动开始下一张的播放。 通过上一篇文章的学习,我们基本掌握了一个轮子的封装和开发流程。那么这次将带大家开发一个更有难度的项目——轮播图,希望能进一步加深大家对于面向对象插件开发的理解和认识。 So, Lets begin! 目前项目使用 ES5及UMD 规范封装,所以在前端暂时只支持标签的引入方式...

    jasperyang 评论0 收藏0
  • vue内置组件——transition简单原理图文详解

    摘要:在元素被插入之前生效,在元素被插入之后的下一帧移除。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。版及以上定义进入过渡的结束状态。 基本概念 Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果 在 CSS 过渡和动画中自动应用 class 可以配合使用第三方 CSS 动画库,如...

    lingdududu 评论0 收藏0
  • vue内置组件——transition简单原理图文详解

    摘要:在元素被插入之前生效,在元素被插入之后的下一帧移除。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。版及以上定义进入过渡的结束状态。 基本概念 Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果 在 CSS 过渡和动画中自动应用 class 可以配合使用第三方 CSS 动画库,如...

    nihao 评论0 收藏0

发表评论

0条评论

Dogee

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<