本篇文章是细谈 vue 系列的第四篇,按理说这篇文章是上篇 《细谈 vue - transition 篇》中的一个单独的大章节。然鹅,上篇文章篇幅过长,因此不得已将其单独拎出来写成一篇了。对该系列之前的文章感兴趣的能够点击如下连接进行传送javascript
书接上文,上篇文章咱们主要介绍了 <transition>
组件对 props
和 vnode hooks
的 输入 => 输出
处理设计,它针对单一元素的 enter
以及 leave
阶段进行了过渡效果的封装处理,使得咱们只需关注 css
和 js
钩子函数的业务实现便可。css
可是咱们在实际开发中,却终究难逃多个元素都须要进行使用过渡效果进行展现,很显然,<transition>
组件并不能实现个人业务需求。这个时候,vue
内部封装了 <transition-group>
这么一个内置组件来知足咱们的须要,它很好的帮助咱们实现了列表的过渡效果。html
老样子,直接先上一个官方的例子前端
<template>
<div id="list-demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
</template>
<script> export default { name: 'home', data () { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 } }, methods: { randomIndex: function () { return Math.floor(Math.random() * this.items.length) }, add: function () { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove: function () { this.items.splice(this.randomIndex(), 1) } } } </script>
<style lang="scss"> .list-item { display: inline-block; margin-right: 10px; } .list-enter-active, .list-leave-active { transition: all 1s; } .list-enter, .list-leave-to { opacity: 0; transform: translateY(30px); } </style>
复制代码
效果以下图vue
接下来,我将带着你们一块儿探究一下 <transition-group>
组件的设计java
和 <transition>
组件相比,<transition>
是一个抽象组件,且只对单个元素生效。而 <transition-group>
组件实现了列表的过渡,而且它会渲染一个真实的元素节点。node
但他们的设计理念倒是一致的,一样会给咱们提供一个 props
和一系列钩子函数给咱们当作 输入
的接口,内部进行 输入 => 输出
的转换或者说绑定处理浏览器
export default {
props,
beforeMount () {
// ...
},
render (h: Function) {
// ...
},
updated () {
// ...
},
methods: {
// ...
}
}
复制代码
<transition-group>
的 props
和 <transition>
的props
基本一致,只是多了一个 tag
和 moveClass
属性,删除了 mode
属性缓存
// props
import { transitionProps, extractTransitionData } from './transition'
const props = extend({
tag: String,
moveClass: String
}, transitionProps)
delete props.mode
// other import
import { warn, extend } from 'core/util/index'
import { addClass, removeClass } from '../class-util'
import { setActiveInstance } from 'core/instance/lifecycle'
import {
hasTransition,
getTransitionInfo,
transitionEndEvent,
addTransitionClass,
removeTransitionClass
} from '../transition-util'
复制代码
首先,咱们须要定义一系列变量,方便后续的操做app
tag
:从上面设计的总体脉络咱们能看到,<transition-group>
并无 abstract
属性,即它将渲染一个真实节点,那么节点 tag
则是必须的,其默认值为 span
。map
:建立一个空对象prevChildren
:用来存储上一次的子节点rawChildren
:获取 <transition-group>
包裹的子节点children
:用来存储当前的子节点transitionData
:获取组件上的渲染数据const tag: string = this.tag || this.$vnode.data.tag || 'span'
const map: Object = Object.create(null)
const prevChildren: Array<VNode> = this.prevChildren = this.children
const rawChildren: Array<VNode> = this.$slots.default || []
const children: Array<VNode> = this.children = []
const transitionData: Object = extractTransitionData(this)
复制代码
紧接着是对节点遍历的操做,这里主要对列表中每一个节点进行过渡动画的绑定
rawChildren
进行遍历,并将每一个 vnode
节点取出;key
,则将 vnode
丢到 children
中;transitionData
添加到 vnode.data.transition
上,这样便能实现列表中单个元素的过渡动画for (let i = 0; i < rawChildren.length; i++) {
const c: VNode = rawChildren[i]
if (c.tag) {
if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
children.push(c)
map[c.key] = c
;(c.data || (c.data = {})).transition = transitionData
} else if (process.env.NODE_ENV !== 'production') {
const opts: ?VNodeComponentOptions = c.componentOptions
const name: string = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag
warn(`<transition-group> children must be keyed: <${name}>`)
}
}
}
复制代码
随后对 prevChildren
进行处理
prevChildren
存在,则对其进行遍历,将 transitionData
赋值给 vnode.data.transition
,如此以后,当 vnode
子节点 enter
和 leave
阶段存在过渡动画的时候,则会执行对应的过渡动画getBoundingClientRect
获取元素的位置信息,将其记录到 vnode.data.pos
中map
中是否存在 vnode.key
,若存在,则将 vnode
放到 kept
中,不然丢到 removed
队列中this.kept
中,this.removed
则用来记录被移除掉的节点if (prevChildren) {
const kept: Array<VNode> = []
const removed: Array<VNode> = []
for (let i = 0; i < prevChildren.length; i++) {
const c: VNode = prevChildren[i]
c.data.transition = transitionData
c.data.pos = c.elm.getBoundingClientRect()
if (map[c.key]) {
kept.push(c)
} else {
removed.push(c)
}
}
this.kept = h(tag, null, kept)
this.removed = removed
}
复制代码
最后 <transition-group>
进行渲染
return h(tag, null, children)
复制代码
上面咱们已经在 render
阶段对列表中的每一个元素绑定好了 transition
相关的过渡效果,接下来就是每一个元素动态变动时,整个列表进行 update
时候的动态过渡了。那具体这块又是如何操做的呢?接下来咱们就捋捋这块的逻辑
update
钩子函数里面,会先获取上一次的子节点 prevChildren
和 moveClass
;随后判断 children
是否存在以及 children
是否 has move ,若 children
不存在,或者 children
没有 move
状态,那么也没有必要继续进行 update
的 move
过渡了,直接 return
便可const children: Array<VNode> = this.prevChildren
const moveClass: string = this.moveClass || ((this.name || 'v') + '-move')
if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
return
}
复制代码
hasMove()
:该方法主要用来判断 el
节点是否有 move
的状态。return
条件不符合的状况下,它会先克隆一个 DOM 节点,而后为了不元素内部已经有了 css 过渡,因此会移除掉克隆节点上的全部的 transitionClasses
moveClass
,并将其 display
设为 none
,而后添加到 this.$el
上getTransitionInfo
获取它的 transition
相关的信息,而后从 this.$el
上将其移除。这个时候咱们已经获取到了节点是否有 transform
的信息了export const hasTransition = inBrowser && !isIE9
hasMove (el: any, moveClass: string): boolean {
// 若不在浏览器中,或者浏览器不支持 transition,直接返回 false 便可
if (!hasTransition) {
return false
}
// 若当前实例上下文的有 _hasMove,直接返回 _hasMove 的值便可
if (this._hasMove) {
return this._hasMove
}
const clone: HTMLElement = el.cloneNode()
if (el._transitionClasses) {
el._transitionClasses.forEach((cls: string) => { removeClass(clone, cls) })
}
addClass(clone, moveClass)
clone.style.display = 'none'
this.$el.appendChild(clone)
const info: Object = getTransitionInfo(clone)
this.$el.removeChild(clone)
return (this._hasMove = info.hasTransform)
}
复制代码
children.forEach(callPendingCbs)
children.forEach(recordPosition)
children.forEach(applyTranslation)
复制代码
三个函数的处理分别以下
callPendingCbs()
:判断每一个节点前一帧的过渡动画是否执行完毕,若是没有执行完,则提早执行 _moveCb()
和 _enterCb()
recordPosition()
:记录每一个节点的新位置applyTranslation()
:分别获取节点新旧位置,并计算差值,若存在差值,则经过设置节点的 transform
属性将须要移动的节点位置偏移到以前的位置,为列表 move
作准备function callPendingCbs (c: VNode) {
if (c.elm._moveCb) {
c.elm._moveCb()
}
if (c.elm._enterCb) {
c.elm._enterCb()
}
}
function recordPosition (c: VNode) {
c.data.newPos = c.elm.getBoundingClientRect()
}
function applyTranslation (c: VNode) {
const oldPos = c.data.pos
const newPos = c.data.newPos
const dx = oldPos.left - newPos.left
const dy = oldPos.top - newPos.top
if (dx || dy) {
c.data.moved = true
const s = c.elm.style
s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
s.transitionDuration = '0s'
}
}
复制代码
move
过渡。遍历前会经过获取 document.body.offsetHeight
,从而发生计算,触发回流,让浏览器进行重绘children
进行遍历,期间若 vnode.data.moved
为 true
,则执行 addTransitionClass
为子节点加上 moveClass
,并将其 style.transform
属性清空,因为咱们在子节点预处理中已经将子节点偏移到了以前的旧位置,因此此时它会从旧位置过渡偏移到当前位置,这就是咱们要的 move
过渡的效果transitionend
过渡结束的监听事件,在事件里作一些清理的操做this._reflow = document.body.offsetHeight
children.forEach((c: VNode) => {
if (c.data.moved) {
const el: any = c.elm
const s: any = el.style
addTransitionClass(el, moveClass)
s.transform = s.WebkitTransform = s.transitionDuration = ''
el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
if (e && e.target !== el) {
return
}
if (!e || /transform$/.test(e.propertyName)) {
el.removeEventListener(transitionEndEvent, cb)
el._moveCb = null
removeTransitionClass(el, moveClass)
}
})
}
})
复制代码
注:浏览器回流触发条件我稍微作个总结,好比浏览器窗口改变、计算样式、对 DOM 进行元素的添加或者删除、改变元素 class 等
- 添加或者删除可见的DOM元素
- 元素位置改变
- 元素尺寸改变 —— 边距、填充、边框、宽度和高度
- 内容变化,好比用户在 input 框中输入文字,文本或者图片大小改变而引发的计算值宽度和高度改变
- 页面渲染初始化
- 浏览器窗口尺寸改变 —— resize 事件发生时
- 计算 offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
因为 VDOM
在节点 diff
更新的时候是不能保证被移除元素它的一个相对位置。因此这里须要在 beforeMount
钩子函数里面对 update
渲染逻辑重写,来达到咱们想要的效果
update
方法,进行缓存this.kept
是缓存的上次的节点,而且里面的节点增长了一些 transition
过渡属性。这里首先经过 setActiveInstance
缓存好当前实例,随即对 vnode
进行 __patch__
操做并移除须要被移除掉的 vnode
,而后执行 restoreActiveInstance
将其实例指向恢复this.kept
赋值给 this._vnode
,使其触发过渡update
渲染节点beforeMount () {
const update = this._update
this._update = (vnode, hydrating) => {
const restoreActiveInstance = setActiveInstance(this)
// force removing pass
this.__patch__(
this._vnode,
this.kept,
false, // hydrating
true // removeOnly (!important, avoids unnecessary moves)
)
this._vnode = this.kept
restoreActiveInstance()
update.call(this, vnode, hydrating)
}
}
复制代码
setActiveInstance
export let activeInstance: any = null
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
复制代码
文章到这就已经差很少了,对 transition
相关的内置组件 <transition>
以及 <transition-group>
的解析也已是结束了。不一样的组件类型,一个抽象组件、一个则会渲染实际节点元素,想要作的事情倒是同样的,初始化给用户的 输入
接口,输入
后便可获得 输出
的过渡效果。
前端交流群:731175396,热烈欢迎各位妹纸,汉纸踊跃加入
我的准备从新捡回本身的公众号了,以后每周保证一篇高质量好文,感兴趣的小伙伴能够关注一波。