持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情
本篇文章会带你实现组件的slots插槽功能,包括默认插槽、具名插槽和作用域插槽
为了防止篇幅过长,我会将组件插槽的实现分成三篇文章去讲解,本篇是第一篇,带你实现组件的默认插槽
主要包括两个功能:
- 在组件的
render函数中通过this.$slots获取到父组件传入的默认插槽内容 - 能够支持插槽传入数组
1. 案例环境
本节会实现一下组件的slots功能,首先来看一下案例场景
// App.js
import { h } from '../../lib/plasticine-mini-vue.esm.js';
import { Foo } from './Foo.js';
export const App = {
setup() {
return {};
},
render() {
const foo = h(Foo, {}, h('p', {}, 'default slot'));
return h('div', {}, [foo]);
},
};
// Foo.js
import { h } from '../../lib/plasticine-mini-vue.esm.js';
export const Foo = {
setup() {
return {};
},
render() {
const foo = h('p', {}, 'foo');
return h('div', {}, [foo]);
},
};
我们希望实现默认插槽的功能,也就是在渲染子组件时,能够将父组件传入的children在子组件中的默认插槽中渲染出来
以模板的方式写出来就像下面这样:
<div id="app">
<Foo>
<template>
<p>default slot</p>
</template>
</Foo>
</div>
目前尚未实现默认插槽的功能,因此渲染出的结果是下面这样的
2. this.$slots
2.1 添加组件公共实例属性$slots
一个基本的思路就是,在子组件Foo中能够通过this.$slots获取到父组件传入的slots插槽内容,然后放到h函数中进行渲染
// Foo.js
import { h } from '../../lib/plasticine-mini-vue.esm.js';
export const Foo = {
setup() {
return {};
},
render() {
const foo = h('p', {}, 'foo');
return h('div', {}, [foo, this.$slots]);
},
};
$slots也就是Foo的vnode的children属性中的内容,要想通过this访问到,需要将它作为组件实例的属性,然后能够在组件公共实例中通过$slots获取到
// src/runtime-core/component.ts
export function createComponentInstance(vnode) {
const component = {
vnode,
type: vnode.type,
setupState: {},
props: {},
emit: () => {},
+ slots: {}
};
component.emit = emit as any;
return component;
}
// src/runtime-core/componentPublicInstance.ts
const publicPropertiesMap = {
$el: (i) => i.vnode.el,
+ $slots: (i) => i.slots,
};
2.2 initSlots
然后去实现initSlots,对于组件而言,slots就是它的vnode.children,因此最基本的我们需要给initSlots传入instance组件实例,以及vnode.children,然后将vnode.children挂载到instance.slots上
// src/runtime-core/componentSlots.ts
export function initSlots(instance, children) {
instance.slots = children;
}
+ import { initSlots } from './componentSlots';
export function setupComponent(instance) {
- // TODO
initProps(instance, instance.vnode.props);
- initSlots();
+ initSlots(instance, instance.vnode.children);
setupStatefulComponent(instance);
}
现在我们再到Foo.js中打印一下this.$slots看看是否有将父组件传递的children传过来
export const Foo = {
setup() {
return {};
},
render() {
+ console.log(this.$slots);
const foo = h('p', {}, 'foo');
return h('div', {}, [foo]);
},
};
可以看到已经能够接收到了,那么接下来要做的就是将
this.$slots在Foo.js中渲染出来
import { h } from '../../lib/plasticine-mini-vue.esm.js';
export const Foo = {
setup() {
return {};
},
render() {
const foo = h('p', {}, 'foo');
- return h('div', {}, [foo]);
+ return h('div', {}, [foo, this.$slots]);
},
};
现在就成功渲染出来了!
3. 插槽传入数组
3.1 看看模板方式的数组插槽长啥样
目前的案例中父组件给子组件传入的插槽的内容是一个Element类型的vnode,但是实际上应当支持多个vnode的渲染,也就是:
import { h } from '../../lib/plasticine-mini-vue.esm.js';
import { Foo } from './Foo.js';
export const App = {
setup() {
return {};
},
render() {
const foo = h(Foo, {}, [
h('p', {}, 'default slot'),
h('p', {}, 'plasticine'),
]);
return h('div', {}, [foo]);
},
};
写成模板的形式也就是
<div id="app">
<Foo>
<template>
<p>default slot</p>
<p>plasticine</p>
</template>
</Foo>
</div>
3.2 解决嵌套数组问题
此时的this.$slots是一个数组类型,里面的元素全是vnode,而Foo组件内部调用h函数生成vnode的时候,如果直接将this.$slots传入一个数组中,就变成了嵌套数组
export const Foo = {
setup() {
return {};
},
render() {
const foo = h('p', {}, 'foo');
// children -- [foo, [h('p', {}, 'default slot'), h('p', {}, 'plasticine')]]
return h('div', {}, [foo, this.$slots]);
},
};
由于h函数并不支持嵌套数组的处理,因此自然是无法渲染出结果的,那么我们可以想到一个简单的办法:
export const Foo = {
setup() {
return {};
},
render() {
const foo = h('p', {}, 'foo');
return h('div', {}, [foo, h('div', {}, this.$slots)]);
},
};
就是将原来的children数组直接传给h函数,处理后得到的vnode再作为真正的children传给Foo组件,这样一来就可以渲染成功了
3.3 封装提高可读性
但是这样的代码可读性太差了,我们可以考虑将代码封装一下,用一个renderSlots函数去封装,创建src/runtime-core/helpers/renderSlots.ts
import { createVNode } from '../vnode';
export function renderSlots(slots) {
return createVNode('div', {}, slots);
}
为了让它被rollup打包到构建结果中,还需要将其在runtime-core模块中导出
export { createApp } from './createApp';
export { h } from './h';
+ export { renderSlots } from './helpers/renderSlots';
然后现在在Foo组件中就可以用它替代h去将$slots创建成vnode了
export const Foo = {
setup() {
return {};
},
render() {
const foo = h('p', {}, 'foo');
- return h('div', {}, [foo, this.$slots]);
+ return h('div', {}, [foo, renderSlots(this.$slots)]);
},
};
3.4 解决插槽传入单个结点失效的问题
但是现在有一个问题,对于之前的插槽中传入单个结点的情况,居然不支持了!
import { h } from '../../lib/plasticine-mini-vue.esm.js';
import { Foo } from './Foo.js';
export const App = {
setup() {
return {};
},
render() {
// const foo = h(Foo, {}, [
// h('p', {}, 'default slot'),
// h('p', {}, 'plasticine'),
// ]);
const foo = h(Foo, {}, h('p', {}, 'default slot'));
return h('div', {}, [foo]);
},
};
这是为什么呢?这是因为我们的
mountElement只支持TEXT_CHILDREN或者ARRAY_CHILDREN
然而现在renderSlots函数接收到的slots参数却是一个vnode,对于普通的vnode,mountElement是无法处理的,所以我们可以将这个单独的vnode放到数组当中,变成一个ARRAY_CHILDREN类型的children,这样不就解决了吗!
export function initSlots(instance, children) {
- instance.slots = children;
+ // 保证 $slots 一定是存放 vnode 的数组
+ instance.slots = Array.isArray(children) ? children : [children];
}
现在我们的默认插槽就实现完啦!