手撸mini-vue之slots

150 阅读2分钟

普通插槽

happy path
// App.js

export const App = {
  render() {
    const app = h("div", {}, "App");
    const foo = h(Foo, {}, h("p", {}, "123"));

    return h("div", {}, [app, foo]);
  },

  setup() {
    return {};
  },
};
// Foo.js

export const Foo = {
  setup() {
    return {};
  },
  render() {
    const foo = h("p", {}, "foo");
    return h("div", {}, [
      renderSlots(this.$slots),
      foo,
    ]);
  },
};

实现目标:把App.jsh("p", {}, "123")添加到foo组件内

代码实现

通过this获取到$slots

// componentPublicInstance.ts

const publicProertiesMap = {
  $el: (i) => i.vnode.el,
  $slots: (i) => i.slots,
}

export const PublicInstanceProxyhandlers = {
  ...
}

接下来只需要构建instance.slots即可
初始化slots

// component.ts

export function setupComponent(instance) {
  initSlots(instance, instance.vnode.children);
}
// componentSlots

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

通过renderSlots,在Foo.js中渲染插槽

// renderSlots

export function renderSlots(slots) {
  return createVNode("div", {}, slots);
}

这样就实现了一个简答的插槽功能,但是插槽有可能是个数组

// App.js

export const App = {
  render() {
    const app = h("div", {}, "App");
    const foo = h(Foo, {}, [h("span", {}, "footer "), h("span", {}, "123")]);

    return h("div", {}, [app, foo]);
  },

  setup() {
    return {};
  },
};

componentSlots中下文章

// componentSlots

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

具名插槽

实现目标:1. 获取到要渲染的元素 2. 获取到要渲染的位置

happy path
// App.js

export const App = {
  name: "App",
  render() {
    const app = h("div", {}, "App");
    const foo = h(
      Foo,
      {},
      {
        header: h("p", {}, "header"),
        footer: h("p", {}, "footer"),
      }
    );

    return h("div", {}, [app, foo]);
  },

  setup() {
    return {};
  },
};
// Foo.js

export const Foo = {
  name: "Foo",
  setup() {
    return {};
  },
  render() {
    const foo = h("p", {}, "foo");
    const age = 18;
    return h("div", {}, [
      renderSlots(this.$slots, "header"),
      foo,
      renderSlots(this.$slots, "footer"),
    ]);
  },
};
代码实现
// renderSlots.ts

export function renderSlots(slots, name) {
  const slot = slots[name];
  
  if (slot) {
    return createVNode("div", {}, slot);
  }
}
// componentSlots.ts

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

function normalizeObjectSlots(slots, children) {
  for (let key in children) {
    let value = children[key];
    slots[key] = normalizeSlotValue(value);
  }
}

function normalizeSlotValue(value) {
  return Array.isArray(value) ? value : [value];
}

作用域插槽

实现目标:可以获取插槽组件内部的变量

happy path
// Foo.js

export const Foo = {
  name: "Foo",
  setup() {
    return {};
  },
  render() {
    const foo = h("p", {}, "foo");
    const age = 18;
    return h("div", {}, [
      renderSlots(this.$slots, "header", {
        age,
      }),
      foo,
      renderSlots(this.$slots, "footer"),
    ]);
  },
};
// App.js

export const App = {
  name: "App",
  render() {
    const app = h("div", {}, "App");
    const foo = h(
      Foo,
      {},
      {
        header: ({ age }) => h("p", {}, "header" + age),
        footer: () =>
          h("p", {}, [h("span", {}, "footer "), h("span", {}, "123")]),
      }
    );
    // const foo = h(Foo, {}, h("p", {}, "123"));

    return h("div", {}, [app, foo]);
  },

  setup() {
    return {};
  },
};
代码实现

renderSlots函数中slot变成了一个function

// renderSlots

export function renderSlots(slots, name, props) {
  const slot = slots[name];
  if (slot) {
    if (typeof slot === "function") {
      return createVNode("div", {}, slot(props));
    }
  }
}

initSlots的时候也需要做出修改

// componentSlots

function normalizeObjectSlots(slots, children) {
  for (let key in children) {
    let value = children[key];
    slots[key] = (props) => normalizeSlotValue(value(props));
  }
}

优化

因为不是所有的组件实例都有slots,所以在initSlots判断下是否需要做slots的处理

// vnode.ts

export function createVNode(type, props?, children?) {
  ...

  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    if (typeof children === "object") {
      vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN;
    }
  }
    
  ...
}
// ShapeFlags.ts

export const enum ShapeFlags {
  ELEMENT = 1, // 0001
  STATEFUL_COMPONENT = 1 << 1, // 0010
  TEXT_CHILDREN = 1 << 2, // 0100
  ARRAY_CHILDREN = 1 << 3, // 1000
  SLOT_CHILDREN = 1 << 4,
}
// componentSlots.ts

export function initSlots(instance, children) {
  const { vnode } = instance;
  if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
    normalizeObjectSlots(instance.slots, children);
  }
}