Vue 内置组件 keep-alive 和 transition 的实现原理
keep-alive 组件原理
核心机制
javascript
// keep-alive 本质上是一个抽象组件
export default {
name: 'keep-alive',
abstract: true, // 标记为抽象组件,不会出现在 DOM 中
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
created() {
this.cache = Object.create(null) // 缓存组件实例
this.keys = [] // 缓存组件的 key
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
}缓存策略
javascript
render() {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot) // 获取第一个子组件
if (vnode) {
const { componentOptions } = vnode
const key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 检查是否需要缓存
if (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
} else {
this.cache[key] = vnode
this.keys.push(key)
// 检查最大缓存数量
if (this.max && this.keys.length > parseInt(this.max)) {
pruneCacheEntry(this.cache, this.keys[0], this.keys)
}
}
vnode.data.keepAlive = true // 标记为 keep-alive 组件
}
return vnode || (slot && slot[0])
}生命周期处理
javascript
// 激活时调用 activated 钩子
function activated() {
callHook(this, 'activated')
}
// 停用时调用 deactivated 钩子
function deactivated() {
callHook(this, 'deactivated')
}transition 组件原理
核心结构
javascript
export default {
name: 'transition',
props: {
name: String,
mode: String, // in-out / out-in
appear: Boolean
},
render(h) {
// 获取子元素
let children = this.$slots.default
if (!children) return
// 过滤掉文本节点
children = children.filter(c => c.tag || c.isComment)
if (!children.length) return
// 处理模式
const mode = this.mode
if (mode && mode !== 'in-out' && mode !== 'out-in') {
mode = 'in-out'
}
const rawChild = children[0]
return rawChild
}
}CSS 过渡实现
javascript
export function enter(vnode, toggleDisplay) {
const el = vnode.elm
// 调用 before-enter 钩子
callHook(vnode, 'before-enter')
// 添加 enter 类名
addClass(el, enterClass)
addClass(el, enterActiveClass)
nextFrame(() => {
removeClass(el, enterClass)
addClass(el, enterToClass)
if (!vnode.data.show) {
removeClass(el, enterToClass)
removeClass(el, enterActiveClass)
}
})
}JavaScript 钩子实现
javascript
function callHook(vnode, hook) {
const handlers = vnode.data && vnode.data.transition
if (handlers && handlers[hook]) {
handlers[hook]()
}
}
function enter(vnode, done) {
const el = vnode.elm
callHook(vnode, 'before-enter')
// CSS 过渡
addClass(el, enterClass)
addClass(el, enterActiveClass)
nextFrame(() => {
removeClass(el, enterClass)
addClass(el, enterToClass)
if (!userWantsControl) {
whenTransitionEnds(el, type => {
removeClass(el, enterToClass)
removeClass(el, enterActiveClass)
callHook(vnode, 'after-enter')
done()
})
}
})
}类名管理
javascript
// 自动添加的 CSS 类名
const transitionProps = {
name: 'fade',
// 对应的类名:
// .fade-enter-active, .fade-leave-active
// .fade-enter, .fade-enter-to
// .fade-leave, .fade-leave-to
}
function resolveTransitionClass(name, state) {
return name + '-' + state
}
function addClass(el, cls) {
if (cls && !hasClass(el, cls)) {
el.classList.add(cls)
}
}
function removeClass(el, cls) {
if (cls) {
el.classList.remove(cls)
}
}关键实现细节
keep-alive 的 LRU 算法
javascript
function pruneCacheEntry(cache, key, keys) {
const cached = cache[key]
if (cached && (!filter || filter(key))) {
cached.componentInstance.$destroy() // 销毁实例
}
cache[key] = null
remove(keys, key)
}transition 的动画帧处理
javascript
function nextFrame(fn) {
const frame = requestAnimationFrame
? requestAnimationFrame
: setTimeout
frame(() => {
frame(fn)
})
}
function whenTransitionEnds(el, expectedType, cb) {
const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
if (!type) return cb()
const event = type === TRANSITION ? transitionEndEvent : animationEndEvent
let ended = 0
const end = () => {
el.removeEventListener(event, onEnd)
cb()
}
const onEnd = e => {
if (e.target === el) {
if (++ended >= propCount) {
end()
}
}
}
setTimeout(() => {
if (ended < propCount) {
end()
}
}, timeout + 1)
el.addEventListener(event, onEnd)
}使用示例
keep-alive 使用
vue
<template>
<keep-alive :include="['ComponentA']" :max="10">
<component :is="currentComponent"></component>
</keep-alive>
</template>transition 使用
vue
<template>
<transition
name="fade"
mode="out-in"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter">
<div v-if="show" key="content">内容</div>
</transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>