18-实现slots

122 阅读3分钟

实现一个普通的slots

demo

App.js

import { h } from "../../lib/mini-vue.esm.js";
import Child from "./Child.js";

export default {
  name: "App",
  setup() {},

  render() {
    return h("div", {}, [
      h("div", {}, "你好"),
      h(
        Child,
        {
          msg: "your name is child",
        },
        [h("div", {}, "123"), h("div", {}, "456")]
      ),
    ]);
  },
};

Child.js

import { h, renderSlots } from "../../lib/mini-vue.esm.js";
export default {
  name: "Child",
  setup(props, context) {},
  render() {
    console.log("this.$slots", this.$slots);
    return h("div", {}, [
      h("div", {}, "child"),
      h("div", {}, this.$slots),
    ]);
  },
};

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module">
      import { createApp } from "../../lib/mini-vue.esm.js";
      import App from "./App.js";

      const rootContainer = document.querySelector("#app");
      createApp(App).mount(rootContainer);
    </script>
  </body>
</html>

在实例挂载$slots

component.ts

function createComponentInstance(initVNode) {
  const component = {
    vnode: initVNode,
    type: initVNode.type,
    proxy: null,
    setupState: {},
    props: {},
+    slots: {},
    emit: () => {},
  };

  // other code
}

// 初始化setup数据
function setupComponent(instance, container) {
  // 初始化props
  initProps(instance, instance.vnode.props);
+  // 初始化slots
+  initSlots(instance, instance.vnode.children);
  // 初始化setup函数返回值
  setupStatefulComponent(instance, container);
}

componentPublicInstanceProxyHandlers.ts

const PublicInstanceMap = {
  $el: (i) => i.vnode.el,
+  $slots: (i) => i.slots,
};

componentSlot.ts

/*
 * @Author: Lin zefan
 * @Date: 2022-03-27 11:18:29
 * @LastEditTime: 2022-03-27 12:23:50
 * @LastEditors: Lin zefan
 * @Description: 初始化slot
 * @FilePath: \mini-vue3\src\runtime-core\componentSlot.ts
 *
 */

export function initSlots(instance, children) {
  instance.slots =  Array.isArray(children) ? children : [children];
}

具名插槽

App.js

import { h } from "../../lib/mini-vue.esm.js";
import Child from "./Child.js";

export default {
  name: "App",
  setup() {},

  render() {
    return h("div", {}, [
      h("div", {}, "你好"),
      h(
        Child,
        {
          msg: "your name is child",
        },
        {
          default: [h("div", {}, "default")],
          header: [h("div", {}, "header")],
          footer: [h("div", {}, "footer")],
        }
        // [h("div", {}, "123"), h("div", {}, "456")]
      ),
    ]);
  },
};

Child.js

import { h, renderSlots } from "../../lib/mini-vue.esm.js";
export default {
  name: "Child",
  setup(props, context) {},
  render() {
    console.log("this.$slots", this.$slots);
    return h("div", {}, [
      h("div", {}, "child"),
      h("div", {}, [
        renderSlots(this.$slots, "header"),
        renderSlots(this.$slots),
        renderSlots(this.$slots, "footer"),
      ]),
    ]);
  },
};

componentSlot.ts

/*
 * @Author: Lin zefan
 * @Date: 2022-03-27 11:18:29
 * @LastEditTime: 2022-03-27 12:19:35
 * @LastEditors: Lin zefan
 * @Description: 初始化slot
 * @FilePath: \mini-vue3\src\runtime-core\componentSlot.ts
 *
 */

/*
 * @Author: Lin zefan
 * @Date: 2022-03-27 11:18:29
 * @LastEditTime: 2022-03-27 12:23:50
 * @LastEditors: Lin zefan
 * @Description: 初始化slot
 * @FilePath: \mini-vue3\src\runtime-core\componentSlot.ts
 *
 */

export function initSlots(instance, children) {
  normalizeSlotObject(children, instance.slots);
}

function normalizeSlotObject(children, slots) {
  for (const key in children) {
    if (Object.prototype.hasOwnProperty.call(children, key)) {
      const value = children[key];
      // 直接把children对应key的slots给到instance.slots对应的key
      slots[key] = normalizeSlotValue(value);
    }
  }
}

function normalizeSlotValue(slots: any): any {
  // 统一转换为数组,children接收的是一个数组
  return Array.isArray(slots) ? slots : [slots];
}

renderSlots.ts

/*
 * @Author: Lin zefan
 * @Date: 2022-03-27 12:03:47
 * @LastEditTime: 2022-03-27 12:35:22
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\runtime-core\helpers\renderSlots.ts
 *
 */

import { createdVNode } from "../vnode";

export function renderSlots(slots, name = "default") {
 /** 返回一个vnode
  * 1. 其本质和 h 是一样的
  * 2. 通过name取到对应的slots
  */
  return createdVNode("div", {}, slots[name]);
}

作用域插槽

App.js

import { h } from "../../lib/mini-vue.esm.js";
import Child from "./Child.js";

export default {
  name: "App",
  setup() {},

  render() {
    return h("div", {}, [
      h("div", {}, "你好"),
      h(
        Child,
        {},
        {
          header: [h("div", {}, "header")],
          default: ({ age }) => [
            h("p", {}, "我是通过 slot 渲染出来的第一个元素 "),
            h("p", {}, "我是通过 slot 渲染出来的第二个元素"),
            h("p", {}, `我可以接收到 age: ${age}`),
          ],
          main: h("div", {}, "main"),
          footer: ({ name }) =>
            h("p", {}, "我是通过footer插槽 ,名字是:" + name),
        }
      ),
    ]);
  },
};

Child.js

import { h, renderSlots } from "../../lib/mini-vue.esm.js";
export default {
  name: "Child",
  setup(props, context) {},
  render() {
    console.log("this.$slots", this.$slots);
    return h("div", {}, [
      h("div", {}, "child"),
      h("div", {}, [
        renderSlots(this.$slots, "header"),
        renderSlots(this.$slots, "default", {
          age: 18,
        }),
        renderSlots(this.$slots, "main", {
          content: "main的内容",
        }),
        renderSlots(this.$slots, "footer", {
          name: "foo",
        }),
      ]),
    ]);
  },
};

支持函数形式渲染

componentSlot.ts

function normalizeSlotObject(children, slots) {
  for (const key in children) {
    if (Object.prototype.hasOwnProperty.call(children, key)) {
      const value = children[key];

+      if (typeof value === "function") {
+        /**
+         * 1. 如果是一个函数,那初始化的时候就返回一个函数
+         * 2. props为作用域插槽的值,在renderSlots函数中会传递过来
+         */
+        const handler = (props) => normalizeSlotValue(value(props));
+        slots[key] = handler;
      } else {
        // 不是函数,是一个是h对象,或者h对象数组集合
        slots[key] = normalizeSlotValue(value);
      }
    }
  }
}

renderSlots.ts

/*
 * @Author: Lin zefan
 * @Date: 2022-03-27 12:03:47
 * @LastEditTime: 2022-03-27 14:26:00
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\runtime-core\helpers\renderSlots.ts
 *
 */

import { createdVNode } from "../vnode";

export function renderSlots(slots, name = "default", props = {}) {
  /** 返回一个vnode
   * 1. 其本质和 h 是一样的
   * 2. 通过name取到对应的slots
   */
  const slot = slots[name];
  if (slot) {
+    if (typeof slot === "function") {
+      // 是一个函数,需要调用函数,并把当前作用域插槽的数据传过去,把调用结果渲染处理
+      return createdVNode("div", {}, slot(props));
+    }
    // 不是函数,是h对象,或者h对象数组集合
    return createdVNode("div", {}, slot);
  }
}

兼容没有slots的情况

在Child新增了一个main的插槽

 render() {
    console.log("this.$slots", this.$slots);
    return h("div", {}, [
      h("div", {}, "child"),
      h("div", {}, [
        renderSlots(this.$slots, "header"),
        renderSlots(this.$slots, "default", {
          age: 18,
        }),
+        renderSlots(this.$slots, "main", {
+          content: "main的内容",
+        }),
        renderSlots(this.$slots, "footer", {
          name: "foo",
        }),
      ]),
    ]);
  },

但是App.js没有去创建对应的插槽,会报错

 // App.js
 render() {
    return h("div", {}, [
      h("div", {}, "你好"),
      h(
        Child,
        {},
        {
          header: [h("div", {}, "header")],
          default: ({ age }) => [
            h("p", {}, "我是通过 slot 渲染出来的第一个元素 "),
            h("p", {}, "我是通过 slot 渲染出来的第二个元素"),
            h("p", {}, `我可以接收到 age: ${age}`),
          ],
          // main: h("div", {}, "main"),
          footer: ({ name }) =>
            h("p", {}, "我是通过footer插槽 ,名字是:" + name),
        }
      ),
    ]);
  },

image.png

基于这种情况,要加个flag判断,在初始化Slots的时候判断是否需要初始化

// render.ts
export function patch(vnode, container) {
  if (!vnode) return;
  if (isObject(vnode.type)) {
    // 是一个Component
    processComponent(vnode, container);
  } else if (typeof vnode.type === "string") {
    // 是一个element
    processElement(vnode, container);
  }
  // if (root === "component") {
  //   // 是一个Component
  //   processComponent(vnode, container);
  // } else if (root === "element") {
  //   // 是一个element
  //   processElement(vnode, container);
  // }
}