Vue: [废弃] RFC:简化作用域槽的使用

创建于 2018-12-11  ·  36评论  ·  资料来源: vuejs/vue

这是https://github.com/vuejs/vue/issues/7740#issuecomment -371309357 的后续

合理的

当前作用域插槽使用的问题:

  • 如果使用<template slot-scope>
  • 仅限于一个元素/组件直接在插槽元素上使用slot-scope

提议

引入一个新的v-scope指令,它只能用于组件:

<comp v-scope="scope">
  {{ scope.msg }}
</comp>

对于<comp>的默认作用域插槽,它与slot-scope的作用相同( <comp>提供作用域值)。 所以它也适用于解构:

<comp v-scope="{ msg }">
  {{ msg }}
</comp>

为什么要制定新指令

我相信团队在一段时间前在 Slack 上的https://github.com/vuejs/vue/issues/7740#issuecomment -371309357 中简要讨论了我提出的内容,但我再也找不到聊天记录了。 以下是新指令背后的原因:

  • slot-scope被引入作为特殊属性而不是指令(以v-前缀开头的属性),因为slot是一个属性,我们希望保持与插槽相关的属性一致. slot又作为一个非指令属性引入,因为我们希望该用法反映 Shadow DOM 标准中的实际插槽用法。 我们认为,当标准中存在概念上相同的内容时,最好避免使用我们自己的并行v-slot

  • 最初, slot-scope被设计为仅可用于充当抽象容器的<template>元素。 但这很冗长 - 所以我们引入了直接在插槽元素上使用它的能力,而无需包装<template> 。 然而,这也使得它不可能允许使用slot-scope直接组件本身上,因为如图所示这会导致歧义这里

  • 我考虑在slot-scope添加修饰符或特殊前缀,以便我们可以直接在组件上使用它来指示其插槽内容应视为默认范围插槽,但修饰符或前缀如$似乎是合适的。 设计修饰符应该只应用于指令,而单个用例的新特殊语法与整个语法设计不一致。

  • 很长一段时间以来,我们一直避免添加更多指令,其中一部分是模板语法是我们希望尽可能保持稳定的部分,另一部分是我们希望将核心指令保持在最低限度并且只做一些事情用户在用户空间中无法轻松做到的。 但是,在这种情况下,作用域插槽的使用已经足够重要了,我认为可以使用新指令来显着降低其使用的噪音。

顾虑

  • v-scope接受的表达式与大多数其他指令不同:它需要一个临时变量名(也可以是解构),但并非没有优先级:它的作用就像v-for的别名部分v-scopev-for v-scope属于同一阵营,作为为其内部作用域创建临时变量的结构指令。

  • 如果用户有一个名为v-scope的自定义指令并在组件上使用,这将破坏用户代码。

    • 由于 v2 中的自定义指令主要专注于直接 DOM 操作,因此在组件上使用自定义指令的情况相对较少,对于碰巧称为v-scope更是如此,因此影响应该很小。

    • 即使在实际发生的情况下,通过简单地重命名自定义指令来处理也很简单。

discussion intend to implement

最有用的评论

分解问题

尝试综合,听起来我们想要一个解决方案:

  • 减少从作用域插槽访问数据所需的样板
  • 避免暗示默认插槽的数据在命名插槽中可用
  • 尽量减少我们必须引入的新 API 的数量
  • 不要在 Web 组件规范已经有可接受的解决方案的情况下重新发明 API
  • 保持明确,以避免混淆数据来自哪里

我可能有解决所有这些问题的解决方案! 🤞 使用$event表示v-on ,我们有一个先例,它提供了在隐式函数中运行的带有命名参数的表达式。 人们似乎很享受这种便利,而且我没有看到由它引起的很多混乱,所以也许我们应该按照这个例子来处理作用域插槽。

建议的解决方案

我们可以不使用v-scope ,而是将槽范围设为$slot 例如:

<TodoList :todos="todos">
  {{ $slot.todo.text }}

  <template slot="metaBar">
    You have {{ $slot.totals.incomplete }} todos left.
  </template>
</TodoList>

对于上下文,子模板可能如下所示:

<ul>
  <li v-for="todo in todos">
    <slot :todo="todo" />
  </li>
</ul>

<div>
  <slot name="metaBar" v-bind="itemsStats" />
</div>

当插槽嵌套时, $slot对象将合并插槽数据,最里面的插槽具有覆盖优先级。 例如,在:

<Outer>
  <Middle>
    <Inner>
      {{ $slot }}
    </Inner>
  </Middle>
</Outer>

合并可能如下所示:

$slot = {
  ...outerSlotData,
  ...middleSlotData,
  ...innerSlotData
}

您可能担心/想知道如何处理命名空间重叠,在 99.9% 的情况下,我真的不认为这会成为问题。 例如:

<UserData id="chrisvfritz">
  My email is {{ $slot.user.email }}, but you can reach Sarah at
  <UserData id="sdras">
    {{ $slot.user.email }}
  </UserData>.
</UserData>

在上面的示例中,尽管存在嵌套作用域, $slot.user将始终具有正确的值。 但是,有时您确实需要同时访问这两个属性,例如:

<UserData id="chrisvfritz">
  <template slot-scope="me">
    <UserData id="sdras">
      <template slot-scope="sarah">
        <CollaborationPageLink :user-ids="[me.user.id, sarah.user.id]">
          View your collaboration history
        </CollaborationPageLink>
      </template>
    </UserData>
  </template>
</UserData>

对于这些罕见的边缘情况,用户仍然可以使用slot-scope及其当前行为作为逃生舱口。 但是v-scope仍然是不必要的,因为如果我理解正确的话,它的目的是简化最常见的用例, $slot对象已经完成了这些用例而不会引起相同的问题。

好处

  • 当用户想要访问插槽提供的数据时,唯一的样板文件是$slot ,它在保持明确的同时

  • 要知道他们可以访问哪些数据,用户只需考虑内容将呈现到哪个插槽。 我认为对于v-scope ,假设它像v-for一样工作,人们会感到很困惑,因为这是定义范围上下文属性的唯一其他指令。

  • 用户不必熟悉和适应解构来使用作用域槽,从而减少学习曲线。

  • 在某些情况下,尤其是在许多嵌套插槽都提供状态的情况下,某些状态来自_哪个_组件并不明显,用户将不得不再次到达slot-scope 。 这听起来像是一个缺点,但是当我看到嵌套的作用域插槽时,它几乎总是针对状态提供者模式,我强烈认为这是一种反模式。 这是我的推理。 因此,许多嵌套作用域插槽需要更多样板的事实实际上可以减少反模式的使用

  • API 表面积大大减少,因为大多数 Vue 开发人员只需要记住$slot ,它只是一个对象。

  • 通过使用$前缀属性,我们正在构建 Web 组件插槽 API,以明确$slot是 Vue 的东西。

  • 目前,图书馆经常做一些类似<slot v-bind="user" />事情,这样他们的用户可以用slot-scope="user"而不是slot-scope="{ user }来保存几个字符。 乍一看似乎很优雅,但我已经体验到了一种反模式。 当组件想要向用户公开数据以外的数据时,就会出现问题。 然后他们有两个选择:对他们的 API 进行重大更改或将这个新属性强加到user对象上,即使它可能与用户几乎没有关系。 两者都不是一个很好的选择。 幸运的是, $slot将消除使组件不那么面向未来的诱惑,因为虽然$slot仍然比$slot.user短,但您将用户别名为$slot会丢失重要的上下文

缺点

  • 在一些嵌套的作用域插槽中,存在一些边缘情况,其中某些数据来自 _which_ 组件并不明显。 但是,在这些情况下,当用户需要最大程度的明确性时,他们仍然可以达到slot-scope ,所以我认为这没什么大不了的。 另外,正如我之前提到的,我认为如果用户首先遇到这个问题,他们很可能会用状态提供程序组件来攻击自己。

所有36条评论

看起来挺好的。 我正在考虑使用参数来提供插槽名称,但这将允许在同一组件上使用多个指令 v 范围,从而重用提供给组件的内容。 我不确定它是否有用或是否会导致问题

Overral,这将改善无渲染组件的 api 使用,随着时间的推移,这些组件越来越多地使用🙌

如果我理解正确, v-scope仅用于一个用例。 而不是写:

<foo>
  <template slot-scope="{ item }">{{ item.id }}</template>
</foo>

我们可以写:

<foo v-scope="{ item }">{{ item.id }}</foo>

在这种情况下它确实减少了很多噪音,但这似乎是唯一的情况。 引入一个新指令感觉有点矫枉过正(它的工作方式与其他指令完全不同,甚至与v-for因为它只适用于组件并且依赖于下面的<slot>逻辑)。 另一个问题是,当我在这个 repo 中搜索v-scope,只有两次都在讨论减少模板子树的数据范围(https://github.com/vuejs/vue/issues/5269 #issuecomment-288912328, https://github.com/vuejs/vue/issues/6913),就像with在 JavaScript 中的工作方式。

我喜欢。 剩下的一个问题是我们要如何处理命名槽。 如果我们想允许命名槽使用相同的指令,我们也需要在<template>上允许它:

<comp v-scope="scope">
  {{ scope.msg }} <!-- default slot, inheriting from the scope from above -->
  <template slot="someName" v-scope="otherScope">
    <p>{{ otherScope }}</p>
    <div> whatever</div>
  </template>
</comp>

我们可以利用这个机会弃用sot-scope并在 Vue 3 中将其删除,将其替换为v-scope

我认为我们绝对不应该为同一件事使用两个不同的概念( v-scope指令和slot-scope属性)。

旁注:现在,看看那个,人们可能会从元素的层次结构中得到印象 &
指令,他们可以在命名槽中访问otherScopescope 。 可能是一个缺点。

@LinusBorg作为名称,参数可以做v-scope:slotName 。 我认为只允许在组件上这样做的目的是替换

旁注:现在,看看那个,人们可能会从元素的层次结构中得到印象 &
指令,它们可以访问指定插槽中的 otherScope 和范围。 可能是一个缺点。

那行得通,对吧? 🤔 我很确定我已经嵌套了槽作用域

那行得通,对吧? 🤔 我很确定我已经嵌套了槽作用域

不过,我没有嵌套它们,它们是同级插槽。 我在组件上使用v-scope定义了默认插槽的范围,并使用<template slot="someName">了命名插槽(这是一个兄弟插槽)
看? 很纠结^^

@LinusBorg我认为使用会令人困惑。 我认为概念上是:

  • v-scope直接在组件上使用时,这意味着您只使用默认插槽。
  • 如果你想使用命名插槽......你仍然需要使用slot + slot-scope

我同意同时拥有v-scopeslot-scope可能会不一致/令人困惑,特别是对于不知道我们如何得出当前设计的新用户。

分解问题

尝试综合,听起来我们想要一个解决方案:

  • 减少从作用域插槽访问数据所需的样板
  • 避免暗示默认插槽的数据在命名插槽中可用
  • 尽量减少我们必须引入的新 API 的数量
  • 不要在 Web 组件规范已经有可接受的解决方案的情况下重新发明 API
  • 保持明确,以避免混淆数据来自哪里

我可能有解决所有这些问题的解决方案! 🤞 使用$event表示v-on ,我们有一个先例,它提供了在隐式函数中运行的带有命名参数的表达式。 人们似乎很享受这种便利,而且我没有看到由它引起的很多混乱,所以也许我们应该按照这个例子来处理作用域插槽。

建议的解决方案

我们可以不使用v-scope ,而是将槽范围设为$slot 例如:

<TodoList :todos="todos">
  {{ $slot.todo.text }}

  <template slot="metaBar">
    You have {{ $slot.totals.incomplete }} todos left.
  </template>
</TodoList>

对于上下文,子模板可能如下所示:

<ul>
  <li v-for="todo in todos">
    <slot :todo="todo" />
  </li>
</ul>

<div>
  <slot name="metaBar" v-bind="itemsStats" />
</div>

当插槽嵌套时, $slot对象将合并插槽数据,最里面的插槽具有覆盖优先级。 例如,在:

<Outer>
  <Middle>
    <Inner>
      {{ $slot }}
    </Inner>
  </Middle>
</Outer>

合并可能如下所示:

$slot = {
  ...outerSlotData,
  ...middleSlotData,
  ...innerSlotData
}

您可能担心/想知道如何处理命名空间重叠,在 99.9% 的情况下,我真的不认为这会成为问题。 例如:

<UserData id="chrisvfritz">
  My email is {{ $slot.user.email }}, but you can reach Sarah at
  <UserData id="sdras">
    {{ $slot.user.email }}
  </UserData>.
</UserData>

在上面的示例中,尽管存在嵌套作用域, $slot.user将始终具有正确的值。 但是,有时您确实需要同时访问这两个属性,例如:

<UserData id="chrisvfritz">
  <template slot-scope="me">
    <UserData id="sdras">
      <template slot-scope="sarah">
        <CollaborationPageLink :user-ids="[me.user.id, sarah.user.id]">
          View your collaboration history
        </CollaborationPageLink>
      </template>
    </UserData>
  </template>
</UserData>

对于这些罕见的边缘情况,用户仍然可以使用slot-scope及其当前行为作为逃生舱口。 但是v-scope仍然是不必要的,因为如果我理解正确的话,它的目的是简化最常见的用例, $slot对象已经完成了这些用例而不会引起相同的问题。

好处

  • 当用户想要访问插槽提供的数据时,唯一的样板文件是$slot ,它在保持明确的同时

  • 要知道他们可以访问哪些数据,用户只需考虑内容将呈现到哪个插槽。 我认为对于v-scope ,假设它像v-for一样工作,人们会感到很困惑,因为这是定义范围上下文属性的唯一其他指令。

  • 用户不必熟悉和适应解构来使用作用域槽,从而减少学习曲线。

  • 在某些情况下,尤其是在许多嵌套插槽都提供状态的情况下,某些状态来自_哪个_组件并不明显,用户将不得不再次到达slot-scope 。 这听起来像是一个缺点,但是当我看到嵌套的作用域插槽时,它几乎总是针对状态提供者模式,我强烈认为这是一种反模式。 这是我的推理。 因此,许多嵌套作用域插槽需要更多样板的事实实际上可以减少反模式的使用

  • API 表面积大大减少,因为大多数 Vue 开发人员只需要记住$slot ,它只是一个对象。

  • 通过使用$前缀属性,我们正在构建 Web 组件插槽 API,以明确$slot是 Vue 的东西。

  • 目前,图书馆经常做一些类似<slot v-bind="user" />事情,这样他们的用户可以用slot-scope="user"而不是slot-scope="{ user }来保存几个字符。 乍一看似乎很优雅,但我已经体验到了一种反模式。 当组件想要向用户公开数据以外的数据时,就会出现问题。 然后他们有两个选择:对他们的 API 进行重大更改或将这个新属性强加到user对象上,即使它可能与用户几乎没有关系。 两者都不是一个很好的选择。 幸运的是, $slot将消除使组件不那么面向未来的诱惑,因为虽然$slot仍然比$slot.user短,但您将用户别名为$slot会丢失重要的上下文

缺点

  • 在一些嵌套的作用域插槽中,存在一些边缘情况,其中某些数据来自 _which_ 组件并不明显。 但是,在这些情况下,当用户需要最大程度的明确性时,他们仍然可以达到slot-scope ,所以我认为这没什么大不了的。 另外,正如我之前提到的,我认为如果用户首先遇到这个问题,他们很可能会用状态提供程序组件来攻击自己。

我认为$ -prefixed 属性在整个 Vue 实例中应该是一致的。 我不确定通过将$slot隐式注入每个插槽是否会引起混淆,并且它们实际上代表这些插槽范围内的本地参数。 这是一种约定(根据我的观察) $ -prefixed 属性代表每个 Vue 实例可用的东西,因此它们可以在任何地方使用,包括生命周期钩子、方法和渲染函数。 添加这个特殊的$slot将打破这个约定。

@chrisvfritz这是一个有趣的提议,但它有点依赖于统一的普通插槽和作用域插槽(所以也许可以在 v3 中考虑)。 最大的问题是插槽内容是否应该在子组件的$slots$scopedSlots可用? 唯一的提示是$slot于表达式中的某处(可以在树下的任何位置),它不像slot-scope (只能在槽根)那样明确。 虽然从技术上讲我们可以在编译器中检测到这一点,但对于用户来说,每当用户开始在模板中使用$slot ,插槽就会从$slots$scopedSlots ......

这会对 JSX 用户产生重大影响吗?

@donnysim不,这不会以任何方式影响 JSX。

我认为$ -prefixed 属性在整个 Vue 实例中应该是一致的。 我不确定通过将$slot隐式注入每个插槽是否会引起混淆,并且它们实际上代表这些插槽范围内的本地参数。

@Justineo 一开始我也有同样的想法,但我们这样做是为了$event并且似乎没有人抱怨,所以 _is_ 已经有先例了,我们有证据表明用户通常不会感到困惑并且真的很享受这种便利。

最大的问题是插槽内容是否应该在子组件的$slots$scopedSlots可用?

@yyx990803好问题! 我有一个可能可行的想法。 它是暧昧(使用嵌套插槽如果在案件$slot ),我们只是编译所有插槽,一个范围的插槽,但也使所有$scopedSlots下可用$slots吸气剂? 例如,类似于:

for (const slotName in vm.$scopedSlots) {
  // Don't override existing slots of the same name,
  // since that's _technically_ possible right now.
  if (vm.$slots[slotName]) continue

  Object.defineProperty(vm.$slots, slotName, {
    get: vm.$scopedSlots[slotName],
    enumerable: true,
    configurable: true,
    writable: true
  })
}

这样,在以下情况下:

<A>
  <B>
    {{ $slot.foo }}
  </B>
</A>

我们可以编译为:

// Assume new render helper:
// _r = mergeSlotData

_c('A', {
  scopedSlots: _u([
    {
      key: 'default',
      fn: function(_sd) {
        var $slot = _r($slot, _sd)
        return [
          _c('B', {
            scopedSlots: _u([
              {
                key: 'default',
                fn: function(_sd) {
                  var $slot = _r($slot, _sd)
                  return [_v(_s($slot.foo))]
                },
              },
            ]),
          }),
        ]
      },
    },
  ]),
}) 

foo来自<A>还是<B>并不重要,因为在这些组件中this.$slots.defaultthis.$scopedSlots.default(someData)都可以使用。

几个警告:

  • 在嵌套插槽的情况下,并且只有其中一些实际作用域,使用$slot模板编译的渲染函数将比使用slot-scope略大。 虽然不是很明显,所以我认为还可以。

  • 我想不出一个真实的例子,但可能存在一些边缘情况,即用户迭代$slots / $scopedSlots并且拥有新的或重复的属性可能会导致意外行为。 我之前已经迭代了动态命名的$slots$scopedSlots以启用一些有趣的模式,但是这种更改不会影响我遇到的任何用例。

想法?

我们为$event这样做,似乎没有人抱怨

@chrisvfritz哦,我确实错过了$event事情。 尽管这显然是惯例的一个例外(我过去认为它是😅),但$event仅在非常有限的范围内可用(仅在 v-bind 属性文字内且无嵌套)。

并在$scopedSlots上代理$slots

我曾经认为插槽和作用域插槽是不同的概念,Vue 对它们有不同的用途,这意味着我可以让插槽和作用域插槽共享相同的名称但用于不同的目的。 但是后来我发现当指定的作用域插槽不可用时,Vue 实际上回退到同名插槽。 对我来说,这意味着我们应该将它们视为同一件事,但由于我们在每个实例上都有$slots$scopedSlots可用,我们总是可以以更灵活/意想不到的方式在渲染函数中使用它们,就像使用默认插槽覆盖所有列表项和使用默认范围插槽覆盖单个项目一样。 这实际上是不被鼓励的(我们没有在我们的文档和样式指南 AFAIK 中建议对此的推荐用法),因为我们可能会在 3.0 中将它们合并为一个单一的插槽概念,但在 2.x 中这样做会中断很久以前就允许使用一些用法。

我们很可能在 3.0 中将它们合并为一个单一的插槽概念,但是在 2.x 中这样做会破坏很久以前允许的一些用法。

@Justineo我可能会误解,但我并不建议将它们合并到 2.x 中——只是扩展当前的回退行为。 您描述的用例,其中$slots$scopedSlots使用相同的命名空间,仍然是可能的,因为slot-scope的行为将保持不变。 例如:

<div>
  Default slot
  <template slot-scope="foo">
    Default scoped slot {{ foo }}
  </template>
</div>

仍然会完全一样。 那有意义吗?

@克里斯夫弗里茨

我不是在谈论在 2.x 中合并它们。 我是说如果你这样做:

使$slots下的所有$scopedSlots $slots作为 getter 可用

您无法将$slots.foo$scopedSlots.foo分开来,并不再在渲染函数中单独使用它们。

你不能区分 $slots.foo 和 $scopedSlots.foo 并在渲染函数中单独使用它们。

老实说,如果所有东西(插槽和 scopedSlots)都可以作为函数在 scopedSlots 中访问,我会很高兴,这只是意味着不再无用地检查要渲染的插槽。 从组件的角度来看,开发人员使用什么类型的插槽实际上没有区别。 来自混合 JSX 和模板语法用户。

@donnysim是的,我同意你的意见,他们应该合并。 我已经在https://github.com/vuejs/vue/issues/9180#issuecomment -447185512 中解释了为什么我有这样的担忧。 这是关于向后兼容性。

您无法将$slots.foo$scopedSlots.foo分开来,并不再在渲染函数中单独使用它们。

@Justineo你 _can_ 实际上,只要我们先处理$slots并且不替换已经定义的插槽。 请参阅我上面发布的示例实现:

for (const slotName in vm.$scopedSlots) {
  // Don't override existing slots of the same name,
  // since that's _technically_ possible right now.
  if (vm.$slots[slotName]) continue

  Object.defineProperty(vm.$slots, slotName, {
    get: vm.$scopedSlots[slotName],
    enumerable: true,
    configurable: true,
    writable: true
  })
}

@克里斯夫弗里茨

考虑以下示例:

render (h) {
  if (!this.$slots.default) {
    return h(
      'div',
      this.items.map(item => this.$scopedSlots.default(item))
    )
  }
  return h('div', this.$slots.default)
}

如果组件用户只传入vm.$scopedSlots.default ,它应该遍历this.items并将item s 渲染到作用域槽中。 但是现在vm.$scopedSlot.default现在是作用域槽并且它存在,将调用作用域槽而不是槽。

@Justineo我认为这种用例非常罕见,但通过更改模式仍然可以实现:

if (!this.$slots.default) {

到:

if (this.$scopedSlots.default) {

或者,如果用户有时会提供一个由于某种原因应该被忽略的作用域插槽:

if (!Object.getOwnPropertyDescriptor(this.$slots, 'default').value) {

尽管如此,我仍然不会认为这是一个重大变化。 首先,我们从未记录/推荐使用同名的插槽和作用域插槽,因此它从来不是公共合同的一部分。 其次,正如您提到的,模板中的作用域插槽已经回退到同名的非作用域插槽,这是很好的历史证据,我们从未打算像这样使用作用域/非作用域边界。

那有意义吗? 另外,您是否见过重用插槽名称的任何实际用例? 或者你能想到什么? 我不能,但是如果它启用了_are_有用且独特的模式,那么现在了解它们会很好,因为它也会影响我们在 Vue 3 中合并它们的决定。

我认为作用域插槽和用于不同目的的同名插槽是
一个坏主意。 我在 Vue 承诺的 pre 版本中使用了它并删除了它
因为这很混乱。 现在我必须检查两个默认插槽
和作用域插槽,因此开发人员可以在不消耗数据的情况下提供插槽。
如果我没记错的话,使用时不能有相同的名称
模板。

@克里斯夫弗里茨

我并不是说这种模式有帮助,应该得到支持。 它确实会引起混乱。 我只是说,破坏现有代码是可能的。 我们从未记录过在插槽和作用域插槽之间共享相同的名称,但我们从未建议用户不要这样做。 模板中的回退逻辑没有记录。 这可能不是故意的,但至少我自己曾经相信这是一种合法的用法,直到我学会了模板中的回退逻辑......

@posva感谢分享您的经验!

@Justineo在您看来,您是否觉得这种边缘情况会给用户带来太多痛苦,还是为了改进 Vue 2.x 中的 scoped slot API 的便利性可以接受?

这已移至“Todo” - 我们是否就要实施的内容达成共识?

过去一周我一直在思考这个问题。 我喜欢@chrisvfritz$slot变量提案,并试图解决实施障碍。 这是我认为可行的:

  • 所有插槽都编译为函数(如在 3.0 中),因此不再有静态插槽(这会导致更准确的组件更新跟踪)
  • $scopedSlots将每个插槽公开为一个函数
  • $slots将公开每个插槽,作为使用空对象调用相应函数的结果。

因此,以下内容是等效的:

this.$slots.default
// would be the same as
this.$scopedSlots.default({} /* $slot */)

通过上述内部更改, $slots$scopedSlots本质上是统一的,但应该向后兼容! 它还应该导致更容易迁移到 3.0。

使用建议的$slot变量的一些用法示例:

<!-- list and item composition -->
<fancy-list>
  <fancy-item :item="$slot.item"/>
</fancy-list>
<!-- hypothetical data fetching component -->
<fetch :url="`/api/posts/${id}`">
  <div slot="success">{{ $slot.data }}</div>
  <div slot="error">{{ $slot.error }}</div>
  <div slot="pending">pending...</div>
</fetch>

我认为合并嵌套范围不是一个好主意。 我认为在涉及嵌套时最好是明确的(即,如果您有嵌套的作用域插槽,请始终使用slot-scope ):

<!-- nested usage -->
<foo>
  <bar slot-scope="foo">
    <baz slot-scope="bar">
      {{ foo }} {{ bar }} {{ $slot }}
    </baz>
  </bar>
</foo>

在上一个示例中, $slot指的是哪个插槽?

@Akryum{ ...foo, ...bar }

@Akryum @Justineo嗯,我可以说这可能令人困惑...我们使slot-scope可用于插槽foo<foo/>提供的插槽范围bar<bar/>$slot<baz/> ...现在我认为允许这种用法是错误的,因为在这种情况下更直观的用法是这样的:

<!-- nested usage -->
<foo slot-scope="foo">
  <bar slot-scope="bar">
    <baz slot-scope="baz">
      {{ foo }} {{ bar }} {{ baz }}
    </baz>
  </bar>
</foo>

这可能令人困惑

在这种情况下更直观的用法是这样的

绝对同意。 此外,我们可以继续使用解构。
例如:
``` vue

<FieldValidation field="login" slot-scope="{ value, setValue, error }">
  <LoginInput
    :value="value"
    :error="error"
    @input="setValue"
  />
</FieldValidation>

<FieldValidation field="password" slot-scope="{ value, setValue, error }">
  <PasswordInput
    :value="value"
    :error="error"
    @input="setValue"
  />
</FieldValidation>

通过 7988a554 和 5d52262f 关闭

概括:

  • 在所有插槽中支持$slot变量。 ( $slot导致插槽被编译为作用域插槽)。

  • 所有插槽,包括普通插槽,现在都作为函数公开在this.$scopedSlots 。 这意味着假设this.$slots.default存在, this.$scopedSlots.default()将返回其值。 这允许渲染函数用户始终使用this.$scopedSlot并且不再担心传入的插槽是否有作用域。 这也与 3.0 一致,其中所有插槽都公开为函数(但在this.$slots )。

  • slot-scope用法没有变化,没有引入新指令。 我们希望将语法更改降至最低,并将潜在的破坏留给 3.0。

@yyx990803 $slot合并所有外部插槽作用域,还是仅引用最近的插槽?

@Justineo没有合并,只是最近的。

查看我有权访问的应用程序中的作用域插槽使用情况,似乎$slot简写在没有嵌套插槽合并行为的情况下实际上无法在超过一半的情况下使用。 问题是基本组件通常用于代替原始 HTML 元素,并且这些组件通常有自己的插槽。 例如:

<MapMarkers :markers="cities">
  <BaseIcon name="map-marker">
    {{ $slot.marker.name }}
  </BaseIcon>
</MapMarkers>

在上面的例子中,只有一个作用域槽,用于<MapMarkers>组件。 <BaseIcon>接受一个非作用域的插槽,但因为它完全接受一个插槽, $slot将是未定义的或没有合并行为的空对象,迫使重构变得笨拙:

<MapMarkers :markers="cities">
  <template slot-scope="{ marker }">
    <BaseIcon name="map-marker">
      {{ marker.name }}
    </BaseIcon>
  </template>
</MapMarkers>

所以这是我的担忧:

  • 至少从我自己的抽样来看,如果$slot的好处只涉及最里面的插槽,则它的 _most_ 可能会丢失。 事实上,它甚至可能弊大于利,因为有更多的 API 可供人们学习。

  • UI 库可能会开始过度使用带有v-html props,其中插槽更合适,以响应用户关于无法使用$slot抱怨。

  • 在某些情况下,用户可能会开始避免使用基础组件,以使作用域插槽的使用更加优雅,从而导致代码的可维护性降低。

@yyx990803您对$slot合并的主要担忧是否很难分辨哪个组件插槽数据来自? 如果是这样,了解我有权访问使用嵌套作用域插槽的应用程序的唯一情况是状态提供程序组件,正如我之前提到的,我发现这是一种反模式,这可能会有所帮助。 因此,特别是对于命名良好的插槽属性,我认为在合法用例中实际含糊不清的情况非常罕见。 并且通过自动合并行为,用户将能够自己决定是否需要更多的明确性——当他们这样做时,这就是slot-scope存在的意义。

不过,作为一种妥协,这里有一个潜在的替代方案:如果不是合并嵌套的作用域插槽,我们让$slot引用提供作用域的最近插槽 _ 会怎样? 这可以消除最模糊的案例,同时仍然解决我的担忧。 想法?

避免合并的主要原因是不明确:仅通过阅读模板,您真的不知道哪个$slot属性来自哪个提供程序组件。 这需要您充分了解您正在查看的每个组件的实现细节,以确保正在发生的事情,这会增加很多心理负担。 在您编写它的那一刻听起来可能没问题,因为当时您的脑海中已经有了完整的上下文,但这会使未来的读者(无论是您还是其他团队成员)更难以理解代码。

$slot 指提供作用域的最近插槽

我不认为那会奏效。 它会导致同样的问题:您需要了解组件的实现细节以确保发生了什么。 我会说它比合并更加隐含和潜在的混乱。


从长远来看,我认为最好的解决方案是更改slot-scope的语义,使其成为别名/解构$slot 。 所以你的例子看起来像:

<MapMarkers :markers="cities" slot-scope="{ marker }">
  <BaseIcon name="map-marker">
    {{ marker.name }}
  </BaseIcon>
</MapMarkers>

这消除了合并的需要,明确说明哪些变量来自哪个提供者,并且不会过于冗长。 (我认为这就是我们可能会在 3.0 中做的事情)

我们现在尴尬的地方是我们允许在非模板上使用slot-scope ,而现在这阻止了我们在组件本身上使用它。

在 3.0 中更改其语义也可能导致很多混乱和迁移痛苦。

也许我们可以通过引入一个新属性slot-alias来避免 while 问题,它完全符合它所说的 - 将$slot别名slot-scope ,它只能用在组件本身或模板槽容器上(这确实意味着它在<template>上使用时与slot-scope工作方式完全相同) :

嵌套的默认插槽:

<MapMarkers :markers="cities" slot-alias="{ marker }">
  <BaseIcon name="map-marker">
    {{ marker.name }}
  </BaseIcon>
</MapMarkers>
<foo slot-alias="foo">
  <bar slot-alias="bar">
    <baz slot-alias="baz">
      {{ foo }} {{ bar }} {{ baz }}
    </baz>
  </bar>
</foo>

没有嵌套的命名插槽:

它不可避免地变得冗长的唯一情况是带有嵌套的命名插槽:

<foo>
  <template slot="a" slot-alias="a">
     <bar slot-alias="b">
        {{ a }} {{ b }}
     </bar>
  </template>
</foo>

(实际上,通过同时使用slot-scopeslot-alias可以不那么冗长,但我认为这可能会令人困惑。我赞成完全弃用slot-scope

<foo>
   <bar slot="a" slot-scope="a" slot-scope="b">
      {{ a }} {{ b }}
   </bar>
</foo>

最后,我们甚至可以给它一个简写, () (因为在 JSX 中渲染道具通常是以()开头的箭头函数):

<MapMarkers :markers="cities" ()="{ marker }">
  <BaseIcon name="map-marker">
    {{ marker.name }}
  </BaseIcon>
</MapMarkers>
<foo ()="foo">
  <bar ()="bar">
    <baz ()="baz">
      {{ foo }} {{ bar }} {{ baz }}
    </baz>
  </bar>
</foo>

将上述内容与等效的 JSX 进行比较:

<foo>
  {foo => (
    <bar>
      {bar => (
        <baz>
          {baz => `${foo} ${bar} ${baz}`}
        </baz>
      )}
    </bar>
  )}
</foo>

关闭,因为现在的设计与最初提出的设计非常不同。 而是打开新线程。

此页面是否有帮助?
0 / 5 - 0 等级