Vue2.0中一共有五个内置组件:动态渲染组件的component、用于过渡动画的transition-group与transition、缓存组件的keep-alive、内容分发插槽的slot。
component组件配合is属性在编译的过程当中被替换成具体的组件,而slot组件已经在上一篇文章中加以描述,所以本章主要阐述剩余的三个内置组件。
css
<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出如今父组件链中。该组件要求同时只有一个子元素被渲染。前端
KeepAlive 组件源码以下所示:
node
{
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
复制代码
KeepAlive 组件的逻辑相对比较简单,根据传入的 include 与 exclude 规则来决定是否缓存子组件,根据传入的 max 参数来决定最多缓存多少组件。
从 render 函数中能够看出,若是子组件是缓存对象 cache 的属性,则直接返回该子组件的VNode,若是不是,则添加到缓存对象上,并将缓存的VNode的 data.keepAlive 属性置为 true。
这里有两点须要注意:keepAlive组件的 abstract 属性为 true、被缓存的子组件 vnode.data.keepAlive 属性为 true。
react
当 abstract 属性为 true 时,表示该组件为抽象组件:组件自己不会被渲染成DOM元素、不会出如今父组件链中。
在完成一系列初始化的过程当中,会调用 initLifecycle 方法:
web
function initLifecycle(vm) {
const options = vm.$options
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
/* ... */
}
复制代码
由上可知,在 options.abstract 为 true 时,组件实例创建父子关系的时候会被忽略。
算法
在 patch 的过程当中会调用 createComponent 方法:
数组
function createComponent(vnode,insertedVnodeQueue,parentElm,refElm){
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
复制代码
在本系列第七篇文章《组件》中,详细分析过 createComponent 方法,当时没考虑 keepAlive 值为 true的状况,在这里重点介绍。
在首次渲染时 vnode.componentInstance 的值为空,所以不论 keepAlive 是否为空,isReactivated 值老是 false。再次渲染时,若 keepAlive 值为 true 则isReactivated 为true。
浏览器
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false)
}
复制代码
钩子函数 init 在 keepAlive 值为 false 时的功能是调取组件的构造函数生成组件构造实例。
缓存
init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
/* 省略... */
}
}
复制代码
当 keepAlive 值为 true 时,会调用 prepatch 方法,该方法不会再执行组件的 mount 过程,而是直接调用 updateChildComponent 方法更新子组件,这也是被 keepAlive 包裹的组件在有缓存的时候就不会再执行组件的 created、mounted 等钩子函数的缘由。
app
function prepatch (oldVnode, vnode) {
var options = vnode.componentOptions;
var child = vnode.componentInstance = oldVnode.componentInstance;
updateChildComponent(child,options.propsData,options.listeners,
vnode,options.children);
}
复制代码
在 createComponent 函数最后,若是组件再次渲染且 keepAlive 为 true 时,会调用 reactivateComponent 函数,该函数将缓存的DOM元素直接插入到目标位置。
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
/* 省略对 transition 动画不触发的问题的处理*/
insert(parentElm, vnode.elm, refElm);
}
复制代码
Vue 提供了 transition 的封装组件,在下列情形中,能够给任何元素和组件添加进入/离开过渡
一、条件渲染 (使用 v-if)。
二、条件展现 (使用 v-show)。
三、动态组件。
四、组件根节点。
当插入或删除包含在 transition 组件中的元素时,Vue 将会作如下处理:
一、自动嗅探目标元素是否应用了 CSS 过渡或动画,若是是,在恰当的时机添加/删除 CSS 类名。
二、若是过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
三、若是没有找到 JavaScript 钩子而且也没有检测到 CSS 过渡/动画,DOM 操做 (插入/删除) 在下一帧中当即执行。
Transition 组件的定义在 /src/platforms/web/runtime/components/transition.js 文件中,精简代码以下:
export default {
name: 'transition',
props: transitionProps,
abstract: true,
render (h) {
let children = this.$slots.default
if (!children) {return}
children = children.filter(isNotTextNode)
if (!children.length) {return}
/* 省略多个子元素警告 */
const mode = this.mode
/* 省略无效模式警告 */
const rawChild = children[0]
if (hasParentTransition(this.$vnode)) {return rawChild}
const child = getRealChild(rawChild)
if (!child) {return rawChild}
if (this._leaving){return placeholder(h, rawChild)}
/* 省略获取id与key代码 */
const data = (child.data || (child.data = {})).transition = extractTransitionData(this)
const oldRawChild = this._vnode
const oldChild = getRealChild(oldRawChild)
if (child.data.directives && child.data.directives.some(isVShowDirective)) {
child.data.show = true
}
/* 省略多元素过渡模式处理代码 */
return rawChild
}
}
复制代码
Transition 组件与 KeepAlive 组件同样是抽象函数,在该组件定义中比较重要的就是 render 函数,其做用就是渲染生成VNode。
在该渲染函数中有三个功能比较重要:
一、将 Transition 组件上的参数赋值到 child.data.transition 上。
二、若是 Transition 组件上使用 v-show 指令,则将 child.data.show 设为 true。
三、设置多元素过渡的模式。
Vue 提供了两种过渡模式,默认同时生效。
in-out:新元素先进行过渡,完成以后当前元素过渡离开。
out-in:当前元素先进行过渡,完成以后新元素过渡进入。
在 render 函数中相关代码以下所示:
const oldData = oldChild.data.transition = extend({}, data)
if (mode === 'out-in') {
this._leaving = true
mergeVNodeHook(oldData, 'afterLeave', () => {
this._leaving = false
this.$forceUpdate()
})
return placeholder(h, rawChild)
} else if (mode === 'in-out') {
if (isAsyncPlaceholder(child)) {
return oldRawChild
}
let delayedLeave
const performLeave = () => { delayedLeave() }
mergeVNodeHook(data, 'afterEnter', performLeave)
mergeVNodeHook(data, 'enterCancelled', performLeave)
mergeVNodeHook(oldData,'delayLeave',leave=>{delayedLeave=leave})
}
复制代码
从上述代码可知:当过渡模式为 out-in,在切换元素时,当前元素彻底 leave 后才会加载新元素。当过渡模式为 in-out,当前元素延时到新元素 enter 后再 leave。
过渡相关的逻辑在 /src/platforms/web/runtime/modules/transition.js 文件中实现,Vue会将相关逻辑插入到 patch 的生命周期中去处理。
export default inBrowser ? {
create: _enter,
activate: _enter,
remove (vnode, rm) {
if (vnode.data.show !== true) {
leave(vnode, rm)
} else {
rm()
}
}
} : {}
function _enter (_,vnode) {
if (vnode.data.show !== true) {
enter(vnode)
}
}
复制代码
能够看出过渡的逻辑本质上就是在元素插入时调用 enter 函数,在元素移除时调用 leave 函数。
由于在使用 v-show 指令时元素始终会被渲染并保留在 DOM 中,只是简单地切换元素的 CSS 属性 display。因此会对使用 v-show 指令的状况进行特殊处理,在下一小结阐述具体处理过程。
整体来看 enter 函数与 leave 函数几乎是一个镜像过程,下面仅分析 enter 函数。
function enter (vnode, toggleDisplay) {
const el = vnode.elm
/* 省略... */
const expectsCSS = css !== false && !isIE9
const userWantsControl = getHookArgumentsLength(enterHook)
/* 省略 cb 函数实现 */
/* 合并 insert 钩子函数 */
if (!vnode.data.show) {
mergeVNodeHook(vnode, 'insert', () => {
const parent = el.parentNode
const pendingNode = parent && parent._pending && parent._pending[vnode.key]
if (pendingNode &&
pendingNode.tag === vnode.tag &&
pendingNode.elm._leaveCb
) {
pendingNode.elm._leaveCb()
}
enterHook && enterHook(el, cb)
})
}
/* 开始执行过渡动画 */
beforeEnterHook && beforeEnterHook(el)
if (expectsCSS) {
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
nextFrame(/* 省略... */)
}
/* 省略使用 v-show 指令的状况 */
if (!expectsCSS && !userWantsControl) {
cb()
}
}
复制代码
enter 函数看上去很复杂,其核心代码是开始执行过渡动画的部分。首先执行 beforeEnterHook 钩子函数,若使用 css 过渡类,则接着执行:
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
复制代码
addTransitionClass 函数做用就是给元素添加样式,而后执行 nextFrame 函数。
function nextFrame (fn: Function) {
raf(() => {
raf(fn)
})
}
const raf = inBrowser
? window.requestAnimationFrame
? window.requestAnimationFrame.bind(window)
: setTimeout
复制代码
nextFrame 函数在支持 requestAnimationFrame 方法的浏览器中使用该方法,参数 fn 会在下一帧执行。若是不支持则使用 setTimeout 代替。
nextFrame(() => {
removeTransitionClass(el, startClass)
if (!cb.cancelled) {
addTransitionClass(el, toClass)
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
}
})
复制代码
在下一帧时,首先移除 startClass 样式,而后判断过渡是否被取消。若是没有取消,添加 toClass 样式,而后根据是否经过 enterHook 钩子函数控制动画来决定 cb 函数的执行时机。
const cb = el._enterCb = once(() => {
if (expectsCSS) {
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass)
}
enterCancelledHook && enterCancelledHook(el)
} else {
afterEnterHook && afterEnterHook(el)
}
el._enterCb = null
})
复制代码
cb 函数首先移除 toClass 与 activeClass 样式,若是过渡被取消则先移除 startClass 样式,再执行 enterCancelledHook 钩子函数。若是过渡没有被取消,则调用 afterEnterHook 钩子函数。
对于在 Transition 组件上使用 v-show 指令的状况,在 v-show 指令的实现中有特殊处理。相关代码在 /src/platforms/web/runtime/directives/show.js 文件中。
export default {
bind (el, { value }, vnode) {
/* ... */
const transition = vnode.data && vnode.data.transition
const originalDisplay = el.__vOriginalDisplay =
el.style.display === 'none' ? '' : el.style.display
if (value && transition) {
vnode.data.show = true
enter(vnode, () => {
el.style.display = originalDisplay
})
}
/* ... */
},
update (el, { value, oldValue }, vnode) {
/* ... */
const transition = vnode.data && vnode.data.transition
if (transition) {
vnode.data.show = true
if (value) {
enter(vnode, () => {
el.style.display = el.__vOriginalDisplay
})
} else {
leave(vnode, () => {
el.style.display = 'none'
})
}
}
},
/* 省略... */
复制代码
能够看到在 v-show 指令的实现中,若在 Transition 组件上使用则调用 enter 与 leave 函数,与 patch 生命周期调用这两个函数不一样的会额外的传入第二个参数。
在 enter 与 leave 函数也有对应的处理,以保证在DOM元素没有新增和移除的状况下实现过渡效果。
if (vnode.data.show) {
toggleDisplay && toggleDisplay()
enterHook && enterHook(el, cb)
}
复制代码
Vue 使用 <transition-group> 组件完成列表过渡效果,该组件有如下几个特色:
一、该组件不是抽象组件,会以一个真实元素呈现,默认是 <span>,能够经过 tag参数 指定。
二、过渡模式不可用。
三、内部元素老是须要提供惟一的 key 属性值。
四、CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器自己。
TransitionGroup 组件定义在 /src/platforms/web/runtime/components/transition-group.js 文件中,精简代码以下:
const props = extend({
tag,
moveClass
}, transitionProps)
delete props.mode
export default {
props,
beforeMount () { /* 省略具体实现 */ },
render (h) { /* 省略具体实现 */ },
updated () { /* 省略具体实现 */ },
methods: {
hasMove (el, moveClass){ /* 省略具体实现 */ }
}
}
复制代码
TransitionGroup 组件有两种过渡效果:基本过渡效果、平滑过渡效果,后者经过 v-move 特性来实现。
在源码实现中,基本过渡效果由组件的 render 函数完成,当数据发生变化时的平滑过渡效果由 updated 生命周期钩子函数完成。
TransitionGroup 组件 render 方法的完整代码以下所示:
render (h) {
const tag = this.tag || this.$vnode.data.tag || 'span'
const map = Object.create(null)
const prevChildren = this.prevChildren = this.children
const rawChildren = this.$slots.default || []
const children = this.children = []
const transitionData = extractTransitionData(this)
for (let i = 0; i < rawChildren.length; i++) {
const c = 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 = c.componentOptions
const name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag
warn(`<transition-group> children must be keyed: <${name}>`)
}
}
}
if (prevChildren) {
const kept = []
const removed = []
for (let i = 0; i < prevChildren.length; i++) {
const c = 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
}
return h(tag, null, children)
}
复制代码
render 函数的本质功能就是生成VNode,其中该函数的参数 h 为用来生成VNode的 createElement 函数。
函数中首先声明的几个变量具体含义以下所示:
一、tag:TransitionGroup 组件最终渲染的元素类型,默认是 span。
二、map:存储原始子节点 key 与值的对象。
三、prevChildren:存储上一次的子节点数组。
四、rawChildren:原始子节点数组。
五、children:当前子节点数组。
五、transitionData:TransitionGroup 组件上提取的过渡参数。
紧接着的 for 循环是处理原始子节点的,由于 TransitionGroup 组件要求全部子节点都显式提供 key 值,若是没有提供 key 值在开发环境下会报错。
if (c.key != null && String(c.key).indexOf('__vlist') !== 0)
复制代码
判断是否显式提供 key 值的条件语句之因此这样写,是由于在 for 循环的渲染过程当中,在没有提供 key 值的状况下,会自动加上 __vlist 为开头的字符串做为 key 值。
这个 for 循环还有一个重要的功能是将组件过渡参数赋值给子组件的 data.transition 属性,在上一节讲述 Transition 组件时有讲过,在元素进入和移除时会根据这个属性来显示相应的过渡效果。
最后处理改变前的子节点,调用了原生 DOM 的 getBoundingClientRect 方法获取到原生 DOM 的位置信息,记录到 vnode.data.pos 中。而后将存在的节点放入 kept 中,将删除的节点放入 removed 中。最后返回由 createElement 函数生成的VNode。
TransitionGroup 组件的 render 方法因为将过渡信息下沉到子节点上,是能够实现基本的子节点添加删除的过渡效果的。因为插入和删除操做与须要移动的元素没有过渡效果控制的关联,因此并无平滑过渡的效果。
当数据改变时会调用 updated 生命周期钩子,TransitionGroup 组件当子节点添加与删除的平滑过渡效果在该钩子函数中实现。
updated () {
const children = this.prevChildren
const moveClass = this.moveClass || ((this.name || 'v') + '-move')
if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
return
}
children.forEach(callPendingCbs)
children.forEach(recordPosition)
children.forEach(applyTranslation)
this._reflow = document.body.offsetHeight
children.forEach((c) => {
if (c.data.moved) {
const el = c.elm
const s = 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)
}
})
}
})
}
复制代码
updated 函数首先使用 hasMove 方法判断子节点是否认义了 move 相关的动画样式,接着对子节点进行预处理:
一、callPendingCbs:在前一个过渡动画没执行完又再次执行到该方法的时候,会提早执行 _moveCb 和 _enterCb。
二、recordPosition:记录节点的新位置,赋值给 data.newPos 属性。
三、applyTranslation:先计算节点新位置和旧位置的差值,把须要移动的节点的位置又偏移到以前的旧位置,目的是为了作 move 缓动作准备。
接着经过读取 document.body.offsetHeight 强制触发浏览器重绘。
而后遍历子节点,先给子节点添加 moveClass,接着把子节点的 style.transform 设置为空,因为以前使用 applyTranslation 方法将子节点偏移到旧位置,此时会按照设置的过渡效果偏移到当前位置,进而实现平滑过渡的效果。
最后监听 transitionEndEvent 过渡结束的事件,作一些清理的操做。
Vue 中虚拟 DOM 的子元素更新算法是不稳定的,它不能保证被移除元素的相对位置。Vue 在 beforeMount 生命周期钩子函数中对这种状况进行了处理。
beforeMount () {
const update = this._update
this._update = (vnode, hydrating) => {
const restoreActiveInstance = setActiveInstance(this)
this.__patch__(this._vnode, this.kept, false, true)
this._vnode = this.kept
restoreActiveInstance()
update.call(this, vnode, hydrating)
}
}
复制代码
在 beforeMount 函数中,首先重写了 _update 方法,_update 方法自己的做用是根据VNode生成真实DOM的。重写后的 _update 方法主要有两步:首先移除须要移除的 vnode,同时触发它们的 leaving 过渡;而后须要把插入和移动的节点达到它们的最终态,同时还要保证移除的节点保留在应该的位置。
Vue 经过这两步处理来解决子元素更新算法是不稳定的问题,做者在 TransitionGroup 组件实现的文件中也有详细的注释说明。
KeepAlive 组件不渲染真实DOM节点,会将缓存的子组件放入 cache 数组中,并将被缓存子组件的 data.keepAlive 属性置为 true。若是须要再次渲染被缓存的子组件,则直接返回该子组件的VNode,而组件的 created、mounted 等钩子函数不会再执行。
Transition 组件的 render 函数会将组件上的参数赋值到 child.data.transition 上,而后在 patch 的过程当中会调用 enter 与 leave 函数完成相关过渡效果。在使用 v-show 指令时,DOM元素并无新增和删除,Vue 对这种状况进行了特别处理,保证在DOM元素没有新增和移除的状况下实现过渡效果。
TransitionGroup 组件的基本过渡效果跟 Transition 组件实现效果同样。修改列表数据的时候,若是是添加或者删除数据,则会触发相应元素自己的过渡动画。平滑过渡效果本质上就是先将元素移动到旧位置,而后再根据定义的过渡效果将其移动到新位置。
欢迎关注公众号:前端桃花源,互相交流学习!