KeepAlive组件的实现原理
KeepAlive组件在卸载时, 是将被KeepAlive的组件从原容器搬运到另外一个隐藏的容器当中, 实现“假卸载”. 再次挂载时, 也是将该组件从隐藏容器中搬运到原容器. 此过程对应生命周期activated和deactivated.
const KeepAlive = {
__isKeepAlive: true,
props: {
include: RegExp,
exclude: RegExp
},
setup(props, { slots }){
const cache = new Map() // Map<vnode.type, vnode>
const instance = currentInstance
// keepAlive组件特殊属性, 由渲染器注入
const { move, createElement } = instance.keepAliveCtx
const storageContainer = createElement('div')
// 失活。搬运至隐藏容器
instance._deActivate = vnode => {
move(vnode, storageContainer)
}
// 激活。搬运至原容器
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor)
}
return () => {
// 被keepAlive的原组件
let rawVnode = slots.default()
// 如果不是组件, 直接return (只有组件才可以被 keepAlive)
if(typeof rawVnode.type !== 'object') {
return rawVnode
}
const name = vnode.type.name
// 不符合条件的不进行缓存
if(name && (!props?.include.test(name)||props?.exclude.test(name))) {
return rawVnode
}
// 对 include 和 exclude 进行处理
const name = vnode.type.name
if(name && (!props?.include.test(name) || props?.exclude.test(name))) {
// 如果组件不符合条件, 则不进行keepAlive
return rawVnode
}
const cachedVnode = cache.get(rawVnode.type)
if(cachedVnode) {
// 获取缓存的组件实例
rawVnode.component = cachedVnode.component
// 标记此组件已被keepAlive
rawVnode.keptAlive = true
} else {
// 添加至缓存。
cache.set(rawVnode.type, rawVnode)
}
// 打上标记, 避免被真的卸载
rawVnode.shouldKeepAlive = true
// KeepAlive 组件实例
rawVnode.keepAliveInstance = instance
return rawVnode
}
}
}
// patch函数 增加 KeepAlive 组件的渲染逻辑
function patch(n1, n2, container, anchor){
// ...
// else if
if(typeof n2.type === 'object' || n2.type === 'function') {
if(!n1) {
// 如果已被keepAlive 则激活
if(n2.keptAlive){
n2.keepAliveInstance._activate(n2, container, anchor)
} esle {
mountComponent(n2, container, anchor)
}
} else {
patchComponent(n2, container, anchor)
}
}
// ...
}
// keepAlive 挂载(激活)操作
function mountComponent(){
// ...
const instance = {
// ...
keepAliveCtx: null
}
const isKeepAlive = vnode.type.__isKeepAlive
// 为KeepAlive组件添加 keepAliveCtx 对象, 提供两个方法
if(isKeepAlive) {
instance.keepAliveCtx = {
move(vnode, container, anchor) {
// 将组件渲染的内容移动到指定容器
insert(vnode.component.subTree.el, container, anchor)
},
createElement
}
}
}
// keepAlive 假卸载操作
function unmount(vnode){
if(vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
return
} else if (typeof vnode.type === 'object') {
// keepAlive 执行假卸载
if(vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode)
} else {
unmount(vnode.component.subTree)
}
return
}
// 卸载节点
const parent = vnode.el.parentNode
if(parent) {
parent.removeChild(vnode.el)
}
}
KeepAlive组件的缓存策略默认为最新一次访问. (超出限制时, 优先裁剪最久未访问的组件实例.)除此之外, 也应该允许用户自定义缓存策略. 在用户接口层面, 增加cache接口, 允许用户指定缓存实例:
<KeepAlive :cache="cache">
<Comp />
</KeepAlive>
// 一个基本的缓存实例
const _cache = new Map()
const cache = {
get(key){
_cache.get(key)
},
set(key, val){
_cache.set(key, val)
},
delete(key){
_cache.delete(key)
},
forEach(fn){
_cache.forEach(fn)
}
}
Teleport组件的实现原理
Teleport组件是新增的内置组件. 以前需要通过原生dom操作来移动元素节点, 现在Teleport组件可以将插槽内容渲染在任何期望的地方.
// ./Overlay.vue
<template>
<Teleport to="body">
<div class="overlay"></div>
</Teleport>
</template>
// 将<Overlay />的内容通过 Teleport组件 渲染在body上
const Teleport = {
// Teleport组件的标识
__isTeleport: true,
// 通过process函数实现渲染
// 1. 缩减渲染器代码体积。 2.更好地利用Tree-shaking
process(n1, n2, container, anchor, internals) {
const { patch, patchChildren, move } = internals
if(!n1) {
// 挂载至指定节点target
const target = typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to
n2.children.forEach(c => patch(null, c, target, anchor))
} else {
// 更新
patchChildren(n1, n2, container)
// props.to 变化则进行移动
if(n2.props.to !== n1.props.to) {
const newTarget = typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to
n2.children.forEach(c => move(c, newTarget))
}
}
}
}
// 修改patch函数 抽离Teleport组件的渲染逻辑
function patch(n1, n2, container, anchor){
// ...
// else if
if(typeof n2.type === 'object' && n2.type.__isTeleport) {
// 如果是Teleport 则执行组件的process方法
n2.type.process(n1, n2, container, anchor, {
patch,
patchChildren,
unmount,
move(){
// 简易实现. 实际上虚拟节点的类型有很多种, 不止组件和普通元素
insert(vnode.component ? vnode.component.subTree.el : vnode.el, container, anchor)
}
})
}
// ...
}
Transition组件饿实现原理
- 当dom元素被挂载时, 将动效附加到该dom元素上
- 当dom元素被卸载时, 等到附加到该元素上的动效执行完毕后再卸载
Transition不会渲染任务内容, 只是读取默认插槽内容, 渲染要过渡的元素. 并在过渡元素的虚拟节点上添加相关的钩子函数.
const Transition = {
name: 'Transition',
props: {
// 简易实现 还需要处理name、mode等props
// ... name mode
},
setup(props, { slots }){
return () => {
const innerVnode = slots.default()
// 相关钩子。
innerVnode.transition = {
beforeEnter(el){
el.classList.add('enter-from')
el.classList.add('enter-active')
},
enter(el){
// 下一帧切换到入场结束状态
nextFrame(() => {
el.classList.remove('enter-from')
el.classList.add('enter-to')
el.addEventListener('transitionend', () => {
el.classList.remove('enter-active')
el.classList.remove('enter-to')
})
})
},
leave(el, performRemove){
el.classList.add('leave-from')
el.classList.add('leave-active')
// 强制reflow 使初始状态生效
document.body.offsetHeight
// 下一帧切换到离场结束状态
nextFrame(() => {
el.classList.remove('leave-from')
el.classList.add('leave-to')
el.addEventListener('transitionend', () => {
el.classList.remove('leave-active')
el.classList.remove('leave-to')
performRemove()
})
})
}
}
return innerVnode
}
}
}
// 挂载前后执行对应钩子函数
function mountComponent(vnode, container, anchor){
const needTransition = vnode.transition
if(needTransition) {
vnode.transition.beforeEnter(el)
}
insert(el, container, anchor)
if(needTransition) {
vnode.transition.enter(el)
}
}
// Transition组件的卸载
function unmount(vnode){
const needTransition = vnode.transition
if(vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
return
} else if (typeof vnode.type === 'object') {
if(vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode)
} else {
unmount(vnode.component.subTree)
}
return
}
const parent = vnode.el.parentNode
if(parent) {
// 封装卸载动作
const performRemove = () => parent.removeChild(vnode.el)
if(needTransition) {
// 如果需要过渡处理
// 则先调用leave钩子, 并将卸载操作作为参数传递
vnode.transition.leave(vnode.el, performRemove)
} else {
performRemove()
}
}
}
总结
内建组件KeepAlive、Teleport、Transition与渲染器结合紧密, 都需要框架提供底层的实现与支持.