深入应用Vue render 函数
- Published on
- 发布于·预估阅读16分钟
- Authors
- Name
- willson-wang
我们在vue项目的日常开发中基本都是template模版形式,几乎不会用到render函数去生成模版,最近因为有需求,需要更加灵活的生成模版,所以深入了解了一下render函数
如果对render函数还没有一点了解建议先看下文档渲染函数 & JSX
内容分为4个部分
1、如何使用render函数生成与template写法一致的模版 2、理解render函数中的slot 3、理解render函数中的scopeSlot 4、对比2.6之后slot的新旧语法
如何使用render函数生成与template写法一致的模版
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM,而虚拟DOM则通过createElement方法创建,一个简单的虚拟DOM如下所示
createElement('h1', this.blogTitle)
而render函数正是Vue用来生成虚拟DOM的,我们平常的template写法,也会被vue-loader转换成createElement的形式
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM property
domProps: {
innerHTML: 'baz'
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
其实看完上面的第二个参数,是不怎么好理解的,尤其是针对props、domProps、attrs、on、nativeOn、slot、scopedSlots这几个参数
我们先看domProps、attrs、on、class、style这几个参数
我们从我们最熟悉的template写法,来先写一个模版,然后在通过render函数来写出渲染一致的模版,通过二者比较来快速熟悉各个参数的含义
template写出例子
<template>
<!-- 通过模版模版功能写一个组件,之后在通过render方法来写template部分,最后做对比 -->
<div class="wrap" :class="[isDesign && 'wrap-design']" style="color: #ccc" :style="{backGroundColor: bg}" @click="goTo" id="normal" data-type="normar-type" :type="innerType">
<div>{{count}}</div>
<div>{{msg}}</div>
<div>
<button @click.stop="incr">+</button>
<button @click.stop="decr">-</button>
</div>
<div>
<span v-if="count % 2">奇数</span>
<span v-else>偶数</span>
</div>
<div>
<img src="../assets/logo.png" alt="" />
</div>
<ul>
<li v-for="(item, index) in count" :key="index">{{item}}</li>
</ul>
<div @click.stop="emitHandle">
emit
</div>
<div title='aaa'>
<a href="www.baidu.com">我是一个超链接</a>
<input type="text" maxlength="10" value="1111">
</div>
</div>
</template>
<script>
/* eslint-disable */
export default {
name: 'normalTemplate',
components: {},
props: {
msg: String
},
data() {
return {
isDesign: true,
count: 0,
innerType: 'normal'
}
},
computed: {
bg() {
return '#fff'
},
newCount() {
return `新count${this.count}`
}
},
methods: {
goTo() {
console.log('goto', this.$listeners)
// this.$emit('click')
},
incr() {
this.count += 1
this.$emit('incr', this.count)
},
decr() {
this.count -= 1
},
emitHandle() {
this.$emit('my-emit')
}
}
}
</script>
生成的dom结构如下所示
<div
id="normal"
data-type="normar-type"
type="normal"
class="wrap wrap-design"
style="color: rgb(204, 204, 204);"
>
<div>0</div>
<div>basicTemplate</div>
<div>
<button>+</button>
<button>-</button>
</div>
<div>
<span>偶数</span>
</div>
<div>
<img src="/img/logo.82b9c7a5.png" alt />
</div>
<ul></ul>
<div>emit</div>
<div title="aaa">
<a href="www.baidu.com">我是一个超链接</a>
<input type="text" maxlength="10" value="1111" />
</div>
</div>
render函数写出跟template一样的例子
<script>
/* eslint-disable */
export default {
name: 'basicRender',
props: {
msg: String
},
data() {
return {
isDesign: true,
count: 0,
innerType: 'normal',
arr: [0]
}
},
computed: {
bg() {
return '#fff'
},
newCount() {
return `新count${this.count}`
}
},
methods: {
goTo() {
console.log('goto', this)
},
incr() {
this.count += 1
this.arr.push(this.count)
},
decr() {
this.count -= 1
this.arr.pop()
},
emitHandle() {
this.$emit('my-emit')
}
},
render(h) {
return h(
'div',
{
class: [
'warp',
this.isDesign && 'wrap-design'
],
style: {
color: '#333',
backGroundColor: this.bg
},
props: {
a: 1
},
attrs: {
id: 'Test',
'data-type': "normar-type",
type: this.innerType
},
on: {
click: this.goTo
}
},
[
h(
'div',
[
`basicRender${this.count}`,
]
),
h(
'div',
[
`新${this.newCount}`,
]
),
h(
'div',
[
`${this.msg}`,
]
),
h(
'div',
[
h(
'button',
{
on: {
click: this.incr,
}
},
[
'+'
]
),
h(
'button',
{
on: {
click: this.decr,
}
},
[
'-'
]
)
]
),
h(
'div',
[
h(
'span',
[
this.count % 2 ? '奇数' : '偶数'
]
)
]
),
h(
'div',
[
h(
'img',
{
attrs: {
src: require('../assets/logo.png'),
alt: ''
}
}
)
]
),
h(
'ul',
[
this.arr.map((item, key) => {
return h(
'li',
{
key
},
[
item
]
)
})
]
),
h(
'div',
{
on: {
click: this.emitHandle
},
domProps: {
innerHTML: 'emit'
}
}
),
h(
'div',
{
attrs: {
title: 'aaa'
}
},
[
h(
'a',
{
domProps: {
href: 'www.baidu.com'
}
},
[
'我是一个超链接'
]
),
h(
'input',
{
attrs: {
type: 'text',
maxlength: 10,
value: '1111'
}
}
)
]
)
]
)
}
}
</script>
生成的dom结构如下所示
<div
id="Test"
data-type="normar-type"
type="normal"
class="warp wrap-design"
style="color: rgb(51, 51, 51);"
>
<div>basicRender0</div>
<div>新新count0</div>
<div>basicRender</div>
<div>
<button>+</button>
<button>-</button>
</div>
<div>
<span>偶数</span>
</div>
<div>
<img src="/img/logo.82b9c7a5.png" alt />
</div>
<ul>
<li>0</li>
</ul>
<div>emit</div>
<div title="aaa">
<a href="www.baidu.com">我是一个超链接</a>
<input type="text" maxlength="10" value="1111" />
</div>
</div>
对比template与render二者最终生成的dom结构可以发现
{
'class': { // 相当于template内的:class,可以是单个值、数组、对象
foo: true,
bar: false
},
style: { // 相当于template内的:style
color: 'red',
fontSize: '14px'
},
attrs: { // 给html元素添加属性
id: 'foo'
},
domProps: { // 给html原属添加DOM属性,具体的区别是参考https://juejin.im/post/58d11689a22b9d00644015f1
innerHTML: 'baz'
},
on: { // dom元素上绑定事件,相当于template内的@click等事件
click: this.clickHandler
}
}
理解render函数中的slot
slot的目的是,允许在父组件内使用子组件时可以向子组件传入不同的内容
子组件内定义有slot,父组件内使用的时候传入的内容才会生效
child template写法
<template>
<div>
child --a = {{a}} --- b = {{b}}
<slot class="defalt-slot"></slot>
<slot name="child-solt" class="name-slot"></slot>
</div>
</template>
<script>
/* eslint-disable */
export default {
name: 'childTemplateSlot',
props: {
a: String,
b: String
}
}
</script>
child render写法
<script>
/* eslint-disable */
export default {
name: 'childRenderSlot',
props: {
a: String,
b: String
},
render(h) {
return h(
'div',
[
`child --a = ${this.a} --- b = ${this.b} `,
this.$scopedSlots.default,
this.$scopedSlots['child-solt']
]
)
}
}
</script>
parent template 使用子组件内包含slot写法
<div class="default-solt">
<!-- 匿名slot -->
<slot></slot>
</div>
<div class="name-slot">
<!-- 具名slot -->
<slot name="my-slot"></slot>
</div>
<ChildTemplateSlot a="aa" b="bb">
<div>child template default solt </div>
<div slot="child-solt">child template child-solt solt </div>
</ChildTemplateSlot>
<ChildRenderSlot a="aa" b="bb">
<div>child render default solt </div>
<div slot="child-solt">child render child-solt solt </div>
</ChildRenderSlot>
最终dom结构
<div>
child --a = aa --- b = bb
<div>child template default solt</div>
<div>child template child-solt solt</div>
</div>
<div>
child --a = aa --- b = bb
<div>child render default solt</div>
<div>child render child-solt solt</div>
</div>
父组件template写法,子组件采用template、render写法得到的dom结构是一致的
parent render 写法
<script>
/* eslint-disable */
import ChildRenderSlot from '../components/childRenderSlot'
import ChildTemplateSlot from '../components/childTemplateSlot'
export default {
name: 'parentRenderSlot',
render(h) {
return h(
'div',
[
h(
'div',
[
this.$slots.default
]
),
h(
'div',
[
this.$slots['my-slot']
]
),
h(
'div',
[
h(
ChildRenderSlot,
{
props: {
a: '1',
b: '2'
}
},
[
h(
'div',
[
'child render default solt render'
]
),
h(
'div',
{
slot: 'child-solt',
},
[
'child render child-slot solt render'
]
)
]
),
h(
ChildTemplateSlot,
{
props: {
a: '3',
b: '4'
}
},
[
h(
'div',
[
'child template default solt render'
]
),
h(
'div',
{
slot: 'child-solt',
},
[
'child template child-solt solt render'
]
)
]
)
]
)
]
)
}
}
</script>
最终dom结构
<div>
child --a = aa --- b = bb
<div>child render default solt</div>
<div>child render child-solt solt</div>
</div>
<div>
child --a = aa --- b = bb
<div>child template default solt</div>
<div>child template child-solt solt</div>
</div>
<div>
child --a = 1 --- b = 2
<div>child render default solt render</div>
<div>child render child-slot solt render</div>
</div>
<div>
child --a = 3 --- b = 4
<div>child template default solt render</div>
<div>child template child-solt solt render</div>
</div>
从上面我们可以看出,当createElemet的第一个元素是组件时,第二个参数内的props是传入第一个组件参数内的props;第二个参数内的slot参数用于指定当前的虚拟dom元素,插入子组件内的哪个slot内;如果子组件也是render写法,默认插槽通过this.slots['my-slot']渲染
理解render函数中的scopeSlot
child template 写法
<template>
<div>
child --a = {{a}} --- b = {{b}}
<slot class="defalt-slot" :info="info"></slot>
<slot name="child-solt" class="name-slot" :info="info2"></slot>
</div>
</template>
<script>
/* eslint-disable */
export default {
name: 'childTemplateSlotScope',
props: {
a: String,
b: String
},
data() {
return {
info: {
name: `child-${this.a}`,
sex: `child-${this.b}`,
},
info2: {
name: `child2-${this.a}`,
sex: `child2-${this.b}`,
}
}
}
}
</script>
child render 写法
<script>
/* eslint-disable */
export default {
name: 'childRenderSlotScope',
props: {
a: String,
b: String
},
data() {
return {
info: {
name: `child-render-${this.a}`,
sex: `child-render-${this.b}`,
},
info2: {
name: `child2-render-${this.a}`,
sex: `child2-render-${this.b}`,
}
}
},
render(h) {
return h(
'div',
[
`child --a = ${this.a} --- b = ${this.b} `,
this.$scopedSlots.default({
info: this.info
}),
this.$scopedSlots['child-solt']({
info: this.info2
})
]
)
}
}
</script>
parent template 写法
<ChildRenderSlotScope a="aa" b="bb">
<div slot-scope="slotScope3">child render default solt {{slotScope3.info.name}} --- {{slotScope3.info.sex}}</div>
<div slot="child-solt" slot-scope="slotScope4">child render child-solt solt {{slotScope4.info.name}} --- {{slotScope4.info.sex}} </div>
</ChildRenderSlotScope>
<ChildTemplateSlotScope a="aa" b="bb">
<div slot-scope="slotScope">child template default solt {{slotScope.info.name}} --- {{slotScope.info.sex}}</div>
<div slot="child-solt" slot-scope="slotScope2">child template child-solt solt {{slotScope2.info.name}} --- {{slotScope2.info.sex}} </div>
</ChildTemplateSlotScope>
得到的dom结构
<div>
child --a = aa --- b = bb
<div>child render default solt child-render-aa --- child-render-bb</div>
<div>child render child-solt solt child2-render-aa --- child2-render-bb</div>
</div>
<div>
child --a = aa --- b = bb
<div>child template default solt child-aa --- child-bb</div>
<div>child template child-solt solt child2-aa --- child2-bb</div>
</div>
parent render 写法
<script>
/* eslint-disable */
import ChildRenderSlot from '../components/childRenderSlot'
import ChildTemplateSlot from '../components/childTemplateSlot'
export default {
name: 'parentRenderSlotScope',
render(h) {
return h(
'div',
[
h(
'div',
[
'scope slot',
h(
ChildRenderSlot,
{
props: {
a: '999',
b: '888'
},
scopedSlots: {
default: (props) => {
return h(
'div',
[
`child render default solt render ${props.info.name} xxx ${props.info.sex}`
]
)
},
'child-solt': (props) => {
return h(
'div',
[
`child render child-slot solt render ${props.info.name} xxx ${props.info.sex}`
]
)
}
}
}
),
h(
ChildTemplateSlot,
{
props: {
a: '22',
b: '44'
},
scopedSlots: {
default: (props) => {
return h(
'div',
[
`child template default solt render ${props.info.name} xxx ${props.info.sex}`
]
)
},
'child-solt': (props) => {
return h(
'div',
[
`child template child-solt solt render ${props.info.name} xxx ${props.info.sex}`
]
)
}
}
}
)
]
)
]
)
}
}
</script>
最终得到的dom结构
<div>
child --a = 999 --- b = 888
<div>child render default solt render child-render-999 xxx child-render-888</div>
<div>child render child-slot solt render child2-render-999 xxx child2-render-888</div>
</div>
<div>
child --a = 22 --- b = 44
<div>child template default solt render child-22 xxx child-44</div>
<div>child template child-solt solt render child2-22 xxx child2-44</div>
</div>
从上面我们可以看出,当createElemet的第一个元素是组件时,第二个参数内的scopedSlots参数用于,传入子slot传入的参数并返回最终插入子slot的模版
如果子组件也是render写法, 默认插槽通过this.$scopedSlots.default传入scope参数
this.$scopedSlots.default({
info: this.info
})
具名插槽通过this.$slots['slot名']传入scope参数
this.$scopedSlots['child-solt']({
info: this.info2
})
对比2.6之后slot的新旧语法
定义包含slot的子组件
<template>
<div>
用于新的slot语法,子组件内的slot写法保持不变
<div class="default-slot">
<!-- 匿名插槽 -->
<slot></slot>
</div>
<div class="name-slot">
<!-- 具名插槽 -->
<slot name="test"></slot>
</div>
<div>
<!--具名作用域插槽 -->
<slot name="test2" :info="test"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'newVSlot',
data() {
return {
test: {
name: 'xiaoming',
age: 18,
}
}
},
}
</script>
旧写法
<NewVSlot>
<div>我是匿名插槽2.6之前写法,现已废弃,3.0中不会支持</div>
<div slot="test">
我是具名插槽
</div>
<div slot="test2" slot-scope="slotScope">
我是具名作用域插槽{{slotScope.info.name}}---{{slotScope.info.age}}
</div>
</NewVSlot>
新写法,注意新写法的v-slot只能写到template or component上
<NewVSlot>
<div>我是匿名插槽2.6之后写法,新特性</div>
<template v-slot:test>
<div>
我是具名插槽
</div>
</template>
<template v-slot:test2="test2">
<div >
我是具名作用域插槽{{test2.info.name}}---{{test2.info.age}}
</div>
</template>
</NewVSlot>
新写法,简写,使用#代替v-slot:
<NewVSlot>
<div>我是匿名插槽2.6之后写法,新特性,缩写形式</div>
<template #test>
<div>
我是具名插槽
</div>
</template>
<template #test2="test2">
<div >
我是具名作用域插槽{{test2.info.name}}---{{test2.info.age}}
</div>
</template>
</NewVSlot>
总结:
1、 createElement的第一个参数,可以是html标签、组件、全局注册的组件名、一个可以返回html标签、组件、全局注册的组件名的函数 2、 createElement的第二个参数中的,当第一个参数是组件时,props、slot、scopedSlots才有意义
具体demo可查看vue-render
参考链接: https://cn.vuejs.org/v2/guide/render-function.html#createElement-%E5%8F%82%E6%95%B0 https://cn.vuejs.org/v2/guide/components-slots.html#%E5%BA%9F%E5%BC%83%E4%BA%86%E7%9A%84%E8%AF%AD%E6%B3%95