前言

vue中关于组件间数据的流向一般是父组件往子组件传参,然后子组件回调父组件更新这种单向的数据流的
但是,它也存在一种机制–作用域插槽,使得我们能够在父组件中通过插槽的方式在父组件中使用到子组件中所定义的属性+方法,本文将从一个简单的函数切入,从最简单的角度来观察关于作用域插槽的执行过程!

作用域插槽的简单应用

官方插槽例子

通过这里例子我们可以发现在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据!
🤔 那么这个过程是怎样子的呢?下面我们通过一个类比的函数以及实际过程分析一波

一个简单的函数调用

比如我们有 👇 两个函数以及它们的调用过程:

1
2
3
4
5
6
7
function fun1(props: { num: number, str: string }){
function fun2(){
const obj = { num: 888, str: '我是字符串' }
fun1(obj)
}
console.info('这是fun1的操作:num=' + number + ', str->' + str)
}

💁♂ 这里我们简单定义了两个函数fun1fun2,然后在fun2中调用fun1,并传递fun2中的obj变量给到fun1作为参数!

🤔 假如fun1中的函数的实现体是交给其成员函数来实现的话,是否也是可行,答案是肯定的,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这里是fun1函数的改造
function fun1(){
// 这里我们将其包装到一个对象$slot中
const $slots = {
default: (num: number, str: string) => {
console.info('这是fun1的操作:num=' + num + ', str->' + str)
}
}
function fun2(){
const obj = { num: 888, str: '我是字符串' }
$slots.default(obj.num, obj.str)
}
fun2()
}
fun1() // 这是fun1的操作:num=888, str->我是字符串

🤔 为什么要介绍这样子的一个函数调用? 👉 因为vue中的作用域插槽的实现就是这么的简单!

实际的作用域插槽的执行过程

这里我们借助于vue官方的在线运行工具,拿官方的例子来分析一波,如下截图所示:
作用域插槽的render结果
这是一个简单的例子,在父组件中使用插槽,子组件通过v-slot将属性对外暴露给到父组件,然后父组件做展示。
由于在vue中,所有的组件都会被render为虚拟的dom(上图右侧),所有的组件都将会被转换为一个render函数,而在App.vue的render函数如下所示:

1
2
3
4
5
6
7
8
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock($setup["MyComponent"], null, {
default: _withCtx((slotProps) => [
_createTextVNode(_toDisplayString(slotProps.text) + " " + _toDisplayString(slotProps.count), 1 /* TEXT */)
]),
_: 1 /* STABLE */
}))
}

🤔 关于这个程序的执行分析如下:

  1. 首先render()函数返回了一个()操作符,代表的是N个顺序执行的操作(openBlock与createBlock);
  2. openBlock()创建一个新的block,标志着一个新块的开始;
  3. createBlock()针对这个新创建的块来进行设置,这里我们重点关注下最后一个参数对象;
  4. 这里使用default命名了一个插槽,并通过_withCtx向插槽中传递了一回调函数
  5. 关于插槽中的回调函数就是App.vue中插槽的render函数。

这里由于是App.vue包裹MyComponent.vue,因为两者拥有着同一个上下文ctx,而App.vue在使用这个v-slot指令的时候,会将当前节点标签对应的渲染函数存放在ctx下的$slots属性中,那么当MyComponent.vue在render的时候,就可以从这个ctx中的$slots获取到App.vue所传递过来的插槽,也就是可以通过_ctx.$slot即可访问到App.vue中的插槽render函数,这里可以配合关于这个h函数配合插槽的使用
从而生成对应的结果对象待执行:

1
2
3
4
5
6
7
{
$slots: {
default: (scopedProps) => {
// 这里是App.vue中的插槽的render动作
}
}
}

而这个MyComponent.vue的渲染结果如下:

1
2
3
4
5
6
7
8
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_renderSlot(_ctx.$slots, "default", {
text: $setup.greetingMessage,
count: 1
})
]))
}

🌟 关于上述这个renderSlot()的执行过程,其实就是从ctx.$slots中捞default作为key,并执行对应的函数传递对应的属性参数,也就是执行上述的(scopedProps) => {}函数

:+1: 因此,将这里的执行过程与上述提供的js函数对比不难发现,两者的执行其实是一样的,上述的fun1就是App.vue的插槽的渲染函数fun2就是MyComponent.vuefun1嵌套fun2并执行fun2,同时拿到fun1的环境所提供的变量,即可实现一样的父组件通过插槽的方式访问到子组件的数据!

vue3针对插槽提供的类型检测: defineSlots

defineSlots()只接受类型参数,没有运行时参数。类型参数应该是一个类型字面量,其中属性键是插槽名称,值类型是插槽函数。函数的第一个参数是插槽期望接收的 props,其类型将用于模板中的插槽 props。返回类型目前被忽略,可以是 any,但我们将来可能会利用它来检查插槽内容。关于此宏的定义如下:

1
2
3
4
5
6
7
8
export function defineSlots<
S extends Record<string, any> = Record<string, any>,
>(): StrictUnwrapSlotsType<SlotsType<S>> {
if (__DEV__) {
warnRuntimeUsage(`defineSlots`)
}
return null as any
}

🌠 从上述我们可以看出此方法没有实现体,仅做类型定义,使用如下:

它还返回 slots 对象,该对象等同于在 setup 上下文中暴露或由 useSlots() 返回的 slots 对象。

总结

关于整个过程,可以简单概括为 👇 的执行流程图:
作用域插槽的执行流程图