|
| 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