Vue0.11版本源码阅读系列六:过渡原理

简介: Vue0.11版本源码阅读系列六:过渡原理

css过渡


首先看一下这个版本的vuecss过渡和动画的应用方式:


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




相关文章
|
5月前
|
JavaScript 前端开发 Serverless
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
171 2
|
3月前
|
JavaScript 算法 编译器
vue3 原理 实现方案
【8月更文挑战第15天】vue3 原理 实现方案
45 1
|
23天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
34 9
|
2月前
|
缓存 JavaScript 前端开发
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
该文章全面覆盖了Vue.js从基础知识到进阶原理的48个核心知识点,包括Vue CLI项目结构、组件生命周期、响应式原理、Composition API的使用等内容,并针对Vue 2与Vue 3的不同特性进行了详细对比与讲解。
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
|
1月前
|
JavaScript UED
Vue双向数据绑定的原理
【10月更文挑战第7天】
|
26天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
24 0
|
2月前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
|
2月前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
2月前
|
缓存 JavaScript 容器
vue动态组件化原理
【9月更文挑战第2天】vue动态组件化原理
47 2
|
3月前
|
缓存 JavaScript 前端开发
[译] Vue.js 内部原理浅析
[译] Vue.js 内部原理浅析
下一篇
无影云桌面