乐趣区

关于javascript:Vue011版本源码阅读系列六过渡原理

css 过渡

首先看一下这个版本的 vue css 过渡和动画的利用形式:

<p class="msg" v-if="show" v-transition="expand">Hello!</p>
<p class="animated" v-if="show" v-transition="bounce">Look at me!</p>
.msg {
    transition: all .3s ease;
    height: 30px;
    padding: 10px;
    background-color: #eee;
    overflow: hidden;
}
.msg.expand-enter, .msg.expand-leave {
    height: 0;
    padding: 0 10px;
    opacity: 0;
}

.animated {display: inline-block;}

.animated.bounce-enter {animation: bounce-in .5s;}

.animated.bounce-leave {animation: bounce-out .5s;}

@keyframes bounce-in {
    0% {transform: scale(0);
    }

    50% {transform: scale(1.5);
    }

    100% {transform: scale(1);
    }
}

@keyframes bounce-out {
    0% {transform: scale(1);
    }

    50% {transform: scale(1.5);
    }

    100% {transform: scale(0);
    }
}

能够看到也是通过指令的形式,这个版本只有反对两个类,一个是进入的时候增加的v-enter,另一个是来到时候增加的v-leave

先看一下这个指令:

module.exports = {
    isLiteral: true,// 为 true 不会创立 watcher 实例
    bind: function () {this.update(this.expression)
    },
    update: function (id) {
        var vm = this.el.__vue__ || this.vm
        this.el.__v_trans = {
            id: id,
            // 这个版本的 vue 能够应用 transitions 选项来定义 JavaScript 动画
            fns: vm.$options.transitions[id]
        }
    }
}

这个指令不会创立 watcher,因为指令的值要么是css 的类名,要么是 JavaScript 动画选项的名称,都不须要进行察看。指令绑定时所做的事件就是给 el 元素增加了个自定义属性,保留了表达式的值,这里是 expandJavaScript 动画函数,这里是undefined

要触发动画须要批改 if 指令 show 的值,假如开始是 false,咱们把它改成true,这会触发if 指令的 update 办法,依据第三篇 vue0.11 版本源码浏览系列三:指令编译最初局部对 if 指令过程的解析咱们晓得在进入时调用了transition.blockAppend(frag, this.end, vm),在来到时调用了transition.blockRemove(this.start, this.end, this.vm),这里显然会调用blockAppend

// block 是蕴含了 if 指令绑定元素的代码片段
// target 是一个正文节点,在 if 指令绑定元素所在的地位
exports.blockAppend = function (block, target, vm) {
    // 代码片段的子节点
    var nodes = _.toArray(block.childNodes)
    for (var i = 0, l = nodes.length; i < l; i++) {apply(nodes[i], 1, function () {_.before(nodes[i], target)
        }, vm)
    }
}

遍历元素调用 apply 办法:

var apply = exports.apply = function (el, direction, op, vm, cb) {
    var transData = el.__v_trans
    if (
        !transData ||// 没有过渡数据
        !vm._isCompiled ||// 以后实例没有调用过 $mount 办法插入到页面
        (vm.$parent && !vm.$parent._isCompiled)// 父组件没有插入到页面
    ) {// 上述情况不须要动画,间接跳过
        op()
        if (cb) cb()
        return
    }
    var jsTransition = transData.fns
    // JavaScript 动画,下一大节再看
    if (jsTransition) {
        applyJSTransition(
            el,
            direction,
            op,
            transData,
            jsTransition,
            vm,
            cb
        )
    } else if (
        _.transitionEndEvent &&
        // 页面不可见的话不进行过渡
        !(doc && doc.hidden)
    ) {
        // css
        applyCSSTransition(
            el,
            direction,
            op,
            transData,
            cb
        )
    } else {
        // 不须要利用过渡
        op()
        if (cb) cb()}
}

这个办法会判断是利用 JavaScript 动画还是 css 动画,并分发给不同的函数解决。函数套娃,又套到了 applyCSSTransition 办法:

module.exports = function (el, direction, op, data, cb) {
    var prefix = data.id || 'v'// 此处是 expand
    var enterClass = prefix + '-enter'// expand-enter
    var leaveClass = prefix + '-leave'// expand-leave
    if (direction > 0) { // 进入
        // 给元素增加进入的类名
        addClass(el, enterClass)
        // op 就是_.before(nodes[i], target)操作,这一步会把元素增加到页面上
        op()
        push(el, direction, null, enterClass, cb)
    } else { // 来到
        // 给元素增加来到的类名
        addClass(el, leaveClass)
        push(el, direction, op, leaveClass, cb)
    }
}

能够看到进入和来到的操作是有区别的,本次咱们是把 show 的值改成 true,所以会走direction > 0 的分支,先给元素增加进入的类名,而后再把元素理论插入到页面上,最初调用 push 办法;

如果是来到的话会先给元素增加来到的类名,而后调用 push 办法;

看一下 push 办法:

var queue = []
var queued = false
function push (el, dir, op, cls, cb) {
    queue.push({
        el  : el,
        dir : dir,
        cb  : cb,
        cls : cls,
        op  : op
    })
    if (!queued) {
        queued = true
        _.nextTick(flush)
    }
}

把本次的工作增加到队列,注册了个异步回调在下一帧执行,对于 nextTick 的详细分析请返回 vue0.11 版本源码浏览系列五:批量更新是怎么做的。

addClassop 都是同步工作,会立刻执行,如果此刻有多个被这个 if 指令管制的元素都会被顺次增加到队列里,后果就是这些元素都会被增加到页面上,然而因为咱们给进入的款式设置的是 height: 0;opacity: 0;,所以是看不见的,这些同步工作执行完后才会去异步队列里把注册的flush 办法拉进去执行:

function flush () {
  // 这个办法用来触发强制回流,确保咱们增加的 expand-enter 款式能失效,不过我试过不回流也能失效
  var f = document.documentElement.offsetHeight
  queue.forEach(run)
  queue = []
  queued = false
}

flush办法遍历方才增加到 queue 里的工作对象调用 run 办法,因为进行了异步批量更新,所以同一时刻有多个元素动画也只会触发一次回流:

function run (job) {
    var el = job.el
    var data = el.__v_trans
    var cls = job.cls
    var cb = job.cb
    var op = job.op
    // getTransitionType 办法用来获取是 transition 过渡还是 animation 动画,原理是判断元素的 style 对象或者 getComputedStyle()办法获取的款式对象里的 transitionDuration 或 animationDuration 属性是否存在以及是否为 0s
    var transitionType = getTransitionType(el, data, cls)
    if (job.dir > 0) { // 进入
        if (transitionType === 1) {// transition 过渡
            // 因为 v -enter 的款式是暗藏元素的款式,另外因为给元素设置了 transition: all .3s ease,所以只有把这个类删除了天然就会利用过渡成果
            removeClass(el, cls)
            // 存在回调时才须要监听 transitionend 事件
            if (cb) setupTransitionCb(_.transitionEndEvent)
        } else if (transitionType === 2) {// animation 动画
            // animation 动画只有增加了 v -enter 类自行就会触发,须要做的只是监听 animationend 事件在动画完结后把这个类删除
            setupTransitionCb(_.animationEndEvent, function () {removeClass(el, cls)
            })
        } else {
            // 没有过渡
            removeClass(el, cls)
            if (cb) cb()}
    } else { // 来到
        // 来到动画很简略,两者都是只有增加了 v -leave 类就能够触发动画
        // 要做的只是在监听动画完结的事件把元素从页面删除和把类名从元素上删除
        if (transitionType) {
            var event = transitionType === 1
            ? _.transitionEndEvent
            : _.animationEndEvent
            setupTransitionCb(event, function () {op()
                removeClass(el, cls)
            })
        } else {op()
            removeClass(el, cls)
            if (cb) cb()}
    }
}

当初看一下当把 show 的值由 true 改成 false 时调用的 blockRemove 办法:

// start 和 end 是两个正文节点,突围了该 if 指令管制的所有元素
exports.blockRemove = function (start, end, vm) {
    var node = start.nextSibling
    var next
    while (node !== end) {
        next = node.nextSibling
        apply(el, -1, function () {_.remove(el)
        }, vm, cb)
        node = next
    }
}

遍历元素同样调用 apply 办法,只不过参数传了 -1 代表是来到。

到这里能够总结一下 vuecss过渡:

1. 进入

先给元素增加 v-enter 类,而后把元素插入到页面,最初创立一个工作增加到队列,如果有多个元素的话会一次性全副实现,而后在下一帧来执行方才增加的工作:

1.1css 过渡

v-enter类名里的款式个别是用来暗藏元素的,比方把元素的宽高设为 0、透明度设为0 等等,反正让人看不见就对了,要触发动画须要把这个类名删除了,所以这里的工作就是移除元素的 v-enter 类名,而后浏览器会本人利用过渡成果。

1.2css 动画

animation不一样,v-enter类的款式个别是定义 animation 的属性值,比方:animation: bounce-out .5s;,只有增加了这个类名,就会开始动画,所以这里的工作是监听动画完结事件来移除元素的 v-enter 类名。

2. 来到

css过渡和动画在来到时是一样的,都是给元素增加一个 v-leave 类就能够了,v-leave类要设置的款式个别和 v-enter 是一样的,除非进出成果就是要不一样,否则都是要让元素不可见,而后增加一个工作,因为款式上不可见了但元素实际上还是在页面上,所以最初的工作就是监听动画完结事件把元素真正的从页面上移除,当然,相应的 v-leave 类也是要 从元素上移除的。

JavaScript 动画

在这个版本要应用 JavaScript 进行动画过渡须要应用申明过渡选项:

Vue.transition('fade', {beforeEnter: function (el) {// 元素插入文档之前调用,比方提取把元素变成不可见,否则会有闪屏的问题},
  enter: function (el, done) {
    // 元素曾经插入到 DOM,动画实现后须要手动调用 done 办法
    $(el)
      .css('opacity', 0)
      .animate({opacity: 1}, 1000, done)
    // 返回一个函数当动画勾销时被调用
    return function () {$(el).stop()}
  },
  leave: function (el, done) {$(el).animate({opacity: 0}, 1000, done)
    return function () {$(el).stop()}
  }
})

就是定义三个钩子函数,定义了 JavaScript 过渡选项,在 transition 指令的 update 办法就能依据表达式获取到,这样就会走到上述 apply 办法里的 jsTransition 分支,调用 applyJSTransition 办法:

module.exports = function (el, direction, op, data, def, vm, cb) {if (data.cancel) {data.cancel()
        data.cancel = null
    }
    if (direction > 0) { // 进入
        // 调用 beforeEnter 钩子
        if (def.beforeEnter) {def.beforeEnter.call(vm, el)
        }
        op()// 把元素插入到页面 dom
        // 调用 enter 钩子
        if (def.enter) {data.cancel = def.enter.call(vm, el, function () {
                data.cancel = null
                if (cb) cb()})
        } else if (cb) {cb()
        }
    } else { // 来到
        // 调用 leave 钩子
        if (def.leave) {data.cancel = def.leave.call(vm, el, function () {
                data.cancel = null
                // 来到动画完结了从页面移除元素
                op()
                if (cb) cb()})
        } else {op()
            if (cb) cb()}
    }
}

css 过渡相比,JavaScript过渡很简略,进入过渡就是在元素理论插入到页背后执行以下你的初始化办法,而后把元素插入到页面,接下来调用 enter 钩子随你怎么让元素静止,动画完结后再调一下 vue 注入的办法通知 vue 动画完结了,来到过渡先调一下你的来到钩子,在你的动画完结后再把元素从页面上删除,逻辑很简略。

退出移动版