在开发MUI的Table组件时,我们遇到了一个问题。用户在顶层组件中嵌套的内容,需要被保存到组件的数据中,并且在表格内部渲染出来。

通常,Vue内嵌内容是使用slot进行渲染的。在父组件的模板中,在子组件的标签中嵌入模板,然后在子组件的内部,使用<slot/>标签进行渲染。但现在我们的需求是非父子组件的slot渲染,这就要求我们换一种思路去保存和调用slot。

要理解以下的内容,请确保你阅读了Render Functions & JSX,理解了Vue的VNode、render function、模板等概念以及这些概念之间的关系

slot方案

首先要明确一个点,所谓的slot就指的是一个组件在声明时候的内嵌内容。Vue的模板都会被编译成VNode节点树,slot指的是一个VNode的children属性这个数组里包含的VNode节点集合,这些内容由组件声明时的内嵌内容编译而来。

我们可以通过this.$slots.default拿到默认的子VNode列表。如果内嵌内容上没有声明name属性,那这些内容都归属于default这个属性。

所以Slot其实就是一个VNode数组,我们可以把这个数组作为prop传入子节点进行渲染。

{{ vnode }}这种语法会把vnode作为一个对象去序列化,这不是我们所期望的。所以我们需要用v-bind去传递VNodes的引用。

想要渲染slot,可以使用render function。之前讲过,slot其实就是VNode的children,所以我们在render functioncreateElement的时候把slot的引用作为children传入就可以了。

render(createElement) {
        return createElement('div', this.content)
    }

在组件初始化时给this.$slots赋值,然后在模板中使用slot渲染或许也是一种办法,但不一定行的通,也比较hacky。

但我们发现这样不能达到目的。VNode是Vue中对一个DOM节点的内部表示,VNode是有状态的,一个VNode同时只能渲染出一个DOM节点实例。也就是说一个VNode在渲染之后不能再次渲染,除非先把这个VNode从文档中移除,然后才可以再次渲染。

所以,因为我们的表格中的VNodes是会被每一个row复用的,现在这种用法只能渲染第一行的slot内容。

解决方案就是,用一个deepClone函数clone VNode,在每次渲染时初始化新的VNodes实例。

render(createElement) {
        return createElement('div', deepClone(this.content, createElement))
    }

scopedSlots方案

这样似乎就可以解决问题了,但我们发现Table的自定义内容常常是一个按钮这样的可以交互的组件,会有事件绑定,如果我们要在子组件中给slot动态传入属性,这是办不到的。

所以slot就不能满足我们的需求了,更好的解决方案就是scopedSlots。

要了解什么是scopedSlots,我们首先将scopedSlots的模板:

<template scoped="prop">
  <div></div>
</template>

进行编译,结果是:

function anonymous() {
  with(this){return _c('div',{scopedSlots:_u([{key:"default",fn:function(prop){return [_c('div')]}}])})}
}

这种形式是我们之前没有遇到过的,scopedSlots被编译后,生成了一个函数,而且scopedSlot是被存放在VNode的data属性中,而不是在children中。

仔细观察这个函数,这个函数接收一个参数,然后返回一个VNode,这个VNode的属性是从这个参数中获取的。那scopedSlots的原理就很清楚了,scopedSlots就是一个lazy evaluation的函数,在需要渲染的时候,接收scope对象,然后渲染。这样就可以达到一个类似动态作用域的效果

既然scopedSlots是一个函数,我们在render function里面只要调用这个函数,并且传入对应的scope对象作为参数就可以了:

render(createElement) {
        const prop = {
            index: this.id
        }
        return createElement('div', [
            this.content.call(this, prop)
        ])
    }

这种形式顺便解决了之前slot无法重复利用VNode的问题,因为scopedSlots函数每次返回的都是一个新的VNode节点