实现mini-vue -- runtime-core模块(八)实现组件具名插槽

91 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

上一篇文章中讲了默认插槽的实现原理,这一篇会讲一下具名插槽的实现方式

1. 实现思路

具名插槽顾名思义,也就是能够在父组件中指定插槽要渲染在子组件中的位置,具体使用可以参考vue官方文档 既然要渲染到指定的位置,那么我们最起码要知道以下两点:

  1. 获取到要渲染的元素
  2. 获取到要渲染的位置,将元素渲染到指定位置

2. 获取到要渲染的元素

父组件中渲染子组件的时候,给子组件的children传入的是vnode数组,但是我们现在要实现的是通过具体的name去获取到对应的插槽,数组的方式注定只能通过下标索引的方式在子组件中获取到对应的插槽 因此我们需要修改一下children的数据结构,不传入数组,而是传入对象,具体的插槽的名字作为key,对应的vnode作为value

export const App = {
  setup() {
    return {};
  },
  render() {
-   const foo = h(Foo, {}, [
-     h('p', {}, 'default slot'),
-     h('p', {}, 'plasticine'),
-   ]);
+   const foo = h(
+     Foo,
+     {},
+     {
+       header: h('p', {}, 'header'),
+       footer: h('p', {}, 'footer'),
+     }
+   );

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

子组件中要给对应插槽命名,因此我们的renderSlots要能够接收第二个参数,也就是插槽的名字

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

然后再到initSlots中修改初始化的逻辑,由原来的数组获取slots变为对象

export function initSlots(instance, children) {
  // 保证 $slots 一定是存放 vnode 的数组
  // instance.slots = Array.isArray(children) ? children : [children];

  // slots 是对象
  const slots = {};
  for (const [key, value] of Object.entries(children)) {
    slots[key] = Array.isArray(value) ? value : [value];
  }

  instance.slots = slots;
}

这段代码可以进行一定的重构,像Array.isArray(value) ? value : [value]这段代码它的意思是给具名插槽的值进行一个标准化,将值统一转换成数组,那么我们可以抽离成一个名为normalizeSlotValue的函数去处理

export function initSlots(instance, children) {
  // 保证 $slots 一定是存放 vnode 的数组
  // instance.slots = Array.isArray(children) ? children : [children];

  // slots 是对象
  const slots = {};
  for (const [key, value] of Object.entries(children)) {
    slots[key] = normalizeSlotValue(value);
  }

  instance.slots = slots;
}

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

initSlots中整段代码都是在对对象类型的插槽进行一个标准化处理,所以还可以将它抽离成一个名为normalizeObjectSlots的函数

export function initSlots(instance, children) {
  // slots 是对象
  if (children) normalizeObjectSlots(children, instance.slots);
}

function normalizeObjectSlots(children: any, slots: any) {
  for (const [key, value] of Object.entries(children)) {
    slots[key] = normalizeSlotValue(value);
  }
}

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

3. 获取要渲染的位置

我们已经能够在子组件中通过this.$slots对象的key获取到要渲染的vnode了,现在就是要将它们渲染到子组件插槽中的特定位置,因此要修改renderSlots,接收第二个参数 -- 插槽名字

import { createVNode } from '../vnode';

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

根据name通过slots对象获取到对应的插槽vnode,然后创建一个div容器将要渲染的插槽内容包裹返回

image.png

这样一个简单的具名插槽就实现啦!