实现 fragment 和 Text 类型节点
- 背景介绍
-
在之前的章节中,我们实现了slots功能,可以让组件的使用者在使用组件时,传入自定义的内容,从而实现组件的复用。
-
但是在实际实现过程中,我们发现,我们传入的 slots 内容,最终都会被包裹在一个div中进行渲染,这样会导致生成的DOM结构不够简洁。
-
之前这里是为了解决 type 为 element 时, children 如果是数组,每个子节点都需要有一个父节点包裹的问题,但我们真的不需要这个父节点来包裹
*jsreturn h("div", {}, [foo, this.$slots]); // 这里的 this.$slots 会被包裹在一个 div 中 // 具体实现 export function renderSlots(slots) { return createVNode("div", {}, slots); // 这里创建了一个 div 节点 }
-
-
为了解决这个问题,我们需要实现 fragment 和 Text 类型节点,这样可以让我们的组件更加灵活,生成的DOM结构也更加简洁。
-
fragment 的实现
- fragment 是一种特殊的节点类型,它可以包含多个子节点,而不需要一个额外的父节点进行包裹。
- 我们渲染的时候使用 patch 函数进行处理,区分为 element 类型和 component 类型,我们再加一个 fragment 类型进行处理。
- 具体实现
js// runtime-core/helpers/renderSlots.ts export function renderSlots(slots, name, props) { const slot = slots[name]; if (slot) { if (typeof slot === "function") { return createVNode("Fragment", {}, slot(props)); // ✅增加一个 Fragment 类型虚拟节点 } } } // runtime-core/renderer.ts function patch(vnode, container) { const { type, shapeFlag } = vnode; switch (type) { case "Fragment": // ✅增加 Fragment 类型的处理 processFragment(vnode, container); break; default: // ✅其他类型的处理,之前的逻辑放在这里处理 if (shapeFlag & shapeFlags.ELEMENT) { processElement(vnode, container); } else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) { processComponent(vnode, container); } break; } } function processFragment(vnode, container) { mountChildren(vnode.children, container); } -
Fragment 优化点
- 我们再 createVNode 函数中,增加 Fragment 标识,在 patch 函数中通过 Fragment 来区分类型。
- 这两个地方都用到了 Fragment 类型, 我们可以把这个类型的创建抽离出来,放到 renderer.ts 中进行处理。
js
// runtime-core/renderer.ts
export const Fragment = Symbol("Fragment"); // ✅ 抽离 Fragment 类型
// runtime-core/helpers/renderSlots.ts
import { Fragment } from "../renderer.ts"; // ✅ 引入 Fragment 变量
export function renderSlots(slots, name, props) {
const slot = slots[name];
if (slot) {
if (typeof slot === "function") {
return createVNode(Fragment, {}, slot(props)); // ✅ 使用 Fragment 变量
}
}
}
// runtime-core/renderer.ts
function patch(vnode, container) {
const { type, shapeFlag } = vnode;
switch (type) {
case Fragment: // ✅ 使用 Fragment 变量
processFragment(vnode, container);
break;
default:
if (shapeFlag & shapeFlags.ELEMENT) {
processElement(vnode, container);
} else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) {
processComponent(vnode, container);
}
break;
}
}
function processElement(vnode, container) { // ✅
mountElement(vnode, container)
}
-
最后结果,页面渲染不会再多一层 div
html<div>App</div> <div> <p>header18</p> <p>foo</p> <p>footer</p> </div>
-
Text 类型节点的实现
-
Text 类型节点是指纯文本节点,在虚拟DOM中,我们可以通过一个特殊的类型来表示文本节点。
-
场景:当我们在组件中直接使用字符串时,比如 "hello mini-vue",这个字符串会被作为文本节点进行渲染,而不需要一个额外的元素进行包裹。
-
具体实现
- 我们在 createVNode 函数中,增加 Text 类型的处理,在 patch 函数中进行区分处理。
js// runtime-core/vnode.ts import { Text } from "./renderer.ts"; // 引入 Text 类型,这里不能忘记引入,否则会触发 proxy 的 call 找不到的问题 export function createTextVNode(text: string) { return createVNode(Text, {}, text) } // runtime-core/renderer.ts export const Text = Symbol("Text"); function patch(vnode, container) { const { type, shapeFlag } = vnode; switch (type) { case Fragment: processFragment(vnode, container); break; case Text: // ✅增加 Text 类型的处理 processText(vnode, container); break; default: if (shapeFlag & shapeFlags.ELEMENT) { processElement(vnode, container); } else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) { processComponent(vnode, container); } break; } } function processText(vnode, container) { // ✅处理 Text 类型节点 const { children } = vnode; const textNode = (vnode.el = document.createTextNode(children)); container.append(textNode); }- 在页面引入方法创建文本节点验证效果,可以看到 text 节点被正确渲染
jsexport const App = { name: "App", render() { const app = h("div", {}, "app"); // const foo = h(Foo,{},[h('p',{},"123"),h('p',{},"456")]) const foo = h( Foo, {}, { header: ({ age }) => h("p", {}, "header" + age), footer: () => [h("p", {}, "footer"), createTextVNode("text节点")], // ✅ 使用 createTextVNode 创建文本节点 }, ); return h("div", {}, [app, foo]); }, setup() { return {}; }, };
-
-
注意点
- Text 引入,编辑器不提示,或提示错误容易忘记引入 Text 类型,导致报错
- App.js 或 Foo.js 中使用 createTextVNode 创建文本节点, 需要 ()=> [] ,要遵循 slots 规范,放入数组中