25-mini-vue fragment & Text

实现 fragment 和 Text 类型节点

  1. 背景介绍
  • 在之前的章节中,我们实现了slots功能,可以让组件的使用者在使用组件时,传入自定义的内容,从而实现组件的复用。

  • 但是在实际实现过程中,我们发现,我们传入的 slots 内容,最终都会被包裹在一个div中进行渲染,这样会导致生成的DOM结构不够简洁。

    • 之前这里是为了解决 type 为 element 时, children 如果是数组,每个子节点都需要有一个父节点包裹的问题,但我们真的不需要这个父节点来包裹
      *

      js 复制代码
      return h("div", {}, [foo, this.$slots]); // 这里的 this.$slots 会被包裹在一个 div 中
      // 具体实现
      export function renderSlots(slots) {
        return createVNode("div", {}, slots); // 这里创建了一个 div 节点
      }
  • 为了解决这个问题,我们需要实现 fragment 和 Text 类型节点,这样可以让我们的组件更加灵活,生成的DOM结构也更加简洁。

  1. 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);
    }
  2. 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>
  1. 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 节点被正确渲染
      js 复制代码
      export 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 {};
        },
      };
  2. 注意点

    • Text 引入,编辑器不提示,或提示错误容易忘记引入 Text 类型,导致报错
    • App.js 或 Foo.js 中使用 createTextVNode 创建文本节点, 需要 ()=> [] ,要遵循 slots 规范,放入数组中
相关推荐
basestone21 小时前
🚀 从重复 CRUD 到工程化封装:我是如何设计 useTableList 统一列表逻辑的
javascript·react.js·ant design
何贤21 小时前
2025 年终回顾:25 岁,从“混吃等死”到别人眼中的“技术专家”
前端·程序员·年终总结
软件开发技术深度爱好者21 小时前
JavaScript的p5.js库使用介绍
javascript·html
冴羽21 小时前
CSS 新特性!瀑布流布局的终极解决方案
前端·javascript·css
满天星辰21 小时前
Vue 响应式原理深度解析
前端·vue.js
怪可爱的地球人21 小时前
em,rem,px,rpx单位换算,你弄懂了吗?
前端
码途潇潇21 小时前
JavaScript有哪些数据类型?如何判断一个变量的数据类型?
前端·javascript
满天星辰21 小时前
Vue真的是单向数据流?
前端·vue.js
细心细心再细心21 小时前
Nice-modal-react的使用
前端