提示:后面有两个完整的实验代码,可以先运行对照来看。
这篇开始解决transition的实现原理,由于内容太多而且复杂,分成几集来写。
基础准备 – 缓动是什么?
为了文章完整性 ,我们也顺带写一下。如果DOM元素有可以量化的样式数据,列如从1到100,从100到50这种的,能计算出一个具体的数学差值,就可以做缓动。
举个例子:

分析:一开始div处在原始位置,而我们想把它移到偏离X轴100px的地方,这样的距离差就是可量化的,因此就可以使用缓动。代码如下:

这个代码提到四个相关的量,1、开始状态 2、结束状态 3、缓动规则 4、事件触发
- 开始状态:本例没有特别设置,那么就是div的原始状态。
- 结束状态:就是想要达到的状态。
- 缓动规则是指缓动的运行轨迹 、持续的时间值等。
- 事件触发。缓动不是无缘无故就自己执行了,需要一个动作来触发它,也就是动作修改状态。
因此,一个缓动需要这四件套。下面是完整的过程:按钮点击后,将结束状态和缓动规则加到box上,那么缓动规则做为一个监控器,马上就发现了位置发生了变化:
//原状态
translate: x=0
//新状态
translate: x=100px
那么就会触发缓动执行。

最后需要提到的是缓动回调,有时我们需要在缓动结束后(列如2s后)能通知我们,原生DOM提供了一个名为transitionend的事件回调。
基础知识差不多这些就够了,下面开始研究vue中的transition。
Vue的transition是做什么的?
列如说,我们用v-if来显隐一个div,这个动作实则是把div DOM从树上直接插入或删除,那么这个状态是无法量化的,动画就不起作用了。但是我又想让它删除或插入之前来一段缓动,可以想见是做不到的,那么transition就出场了,它可以帮我们实现这个效果。
transition基本样式
<transition>
<div v-if='xx'>...</div>
</transition>
如代码所示,它是将一片DOM进行包裹了。那么我们解决的第一个问题是transition标签是什么?根据Vue内部定义,可以知道它是一个内部组件,和keep-alive是一样的,也是一个抽象组件。
这里再详细说下,transition的目标是控制一个DOM的缓动,那么我们如何将这个DOM传给transition呢?有且只有把它嵌套进来,是最直观的方式,即:
<transition>
嵌套的元素
</transition>
再以这个例子来说,transition要控制的是一个DOM,如果我们非要给它一堆DOM,如下:
//不恰当的用法
<transition>
<h2>我是h2</h2>
<h3>我是h3</h3>
<div>我是div</div>
</transition>
那么它肯定不想要这么多,由于和目标冲突了,那么我们肯定会选第一个,就是先来优先级最高。
那么它挑选的就是$slots.default[0]这个元素。其中$slots就是内嵌的这一堆元素,为什么用default呢?由于它和slot有关系,在写slot的那篇文章中提到,slot有具名和无名两种插槽,无名的slot都归类到default这个名字中。而在当前这个语境,它没有slot插槽要替换,因此全部使用无名。
但是我们看到无名的slot有三个,那么它要从三个中挑选第一个,也即$slots.default[0]。
tansition的结构我们理解了,它是一个抽象组件,也是一个占位符,需要控制哪一个DOM,可以直接嵌套进来,而且只能传一个。
技术点1:transition控制首次渲染的缓动
有时我们会有这样的要求,就是组件第一次渲染时列如打开页面时,就让它有一个缓动。transition也会思考这个问题,它的props中提供了这几个接口,分别是appear-class、appear-active-class和appear-to-class,也就是说,如果你想让组件首次渲染时就执行缓动,可以用prop给它传这几个class。
- appear-class:对应缓动的开始状态
- appear-active-class:对应缓动规则
- appear-to-class:对应缓动结束状态
下面看一下例子:

分析:transition组件提供有许多prop,其中appear-class(首次渲染开始状态)、appear-active-class(首次渲染缓动规则)、appear-to-class(首次渲染结束状态)三个专门负责首次渲染的相关class。
最下面的appear是干啥的呢?它是一个开关,表明开启首次渲染。在Vue的属性设置中,只设置appear名,解析结果就当做true来处理。因此,我们如果想开启首次渲染,要加上appear这个prop!
对于本例来说,实例是想做这样的效果:

如图所示,从偏离x 100px缓动到偏离x 50px,这个容易理解,暂时不讨论了。目前我们面临一个超级大的问题就是,谁触发这个动作呢?由于页面一打开就缓动了,我们可没有参与。
正如前文所说,缓动有四件套,目前已经知道三个了,就差一个触发动作,它是咋弄的呢,很好奇这个问题。
requestAnimationFrame方法
完整是
window.requestAnimationFrame方法,它是做什么的呢?这个方法可以放入一个回调,在下一帧渲染前执行,大致意思如下:

我们第一需要知道的是,浏览器会根据情况不断的刷新页面,也就是所谓的重绘。而我们传给requestAnimationFrame的回调,意思是提前把它给浏览器,让它在下一次渲染之前执行。而Vue正是使用这个方法,将结束状态在这个回调中做了绑定。
可能有点晦涩,这里我做了一个完整的模拟流程。
requestAnimationFrame实验模拟
第一看代码:

分析:我们放入一个回调到requestAnimationFrame中,需要注意的,这个回调和当前这次渲染没关系了,此次回调的结果是下一帧渲染的目标结果,这也是回调的核心作用!
在这个回调中,我们做了两件事:
- 将旧状态’box-start’先删除
- 将结束状态加入
对于第2步,这样就会产生差值,如下:
原偏离x -> 100px
现偏离x -> 50px
这样就会触发在下一次渲染中让缓动执行。
那么第一步为什么要删除box-start呢,这要和Vue的设计思路有关,我的理解是这样的:如果说有一个div,它本来好好的,就是处在它本应该的位置。但是我们人为的通过添加开始状态、结束状态和缓动规则这三件套让它执行了一段动画,那么完成之后就要把这三件套删除,让这个div恢复到它最开始的那个状态。所以在下一帧的回调中,我们将box-start删除了。
那么可能有人疑惑,它删除了会不有会有影响呢?没有,这还要从代码说起,我们看代码结构:

分析:这个问题的关键在于需要知道浏览器渲染的时机。列如说,我们通过add添加样式之后,是不会马上执行,它是一个异步执行的过程。因此第一次拍照就是得到第一帧的状态,只要这个状态确定,box-start这个类要不要都无所谓了,从动画的角度来说,它的任务就完成了,也就是说它的使命就是确定第一帧的状态。
第一帧执行,结果是将box拉到偏移X 100px的位置。
接着准备执行第二帧,但在这之前先把下一帧回调执行一下,在回调中又拍了第二次照,这个回调的核心作用就是确定第二帧的状态,我们得到结果是偏离x 50px。而缓动能执行的主要缘由就是两个帧的状态不同,结果一看这两个不一样的,那么从第二帧开始就执行缓动。
好了,通过这样解释,我们就知道为什么可以在回调中删除box-start,由于它只要确定第一帧的状态就可以了。
那么不删行不行呢?看代码

分析:会发生错误!!如果不删可以看出,最后剩下的class=box box-start,那么div就会一下跳到偏离x 100px的位置,但这不是div最初的状态,和设计目标冲突!
Vue是怎么做的?
和上面这个代码基本一模一样,这里就不贴源码了。
总结
这篇文章写的太长了,再写下去就成长篇小说了,就此停住。
在这篇文章中,提到许多知识点,目前做一个汇总:
- 主要讲了首次渲染时的插入操作的动画。Vue是怎么实现的呢?第一,在生成dom的过程中,将dom绑定了缓动三件套,分别是开始状态,结束状态和缓动规则。状态是可选的,缓动规则是必须的。三件套由我们定义并通过prop传给transition组件。
- 在创建DOM的过程中,是JS主线程在执行,所以给DOM添加样式并不会立即生效,等到主线程执行完成后,下一帧的渲染中开始执行页面刷新。
- 在第二帧的回调中添加结束状态,这样就造成第一帧的开始状态和第二帧有差值,动画启动,并在第二帧前的回调中,将开始状态删除了。
- 在动画结束之后,我们将三件套中的结束状态和缓动规则也一并删除,至此三件套全部清理干净,那么div就恢复到它最初的状态。
完整的实验代码一:下一帧的实验
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.box{
width:100px;
height:100px;
border:1px solid black;
}
.box-start{transform: translateX(100px);}
.box-rule{transition: all .5s ease;}
.box-end{transform: translateX(50px);}
</style>
</head>
<body>
<h2>下一帧实验</h2>
<div id="box" class="box"></div>
<script>
var box=document.getElementById("box")
/* 先做好开始准备 */
box.classList.add('box-start','box-rule')
/* 下一次渲染前的回调 */
window.requestAnimationFrame(function (){
box.classList.remove('box-start')
box.classList.add('box-end')
})
/* 缓动结束之后 */
box.addEventListener('transitionend',function (){
box.classList.remove('box-end')
box.classList.remove('box-rule')
})
</script>
</body>
完整的实验代码二:transition首次渲染观察
<style>
.box {
width: 200px;
height: 100px;
background: #673ab7;
color: white;
text-align: center;
line-height: 100px;
margin: 20px;
}
.my-appear-class {
transform: translateX(100px);
}
.my-active-class {
transition: all 1s ease;
}
.my-to-class {
transform: translateX(50px);
}
</style>
</head>
<body>
<div id="app">
<transition
appear-class="my-appear-class"
appear-active-class="my-active-class"
appear-to-class="my-to-class"
appear
>
<div class="box">
首次加载会从右边滑入
</div>
</transition>
</div>
<script src="../vue.js"></script>
<script>
new Vue({
el: '#app'
})
</script>
</body>















暂无评论内容