资讯专栏INFORMATION COLUMN

细谈 vue - slot 篇

kaka / 2809人阅读

摘要:结合我们的例子,子组件则会生成以下代码到目前为止,对于普通插槽和作用域插槽已经谈的差不多了。下面我们将仔细谈谈这块的内容。在看具体实现逻辑前,我们先通过一个例子来先了解下其基本用法然后进行使用页面展示效果如下看着好。

本篇文章是细谈 vue 系列第二篇了,上篇我们已经细谈了 vue 的核心之一 vdom,传送门

今天我们将分析我们经常使用的 vue 功能 slot 是如何设计和实现的,本文将围绕 普通插槽作用域插槽 以及 vue 2.6.x 版本的 v-slot 展开对该话题的讨论。当然还不懂用法的同学建议官网先看看相关 API 先。接下来,我们直接进入正文吧

一、普通插槽

首先我们看一个我们对于 slot 最常用的例子

</>复制代码

  1. <template>
  2. <div class="slot-demo">
  3. <slot>this is slot default content text.slot>
  4. div>
  5. template>

然后我们直接使用,页面则正常显示一下内容

然后,这个时候我们使用的时候,对 slot 内容进行覆盖

</>复制代码

  1. <slot-demo>this is slot custom content.slot-demo>

内容则变成下图所示

对于此,大家可能都能清楚的知道会是这种情况。今天我就将带领大家直接看看 vue 底层对 slot 插槽的具体实现。

1、vm.$slots

我们开始前,先看看 vueComponent 接口上对 $slots 属性的定义

</>复制代码

  1. $slots: { [key: string]: Array };

多的咱不说,咱直接 console 一下上面例子中的 $slots

剩下的篇幅将讲解 slot 内容如何进行渲染以及如何转换成上图内容

2、renderSlot

看完了具体实例中 slot 渲染后的 vm.$slots 对象,这一小篇我们直接看看 renderSlot 这块的逻辑,首先我们先看看 renderSlot 函数的几个参数都有哪些

renderSlot()

</>复制代码

  1. export function renderSlot (
  2. name: string, // 插槽名 slotName
  3. fallback: ");// 插槽默认内容生成的 vnode 数组
  4. props: ");// props 对象
  5. bindObject: ");// v-bind 绑定对象
  6. ): ");Array<VNode> {}

这里我们先不看 scoped-slot 的逻辑,我们只看普通 slot 的逻辑。

</>复制代码

  1. const slotNodes = this.$slots[name]
  2. nodes = slotNodes || fallback
  3. return nodes

这里直接先取值 this.$slots[name] ,若存在则直接返回其对其的 vnode 数组,否则返回 fallback。看到这,很多人可能不知道 this.$slots 在哪定义的。解释这个之前我们直接往后看另外一个方法

renderslots()

</>复制代码

  1. export function resolveSlots (
  2. children: ");// 父节点的 children
  3. context: ");// 父节点的上下文,即父组件的 vm 实例
  4. ): { [key: string]: Array } {}

看完 resolveSlots 的参数后我们接着往后过其中具体的逻辑。如果 children 参数不存在,直接返回一个空对象

</>复制代码

  1. const slots = {}
  2. if (!children) {
  3. return slots
  4. }

如果存在,则直接对 children 进行遍历操作

</>复制代码

  1. for (let i = 0, l = children.length; i < l; i++) {
  2. const child = children[i]
  3. const data = child.data
  4. // 如果 data.slot 存在,将插槽名称当做 key,child 当做值直接添加到 slots 中去
  5. if ((child.context === context || child.fnContext === context) &&
  6. data && data.slot != null
  7. ) {
  8. const name = data.slot
  9. const slot = (slots[name] || (slots[name] = []))
  10. // child 的 tag 为 template 标签的情况
  11. if (child.tag === "template") {
  12. slot.push.apply(slot, child.children || [])
  13. } else {
  14. slot.push(child)
  15. }
  16. // 如果 data.slot 不存在,则直接将 child 丢到 slots.default 中去
  17. } else {
  18. (slots.default || (slots.default = [])).push(child)
  19. }
  20. }

slots 获取到值后,则进行一些过滤操作,然后直接返回有用的 slots

</>复制代码

  1. // ignore slots that contains only whitespace
  2. for (const name in slots) {
  3. if (slots[name].every(isWhitespace)) {
  4. delete slots[name]
  5. }
  6. }
  7. return slots
  8. // isWhitespace 相关逻辑
  9. function isWhitespace (node: VNode): boolean {
  10. return (node.isComment && !node.asyncFactory) || node.text === " "
  11. }

数组 every() 方法传送门 - Array.prototype.every()

initRender()

我们从上面已经知道了 vueslots 是如何进行赋值保存数据的。而在 src/core/instance/render.jsinitRender 方法中则是对 vm.$slots 进行了初始化的赋值。

</>复制代码

  1. const options = vm.$options
  2. const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  3. const renderContext = parentVnode && parentVnode.context
  4. vm.$slots = resolveSlots(options._renderChildren, renderContext)

genSlot()

了解了是 vm.$slots 这块逻辑后,肯定有人会问:你这不就只是拿到了一个对象么,怎么把其中的内容给搞出来呢?别急,我们接着就来讲一下对于 slot 这块 vue 是如何进行编译的。这里咱就把 slot generate 相关逻辑过上一过,话不多说,咱直接上代码

</>复制代码

  1. function genSlot (el: ASTElement, state: CodegenState): string {
  2. const slotName = el.slotName || ""default"" // 取 slotName,若无,则直接命名为 "default"
  3. const children = genChildren(el, state) // 对 children 进行 generate 操作
  4. let res = `_t(${slotName}${children ");`,${children}` : ""}`
  5. const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(",")}}` // 将 attrs 转换成对象形式
  6. const bind = el.attrsMap["v-bind"] // 获取 slot 上的 v-bind 属性
  7. // 若 attrs 或者 bind 属性存在但是 children 却木得,直接赋值第二参数为 null
  8. if ((attrs || bind) && !children) {
  9. res += `,null`
  10. }
  11. // 若 attrs 存在,则将 attrs 作为 `_t()` 的第三个参数(普通插槽的逻辑处理)
  12. if (attrs) {
  13. res += `,${attrs}`
  14. }
  15. // 若 bind 存在,这时如果 attrs 存在,则 bind 作为第三个参数,否则 bind 作为第四个参数(scoped-slot 的逻辑处理)
  16. if (bind) {
  17. res += `${attrs ");"" : ",null"},${bind}`
  18. }
  19. return res + ")"
  20. }

注:上面的 slotNamesrc/compiler/parser/index.jsprocessSlot() 函数中进行了赋值,并且 父组件编译阶段用到的 slotTarget 也在这里进行了处理

</>复制代码

  1. function processSlot (el) {
  2. if (el.tag === "slot") {
  3. // 直接获取 attr 里面 name 的值
  4. el.slotName = getBindingAttr(el, "name")
  5. // ...
  6. }
  7. // ...
  8. const slotTarget = getBindingAttr(el, "slot")
  9. if (slotTarget) {
  10. // 如果 slotTarget 存在则直接取命名插槽的 slot 值,否则直接为 "default"
  11. el.slotTarget = slotTarget === """" ");""default"" : slotTarget
  12. if (el.tag !== "template" && !el.slotScope) {
  13. addAttr(el, "slot", slotTarget)
  14. }
  15. }
  16. }

随即在 genData() 中使用 slotTarget 进行 data 的数据拼接

</>复制代码

  1. if (el.slotTarget && !el.slotScope) {
  2. data += `slot:${el.slotTarget},`
  3. }

此时父组件将生成以下代码

</>复制代码

  1. with(this) {
  2. return _c("div", [
  3. _c("slot-demo"),
  4. {
  5. attrs: { slot: "default" },
  6. slot: "default"
  7. },
  8. [ _v("this is slot custom content.") ]
  9. ])
  10. }

然后当 el.tagslot 的情况,则直接执行 genSlot()

</>复制代码

  1. else if (el.tag === "slot") {
  2. return genSlot(el, state)
  3. }

按照我们举出的例子,则子组件最终会生成以下代码

</>复制代码

  1. with(this) {
  2. // _c => createElement ; _t => renderSlot ; _v => createTextVNode
  3. return _c(
  4. "div",
  5. {
  6. staticClass: "slot-demo"
  7. },
  8. [ _t("default", [ _v("this is slot default content text.") ]) ]
  9. )
  10. }
二、作用域插槽

上面我们已经了解到 vue 对于普通的 slot 标签是如何进行处理和转换的。接下来我们来分析下作用域插槽的实现逻辑。

1、vm.$scopedSlots

了解之前还是老规矩,先看看 vueComponent 接口上对 $scopedSlots 属性的定义

</>复制代码

  1. $scopedSlots: { [key: string]: () => VNodeChildren };

其中的 VNodeChildren 定义如下:

</>复制代码

  1. declare type VNodeChildren = Array<");

先来个相关的例子

</>复制代码

  1. <template>
  2. <div class="slot-demo">
  3. <slot text="this is a slot demo , " :msg="msg">slot>
  4. div>
  5. template>
  6. <script>
  7. export default {
  8. name: "SlotDemo",
  9. data () {
  10. return {
  11. msg: "this is scoped slot content."
  12. }
  13. }
  14. }
  15. script>

然后进行使用

</>复制代码

  1. <template>
  2. <div class="parent-slot">
  3. <slot-demo>
  4. <template slot-scope="scope">
  5. <p>{{ scope.text }}p>
  6. <p>{{ scope.msg }}p>
  7. template>
  8. slot-demo>
  9. div>
  10. template>

效果如下

从使用层面我们能看出来,子组件的 slot 标签上绑定了一个 text 以及 :msg 属性。然后父组件在使用插槽使用了 slot-scope 属性去读取插槽带的属性对应的值

注:提及一下 processSlot() 对于 slot-scope 的处理逻辑

</>复制代码

  1. let slotScope
  2. if (el.tag === "template") {
  3. slotScope = getAndRemoveAttr(el, "scope")
  4. // 兼容 2.5 以前版本 slot scope 的用法(这块有个警告,我直接忽略掉了)
  5. el.slotScope = slotScope || getAndRemoveAttr(el, "slot-scope")
  6. } else if ((slotScope = getAndRemoveAttr(el, "slot-scope"))) {
  7. el.slotScope = slotScope
  8. }

从上面的代码我们能看出,vue 对于这块直接读取 slot-scope 属性并赋值给 AST 抽象语法树的 slotScope 属性上。而拥有 slotScope 属性的节点,会直接以 **插槽名称 namekey、本身为 value **的对象形式挂载在父节点的 scopedSlots 属性上

</>复制代码

  1. else if (element.slotScope) {
  2. currentParent.plain = false
  3. const name = element.slotTarget || ""default""
  4. ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  5. }

然后在 src/core/instance/render.jsrenderMixin 方法中对 vm.$scopedSlots 则是进行了如下赋值:

</>复制代码

  1. if (_parentVnode) {
  2. vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
  3. }

然后 genData() 里会进行以下逻辑处理

</>复制代码

  1. if (el.scopedSlots) {
  2. data += `${genScopedSlots(el, el.scopedSlots, state)},`
  3. }

紧接着我们来看看 genScopedSlots 中的逻辑

</>复制代码

  1. function genScopedSlots (
  2. slots: { [key: string]: ASTElement },
  3. state: CodegenState
  4. ): string {
  5. // 对 el.scopedSlots 对象进行遍历,执行 genScopedSlot,且将结果用逗号进行拼接
  6. // _u => resolveScopedSlots (具体逻辑下面一个小节进行分析)
  7. return `scopedSlots:_u([${
  8. Object.keys(slots).map(key => {
  9. return genScopedSlot(key, slots[key], state)
  10. }).join(",")
  11. }])`
  12. }

然后我们再来看看 genScopedSlot 是如何生成 render function 字符串的

</>复制代码

  1. function genScopedSlot (
  2. key: string,
  3. el: ASTElement,
  4. state: CodegenState
  5. ): string {
  6. if (el.for && !el.forProcessed) {
  7. return genForScopedSlot(key, el, state)
  8. }
  9. // 函数参数为标签上 slot-scope 属性对应的值 (getAndRemoveAttr(el, "slot-scope"))
  10. const fn = `function(${String(el.slotScope)}){` +
  11. `return ${el.tag === "template"
  12. ");if
  13. ");`${el.if}");${genChildren(el, state) || "undefined"}:undefined`
  14. : genChildren(el, state) || "undefined"
  15. : genElement(el, state)
  16. }}`
  17. // key 为插槽名称,fn 为生成的函数代码
  18. return `{key:${key},fn:${fn}}`
  19. }

我们把上面例子的 $scopedSlots 打印一下,结果如下

然后上面例子中父组件最终会生成如下代码

</>复制代码

  1. with(this){
  2. // _c => createElement ; _u => resolveScopedSlots
  3. // _v => createTextVNode ; _s => toString
  4. return _c("div",
  5. { staticClass: "parent-slot" },
  6. [_c("slot-demo",
  7. { scopedSlots: _u([
  8. {
  9. key: "default",
  10. fn: function(scope) {
  11. return [
  12. _c("p", [ _v(_s(scope.text)) ]),
  13. _c("p", [ _v(_s(scope.msg)) ])
  14. ]
  15. }
  16. }])
  17. }
  18. )]
  19. )
  20. }
2、renderSlot(slot-scope)

renderSlot()

上面我们提及对于插槽 render 逻辑的时候忽略了 slot-scope 的相关逻辑,这里我们来看看这部分内容

</>复制代码

  1. export function renderSlot (
  2. name: string,
  3. fallback: ");): ");Array<VNode> {
  4. const scopedSlotFn = this.$scopedSlots[name]
  5. let nodes
  6. if (scopedSlotFn) { // scoped slot
  7. props = props || {}
  8. // ...
  9. nodes = scopedSlotFn(props) || fallback
  10. }
  11. // ...
  12. return nodes
  13. }

resolveScopedSlots()

这里我们看看 renderHelps 里面的 _u ,即 resolveScopedSlots,其逻辑如下

</>复制代码

  1. export function resolveScopedSlots (
  2. fns: ScopedSlotsData, // Array<{ key: string, fn: Function } | ScopedSlotsData>
  3. res");): { [key: string]: Function } {
  4. res = res || {}
  5. // 遍历 fns 数组,生成一个 `key 为插槽名称,value 为函数` 的对象
  6. for (let i = 0; i < fns.length; i++) {
  7. if (Array.isArray(fns[i])) {
  8. resolveScopedSlots(fns[i], res)
  9. } else {
  10. res[fns[i].key] = fns[i].fn
  11. }
  12. }
  13. return res
  14. }

genSlot

这块会对 attrsv-bind 进行,对于这块内容上面我已经提过了,要看请往上翻阅。结合我们的例子,子组件则会生成以下代码

</>复制代码

  1. with(this) {
  2. return _c(
  3. "div",
  4. {
  5. staticClass: "slot-demo"
  6. },
  7. [
  8. _t("default", null, { text: "this is a slot demo , ", msg: msg })
  9. ]
  10. )
  11. }

到目前为止,对于普通插槽和作用域插槽已经谈的差不多了。接下来,我们将一起看看 vue 2.6.x 版本的 v-slot

三、v-slot 1、基本用法

vue 2.6.x 已经出来有一段时间了,其中对于插槽这块则是放弃了 slot-scope 作用域插槽推荐写法,直接改成了 v-slot 指令形式的推荐写法(当然这只是个语法糖而已)。下面我们将仔细谈谈 v-slot 这块的内容。

在看具体实现逻辑前,我们先通过一个例子来先了解下其基本用法

</>复制代码

  1. <template>
  2. <div class="slot-demo">
  3. <slot name="demo">this is demo slot.slot>
  4. <slot text="this is a slot demo , " :msg="msg">slot>
  5. div>
  6. template>
  7. <script>
  8. export default {
  9. name: "SlotDemo",
  10. data () {
  11. return {
  12. msg: "this is scoped slot content."
  13. }
  14. }
  15. }
  16. script>

然后进行使用

</>复制代码

  1. <template>
  2. <slot-demo>
  3. <template v-slot:demo>this is custom slot.template>
  4. <template v-slot="scope">
  5. <p>{{ scope.text }}{{ scope.msg }}p>
  6. template>
  7. slot-demo>
  8. template>

页面展示效果如下

看着好 easy 。

2、相同与区别

接下来,咱来会会这个新特性

round 1. $slots & $scopedSlots

$slots 这块逻辑没变,还是沿用的以前的代码

</>复制代码

  1. // $slots
  2. const options = vm.$options
  3. const parentVnode = vm.$vnode = options._parentVnode
  4. const renderContext = parentVnode && parentVnode.context
  5. vm.$slots = resolveSlots(options._renderChildren, renderContext)

$scopedSlots 这块则进行了改造,执行了 normalizeScopedSlots() 并接收其返回值为 $scopedSlots 的值

</>复制代码

  1. if (_parentVnode) {
  2. vm.$scopedSlots = normalizeScopedSlots(
  3. _parentVnode.data.scopedSlots,
  4. vm.$slots,
  5. vm.$scopedSlots
  6. )
  7. }

接着,我们来会一会 normalizeScopedSlots ,首先我们先看看它的几个参数

</>复制代码

  1. export function normalizeScopedSlots (
  2. slots: { [key: string]: Function } | void, // 某节点 data 属性上 scopedSlots
  3. normalSlots: { [key: string]: Array }, // 当前节点下的普通插槽
  4. prevSlots");// 当前节点下的特殊插槽
  5. ): any {}

首先,如果 slots 参数不存在,则直接返回一个空对象 {}

</>复制代码

  1. if (!slots) {
  2. res = {}
  3. }

prevSlots 存在,且满足系列条件的情况,则直接返回 prevSlots

</>复制代码

  1. const hasNormalSlots = Object.keys(normalSlots).length > 0 // 是否拥有普通插槽
  2. const isStable = slots ");// slots 上的 $stable
  3. const key = slots && slots.$key // slots 上的 $key 值
  4. else if (
  5. isStable &&
  6. prevSlots &&
  7. prevSlots !== emptyObject &&
  8. key === prevSlots.$key && // slots $key 值与 prevSlots $key 相等
  9. !hasNormalSlots && // slots 中没有普通插槽
  10. !prevSlots.$hasNormal // prevSlots 中没有普通插槽
  11. ) {
  12. return prevSlots
  13. }

注:这里的 $key , $hasNormal , $stable 是直接使用 vue 内部对 Object.defineProperty 封装好的 def() 方法进行赋值的

</>复制代码

  1. def(res, "$stable", isStable)
  2. def(res, "$key", key)
  3. def(res, "$hasNormal", hasNormalSlots)

否则,则对 slots 对象进行遍历,操作 normalSlots ,赋值给 keykeyvaluenormalizeScopedSlot 返回的函数 的对象 res

</>复制代码

  1. let res
  2. else {
  3. res = {}
  4. for (const key in slots) {
  5. if (slots[key] && key[0] !== "$") {
  6. res[key] = normalizeScopedSlot(normalSlots, key, slots[key])
  7. }
  8. }
  9. }

随后再次对 normalSlots 进行遍历,若 normalSlots 中的 keyres 找不到对应的 key,则直接进行 proxyNormalSlot 代理操作,将 normalSlots 中的 slot 挂载到 res 对象上

</>复制代码

  1. for (const key in normalSlots) {
  2. if (!(key in res)) {
  3. res[key] = proxyNormalSlot(normalSlots, key)
  4. }
  5. }
  6. function proxyNormalSlot(slots, key) {
  7. return () => slots[key]
  8. }

接着,我们看看 normalizeScopedSlot() 都做了些什么事情。该方法接收三个参数,第一个参数为 normalSlots,第二个参数为 key,第三个参数为 fn

</>复制代码

  1. function normalizeScopedSlot(normalSlots, key, fn) {
  2. const normalized = function () {
  3. // 若参数为多个,则直接使用 arguments 作为 fn 的参数,否则直接传空对象作为 fn 的参数
  4. let res = arguments.length ");null, arguments) : fn({})
  5. // fn 执行返回的 res 不是数组,则是单 vnode 的情况,赋值为 [res] 即可
  6. // 否则执行 normalizeChildren 操作,这块主要对针对 slot 中存在 v-for 操作
  7. res = res && typeof res === "object" && !Array.isArray(res)
  8. ");// single vnode
  9. : normalizeChildren(res)
  10. return res && (
  11. res.length === 0 ||
  12. (res.length === 1 && res[0].isComment) // slot 上 v-if 相关处理
  13. ) ");undefined
  14. : res
  15. }
  16. // v-slot 语法糖处理
  17. if (fn.proxy) {
  18. Object.defineProperty(normalSlots, key, {
  19. get: normalized,
  20. enumerable: true,
  21. configurable: true
  22. })
  23. }
  24. return normalized
  25. }

round 2. renderSlot

这块逻辑处理其实和之前是一样的,只是删除了一些警告的代码而已。这点这里就不展开叙述了

round 3. processSlot

首先,这里解析 slot 的方法名从 processSlot 变成了 processSlotContent,但其实前面的逻辑和以前是一样的。只是新增了一些对于 v-slot 的逻辑处理,下面我们就来捋捋这块。过具体逻辑前,我们先看一些相关的正则和方法

1、相关正则 & functions

dynamicArgRE 动态参数匹配

</>复制代码

  1. const dynamicArgRE = /^[.*]$/ // 匹配到 "[]" 则为 true,如 "[ item ]"

slotRE 匹配 v-slot 语法相关正则

</>复制代码

  1. const slotRE = /^v-slot(:|$)|^#/ // 匹配到 "v-slot""v-slot:" 则为 true

getAndRemoveAttrByRegex 通过正则匹配绑定的 attr

</>复制代码

  1. export function getAndRemoveAttrByRegex (
  2. el: ASTElement,
  3. name: RegExp //
  4. ) {
  5. const list = el.attrsList // attrsList 类型为 Array
  6. // 对 attrsList 进行遍历,若有满足 RegExp 的则直接返回当前对应的 attr
  7. // 若参数 name 传进来的是 slotRE = /^v-slot(:|$)|^#/
  8. // 那么匹配到 "v-slot" 或者 "v-slot:xxx" 则会返回其对应的 attr
  9. for (let i = 0, l = list.length; i < l; i++) {
  10. const attr = list[i]
  11. if (name.test(attr.name)) {
  12. list.splice(i, 1)
  13. return attr
  14. }
  15. }
  16. }

ASTAttr 接口定义

</>复制代码

  1. declare type ASTAttr = {
  2. name: string;
  3. value: any;
  4. dynamic");

createASTElement 创建 ASTElement

</>复制代码

  1. export function createASTElement (
  2. tag: string, // 标签名
  3. attrs: Array, // attrs 数组
  4. parent: ASTElement | void // 父节点
  5. ): ASTElement {
  6. return {
  7. type: 1,
  8. tag,
  9. attrsList: attrs,
  10. attrsMap: makeAttrsMap(attrs),
  11. rawAttrsMap: {},
  12. parent,
  13. children: []
  14. }
  15. }

getSlotName 获取 slotName

</>复制代码

  1. function getSlotName (binding) {
  2. // "v-slot:item" 匹配获取到 "item"
  3. let name = binding.name.replace(slotRE, "")
  4. if (!name) {
  5. if (binding.name[0] !== "#") {
  6. name = "default"
  7. } else if (process.env.NODE_ENV !== "production") {
  8. warn(
  9. `v-slot shorthand syntax requires a slot name.`,
  10. binding
  11. )
  12. }
  13. }
  14. // 返回一个 key 包含 name,dynamic 的对象
  15. // "v-slot:[item]" 匹配然后 replace 后获取到 name = "[item]"
  16. // 进而进行动态参数进行匹配 dynamicArgRE.test(name) 结果为 true
  17. return dynamicArgRE.test(name)
  18. ");name: name.slice(1, -1), dynamic: true } // 截取变量,如 "[item]" 截取后变成 "item"
  19. : { name: `"${name}"`, dynamic: false }
  20. }
2、processSlotContent

这里我们先看看 slot 对于 template 是如何处理的

</>复制代码

  1. if (el.tag === "template") {
  2. // 匹配绑定在 template 上的 v-slot 指令,这里会匹配到对应 v-slot 的 attr(类型为 ASTAttr)
  3. const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
  4. // 若 slotBinding 存在,则继续进行 slotName 的正则匹配
  5. // 随即将匹配出来的 name 赋值给 slotTarget,dynamic 赋值给 slotTargetDynamic
  6. // slotScope 赋值为 slotBinding.value 或者 "_empty_"
  7. if (slotBinding) {
  8. const { name, dynamic } = getSlotName(slotBinding)
  9. el.slotTarget = name
  10. el.slotTargetDynamic = dynamic
  11. el.slotScope = slotBinding.value || emptySlotScopeToken
  12. }
  13. }

如果不是 template,而是绑定在 component 上的话,对于 v-slot 指令和 slotName 的匹配操作是一样的,不同点在于由于这里需要将组件的 children 添加到其默认插槽中去

</>复制代码

  1. else {
  2. // v-slot on component 表示默认插槽
  3. const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
  4. // 将组件的 children 添加到其默认插槽中去
  5. if (slotBinding) {
  6. // 获取当前组件的 scopedSlots
  7. const slots = el.scopedSlots || (el.scopedSlots = {})
  8. // 匹配拿到 slotBinding 中 name,dynamic 的值
  9. const { name, dynamic } = getSlotName(slotBinding)
  10. // 获取 slots 中 key 对应匹配出来 name 的 slot
  11. // 然后再其下面创建一个标签名为 template 的 ASTElement,attrs 为空数组,parent 为当前节点
  12. const slotContainer = slots[name] = createASTElement("template", [], el)
  13. // 这里 name、dynamic 统一赋值给 slotContainer 的 slotTarget、slotTargetDynamic,而不是 el
  14. slotContainer.slotTarget = name
  15. slotContainer.slotTargetDynamic = dynamic
  16. // 将当前节点的 children 添加到 slotContainer 的 children 属性中
  17. slotContainer.children = el.children.filter((c: any) => {
  18. if (!c.slotScope) {
  19. c.parent = slotContainer
  20. return true
  21. }
  22. })
  23. slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
  24. // 清空当前节点的 children
  25. el.children = []
  26. el.plain = false
  27. }
  28. }

这样处理后我们就可以直接在父组件上面直接使用 v-slot 指令去获取 slot 绑定的值。举个官方例子来表现一下

Default slot with text

</>复制代码

  1. <foo>
  2. <template slot-scope="{ msg }">
  3. {{ msg }}
  4. template>
  5. foo>
  6. <foo v-slot="{ msg }">
  7. {{ msg }}
  8. foo>

Default slot with element

</>复制代码

  1. <foo>
  2. <div slot-scope="{ msg }">
  3. {{ msg }}
  4. div>
  5. foo>
  6. <foo v-slot="{ msg }">
  7. <div>
  8. {{ msg }}
  9. div>
  10. foo>

更多例子请点击 new-slot-syntax 自行查阅

round 4. generate

genSlot() 在这块逻辑也没发生本质性的改变,唯一一个改变就是为了支持 v-slot 动态参数做了些改变,具体如下

</>复制代码

  1. // old
  2. const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(",")}}`
  3. // new
  4. // attrs、dynamicAttrs 进行 concat 操作,并执行 genProps 将其转换成对应的 generate 字符串
  5. const attrs = el.attrs || el.dynamicAttrs
  6. ");attr => ({
  7. // slot props are camelized
  8. name: camelize(attr.name),
  9. value: attr.value,
  10. dynamic: attr.dynamic
  11. }))
  12. )
  13. : null
最后

文章到这,对于 普通插槽作用域插槽v-slot 基本用法以及其背后实现原理的相关内容已经是结束了,还想要深入了解的同学,可自行查阅 vue 源码进行研究。

老规矩,文章末尾打上波广告

前端交流群:731175396

群里不定期进行视频或语音的技术类分享,欢迎小伙伴吧上车,帅哥美女等着你们呢。

然后,emmm,我要去打篮球了 ~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7236.html

相关文章

  • Vue原理】Compile - 源码版 之 generate 节点数据拼接

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之节点数据拼接上一篇我们 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...

    fizz 评论0 收藏0
  • Vue 的组件

    摘要:组件上一篇的表单输入绑定使用组件注册组件首先我们要创建一个实例要注册一个全局组件,你可以使用。使用传递数据组件实例的作用域是孤立的。这意味着不能也不应该在子组件的模板内直接引用父组件的数据。 组件 上一篇:Vue的表单输入绑定:https://segmentfault.com/a/11... 使用组件 注册组件 首先我们要创建一个实例: new Vue({ el:.exp, ...

    kk_miles 评论0 收藏0
  • vue 2.6 中 slot 的新用法

    摘要:最近发布不久的,使用插槽的语法变得更加简洁。插槽可用包裹外部的标签或者组件,并允许其他或组件放在具名插槽对应名称的插槽上。在部分中,监听的变化,当发生变化时,清除状态,然后调用并,当成功完成或失败时更新状态。 为了保证的可读性,本文采用意译而非直译。 最近发布不久的Vue 2.6,使用插槽的语法变得更加简洁。 对插槽的这种改变让我对发现插槽的潜在功能感兴趣,以便为我们基于Vue的项目提...

    genedna 评论0 收藏0
  • 7个有用的Vue开发技巧

    摘要:另外需要说明的是,这里只是冻结了的值,引用不会被冻结,当我们需要数据的时候,我们可以重新给赋值。1 状态共享 随着组件的细化,就会遇到多组件状态共享的情况,Vuex当然可以解决这类问题,不过就像Vuex官方文档所说的,如果应用不够大,为避免代码繁琐冗余,最好不要使用它,今天我们介绍的是vue.js 2.6新增加的Observable API ,通过使用这个api我们可以应对一些简单的跨组件数...

    Godtoy 评论0 收藏0
  • 预计今年发布的Vue3.0到底有什么不一样的地方?

    摘要:模板语法的将保持不变。基于的观察者机制目前,的反应系统是使用的和。为了继续支持,将发布一个支持旧观察者机制和新版本的构建。 showImg(https://segmentfault.com/img/remote/1460000017862774?w=1898&h=796); 还有几个月距离vue2的首次发布就满3年了,而vue的作者尤雨溪也在去年年末发布了关于vue3.0的计划,如果不...

    fnngj 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<