Vue.js 渲染函数是否允许 return 个 VNode 数组?

Do Vue.js render functions allow return of an array of VNodes?

我正在努力扩展 Vue.js 前端应用程序。我目前正在检查功能组件中的渲染功能。查看文档后,我目前的理解是功能组件中的渲染函数将 return 使用 CreateElement aka h 创建的单个 VNode。 当我看到一个 VNode 被 returned 作为数组中的一个元素时,我感到很困惑。我在文档中找不到任何对此语法的引用。有没有人有任何见解?

export default {
  name: 'OfferModule',
  functional: true,
  props: {
    data: Object,
    placementInt: Number,
    len: Number
  },
  render (h, ctx) {
    let adunitTheme = []
    const isDev = str => (process.env.dev ? str : '')

    const i = parseInt(ctx.props.placementInt)
    const isDevice = ctx.props.data.Component === 'Device'
    const Component = isDevice ? Device : Adunit

    /* device helper classes */
    const adunitWrapper = ctx.props.data.Decorate?.CodeName === 'AdunitWrapper'

    if (!isDevice /* is Adunit */) {
      const offerTypeInt = ctx.props.data.OfferType
      adunitTheme = [
        'adunit-themes',
        `adunit-themes--${type}`.toLowerCase(),
        `adunit-themes--${theme}`.toLowerCase(),
        `adunit-themes--${type}-${theme}`.toLowerCase(),
      ]
    }

    const renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate =
      ctx.props.data.Decorate?.Position === 'AboveAdunit' || false

    const renderOfferModuleWithoutDisplayAdContainers =
      ctx.props.data.Decorate?.RemoveAds /* for adunits */ ||
      ctx.props.data.DeviceData?.RemoveAds /* for devices */ ||
      false

    const getStyle = (className) => {
      try {
        return ctx.parent.$style[className]
      } catch (error) {
        console.log('$test', 'invalid style not found on parent selector')
      }
    }

    const PrimaryOfferModule = (aboveAdunitSlot = {}) =>
      h(Component, {
        props: {
          data: ctx.props.data,
          itemIndex: i,
          adunitTheme: adunitTheme.join('.')
        },
        attrs: {
          class: [
            ...adunitTheme,
            getStyle('product')
          ]
            .join(' ')
            .trim()
        },
        scopedSlots: {
          ...aboveAdunitSlot
        }
      })

    if (renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate) {
      return [
        PrimaryOfferModule({
          aboveAdunit (props) {
            return h({
              data () {
                return ctx.props.data.Decorate
              },
              template: ctx.props.data.Decorate?.Template.replace(
                'v-show="false"',
                ''
              )
            })
          }
        })
      ]
    } else if (renderOfferModuleWithoutDisplayAdContainers) {
      return [PrimaryOfferModule()]
    } else {
      const withAd = i > 0 && i % 1 === 0

      const adWrap = (placement, position, className) => {
        return h(
          'div',
          {
            class: 'm4d-wrap-sticky'
          },
          [
            h(Advertisement, {
              props: {
                placement,
                position: String(position)
              },
              class: getStyle(className)
            })
          ]
        )
      }

      return [
        withAd && adWrap('inline-sticky', i, 'inlineAd'),
        h('div', {
          class: 'm4d-wrap-sticky-adjacent'
        }),

        h(
          'div',
          {
            attrs: {
              id: `inline-device--${String(i)}`
            },
            class: 'inline-device'
          },
          isDev(`inline-device id#: inline-device--${String(i)}`)
        ),

        withAd &&
          i !== ctx.props.len - 1 &&
          h(EcomAdvertisement, {
            props: {
              placement: 'inline-static',
              position: String(i)
            },
            class: getStyle('inlineStaticAd')
          }),

        PrimaryOfferModule()
      ]
    }
  }
}

这似乎是在以下时间实施的:

https://github.com/vuejs/vue/commit/c7c13c2a156269d29fd9c9f8f6a3e53a2f2cac3d

这是 2018 年提出的问题 (https://github.com/vuejs/vue/issues/8056) 的结果,因为 this.$scopedSlots.default() returned 了一个 VNode 或一个 VNode 数组,具体取决于内容。

主要论点是这与常规插槽在渲染函数中的行为方式不一致,并且意味着任何渲染函数组件渲染作用域插槽作为子级需要类型检查调用插槽的结果以确定是否需要包裹在数组中

所以 Evan 在问题线程 here 上发表评论,解释说 this.$scopedSlots.default 总是 return 数组开始 v2.6 以允许一致性,但避免破坏性更改对于 $scopedSlots 的使用方式,更新还允许 return 来自渲染函数的单个 VNode 的数组。

事实证明,return创建一个 VNode 数组实际上早于 the scopedSlots update

我也无法在文档中的任何地方找到它的记录,但是通过 this comment on a Vue GitHub issue by a member of the Vue.js core team (which predates the scopedSlots commit by ~1 year), render() can return an Array of VNodes, which Vue will take and render in order. However, this only works in one, singular case: functional components

尝试 return 正常(非功能性、有状态)组件中包含大于 1 个元素的 VNode 数组会导致错误:

Vue.config.productionTip = false;
Vue.config.devtools = false;

Vue.component('render-func-test', {
  render(h, ctx) {
    return [
      h('h1', "I'm a heading"),
      h('h2', "I'm a lesser heading"),
      h('h3', "I'm an even lesser heading")
    ];
  },
});

new Vue({
  el: '#app',
});
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>

<div id="app">
  Test
  <render-func-test></render-func-test>
</div>

[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.

但是在 功能性 组件中执行此操作,正如您的示例所做的那样,效果很好:

Vue.config.productionTip = false;
Vue.config.devtools = false;

Vue.component('render-func-test', {
  functional: true, // <--- This is the key
  render(h, ctx) {
    return [
      h('h1', "I'm a heading"),
      h('h2', "I'm a lesser heading"),
      h('h3', "I'm an even lesser heading")
    ];
  },
});

new Vue({
  el: '#app',
});
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>

<div id="app">
  Test
  <render-func-test></render-func-test>
</div>


如果您对原因感兴趣,Vue 核心团队的另一位成员解释了此限制further down in the thread

它基本上归结为 Vue 修补和差异算法所做的假设,主要是“每个子组件在其父虚拟 DOM 中由单个 VNode 表示”,这是不正确的如果允许多个根节点。

复杂性的增加需要对 Vue 的核心算法进行大量更改。这很重要,因为这个算法不仅要擅长它所做的事情,而且还要非常,非常 性能。

功能组件不需要符合这个限制,因为“它们在父级中没有用 VNode 表示,因为它们没有实例并且不管理自己的实例virtual DOM"– 它们是无状态的,因此不需要限制。

但是,应该注意的是,this is possible on non-functional components in Vue 3,因为所讨论的算法已被重新设计以允许它。