今天我们将分析我们经常使用的 vue 功能 slot 是如何设计和实现的,本文将围绕 普通插槽 和 作用域插槽 以及 vue 2.6.x 版本的 v-slot 展开对该话题的讨论。当然还不懂用法的同学建议官网先看看相关 API 先。接下来,我们直接进入正文吧
普通插槽
首先我们看一个我们对于 slot 最常用的例子
然后我们直接使用,页面则正常显示一下内容
然后,这个时候我们使用的时候,对 slot 内容进行覆盖
this is slot custom content.
内容则变成下图所示
对于此,大家可能都能清楚的知道会是这种情况。今天我就将带领大家直接看看 vue 底层对 slot 插槽的具体实现。
vm.$slots
我们开始前,先看看 vue 的 Component 接口上对 $slots 属性的定义
$slots: { [key: string]: Array };
多的咱不说,咱直接 console 一下上面例子中的 $slots
剩下的篇幅将讲解 slot 内容如何进行渲染以及如何转换成上图内容
renderSlot
看完了具体实例中 slot 渲染后的 vm.$slots 对象,这一小篇我们直接看看 renderSlot 这块的逻辑,首先我们先看看 renderSlot 函数的几个参数都有哪些
这里我们先不看 scoped-slot 的逻辑,我们只看普通 slot 的逻辑。
const slotNodes = this.$slots[name]nodes = slotNodes || fallbackreturn nodes
这里直接先取值 this.$slots[name] ,若存在则直接返回其对其的 vnode 数组,否则返回 fallback。看到这,很多人可能不知道 this.$slots 在哪定义的。解释这个之前我们直接往后看另外一个方法
看完 resolveSlots 的参数后我们接着往后过其中具体的逻辑。如果 children 参数不存在,直接返回一个空对象
const slots = {}if (!children) { return slots}
如果存在,则直接对 children 进行遍历操作
slots 获取到值后,则进行一些过滤操作,然后直接返回有用的 slots
我们从上面已经知道了 vue 对 slots 是如何进行赋值保存数据的。而在 src/core/instance/render.js 的 initRender 方法中则是对 vm.$slots 进行了初始化的赋值。
了解了是 vm.$slots 这块逻辑后,肯定有人会问:你这不就只是拿到了一个对象么,怎么把其中的内容给搞出来呢?别急,我们接着就来讲一下对于 slot 这块 vue 是如何进行编译的。这里咱就把 slot generate 相关逻辑过上一过,话不多说,咱直接上代码
注:上面的 slotName 在 src/compiler/parser/index.js 的 processSlot() 函数中进行了赋值,并且 父组件编译阶段用到的 slotTarget 也在这里进行了处理
随即在 genData() 中使用 slotTarget 进行 data 的数据拼接
if (el.slotTarget && !el.slotScope) { data += `slot:${el.slotTarget},`}
此时父组件将生成以下代码
然后当 el.tag 为 slot 的情况,则直接执行 genSlot()
else if (el.tag === 'slot') { return genSlot(el, state)}
按照我们举出的例子,则子组件最终会生成以下代码
作用域插槽
上面我们已经了解到 vue 对于普通的 slot 标签是如何进行处理和转换的。接下来我们来分析下作用域插槽的实现逻辑。
1、vm.$scopedSlots
了解之前还是老规矩,先看看 vue 的 Component 接口上对 $scopedSlots 属性的定义
$scopedSlots: { [key: string]: () => VNodeChildren };
其中的 VNodeChildren 定义如下:
declare type VNodeChildren = Array<?VNode | string | VNodeChildren> | string;
先来个相关的例子
然后进行使用
效果如下
从使用层面我们能看出来,子组件的 slot 标签上绑定了一个 text 以及 :msg 属性。然后父组件在使用插槽使用了 slot-scope 属性去读取插槽带的属性对应的值
注:提及一下 processSlot() 对于 slot-scope 的处理逻辑
从上面的代码我们能看出,vue 对于这块直接读取 slot-scope 属性并赋值给 AST 抽象语法树的 slotScope 属性上。而拥有 slotScope 属性的节点,会直接以 **插槽名称 name 为 key、本身为 value **的对象形式挂载在父节点的 scopedSlots 属性上
然后在 src/core/instance/render.js 的 renderMixin 方法中对 vm.$scopedSlots 则是进行了如下赋值:
if (_parentVnode) { vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject}
然后 genData() 里会进行以下逻辑处理
if (el.scopedSlots) { data += `${genScopedSlots(el, el.scopedSlots, state)},`}
紧接着我们来看看 genScopedSlots 中的逻辑
然后我们再来看看 genScopedSlot 是如何生成 render function 字符串的
我们把上面例子的 $scopedSlots 打印一下,结果如下
然后上面例子中父组件最终会生成如下代码
renderSlot(slot-scope)
上面我们提及对于插槽 render 逻辑的时候忽略了 slot-scope 的相关逻辑,这里我们来看看这部分内容
这里我们看看 renderHelps 里面的 _u ,即 resolveScopedSlots,其逻辑如下
这块会对 attrs 和 v-bind 进行,对于这块内容上面我已经提过了,要看请往上翻阅。结合我们的例子,子组件则会生成以下代码
v-slot
1、基本用法
vue 2.6.x 已经出来有一段时间了,其中对于插槽这块则是放弃了 slot-scope 作用域插槽推荐写法,直接改成了 v-slot 指令形式的推荐写法(当然这只是个语法糖而已)。下面我们将仔细谈谈 v-slot 这块的内容。
在看具体实现逻辑前,我们先通过一个例子来先了解下其基本用法
然后进行使用
页面展示效果如下
相同与区别
接下来,咱来会会这个新特性
round 1. $slots & $scopedSlots
$slots 这块逻辑没变,还是沿用的以前的代码
// $slotsconst options = vm.$optionsconst parentVnode = vm.$vnode = options._parentVnodeconst renderContext = parentVnode && parentVnode.contextvm.$slots = resolveSlots(options._renderChildren, renderContext)
$scopedSlots 这块则进行了改造,执行了 normalizeScopedSlots() 并接收其返回值为 $scopedSlots 的值
接着,我们来会一会 normalizeScopedSlots ,首先我们先看看它的几个参数
- 首先,如果 slots 参数不存在,则直接返回一个空对象 {}
if (!slots) { res = {}}
- 若 prevSlots 存在,且满足系列条件的情况,则直接返回 prevSlots
注:这里的 $key , $hasNormal , $stable 是直接使用 vue 内部对 Object.defineProperty 封装好的 def() 方法进行赋值的
def(res, '$stable', isStable)def(res, '$key', key)def(res, '$hasNormal', hasNormalSlots)复制代码
- 否则,则对 slots 对象进行遍历,操作 normalSlots ,赋值给 key 为 key,value 为 normalizeScopedSlot 返回的函数 的对象 res
- 随后再次对 normalSlots 进行遍历,若 normalSlots 中的 key 在 res 找不到对应的 key,则直接进行 proxyNormalSlot 代理操作,将 normalSlots 中的 slot 挂载到 res对象上
- 接着,我们看看 normalizeScopedSlot() 都做了些什么事情。该方法接收三个参数,第一个参数为 normalSlots,第二个参数为 key,第三个参数为 fn
参考文章:
https://juejin.im/post/5cced0096fb9a032426510ad