关于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动画完结了,来到过渡先调一下你的来到钩子,在你的动画完结后再把元素从页面上删除,逻辑很简略。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理