一、KeepAlive组件的实现原理
1、组件的失活和激活
keepAlive的本质:缓存管理, 配上特殊的挂载和卸载逻辑,
keepAlive的卸载:keepAlive的组件从原容器搬运到另一个隐藏的容器, 实现假‘卸载’
keepAlive的挂载:不执行真正的挂载逻辑, keepAlive的组件从隐藏容器搬回到原容器
keepAlive的使用:
<template>
<keepAlive>
<Tab v-if="currentTab === 1">...</Tab>
<Tab v-if="currentTab === 2">...</Tab>
</keepAlive>
</template>
keepAlive的实现
const KeepAlive = {
name: "KeepAlive",
_isKeepAlive: true,
setup(props, {slots}){
const cache = new Map();
const instance = currentInstance; // 当前keepAlive的实例, 对应render中的代码 setCurrentInstance(instance); setup(); setCurrentInstance(null)
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 () => {
let rawVnode = slots.default()
if(typeof rawVnode.type !== "object") { // 非组件的虚拟节点无法被keepAlive
return rawVnode
}
const cacheVnode = cache.get(rawVnode.type);
if(cacheVnode) {
rawVnode.component = cacheVnode.component; // 如果已经缓存过, 说明组件已经被挂载过, 直接继承组件实例则可
rawVnode.keptAlive = true; // 进行标注, 避免再次挂载
} else {
cache.set(rawVnode.type, rawVnode);
}
rawVnode.shouldKeepAlive = true; // 标注组件已经被缓存,避免进行卸载,为什么需要两个keptAlive和 shouldKeepAlive, 一个是避免挂载, 一个是避免卸载
rawVnode.keepAliveInstance = instance; // 把keepAlive组件的实例也添加到vnode上,以便渲染器中访问
return rawVnode
}
}
}
unmounted部分的修改
function unmount(vnode) {
if(vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
} else if(typeof vnode.type === "object") {
if(vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode);
} else {
unmount(vnode.component.subTree) // 如果是组件, 卸载的是subTree;
}
}
const parent = vnode.el.parent;
if(parent) parent.removeChild(vnode.el);
}
patch部分的修改
function patch(n1, n2, container, anchor) {
if(n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
let { type } = n2;
if(typeof type === "string") { // 节点是普通标签元素
} else if(typeof type === "object" || type === "function"){ // 节点是组件 或者 节点是函数式组件
if(!n1) {
if(n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor)
} else {
mountComponent(n2, container, anchor)
}
} else {
patchComponent()
}
} else if(type === Text){
} else if(type === Fragment) {
} else {
}
}
mountComponent中修改
function mountComponent(vnode, container, anchor) {
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
mounted: [],
keepAliveCtx: null,
}
const isKeepAlive = vnode.type._isKeepAlive;
if(isKeepAlive) {
instance.keepAliveCtx = {
move(vnode, container, anchor) {
insert(vnode.component.subTree.el, container, anchor);
},
createElement
}
}
}
vnode.component.subTree.el是怎么来
组件首次加载, 根据patch(null, subTree, container, anchor);, el真实的DOM节点是挂载在subTree上的
在keepAlive组件内部, 如果组件被缓存过,那么会执行 rawVnode.component = cacheVnode.component
2、include和exclude
const KeepAlive = {
name: "KeepAlive",
_isKeepAlive: true,
props: {
include: RegExp,
exclude: RegExp,
},
setup(props, {slots}){
const cache = new Map();
const instance = currentInstance; // 当前keepAlive的实例, 对应render中的代码 setCurrentInstance(instance); setup(); setCurrentInstance(null)
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 () => {
let rawVnode = slots.default()
if(typeof rawVnode.type !== "object") { // 非组件的虚拟节点无法被keepAlive
return rawVnode
}
// 在keep
const name = rawVnode.type.name;
if(name &&( props.include && !props.include.test(name) || props.exclude && props.exclude.test(name))) {
return rawVnode;
}
const cacheVnode = cache.get(rawVnode.type);
if(cacheVnode) {
rawVnode.component = cacheVnode.component; // 如果已经缓存过, 说明组件已经被挂载过, 直接继承组件实例则可
rawVnode.keptAlive = true; // 进行标注, 避免再次挂载
} else {
cache.set(rawVnode.type, rawVnode);
}
rawVnode.shouldKeepAlive = true; // 标注组件已经被缓存,避免进行卸载,为什么需要两个keptAlive和 shouldKeepAlive, 一个是避免挂载, 一个是避免卸载
rawVnode.keepAliveInstance = instance; // 把keepAlive组件的实例也添加到vnode上,以便渲染器中访问
return rawVnode
}
}
}
3、缓存管理
问题: 当缓存不存在, cache总是会设置新的缓存, 这导致缓存不断增加, 极端情况下会占用大量内存
解决: 设置缓存阈值, 当缓存数量超于阈值时对缓存进行修剪
修剪策略: 最新一次访问, 维持一个栈, 最新访问组件压入栈头后, 按照阈值修剪栈尾的组件
二、Teleport组件的实现原理
1、Teleport组件要解决的问题
问题:
一般情况下, 将虚拟DOM渲染为真实DOM时, 最终渲染出来的真实DOM层级结构和虚拟DOM的层级结构一致
<template>
<div id="box" style = “z-index : -1”>
<Overlay/>
</div>
</template>
上面代码中Overlay无法实现跨越DOM层级渲染
解决:
// overlay.vue
<template>
<Teleport to="body">
<div class="overlay"></div>
</Teleport>
</template>
<style scoped>
.overlay{
overlay: 9999;
}
</style>
通过为Teleport组件指定渲染目标body, 即to属性的值, Teleport就把插槽内容渲染到body下, 而不会按照模板的DOM来渲染, 实现了跨DOM层级的渲染
2、实现Teleport组件
Teleport组件的渲染逻辑需要从渲染器中分离出来
- 可以避免渲染器代码膨胀
- 当用户没使用Teleport组件时, 由于Teleport的渲染逻辑被分离, 因此可以利用TreeShaking机制在最终的bundle中删除Teleport相关的代码
patch函数的更改
function patch(n1, n2, container, anchor) {
let { type } = n2;
if(typeof type === "string") { // 节点是普通标签元素
} else if (typeof type === "object" && type._isTeleport) { // 节点是Teleport
type.process(n1, n2, container, anchor, {
patch,
patchChildren,
unmount,
move(vnode, container, anchor) {
insert(vnode.component ? vnode.component.subTree.el : vnode.el, container, anchor)
}
})
} else if(typeof type === "object" || type === "function"){ // 节点是组件 或者 节点是函数式组件
} else if(type === Text){
} else if(type === Fragment) {
} else {
// 省略了其他类型的vnode
}
}
teleport组件的实现
const Teleport = {
_isTeleport: true,
process(n1, n2, container, anchor, internals) {
const { patch, patchChildren, move } = internals;
if(!n1) {
const target = typeof n2.props.to === "string" ? document.querySelector(n2.props.to) : n2.props.to;
n2.children.forEach(c => patch(c, null, target))
} else {
patchChildren(n1, n2, container) // 很类似Fragment, 主要处理Teleport下面的children
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))
}
}
}
}
三、Transistion组件实现原理
1、原生DOM的过渡
1.1核心原理
- 当DOM元素被挂载时, 将动效附加到该DOM元素上
- 当DOM元素被卸载时, 不要立刻卸载DOM元素, 而是等到附加到该DOM元素上的动效执行完成后再卸载它
1.2enter动效
const el = document.createElement("div")
el.classList.add("box");
el.classList.add("enter-active");
el.classList.add("enter-from");
document.body.appendChild(el);
// 切换元素状态, 以下代码不起效 因为浏览器会在当前帧绘制DOM元素, 类没有切换, 绘制到屏幕的结果已经是enter-to
// el.classList.remove("enter-from");
// el.classList.add("enter-to")
// 理论上应该下一帧进行类的切换
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.classList.remove("enter-from");
el.classList.add("enter-to");
// 过渡完成后需要将enter-to和enter-active这两个类从DOM元素中去掉
el.addEventListener("transitionend", () => {
el.classList.remove("enter-to");
el.classList.remove("enter-active")
})
})
})
使用两次requestAnimationFrame的原因: 浏览器的bug, chrome或者Safari使用requestAnimation函数注册回调会在当前帧执行, 除非其他代码已经调用了一次requestAnimationFrame, 实际测试在108版本不会出现
1.3leave动效
el.addEventListener("click", () => {
// 重点: 封装卸载动作
const performRemove = () => el.parentNode.removeChild(el);
el.classList.add("leave-from");
el.classList.add("leave-active");
// 强制reflow: 使初始状态失效
document.body.offsetHeight;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.classList.remove("leave-from");
el.classList.add("leave-to");
el.addEventListener("transitionend", () => {
el.classList.remove("leave-to");
el.classList.remove("leave-active");
performRemove();
})
})
})
})
- 需要对卸载行为进行封装, 让动效执行完成之后才进行卸载
- 强制reflow, 获取offsetHeight可以使页面重排, 但是108版本的浏览器去掉强制reflow也能起效
2、实现Transition的组件
虚拟DOM层面的表现形式
<template>
<Transition>
<div>我是需要过渡的元素</div>
</Transition>
</template>
虚拟DOM的实现
function render() {
return {
type: "Transition",
children: {
default() {
return {type: "div", children: "我是需要过渡的元素"}
}
}
}
}
transition的实现
const Transition = {
name: "Transition",
setup(props, {slots}){
const nextFrame = (cb) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
cb()
})
})
}
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");
// 过渡完成后需要将enter-to和enter-active这两个类从DOM元素中去掉
el.addEventListener("transitionend", () => {
el.classList.remove("enter-to");
el.classList.remove("enter-active")
})
})
},
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-to");
el.classList.remove("leave-active");
performRemove();
})
})
}
}
return innerVnode;
}
}
}
mountElement中
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, anchor)
})
}
if(vnode.props) {
for(let key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
let needTransition = vnode.transition;
if(needTransition) {
vnode.transition.beforeEnter(el);
}
insert(el, container);
if(needTransition) {
vnode.transition.enter(el);
}
}
unmount中
function unmount(vnode) {
if(vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
} else if(typeof vnode.type === "object") {
if(vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode);
} else {
unmount(vnode.component.subTree) // 如果是组件, 卸载的是subTree;
}
}
const parent = vnode.el.parent;
if(parent) {
const performRemove = () => parent.removeChild(vnode.el);
const needTransition = vnode.transition;
if(needTransition) {
vnode.transition.leave(vnode.el, performRemove)
} else {
performRemove();
}
}
}