Skip to content

Commit 5c143d4

Browse files
committed
docs(23): slots
1 parent 7af93e3 commit 5c143d4

File tree

1 file changed

+334
-0
lines changed

1 file changed

+334
-0
lines changed
+334
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# 实现组件的 slot 功能
2+
3+
在本小节中,我们将会实现组件的 slot 功能
4+
5+
## 1. 什么是 slot
6+
7+
我们先看看最简单的 h 函数中的 slot 是什么样子的
8+
9+
```ts
10+
import { h } from '../../lib/mini-vue.esm.js'
11+
import { Foo } from './Foo.js'
12+
13+
export default {
14+
render() {
15+
// 我们在渲染一个组件的时候,向第 3 个函数挂载 h
16+
return h('div', {}, [h(Foo, {}, h('div', {}, '123'))])
17+
},
18+
setup() {},
19+
}
20+
```
21+
22+
```ts
23+
import { h } from '../../lib/mini-vue.esm.js'
24+
25+
export const Foo = {
26+
setup() {},
27+
render() {
28+
// 我们可以在这里通过 `this.$slots` 进行接收到挂载的 $slots
29+
return h('div', {}, this.$slots)
30+
},
31+
}
32+
```
33+
34+
类似于模板中的这样
35+
36+
```html
37+
<Foo>
38+
<div>123</div>
39+
</Foo>
40+
```
41+
42+
## 2. 实现 slots
43+
44+
### 2.1 实现
45+
46+
通过对示例的研究,我们发现其实 slots 就是 component 的第三个参数
47+
48+
首先,我们在创建 `component` 实例的时候初始化一个 slots
49+
50+
```ts
51+
export function createComponentInstance(vnode) {
52+
const component = {
53+
vnode,
54+
type: vnode.type,
55+
setupState: {},
56+
props: {},
57+
emit: () => {},
58+
// 初始化 slots
59+
slots: {},
60+
}
61+
component.emit = emit.bind(null, component) as any
62+
return component
63+
}
64+
```
65+
66+
`setupComponent` 的时候进行处理 slots
67+
68+
```ts
69+
export function setupComponent(instance) {
70+
initProps(instance, instance.vnode.props)
71+
// 处理 slots
72+
initSlots(instance, instance.vnode.children)
73+
setupStatefulComponent(instance)
74+
}
75+
```
76+
77+
```ts
78+
// componentSlots.ts
79+
80+
export function initSlots(instance, slots) {
81+
// 我们这里最粗暴的做法就是直接将 slots 挂载到 instance 上
82+
instance.slots = slots
83+
}
84+
```
85+
86+
然后我们在拦截操作的时候加入对于 `$slots` 的处理
87+
88+
```ts
89+
import { hasOwn } from '../shared/index'
90+
91+
const PublicProxyGetterMapping = {
92+
$el: i => i.vnode.el,
93+
// 加入对于 $slots 的处理
94+
$slots: i => i.slots,
95+
}
96+
97+
// other code ...
98+
```
99+
100+
现在我们就已经可以来实现挂载 slots 了。
101+
102+
### 2.2 优化
103+
104+
现在我们已经实现如何挂载 slots 了,但是如果我们传递多个 slots 呢?
105+
106+
模板中是这样
107+
108+
```ts
109+
<Foo>
110+
<div>123</div>
111+
<div>456</div>
112+
</Foo>
113+
```
114+
115+
在 h 函数中是这样的:
116+
117+
```ts
118+
render() {
119+
return h('div', {}, [
120+
// 可以传递一个数组
121+
h(Foo, {}, [h('div', {}, '123'), h('div', {}, '456')]),
122+
])
123+
}
124+
```
125+
126+
我们再来看看接收 slots 的地方是怎么写的:
127+
128+
```ts
129+
render() {
130+
const foo = h('p', {}, 'foo')
131+
// 第三个参数只能接收 VNode,但是这里我们的 this.$slots 是一个数组
132+
// 所以就无法渲染出来
133+
// 这个时候就可以创建一个 VNode
134+
return h('p', {}, [foo, this.$slots])
135+
},
136+
```
137+
138+
```ts
139+
return h('p', {}, [foo, h('div', {}, this.$slots)])
140+
```
141+
142+
我们可以将这里的渲染 slots 抽离出来,例如我们抽离一个函数叫做 `renderSlots`
143+
144+
```ts
145+
// runtime-core/helpers/renderSlots
146+
147+
import { h } from '../h'
148+
149+
export function renderSlots(slots) {
150+
return h('div', {}, slots)
151+
}
152+
```
153+
154+
```ts
155+
return h('p', {}, [foo, renderSlots(this.$slots)])
156+
```
157+
158+
现在数组的形式已经可以实现了,但是单个的形式我们却无法实现了,所以我们需要改一下,我们是在 `initSlots` 的时候进行挂载 slots 的,我们进行一个判断,判断默认都是数组。
159+
160+
```ts
161+
export function initSlots(instance, slots) {
162+
// 进行类型判断
163+
slots = Array.isArray(slots) ? slots : [slots]
164+
instance.slots = slots
165+
}
166+
```
167+
168+
OK,现在我们无论是数组还是单个都可以实现了。
169+
170+
## 3. 具名 slots
171+
172+
我们在给定 slots 时,还可以给定名字。
173+
174+
### 3.1 例子
175+
176+
我们来看看一个具名插槽的例子
177+
178+
在模板中是这样的:
179+
180+
```html
181+
<Foo>
182+
<template v-slot:header></template>
183+
<template v-slot:bottom></template>
184+
</Foo>
185+
```
186+
187+
在 h 函数中是这样的
188+
189+
```ts
190+
const foo = h(
191+
Foo,
192+
{},
193+
{
194+
header: h('div', {}, '123'),
195+
footer: h('div', {}, '456'),
196+
}
197+
)
198+
return h('div', {}, [app, foo])
199+
```
200+
201+
我们在接收 slots 的时候是如何接收的呢?`renderSlots` 第二个参数可以指定 name
202+
203+
```ts
204+
return h('p', {}, [
205+
renderSlots(this.$slots, 'header'),
206+
foo,
207+
renderSlots(this.$slots, 'footer'),
208+
])
209+
```
210+
211+
### 3.2 实现
212+
213+
首先,我们在挂载的时候就从数组变成了对象。但是在这里我们还是要进行两次判断,第一个判断如果传入的是简单的值,那么就视为这个是 `default`。如果传入的是对象,那么再具体判断
214+
215+
```ts
216+
function initObjectSlots(instance, slots) {
217+
if(!slots) return
218+
// 单独传了一个 h
219+
if (slots.vnode) {
220+
instance.slots.default = [slots]
221+
return
222+
}
223+
// 传了一个数组
224+
if (Array.isArray(slots)) {
225+
instance.slots.default = slots
226+
return
227+
}
228+
// 传了一个对象
229+
for (const slotName of Object.keys(slots)) {
230+
instance.slots[slotName] = normalizeSlots(slots[slotName])
231+
}
232+
}
233+
234+
function normalizeSlots(slots) {
235+
return Array.isArray(slots) ? slots : [slots]
236+
}
237+
```
238+
239+
然后我们在渲染 `slots` 的时候,也要对多个类型进行判断
240+
241+
```ts
242+
export function renderSlots(slots, name = 'default') {
243+
// 此时 slots 就是 Object
244+
const slot = slots[name]
245+
if (slot) {
246+
return h('div', {}, slot)
247+
}
248+
}
249+
```
250+
251+
好了,现在我们的具名插槽就也已经支持了。
252+
253+
## 4. 作用域插槽
254+
255+
### 4.1 例子
256+
257+
在 template 中,作用域插槽是这样的
258+
259+
注册方
260+
261+
```html
262+
<slot :count="1"></slot>
263+
```
264+
265+
使用方
266+
267+
```ts
268+
<template #default="{count}">{{count}} 是 1</template>
269+
```
270+
271+
在 h 函数中是这样的
272+
273+
注册方
274+
275+
```ts
276+
return h('p', {}, [
277+
// 第三个参数就是 props
278+
renderSlots(this.$slots, 'header', {
279+
count: 1,
280+
}),
281+
foo,
282+
renderSlots(this.$slots, 'footer'),
283+
])
284+
```
285+
286+
使用方
287+
288+
```ts
289+
const foo = h(
290+
Foo,
291+
{},
292+
{
293+
// 这样我们的 slots 就变成一个函数了
294+
header: ({ count }) => h('div', {}, '123' + count),
295+
footer: () => h('div', {}, '456'),
296+
}
297+
)
298+
```
299+
300+
### 4.2 实现
301+
302+
首先,在注册的时候,第三个参数是 props,而我们的 slots 也变成了函数
303+
304+
```ts
305+
export function renderSlots(slots, name = 'default', props) {
306+
// 此时 slots 就是函数
307+
const slot = slots[name]
308+
if (slot) {
309+
return h('div', {}, slot(props))
310+
}
311+
}
312+
```
313+
314+
在初始化的时候
315+
316+
```ts
317+
// other code...
318+
319+
function initObjectSlots(instance, slots) {
320+
// other code ...
321+
for (const slotName of Object.keys(slots)) {
322+
// 在这里的时候,我们通过 `slots[slotName]` 来获取到 slot 对应的值
323+
// 但是现在我们对应的值已经变成了函数,所以需要调用 `slots[slotName]()`
324+
// 但是我们在 render 时候,会将这一段整体作为一个函数进行调用
325+
// 所以结合上面我们的 `renderSlots`,就变成了这样
326+
// props => normalizeSlots(slots[slotName](props))
327+
instance.slots[slotName] = props => normalizeSlots(slots[slotName](props))
328+
}
329+
}
330+
331+
// other code...
332+
```
333+
334+
现在我们也已经支持作用域插槽了。

0 commit comments

Comments
 (0)