我想通过 VNode 对象为子data分配一些属性和类。那是行得通的。但是在我对Vue.js的调查中,我没有看到这种模式的使用,这就是为什么我认为修改子类VNode的好主意的原因。

但是这种方法有时会派上用场-例如,我想将aria-label属性分配给默认插槽中的所有按钮。

请参阅下面的示例,使用默认的有状态组件:

Vue.component('child', {
  template: '<div>My role is {{ $attrs.role }}</div>',
})

Vue.component('parent', {
  render(h) {
    const {
      default: defaultSlot
    } = this.$slots

    if (defaultSlot) {
      defaultSlot.forEach((child, index) => {
        if (!child.data) child.data = {}
        if (!child.data.attrs) child.data.attrs = {}

        const {
          data
        } = child

        data.attrs.role = 'button'
        data.class = 'bar'
        data.style = `color: #` + index + index + index
      })
    }

    return h(
      'div', {
        class: 'parent',
      },
      defaultSlot,
    )
  },
})

new Vue({
  el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <parent>
    <child></child>
    <child></child>
    <child></child>
    <child></child>
    <child></child>
  </parent>
</div>


以下是使用无状态功能组件的示例:

Vue.component('child', {
  functional: true,
  render(h, {
    children
  }) {
    return h('div', {
      class: 'bar'
    }, children)
  },
})

Vue.component('parent', {
  functional: true,
  render(h, {
    scopedSlots
  }) {
    const defaultScopedSlot = scopedSlots.default({
      foo: 'bar'
    })

    if (defaultScopedSlot) {
      defaultScopedSlot.forEach((child, index) => {
        child.data = {
          style: `color: #` + index + index + index
        }

        child.data.attrs = {
          role: 'whatever'
        }
      })
    }
    return h(
      'div', {
        class: 'parent',
      },
      defaultScopedSlot,
    )
  },
})

new Vue({
  el: '#app',
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
  <parent>
    <template v-slot:default="{ foo }">
      <child>{{ foo }}</child>
      <child>{{ foo }}</child>
      <child>{{ foo }}</child>
    </template>
  </parent>
</div>


我正在等待以下答案:
  • 是的,您可以使用它,这种方法没有潜在的问题。
  • 是,但是可能会出现这些问题。
  • 不,存在很多问题。

  • UPDATE :

    我发现的另一种好方法是将子VNode包装到另一个具有适当数据对象的VNode中,如下所示:

    const wrappedChildren = children.map(child => {
      return h("div", { class: "foo" }, [child]);
    });
    

    使用这种方法,我不用担心修改子VNode

    先感谢您。

    最佳答案

    这样做有潜在的问题。非常谨慎地使用它可能是一种有用的技术,如果没有简单的替代方法,我个人会很乐意使用它。但是,您处于未公开的领域,如果出现问题,您可能必须通过逐步了解Vue内部进行调试。它不是为胆小的人准备的。

    首先,其他人正在使用一些类似事物的例子。

  • 修补key:
    https://medium.com/dailyjs/patching-the-vue-js-virtual-dom-the-need-the-explanation-and-the-solution-ba18e4ae385b
  • Vuetify从mixin修补VNode的示例:
    https://github.com/vuetifyjs/vuetify/blob/5329514763e7fab11994c4303aa601346e17104c/packages/vuetify/src/components/VImg/VImg.ts#L219
  • Vuetify从有作用域的插槽修补VNode的示例:https://github.com/vuetifyjs/vuetify/blob/7f7391d76dc44f7f7d64f30ad7e0e429c85597c8/packages/vuetify/src/components/VItemGroup/VItem.ts#L58

  • 我认为只有第三个示例确实可以与该问题中的修补程序相提并论。其中的一个关键功能是它使用作用域插槽而不是普通插槽,因此VNode是在相同的render函数内创建的。

    普通插槽会变得更加复杂。问题在于,插槽的VNode是在父级的render函数中创建的。如果 child 的render函数运行多次,它将继续通过该插槽的相同VNode。修改这些VNode不一定会达到您期望的效果,因为差异算法只会看到相同的VNode,并且不会执行任何DOM更新。

    这是一个示例说明:

    const MyRenderComponent = {
      data () {
        return {
          blueChildren: true
        }
      },
    
      render (h) {
        // Add a button before the slot children
        const children = [h('button', {
          on: {
            click: () => {
              this.blueChildren = !this.blueChildren
            }
          }
        }, 'Blue children: ' + this.blueChildren)]
    
        const slotContent = this.$slots.default
    
        for (const child of slotContent) {
          if (child.data && child.data.class) {
            // Add/remove the CSS class 'blue'
            child.data.class.blue = this.blueChildren
    
            // Log it out to confirm this really is happening
            console.log(child.data.class)
          }
    
          children.push(child)
        }
    
        return h('div', null, children)
      }
    }
    
    new Vue({
      el: '#app',
    
      components: {
        MyRenderComponent
      },
    
      data () {
        return {
          count: 0
        }
      }
    })
    .red {
      border: 1px solid red;
      margin: 10px;
      padding: 5px;
    }
    
    .blue {
      background: #009;
      color: white;
    }
    <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
    <div id="app">
      <my-render-component>
        <div :class="{red: true}">This is a slot content</div>
      </my-render-component>
      <button @click="count++">
        Update outer: {{ count }}
      </button>
    </div>


    有两个按钮。第一个按钮可切换名为datablueChildren属性。它用于确定是否向子级添加CSS类。更改blueChildren的值将成功触发子组件的重新渲染,并且VNode确实会更新,但是DOM不变。

    另一个按钮强制外部组件重新渲染。这将重新生成插槽中的VNode。然后,这些将被传递给 child ,并且DOM将被更新。

    Vue对哪些因素可以导致VNode进行更改以及是否做出相应的更改进行了一些假设。在Vue 3中,这种情况只会变得越来越糟(我的意思是说更好),因为随之而来的是更多的优化。关于Vue 3的a very interesting presentation Evan You gave涵盖了即将到来的优化类型,并且假设某些事情不会改变,它们都属于Vue的这一类。

    有一些方法可以解决此示例。当组件执行更新时,VNode将包含对DOM节点的引用,因此可以直接更新。这不是很好,但是可以做到。

    我自己的感觉是,只有在修复补丁后,您才真正安全,这样更新就不成问题了。只要您不希望以后更改它们,添加一些属性或CSS类就可以。

    还有另一类问题需要克服。调整VNode可能确实很麻烦。问题中的例子都暗示了这一点。如果缺少data怎么办?如果缺少attrs怎么办?

    在问题的作用域插槽示例中,child组件的class="bar"上具有<div>。那在parent中被吹走了。也许这是有意的,也许不是故意的,但是试图将所有不同的对象合并在一起是非常棘手的。例如,class可以是字符串,对象或数组。 Vuetify示例使用_b,它是Vue内部bindObjectProps的别名,以避免必须涵盖所有不同的情况。

    除了不同的格式外,还有不同的节点类型。节点不一定代表组件或元素。还有文本节点和注释,其中注释节点是v-if的结果,而不是模板中的实际注释。

    正确处理所有不同的边缘情况非常困难。再说一遍,可能这些边缘情况都不会对您实际想到的用例造成任何实际问题。

    最后一点,以上所有内容仅适用于修改VNode。从插槽中包装VNode或在render函数中在它们之间插入其他子节点是完全正常的。

    10-02 20:17