实现 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'),
]);
}
}
设置插槽 vnode 的 shapeFlag
插槽的条件: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
但有个可以优化的点,就是可以实现 Fragment 和 Text 类型,来优化 renderSlots 和 createTextVNode。
// 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 作为容器标签,这个是冗余的,而使用 Fragment 和 Text 作为 vnode 的 shapeFlag 可以实现无冗余标签的效果。
定义 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);
}