手写 mini-vue3 - 实现 slots(六)

74 阅读3分钟

实现 slots

插槽的使用方式:

const foo = h(
    Foo, 
    {}, 
    {
    header: ({age}) => {
      return [
        h('p', {}, 'header ' + age),
        h('p', {}, 'test slots array children ' + age),
        createTextVNode('Hello')
      ]
    },
    footer: () => h('p', {}, 'footer'),
    }
);
// Foo
export const Foo = {
  name: 'Foo',
  setup(props, { emit }) {
    return {

    }
  },
  render() {
    const foo = h('p', {}, 'foo');
    const age = 18;
    return h('div', { class: 'foo' }, [
      renderSlots(this.$slots, 'header', { age }),
      foo, 
      renderSlots(this.$slots, 'footer'),
    ]);
  }
}

设置插槽 vnodeshapeFlag

插槽的条件:vnode.shapeFlag 是组件 & children 是 object

// runtime-core/vnode.ts
export function createVNode(type, props?, children?) {
  const vnode = {
    shapeFlag: getShapeFlags(type), // 初步根据 type 判断 shapeFlag
  };

  // 针对 children 进一步判断 shapeFlag
  if (typeof children === "string") {
    // 使用 | 运算符,这样就能判断两样,type 和 children
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
  } else if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }

  // slots 的条件:组件 + children 是 object
  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    if (typeof children === "object") {
      vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN;
    }
  }

  return vnode;
}

初始化 slots

export function createComponentInstance(vnode, parent) {
  const component = {
    slots: {}, // 添加 slots 属性
    ...
  };
  return component;
}
export function setupComponent(instance, container) {
  // 初始化插槽
  initSlots(instance, instance.vnode.children);

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

export function normalizeObjectSlots(children, slots) {
  // children 是 object
  for (const key in children) {
    const value = children[key];
    // value 是一个函数,函数返回的是一个数组,
    // 这样是因为在 renderSlots 里面, 
    // createVNode 函数的 children 只能是多个vnode组成的数组或是一个字符串,
    // 如果是字符串,需要另外处理,后面会用 createTextVNode 处理
    
    // 实际上这里的作用的是用 normalizeSlotValue 改变了一下用户写的返回结果,
    // 不是数组的要转成数组
    slots[key] = (props) => normalizeSlotValue(value(props));
  }
}

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

所以会看到,插槽的 children 可以像 header 返回的那样,是一个 vnode 组成的数组,也可以像 footer 那样直接返回一个 vnode

至于文本内容,则需要用 createTextVNode 来转化成被标签包裹着的 vnode

header: ({age}) => {
  return [
    h('p', {}, 'header ' + age),
    h('p', {}, 'test slots array children ' + age),
    createTextVNode('Hello')
  ]
},
footer: () => h('p', {}, 'footer'),

实现 createTextVNode

// runtime-core/vnode.ts
export function createTextVNode(text) {
  return createVNode('div', {}, text); // 这里暂时用 div,后面用 Symbol
}

$slots 挂载到 instance

// runtime-core/componentPublicInstance.ts
const PublicPropertiesMaps = {
  $slots: (i) => i.slots,
}

export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    const { setupState, props } = instance;

    const publicGetter = PublicPropertiesMaps[key];
    if(publicGetter) {
      return publicGetter(instance);
    }
  }
}

实现 renderSlots

// rumtime-core/helpers/renderSlots.ts
import { createVNode } from '../vnode';

export function renderSlots(slots, name, props) {
  const slot = slots[name];
  if(slot) {
    // function
    if(typeof slot === 'function') {
        // slot(props) 执行后返回的是一个数组
        return createVNode('div', {}, slot(props));
    }
  }
}

至此,slots 功能写好了。

实现 Fragment & Text

但有个可以优化的点,就是可以实现 FragmentText 类型,来优化 renderSlotscreateTextVNode

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

export function renderSlots(slots, name, props) {
  const slot = slots[name];
  if(slot) {
    // function
    if(typeof slot === 'function') {
      // slot(props) 执行后返回的是一个数组
    return createVNode('div', {}, slot(props));
    }
  }
}

// createTextVNode
export function createTextVNode(text) {
  return createVNode('div', {}, text);
}

createVNode 这两个地方,直接写了用 div 作为容器标签,这个是冗余的,而使用 FragmentText 作为 vnodeshapeFlag 可以实现无冗余标签的效果。

定义 Fragment & Text

// runtime-core/vnode.ts
export const Fragment = Symbol('Fragment');
export const Text = Symbol('Text');

div 修改为 Fragment & Text

// runtime-core/helpers/renderSlots.ts
import { Fragment, createVNode } from '../vnode';

export function renderSlots(slots, name, props) {
  const slot = slots[name];
  if(slot) {
    // function
    if(typeof slot === 'function') {
      // slot(props) 执行后返回的是一个数组
    return createVNode(Fragment, {}, slot(props));
    }
  }
}
// runtime-core/vnode.ts
export function createTextVNode(text) {
  return createVNode(Text, {}, text);
}

patch 添加 Fragment & Text

// runtime-core/renderer.ts
function patch(vnode, container) {
    const { shapeFlag, type } = vnode;
  
    switch (type) {
      // 处理插槽类型的虚拟节点
      case Fragment:
        processFragment(vnode, container);
        break;
  
      // 处理文本类型的虚拟节点
      case Text:
        processText(vnode, container);
        break;
  
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          // 处理 Element
          processElement(vnode, container);
        } else {
          // 处理组件
          processComponent(vnode, container);
        }
        break;
    }
}

processFragment

以上实现 renderSlots时分析过,实际上 Fragment 类型的 vnode,其 children 是一组 vnode 数组,所以复用 mountChildren 去处理。

function processFragment(vnode, container) {
    mountFragment(node, container);
} 

function mountFragment(vnode, container) {
    mountChildren(vnode, container);
} 

processText

实际上 Text 类型的 vnode,其 children 是一个字符串,所以:

function processText(vnode, container) {
    const { children } = vnode;
    const textNode = document.createTextNode(children);
    container.append(textNode);
}