Can I modify Vue.js VNodes?

And*_*huk 8 javascript vue.js

I want to assign some attributes and classes to the children VNode through data object. That just works. But during my Vue.js investigation, I have not seen such pattern in use, that's why I don't think it's good idea to modify children VNode's.

But that approach sometimes comes in handy – for example I want to assign to all the buttons in default slot the aria-label attribute.

See example below, using default stateful components:

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',
})
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

And here is examples using stateless functional components:

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',
})
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

I am waiting for the following answers:

  1. Yes, you can use it, there are no potential problems with this approach.

  2. Yes, but these problem(s) can happen.

  3. No, there are a lot of problem(s).

UPDATE:

That another good approach I have found it's to wrap child VNode into the another created VNode with appropriate data object, like this:

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

Using this approach I have no fear modifying children VNode's.

Thank you in advance.

ski*_*tle 11

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

首先,一些类似的东西被其他人使用的例子。

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

我认为只有第三个例子才能真正与这个问题中的补丁相媲美。一个关键特性是它使用作用域插槽而不是普通插槽,因此 VNode 是在同一个render函数中创建的。

使用普通插槽会变得更加复杂。问题是插槽的 VNode 是在父render函数中创建的。如果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
    }
  }
})
Run Code Online (Sandbox Code Playgroud)
.red {
  border: 1px solid red;
  margin: 10px;
  padding: 5px;
}

.blue {
  background: #009;
  color: white;
}
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/vue@2.6.10/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>
Run Code Online (Sandbox Code Playgroud)

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

另一个按钮强制外部组件重新渲染。这会重新生成插槽中的 VNode。然后将这些传递给孩子,DOM 将得到更新。

Vue 正在对什么可以和不能导致 VNode 进行相应的更改和优化做出一些假设。在 Vue 3 中,这只会变得更糟(我的意思是更好),因为有更多的这些优化正在进行中。有一个很有趣的演示埃文你给了约3 Vue公司覆盖将未来的多种优化技术,他们都属于这一类Vue公司的假设,有些事情无法改变。

有办法修复这个例子。当组件执行更新时,VNode 将包含对 DOM 节点的引用,因此可以直接更新。这不是很好,但可以做到。

我自己的感觉是,只有当你正在做的修补是固定的,你才真正安全,这样更新就不是问题了。添加一些属性或 CSS 类应该可以工作,只要您以后不想更改它们。

还有另一类问题需要克服。调整 VNode 可能非常繁琐。问题中的例子暗示了它。如果data丢失了怎么办?如果attrs丢失了怎么办?

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

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

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

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