14. 内建组件和模块
14.1 KeepAlive 组件的实现原理
1. 组件的激活与失活
“卸载”一个被 KeepAlive 的组件时,它并不会真的被卸载,而会被移动到一个隐藏容器中。当重新“挂载”该组件时,它也不会被真的挂载,而会被从隐藏容器中取出,再“放回”原来的容器中
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;
};
},
};
// 卸载操作
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);
}
}
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
if (typeof type === "string") {
// 省略部分代码
} else if (type === Text) {
// 省略部分代码
} else if (type === Fragment) {
// 省略部分代码
} else if (typeof type === "object" || typeof type === "function") {
// component
if (!n1) {
// 如果该组件已经被 KeepAlive,则不会重新挂载它,而是会调用 _activate 来激活它
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor);
} else {
mountComponent(n2, container, anchor);
}
} else {
patchComponent(n1, n2, anchor);
}
}
}
上面这段代码中涉及的 move
函数是由渲染器注入的
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,
};
}
// 省略部分代码
}
2. include
和 exclude
include
用来显式地配置应该被缓存组件,而 exclude
用来显式地配置不应该被缓存组件
const cache = new Map();
const KeepAlive = {
__isKeepAlive: true,
// 定义 include 和 exclude
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;
}
// 省略部分代码
};
},
};
3. 缓存管理
Vue.js 当前所采用的修剪策略叫作“最新一次访问”。首先,需要为缓存设置最大容量,也就是通过 KeepAlive
组件的 max
属性来设置。”最新一次访问”的缓存修剪策略的核心在于,需要把当前访问(或渲染)的组件作为最新一次渲染的组件,并且该组件在缓存修剪过程中始终是安全的,即不会被修剪
<KeepAlive :max="2">
<component :is="dynamicComp" />
</KeepAlive>
在 KeepAlive 组件的内部实现中,如果用户提供了自定义的缓存实例,则直接使用该缓存实例来管理缓存
<KeepAlive :cache="cache">
<Comp />
</KeepAlive>
// 自定义实现
const _cache = new Map();
const cache: KeepAliveCache = {
get(key) {
_cache.get(key);
},
set(key, value) {
_cache.set(key, value);
},
delete(key) {
_cache.delete(key);
},
forEach(fn) {
_cache.forEach(fn);
},
};
14.2 Teleport 组件的实现原理
1. Teleport 组件要解决的问题
在将虚拟 DOM 渲染为真实 DOM 时,最终渲染出来的真实 DOM 的层级结构与虚拟 DOM 的层级结构一致,但假如需要渲染一个全屏遮罩组件到 body
元素上,这时就需要用到 Teleport 组件。
<template>
<Teleport to="body">
<div class="overlay"></div>
</Teleport>
</template>
<style scoped>
.overlay {
z-index: 9999;
}
</style>
2. 实现 Teleport 组件
Teleport 组件也需要渲染器的底层支持。首先要将 Teleport 组件的渲染逻辑从渲染器中分离出来
const Teleport = {
__isTeleport: true,
process(n1, n2, container, anchor) {
// 在这里处理渲染逻辑
},
};
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
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(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" || typeof type === "function") {
// 省略部分代码
}
}
先设计虚拟 DOM 的结构。通常,一个组件的子节点会被编译为插槽内容,不过对于 Teleport 组件来说,直接将其子节点编译为一个数组即可
<Teleport to="body">
<h1>Title</h1>
<p>content</p>
</Teleport>
function render() {
return {
type: Teleport,
// 以普通 children 的形式代表被 Teleport 的内容
children: [
{ type: "h1", children: "Title" },
{ type: "p", children: "content" },
],
};
}
const Teleport = {
__isTeleport: true,
process(n1, n2, container, anchor, internals) {
// 通过 internals 参数取得渲染器的内部方法
const { patch, patchChildren } = internals;
// 如果旧 VNode n1 不存在,则是全新的挂载,否则执行更新
if (!n1) {
// 挂载
// 获取容器,即挂载点
const target =
typeof n2.props.to === "string"
? document.querySelector(n2.props.to)
: n2.props.to;
// 将 n2.children 渲染到指定挂载点即可
n2.children.forEach((c) => patch(null, c, target, anchor));
} else {
// 更新
patchChildren(n1, n2, container);
// 如果新旧 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));
}
}
},
};
14.3 Transition 组件的实现原理
Transition
核心原理是:
- 当 DOM 元素被挂载时,将动效附加到该 DOM 元素上;
- 当 DOM 元素被卸载时,不要立即卸载 DOM 元素,而是等到附加到该 DOM 元素上的动效执行完成后再卸载它。
1. 原生 DOM 的过渡
过渡效果本质上是一个 DOM 元素在两种状态间的切换,浏览器会根据过渡效果自行完成 DOM 元素的过渡。这里的过渡效果指的是持续时长、运动曲线、要过渡的属性等
<div class="box"></div>
.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;
}
// 创建 class 为 box 的 DOM 元素
const el = document.createElement("div");
el.classList.add("box");
// 在 DOM 元素被添加到页面之前,将初始状态和运动过程定义到元素上
el.classList.add("enter-from"); // 初始状态
el.classList.add("enter-active"); // 运动过程
// 将元素添加到页面
document.body.appendChild(el);
// 嵌套调用 requestAnimationFrame
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.classList.remove("enter-from"); // 移除 enter-from
el.classList.add("enter-to"); // 添加 enter-to
// 监听 transitionend 事件完成收尾工作
el.addEventListener("transitionend", () => {
el.classList.remove("enter-to");
el.classList.remove("enter-active");
});
});
});
当元素被卸载时,不要将其立即卸载,而是等待过渡效果结束后再卸载它。为了实现这个目标,我们需要把用于卸载 DOM 元素的代码封装到一个函数中,该函数会等待过渡结束后被调用
/* 离场动画 */
/* 初始状态 */
.leave-from {
transform: translateX(0);
}
/* 结束状态 */
.leave-to {
transform: translateX(200px);
}
/* 过渡过程 */
.leave-active {
transition: transform 2s ease-out;
}
el.addEventListener("click", () => {
// 将卸载动作封装到 performRemove 函数中
const performRemove = () => el.parentNode.removeChild(el);
// 设置初始状态:添加 leave-from 和 leave-active 类
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");
// 监听 transitionend 事件做收尾工作
el.addEventListener("transitionend", () => {
el.classList.remove("leave-to");
el.classList.remove("leave-active");
// 当过渡完成后,记得调用 performRemove 函数将 DOM 元素移除
performRemove();
});
});
});
});
2. 实现 Transition 组件
先设计它在虚拟 DOM 层面的表现形式
<template>
<Transition>
<div>我是需要过渡的元素</div>
</Transition>
</template>
function render() {
return {
type: Transition,
children: {
default() {
return { type: "div", children: "我是需要过渡的元素" };
},
},
};
}
const Transition = {
name: "Transition",
setup(props, { slots }) {
return () => {
// 通过默认插槽获取需要过渡的元素
const innerVNode = slots.default();
// 在过渡元素的 VNode 对象上添加 transition 相应的钩子函数
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");
// 强制 reflow,使得初始状态生效
document.body.offsetHeight;
// 在下一帧修改状态
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;
};
},
};
修改 mountElement
函数和 unmount
函数
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]);
}
}
// 判断一个 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);
}
}
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();
}
}
}