整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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

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

ss过渡

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

1)想让元素样式有个变化的过程,那么我们需要以某种方式从一个样式过渡到另一种样式。

  • transition
  • transition-property
  • transition-duration
  • transition-timing-function
  • transition-delay

CSS3 过渡是元素从一种样式逐渐过渡到另一种样式。

其语法如下所示:

transition: 指定属性 持续时间 速度曲线 开始时间;

它是一个复合属性,我们也可以如下分开使用这几个属性。

transition-property: 属性值; /*指定属性名*/
transition-duration: 属性值; /*完成过渡这一过程的时间*/
transition-timing-function: 属性值; /*速度曲线*/
transition-delay: 属性值; /*过渡的开始时间*/
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        div{
            margin-top:40px;
            margin-left:40px;
            width:50px;
            height:50px;
            background-color:#51adcf;
            transition: transform 1s ease-in-out; /*ease-in-out 慢速开始慢速结束*/
        }
        div:nth-child(2){
            transform: rotate(-60deg);
        }
        div:hover{
            transform: scale(2);
        }
    </style>
</head>
<body>
    <div></div>
    <div></div>
</body>
</html>

hover变两倍大

  • div:hover 中,使用 transform: scale(2) 设置当鼠标放在 div 元素上时,元素会放大 1 倍。
  • div:nth-child(2) 中,使用 div:nth-child(2) 设置第二个元素逆时针旋转 60deg
  • 使用 transtion 属性实现过渡的效果,其中 ease-in-outtransition-timing-function 属性的属性值。

transition-timing-function 属性用来设置过渡效果从开始到结束的时间曲线,它有很多可用属性值,常用属性值如下表:

属性值

说明

ease

规定慢速开始,然后变快,然后慢速结束的过渡效果(cubic-bezier(0.25,0.1,0.25,1))。

linear

规定以相同速度开始至结束的过渡效果(等于 cubic-bezier(0,0,1,1))。

ease-in

规定以慢速开始的过渡效果(等于 cubic-bezier(0.42,0,1,1))。

ease-out

规定以慢速结束的过渡效果(等于 cubic-bezier(0,0,0.58,1))。

ease-in-out

规定以慢速开始和结束的过渡效果(等于 cubic-bezier(0.42,0,0.58,1))。

steps(int,start 或者 end)

steps() 有两个参数,第一个为步长,其值必须为整数,第二个值为可选值,它有两个取值,分别是 start 和 end。

step-start

相当于 steps(1, start)。

step-end

相当于 steps(1, end)。

cubic-bezier(n,n,n,n)

规定在 cubic-bezier 函数中定义自己的值。可能的值是 0 至 1 之间的数值。

initial

规定使用默认值。

inherit

规定从父元素继承该属性值。

例子,图片一秒一步,4步变大为原先二倍:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        div{
            width:50px;
            height:50px;
            margin:50px;
            background-color:cornflowerblue;
            transition: transform 4s steps(4);
        }
        div:hover{
            transform:scale(2);
        }
    </style>
</head>
<body>
    <div></div>
</body>
</html>

练习一下:

  • 在 body 中添加一个宽和高均为 100 个像素的 div 元素,其背景颜色为黄色。
  • 设置过渡样式,当鼠标放在元素上,延迟 1 秒后,元素的宽度会慢慢变成 300 个像素。
  • 过渡整个过程会持续 5 秒。
  • 按以上要求用 HTML 完成以下效果:

    《vue 基础》系列是再次回炉 vue 记的笔记,除了官网那部分知识点外,还会加入自己的一些理解。(里面会有大部分和官网相同的文案,有经验的同学择感兴趣的阅读)

    讲到动画,说真的我自己用的的确不多,平时大部分时间都在处理业务问题,或者后端服务。

    但前端的“产品”都是要给用户看,并且使用的。好的网站除了服务响应快外,页面交互也是出类拔萃的。

    这篇就聊下 vue 中怎么来实现动画的过渡效果。

    单元素/组件过渡

    依靠 vue 提供了 transition 组件标签,来对如下特殊的指令或者标签做 “进入/离开”过渡 效果:

    v-if、v-show、动态组件、root 节点。

    先来看段代码,看下过渡效果:

    当点击 button 后,会控制 show 的值来切换 v-if 所要渲染的模板。能看到 hello 这块内容在 v-if 切换时有个小动画:

    页面效果

    那动效怎么产生的呢?

    1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。

    • 当显示时:添加样式 xx-enter-active xx-enter-to
    • 当离开时:添加样式 xx-leave-active xx-leave-to

    页面增加标签

    2. 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。3. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和 Vue 的 nextTick 概念不同)

    过渡类名

    在进入/离开的过渡中,会有 6 个 class 切换:

    v-enter –> v-enter-active –> v-enter-to –> v-leave –> v-leave-active –> v-leave-to

    css 过渡和 css 动画

    css 过渡

    通过 transition 来定义过渡时期的 css 样式:

    过渡

    css 动画

    除了过渡效果,我们还能设置 animation 标签指定 css 动画效果。

    动画

    如何区分 css 过渡和动画共同使用

    当然这两者都是 css 范畴的特效知识,根据实际需要使用。

    不过可能出现 animation 完成,但 transition 还在继续的情况,对于这种情况需要设置 type=animation|transition 来区分 vue 所要监听的类型。

    我们先来看下两种动效单独的使用情况(动画稍显夸张,只为说明现象):

    animation

    粉色方框按照 animation 设置的进度,逐步放大,直至结束,用时 1s:

    页面效果

    transition

    红框从 300px 缩小至 100px,用时 3s:

    页面效果

    一起使用

    页面效果

    因为动效在时间 duration 中存在重叠交叉,所以会出现上面这样变扭的效果,可以动过 type animation|transition 来指定 vue 监听动效的类型加以控制。

    比如,我们设置了 animation 就 屏蔽了 transition 的效果,就会和单使用 animation 一样了。

    设置 type

    自定义过渡类名

    用于配合第三方 animate 类库时使用。可以根据我们的需要细化效果的展示。

    页面标签

    javascript 钩子

    可以在标签上绑定过渡各个时期的钩子,通过 js 来调用触发相关事件的事件。

    注意:当只用 JavaScript 过渡的时候,在 enter 和 leave 中必须使用 done 进行回调。否则,它们将被同步调用,过渡会立即完成。

    多元素过渡

    举个多元素过渡的例子:

    一般情况

    如果存在数据就显示 table 内容,不存在就显示一个无数据的文案,然后通过 transition 动效切换不同效果。

    但是如果当相同标签元素切换时,就需要通过 key 来区分他们的不同。

    相同元素切换

    如果 相同元素模板的 key 一致,效果如下:

    相同 key

    注意,这不是期望的效果。可能你会感觉到生硬,因为相同 key 的元素切换时没有过渡效果。

    设置不同 key 后,动效得以生效:

    不同 key

    对比这两者,是不是后者更为顺滑些。

    过渡模式

    在两个元素切换的时候,可能我们需要更细致的过渡模式,比如上例中:第一个 button 离开,第二个 button 进来之间的过程中,都被重新绘制了,间隙虽然很短,但能明显看到产生了类似滑动的效果(不符合原始意图)。

    vue 提供了 mode 过渡模式:

    • in-out:新元素先进行过渡,完成之后当前元素过渡离开。
    • out-in:当前元素先进行过渡,完成之后新元素过渡进入。

    你只要在原有 transition 标签上,添加 mode="out-in" 即可:

    设置 mode

    当使用 mode="out-in" 时,第二个元素等待第一个元素消失后才入场,使得原始意图符合预期。

    多个组件的过渡

    同时 transition 也可以作用于“动态组件”的过渡效果

    列表过渡

    与 transition 不同的是,列表过渡需要使用 <transition-group> 标签。

    transition-group

    能注意到这里设置了 tag='div' ,可以让最后的列表内容包裹在一个 div 标签内。

    上图中,在列表中添加新元素的过渡效果是不是比没有好多了?

    针对列表中元素的移动,也有专门的属性来定义:v-move,和 v-enter、v-leave 类似,最后会根据过渡时期来添加指定的样式,用法参见 v-enter 等。

    总结

    这是 vue 自带的动过效果,来让各个元素切换的过渡时期,用户体验更为流畅。

    其实内置还有 FLIP 动画队列,并且根据数据驱动的基础,能做更多的动画特效展示,这块内容你在 vue 官网能看到示例,这里就不做展开了。

    关于我

    一位“前端工程师”,乐于实践,并分享前端开发经验。

    如果有问题或者想法,欢迎各位评论留言,愿大家共同进步。

    关注【前端雨爸】,查阅更多前端技术心得。