关于 vue3 中的 render 方法,你可能不知道这些

16,229 阅读5分钟

不出意外vue3马上就能正式面世了,在面世之后的短期一波时间内,毫无疑问vue3会成为前端热度最高的话题。本着蹭热度要抢早的观念,本人在vue3正式发布之前就开始做这方面的准备,近段时间会一直产出vue3相关的内容,希望能收获观众大佬们的喜欢。

那么闲话不多说,今天想聊的话题是关于vue3中的render方法的,如果你认真读过rfc那么你可能会有一些印象,但大概率不会专门去关注这块。对于大部分人其实也不会正在接触到render方法,因为平时我们开发的时候基本直接是 .vue 文件的开发模式,vue-loader会帮助你编译模板到render方法。更进一步你可能会使用jsx来写组件,那么你会更接近render方法一些,但是最终内容还是会由babel-transform-xxx来帮你编译,细节你仍然可能是不清楚的。

那么今天我就来为各位扒一扒vue3中的render方法,如果对你有用,还请点赞,关注哦。

既然讲的是render方法,我们自然不会涉及到模板或者jsx语法,所以接下去所有的展示代码全部都是纯js。我们来看一下最简单的vue组件的写法。

function Comp(props, { slots, attrs }) {
    return h('div', {id: 'root'}, ['this is content'])
}

这是vue3中的函数组件,在vue3中函数组件不再需要通过functional声明来实现,只要你的组件是一个函数,那么他就是一个函数组件,反之,只要是一个对象,他就是一个有状态组件。

当然这不是重点,重点是h函数,换个称呼可能大家更好理解:createElement。在vue3中,我们需要通过import { h } from 'vue'来引入这个函数。h接收三个参数,分别是:

  • type,组件类型,字符串代表原生节点,对象或者方法则表示自定义组件
  • props,组件的属性,通过对象传入
  • slots,组件插槽内容

前两个很好理解也不是我们今天的重点,所以就不在这里深入了,我们主要关心最后一个参数。在vue3中,对于第三个参数我们有很多种写法,就像茴香豆的茴字(误):

  • ['content']
  • 'content'
  • () => 'content'
  • { default: 'content' }
  • { default: () => 'content' }
  • { default: () => ['content'] }

那么以上几种写法,最终的结果都是差不多的。你现在是不是很好奇为什么vue3要整这么多种不同的写法,没关系一开始我也是不清楚的,接下去就由我慢慢为你讲解。

第一种和第二种最好理解,其实第二种是第一种的缩写,只要在children只有一个元素的时候才可以写成这样:

<Comp>
    content
</Comp>

上面的demo对应到jsx其实就类似酱紫,那么前两种写法就非常好理解了。

那么再来讲讲第三种。我们先来看一下vue rfcs里面的内容:

Inside Comp, this.$slots.default will be a function in both cases and returns the same VNodes. However, the 2nd case will be more performant as this.msg will be registered as a dependency of the child component only.

这句话里的demo如下:

h(Comp, [
  h('div', this.msg)
])

// equivalent:
h(Comp, () => [
  h('div', this.msg)
])

简单来说,就是你在写代码的时候不管是前三种里面的哪种写法,在Comp里面通过this.$slots.default获取的时候,都是得到一个方法,这个方法调用之后才会返回真正的content

但是这里说到了,使用() =>的方式是性能更好的,原因就是this.msg的变化只会引起子内容的重新渲染,其实父组件根本不关心this.msg所以在其更新的时候没必要重新渲染。这涉及到vue的响应式原理,这里就不继续展开,后面有机会再跟大家分享。

那么从这里我们可以得出一个结论,那就是我们正常情况下应该摒弃前面两种写法,而使用第三种方式,因为他们最终表现没有区别,而后者性能更好。

这个结论是正确的,但是又需要注意一点,这种写法只对自定义组件有用,如果是原生节点,是无法展示的,也就是说:

// 可行
h(YourComp, () => 'content')

// 不可行
h('div', () => 'content')

可以猜测vue3对于原生节点的渲染是没有slot的概念,原生节点和自定义组件的渲染方式也是大不相同。不太好解释为什么vue不设计统一的interface,这我也要去探究探究。

前面的说完我们再来说一下后面的三种对象的写法。

一看到对象,我们就应该很快反应过来,唉~,key value对嘛。然后再联想一下,对的,就是具名插槽。vue里面每个组件是可以有多个插槽的,那我们怎么表示多个插槽呢?自然就是通过不同的key啦:

h(Comp, null, {
    default: () => 'default',
    foo: () => 'foo',
    bar: () => 'bar'
})

注意这么些的时候,即便没有props,第二个参数也是必须的,因为h函数发现第二个参数如果是对象,那么默认其就是props

因为默认只有一个插槽的话,他就是default插槽,所以只有一个children节点的时候可以省略对象的写法,也就是我们前面看到的几种写法。

同时这种写法也不能用于原生节点,这是自定义组件才有的特性写法。

另外因为slots都是函数,所以scopeSlot就变得非常好理解。在调用该slot的时候传入参数就行:

// Comp
h('div', [slots.default('arg')])

这样的话在调用Comp的地方就可以获取这个参数

h(Comp, (content) => h('div', [content]))

这其实相对于凭空增加一个叫做scopeSlot更好理解,个人也会更推崇这种用法。

探索

在深入理解了vue3中关于slot的用法之后,我不禁想到,既然slot就是函数,那我直接通过props传递函数是不是也可以呢?于是我试了一下:

// Comp
h('div', [props.renderA()])

// App
h(Comp, {renderA: () => h('span', 'content')})

发现这是可行的,而且是完全可以触发reactive的重新渲染的。

当然这只是初步探索,我目前还不确定vue内部对于slot有没有特殊处理,如果没有什么特殊处理,那么renderProps的用法在vue中就是完全可行的,个人表示非常期待。

好了,今天的分享就到这里,我会尽量更多得为大家分享有意义的内容。