KeepAlive组件
组件的激活与失活
"卸载"一个被 KeepAlive 的组件时,它并不会真的被卸载,而会被移动到一个隐藏容器中。当重新"挂载"该组件时, 它也不会被真的挂载,而会被从隐藏容器中取出,再"放回"原来的容器中
思路:
KeepAlive也是一个对象,用__isKeepAlive来做标识,在setup返回vnode
js
const CompVNode = {
type: KeepAlive,
}
const KeepAlive = {
// KeepAlive 组件独有的属性,用作标识
__isKeepAlive: true,
setup() {
return () => {}
}
}
创建缓存对象,key为vnode.type,value为vnode,渲染的时候判断
diff
+ const cache = new Map()
const KeepAlive = {
// KeepAlive 组件独有的属性,用作标识
__isKeepAlive: true,
setup(props, { slots }) {
// 创建一个缓存对象
// key: vnode.type
// value: vnode
return () => {}
}
}
- 挂载组件时判断__isKeepAlive是否为KeepAlive组件,是KeepAlive就在实例上添加 keepAliveCtx 对象
- move函数是将组件渲染的内容移动到指定容器中,createElement是在渲染器createRenderer的参数
diff
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
mounted: [],
+ // 只有 KeepAlive 组件的实例下会有 keepAliveCtx 属性
+ keepAliveCtx: null
}
+ // 检查当前要挂载的组件是否是 KeepAlive 组件
+ const isKeepAlive = vnode.type.__isKeepAlive
+ if (isKeepAlive) {
+ // 在 KeepAlive 组件实例上添加 keepAliveCtx 对象
+ instance.keepAliveCtx = {
+ // move 函数用来移动一段 vnode
+ move(vnode, container, anchor) {
+ // 本质上是将组件渲染的内容移动到指定容器中,即隐藏容器中
+ insert(vnode.component.subTree.el, container, anchor)
+ },
+ createElement
+ }
+ }
// 省略部分代码
}
- KeepAlive有专属的_deActivate、_activate,分别在挂载、卸载时调用
- 卸载时将vnode移到storageContainer,挂载时将vnode插入到anchor的前面
diff
const KeepAlive = {
// KeepAlive 组件独有的属性,用作标识
__isKeepAlive: true,
setup(props, { slots }) {
// 创建一个缓存对象
// key: vnode.type
// value: vnode
const cache = new Map()
+ // 当前 KeepAlive 组件的实例
+ const instance = currentInstance
+ // 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
+ // 该对象会暴露渲染器的一些内部方法,其中 move 函数用来将一段 DOM 移动到另一个容器中
+ const { move, createElement } = instance.keepAliveCtx
+ // 创建隐藏容器
+ const storageContainer = createElement('div')
+ // KeepAlive 组件的实例上会被添加两个内部函数,分别是 _deActivate和 _activate
+ // 这两个函数会在渲染器中被调用
+ instance._deActivate = (vnode) => {
+ move(vnode, storageContainer)
+ }
+ instance._activate = (vnode, container, anchor) => {
+ move(vnode, container, anchor)
+ }
return () => {}
}
}
- slots.default默认插槽就是要被KeepAlive的组件,如果默认插槽的type不是对象也就说明不是组件就直接渲染,因为非组件的虚拟节点无法被 KeepAlive
- 如果默认插槽是组件,就判断组件是否有缓存到cache
- 如果组件有缓存,就继承组件实例,在vnode上添加 keptAlive 属性,标记为 true,避免渲染器重新挂载它
- 如果组件没有缓存就添加到缓存中
- 在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载,组件的实例也添加到 vnode 上,以便在渲染器中访问
diff
const KeepAlive = {
// KeepAlive 组件独有的属性,用作标识
__isKeepAlive: true,
setup(props, { slots }) {
// 创建一个缓存对象
// key: vnode.type
// value: vnode
const cache = new Map()
// 当前 KeepAlive 组件的实例
const instance = currentInstance
// 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
// 该对象会暴露渲染器的一些内部方法,其中 move 函数用来将一段 DOM 移动到另一个容器中
const { move, createElement } = instance.keepAliveCtx
// 创建隐藏容器
const storageContainer = createElement('div')
// KeepAlive 组件的实例上会被添加两个内部函数,分别是 _deActivate和 _activate
// 这两个函数会在渲染器中被调用
instance._deActivate = (vnode) => {
move(vnode, storageContainer)
}
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor)
}
return () => {
+ // KeepAlive 的默认插槽就是要被 KeepAlive 的组件
+ let rawVNode = slots.default()
+ // 如果不是组件,直接渲染即可,因为非组件的虚拟节点无法被 KeepAlive
+ if (typeof rawVNode.type !== 'object') {
+ return rawVNode
+ }
+ // 在挂载时先获取缓存的组件 vnode
+ const cachedVNode = cache.get(rawVNode.type)
+ if (cachedVNode) {
+ // 如果有缓存的内容,则说明不应该执行挂载,而应该执行激活
+ // 继承组件实例
+ rawVNode.component = cachedVNode.component
+ // 在 vnode 上添加 keptAlive 属性,标记为 true,避免渲染器重新挂载它
+ rawVNode.keptAlive = true
+ } else {
+ // 如果没有缓存,则将其添加到缓存中,这样下次激活组件时就不会执行新的挂载动作了
+ cache.set(rawVNode.type, rawVNode)
+ }
+
+ // 在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载
+ rawVNode.shouldKeepAlive = true
+ // 将 KeepAlive 组件的实例也添加到 vnode 上,以便在渲染器中访问
+ rawVNode.keepAliveInstance = instance
+
+ // 渲染组件 vnode
+ return rawVNode
}
}
}
- 在卸载组件时判断vnode.shouldKeepAlive是否为true
- 为true就说明是缓存组件不应该真正的卸载,调用_deActivate将组件移到其他元素上
- 否则正常卸载元素
diff
function unmount(vnode) {
if (vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
return
} else if (typeof vnode.type === 'object') {
+ // vnode.shouldKeepAlive 是一个布尔值,用来标识该组件是否应该被 KeepAlive
+ if (vnode.shouldKeepAlive) {
+ // 对于需要被 KeepAlive 的组件,我们不应该真的卸载它,而应调用该组件的父组件,
+ // 即 KeepAlive 组件的 _deActivate 函数使其失活
+ vnode.keepAliveInstance._deActivate(vnode)
+ } else {
+ unmount(vnode.component.subTree)
+ }
+ return
}
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
}
- 在挂载组件时判断keptAlive是否为true
- 为true说明已经KeepAlive不需要重新挂载,调用activate来激活它
diff
function patch(oldN, newN, container, anchor) {
if (oldN && oldN.type !== newN.type) {
unmount(oldN)
oldN = null
}
const { type } = newN
if (typeof type === 'string') {
// 省略部分代码
} else if (type === Text) {
// 省略部分代码
} else if (type === Fragment) {
// 省略部分代码
} else if (typeof type === 'object') {
// component
if (!oldN) {
+ // 如果该组件已经被 KeepAlive,则不会重新挂载它,而是会调用_activate 来激活它
+ if (newN.keptAlive) {
+ newN.keepAliveInstance._activate(newN, container, anchor)
+ } else {
mountComponent(newN, container, anchor)
}
} else {
patchComponent(oldN, newN, anchor)
}
}
}
结果:
js
const MyComponent = {
name: 'MyComponent',
props: {
title: String
},
setup(props, { emit, slots }) {
const counter = ref(0)
return () => {
return {
type: 'button',
props: {
onClick() {
counter.value++
}
},
children: `count is ${counter.value}`
}
}
}
}
const CompVNode = {
type: KeepAlive,
children: {
default() {
return { type: MyComponent }
}
}
}
renderer.render(CompVNode, document.querySelector('#app'))
setTimeout(() => {
renderer.render(null, document.querySelector('#app'))
}, 1000);
卸载组件时成功保存了组件
js
renderer.render(CompVNode, document.querySelector('#app'))
setTimeout(() => {
renderer.render(null, document.querySelector('#app'))
}, 1000);
setTimeout(() => {
renderer.render(CompVNode, document.querySelector('#app'))
}, 2000);
挂载组件时,向cache拿缓存组件
include 和 exclude
KeepAlive 组件支持两个 props,分别是 include 和 exclude。其中,include 用来显式地配置应该被缓存组件,而 exclude 用来显式地配置不应该被缓存组件
js
const KeepAlive = {
__isKeepAlive: true,
// 定义 include 和 exclude
// 为了简化问题,我们只允许为 include 和 exclude 设置正则类 型的值
props: {
include: RegExp,
exclude: RegExp
},
setup(props, { slots }) {
// 省略部分代码
}
}
思路:
- 通过setup的props参数拿到include、exclude
- 根据include、exclude判断组件的name属性,如果 name 无法被 include 匹配或者被 exclude 匹配就直接渲染组件不缓存
diff
const KeepAlive = {
__isKeepAlive: true,
props: {
include: RegExp,
exclude: RegExp
},
setup(props, { slots }) {
// 省略部分代码
return () => {
let rawVNode = slots.default()
if (typeof rawVNode.type !== 'object') {
return rawVNode
}
+ // 获取"内部组件"的 name
+ const name = rawVNode.type.name
+ // 对 name 进行匹配
+ if (
+ name &&
+ (
+ // 如果 name 无法被 include 匹配
+ (props.include && !props.include.test(name)) ||
+ // 或者被 exclude 匹配
+ (props.exclude && props.exclude.test(name))
+ )
+ ) {
// 则直接渲染"内部组件",不对其进行后续的缓存操作
return rawVNode
}
// 省略部分代码
}
}
}
结果:
exclude将以My开头的组件name排除不进行缓存
js
const CompVNode = {
type: KeepAlive,
props: {
exclude: /^My/
},
children: {
default() {
return { type: MyComponent }
}
}
}
renderer.render(CompVNode, document.querySelector('#app'))
setTimeout(() => {
renderer.render(null, document.querySelector('#app'))
}, 1000);
include将以My开头的组件name排除进行缓存
js
const CompVNode = {
type: KeepAlive,
props: {
include: /^My/
},
children: {
default() {
return { type: MyComponent }
}
}
}
renderer.render(CompVNode, document.querySelector('#app'))
setTimeout(() => {
renderer.render(null, document.querySelector('#app'))
}, 1000);
setTimeout(() => {
renderer.render(CompVNode, document.querySelector('#app'))
}, 2000);
Teleport组件
Teleport 包裹的元素虽然是属于 app.vue 组件,但是渲染过后它却被渲染在了 body 这个 dom 元素下面了
html
<template>
<div class="app">
App组件
<Teleport to="body">
<div>我是被 teleport 包裹的元素</div>
</Teleport>
</div>
</template>
思路:
Teleport组件有__isTeleport 和 process,__isTeleport是Teleport组件标识,process是处理渲染逻辑函数
js
const Teleport = {
__isTeleport: true,
process(oldN, newN, container, anchor) {
// 在这里处理渲染逻辑
}
}
在patch函数中判断Telepor组件时,将参数传递给process,将渲染逻辑分离
diff
function patch(oldN, newN, container, anchor) {
if (oldN && oldN.type !== newN.type) {
unmount(oldN)
oldN = null
}
const { type } = newN
if (typeof type === 'string') {
// 省略部分代码
} else if (type === Text) {
// 省略部分代码
} else if (type === Fragment) {
// 省略部分代码
+ } else if (typeof type === 'object' && type.__isTeleport) {
+ // 组件选项中如果存在 __isTeleport 标识,则它是 Teleport 组件,
+ // 调用 Teleport 组件选项中的 process 函数将控制权交接出去
+ // 传递给 process 函数的第五个参数是渲染器的一些内部方法
+ type.process(oldN, newN, container, anchor, {
+ patch,
+ patchChildren,
+ unmount,
+ move(vnode, container, anchor) {
+ insert(vnode.component ? vnode.component.subTree.el : vnode.el, container, anchor)
+ }
+ })
+ }
}
- 当oldN不存在时说明是新挂载
- 判断父组件传props的to是否是字符串类型,是字符串就通过document.querySelector获取到to对应的真实dom节点,否则就以to为真实dom节点
- 因为Teleport组件是包裹其他组件的,所以children是vnode数组,所以需要遍历调用patch进行挂载
diff
const Teleport = {
__isTeleport: true,
process(oldN, newN, container, anchor, internals) {
+ // 通过 internals 参数取得渲染器的内部方法
+ const { patch } = internals
+ // 如果旧 VNode oldN 不存在,则是全新的挂载,否则执行更新
+ if (!oldN) {
+ // 挂载
+ // 获取容器,即挂载点
+ const target = typeof newN.props.to === 'string'
+ ? document.querySelector(newN.props.to)
+ : newN.props.to
+ // 将 newN.children 渲染到指定挂载点即可
+ newN.children.forEach(c => patch(null, c, target, anchor))
+ } else {
+ // 更新
+ }
}
}
- oldN存在就说明是更新,调用patchChildren比较
- 更新存在新旧的to参数不同,如果新旧to参数不同,将newN.children遍历调用move移动到新的容器
diff
const Teleport = {
__isTeleport: true,
process(oldN, newN, container, anchor, internals) {
const { patch, patchChildren, move } = internals
if (!oldN) {
// 省略部分代码
} else {
+ // 更新
+ patchChildren(oldN, newN, container)
+ // 如果新旧 to 参数的值不同,则需要对内容进行移动
+ if (newN.props.to !== oldN.props.to) {
+ // 获取新的容器
+ const newTarget = typeof newN.props.to === 'string'
+ ? document.querySelector(newN.props.to)
+ : newN.props.to
+ // 移动到新的容器
+ newN.children.forEach(c => move(c, newTarget))
+ }
+ }
}
}
结果:
js
const CompVNode = {
type: Teleport,
props: {
to: 'body'
},
children: [
{ type: 'h1', children: 'Header' },
{ type: 'p', children: 'content' }
]
}
renderer.render(CompVNode, document.querySelector('#app'))
js
const CompVNode = {
type: Teleport,
props: {
to: 'body'
},
children: [
{ type: 'h1', children: 'Header' },
{ type: 'p', children: 'content' }
]
}
renderer.render(CompVNode, document.querySelector('#app'))
const CompVNode2 = {
type: Teleport,
props: {
to: '#test'
},
children: [
{ type: 'h1', children: 'A big Title' },
{ type: 'p', children: 'a small content' }
]
}
setTimeout(() => {
renderer.render(CompVNode2, document.querySelector('#app'))
}, 1000);
Transition组件
- 当 DOM 元素被挂载时,将动效附加到该 DOM 元素上;
- 当 DOM 元素被卸载时,不要立即卸载 DOM 元素,而是等到附加 到该 DOM 元素上的动效执行完成后再卸载它。
html
<template>
<Transition>
<div>我是需要过渡的元素</div>
</Transition>
</template>
Transition组件对应的vnode,Transition组件的子节点被编译为默认插槽
js
function render() {
return {
type: Transition,
children: {
default() {
return { type: 'div', children: '我是需要过渡的元素' }
}
}
}
}
思路:
- Transition组件不会渲染额外内容,只是通过默认插槽(slots.default())读取过渡元素,并渲染需要过渡的元素
- 在过渡元素的虚拟节点上添加transition相关的钩子函数,在挂载、卸载元素时调用
js
const Transition = {
name: 'Transition',
setup(props, { slots }) {
return () => {
// 通过默认插槽获取需要过渡的元素
const innerVNode = slots.default()
// 在过渡元素的 VNode 对象上添加 transition 相应的钩子函数
innerVNode.transition = {
beforeEnter(el) {
// 省略部分代码
},
enter(el) {
// 省略部分代码
},
leave(el, performRemove) {
// 省略部分代码
}
}
// 渲染需要过渡的元素
return innerVNode
}
}
}
在挂载元素前执行beforeEnter,在挂载元素后执行enter
diff
function mountElement(vnode, container, anchor) {
const el = vnode.el = createElement(vnode.type)
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
patch(null, child, el)
})
}
if (vnode.props) {
for (const key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
- insert(el, container, anchor)
+ // 判断一个 VNode 是否需要过渡
+ const needTransition = vnode.transition
+ if (needTransition) {
+ // 调用 transition.beforeEnter 钩子,并将 DOM 元素作为参数传递
+ vnode.transition.beforeEnter(el)
+ }
+ insert(el, container, anchor)
+ if (needTransition) {
+ // 调用 transition.enter 钩子,并将 DOM 元素作为参数传递
+ vnode.transition.enter(el)
+ }
}
如果元素需要过渡处理,那么就需要等待leave执行完后再卸载,否则直接卸载
diff
function unmount(vnode) {
// 判断 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) {
+ // 将卸载动作封装到 performRemove 函数中
+ const performRemove = () => parent.removeChild(vnode.el)
+ if (needTransition) {
+ // 如果需要过渡处理,则调用 transition.leave 钩子,
+ // 同时将 DOM 元素和 performRemove 函数作为参数传递
+ vnode.transition.leave(vnode.el, performRemove)
+ } else {
+ // 如果不需要过渡处理,则直接执行卸载操作
+ performRemove()
+ }
+ }
}
- 在挂载前添加enter-from和enter-active类
- 在挂载后移除enter-from类,添加enter-to类,使元素移动到最左边
- 在卸载时添加leave-from和leave-active类,在下一帧移除leave-from 类,添加leave-to类,使元素向右移动200px,过渡效果执行完最终执行卸载元素
js
const Transition = {
name: 'Transition',
setup(props, { slots }) {
return () => {
const innerVNode = slots.default()
innerVNode.transition = {
beforeEnter(el) {
// 设置初始状态:添加 enter-from 和 enter-active 类
el.classList.add('enter-from')
el.classList.add('enter-active')
},
enter(el) {
// 在下一帧切换到结束状态
nextFrame(() => {
// 移除 enter-from 类,添加 enter-to 类
el.classList.remove('enter-from')
el.classList.add('enter-to')
// 监听 transitionend 事件完成收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('enter-to')
el.classList.remove('enter-active')
})
})
},
leave(el, performRemove) {
// 设置离场过渡的初始状态:添加 leave-from 和 leave-active类
el.classList.add('leave-from')
el.classList.add('leave-active')
// 在下一帧修改状态
nextFrame(() => {
// 移除 leave-from 类,添加 leave-to 类
el.classList.remove('leave-from')
el.classList.add('leave-to')
// 监听 transitionend 事件完成收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('leave-to')
el.classList.remove('leave-active')
// 调用 transition.leave 钩子函数的第二个参数,完成 DOM元素的卸载
performRemove()
})
})
}
}
return innerVNode
}
}
}
js
function nextFrame(cb) {
requestAnimationFrame(() => {
requestAnimationFrame(cb)
})
}
css
.box {
width: 100px;
height: 100px;
background-color: red;
}
.enter-from {
transform: translateX(200px);
}
.enter-to {
transform: translateX(0);
}
.enter-active {
transition: transform 1s ease-in-out;
}
/* 初始状态 */
.leave-from {
transform: translateX(0);
}
/* 结束状态 */
.leave-to {
transform: translateX(200px);
}
/* 过渡过程 */
.leave-active {
transition: transform 2s ease-out;
}
结果:
js
const App = {
name: 'App',
setup() {
const toggle = ref(true)
setTimeout(() => {
toggle.value = false
}, 2000);
return () => {
return {
type: Transition,
children: {
default() {
return toggle.value ? { type: 'div', props: { class: 'box' } } : { type: Text, chidlren: '' }
}
}
}
}
}
}
renderer.render({ type: App }, document.querySelector('#app'))