实现mini-vue -- runtime-core模块(九)实现组件作用域插槽

93 阅读3分钟

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

本篇会讲插槽的最后一个实现 -- 作用域插槽,有了它我们就可以使用子组件在插槽中暴露给父组件使用的属性了

1. 实现思路

作用域插槽允许父组件使用子组件提供的插槽属性,比如Foo组件中现在有一个age变量,我们希望能够在父组件中使用插槽的时候使用到这个变量

import { h, renderSlots } from '../../lib/plasticine-mini-vue.esm.js';

export const Foo = {
  name: 'Foo',
  setup() {
    return {};
  },
  render() {
    const foo = h('p', {}, 'foo');
+   const age = 20;
    return h('div', {}, [
      renderSlots(this.$slots, 'header'),
      foo,
      renderSlots(this.$slots, 'footer'),
    ]);
  },
};
import { h } from '../../lib/plasticine-mini-vue.esm.js';
import { Foo } from './Foo.js';

export const App = {
  name: 'App',
  setup() {
    return {};
  },
  render() {
    const foo = h(
      Foo,
      {},
      {
-       header: h('p', {}, 'header'),
+       header: h('p', {}, 'header' + age),
        footer: h('p', {}, 'footer'),
      }
    );

    // const foo = h(Foo, {}, h('p', {}, 'default slot'));

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

那么应当在子组件中调用renderSlots的时候,将age变量暴露出去,然后父组件中使用插槽的时候能够在一个函数当中接收到暴露出来的属性

import { h, renderSlots } from '../../lib/plasticine-mini-vue.esm.js';

export const Foo = {
  name: 'Foo',
  setup() {
    return {};
  },
  render() {
    const foo = h('p', {}, 'foo');
    const age = 20;
    return h('div', {}, [
-     renderSlots(this.$slots, 'header'),
+     renderSlots(this.$slots, 'header', { age }),
      foo,
      renderSlots(this.$slots, 'footer'),
    ]);
  },
};
import { h } from '../../lib/plasticine-mini-vue.esm.js';
import { Foo } from './Foo.js';

export const App = {
  name: 'App',
  setup() {
    return {};
  },
  render() {
    const foo = h(
      Foo,
      {},
      {
-       header: h('p', {}, 'header'),
-       footer: h('p', {}, 'footer'),
+       header: ({ age }) => h('p', {}, 'header' + age),
+       footer: () => h('p', {}, 'footer'),
      }
    );

    // const foo = h(Foo, {}, h('p', {}, 'default slot'));

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

考虑到子组件插槽可能会有多个属性要暴露给父组件,所以使用对象的方式将要暴露的属性包裹起来,父组件使用插槽的时候,变成了函数调用的方式,接收的参数正是子组件传出来的对象

接下来就应该去修改renderSlots实现子组件能够暴露插槽属性出去 修改initSlots对函数类型的插槽进行特殊处理


2. 子组件暴露插槽属性

要让子组件能够暴露插槽属性,我们需要给renderSlots函数添加第三个参数props

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

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

3. initSlots 处理函数类型的插槽

由于现在父组件传入的插槽是函数了,所以renderSlots中获取到插槽后要调用才能够拿到vnode 接下来要修改initSlots,因为父组件中使用插槽的时候传入的是函数,而initSlots中仍然是将插槽当作vnode来处理,我们应当让它执行slot函数得到vnode才行

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<any>(children)) {
-   slots[key] = normalizeSlotValue(value);
+   slots[key] = (props) => normalizeSlotValue(value(props));
  }
}

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

现在作用域插槽的效果就实现了 image.png


4. 重构代码

4.1 vnode 中添加对 children 为 slots 的判断

export function createVNode(type, props?, children?) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
  };

  // 根据 children 的类型添加 vnode 的类型 -- 是 TEXT_CHILDREN 还是 ARRAY_CHILDREN
  if (typeof children === 'string') {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
  } else if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }

+ // 组件 + children 是 object
+ if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
+   if (typeof children === 'object') {
+     vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN;
+   }
+ }

  return vnode;
}
export const enum ShapeFlags {
  ELEMENT = 1,
  STATEFUL_COMPONENT = 1 << 1,
  TEXT_CHILDREN = 1 << 2,
  ARRAY_CHILDREN = 1 << 3,
+ SLOT_CHILDREN = 1 << 4,
}

4.2 initSlots 的时候判断 vnode 是否是 slots

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