Vue3-05 渲染函数

Vue3学习笔记-05 渲染函数

为什么需要渲染函数

Vue推荐多数情况使用模板创建HTML,但是在一些情况下使用渲染函数会更加方便,例如,例如要生成带有锚点的标题

1
2
3
4
5
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>

将这段代码封装为组件,传入level作为参数,level取值1 - 6,对应H1 - H6,如果使用模板需要借助slot并且配合大量的if...else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</template>

模板很啰嗦,可以考虑使用渲染函数来进行优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script lang="ts">
import {defineComponent, h} from 'vue';

export default defineComponent({
name: 'anchored-heading',
props: {
level: {
type: Number,
required: true
}
},
render() {
const tag = `h${this.level}`;
return h(tag, {}, this.$slots.default!());
}
});
</script>

代码简洁了许多,但是要深入使用渲染函数前,需要连接实例的Property,才能将模板中的指令和参数在渲染函数中找到

实例Property API

  • $data:组件实例正在监听的数据对象,等于选项API中的data
  • $props:组件接收到的props对象,等于选项API的props
  • $el:组件实例正在使用的根DOM元素,建议使用ref模板引用来直接访问DOM元素,而不是依赖于$el
  • $options:组件实例的初始化选项,当在选项中定义了自定义属性时,可以使用$options来访问
  • $parent:组件实例的父实例
  • $root:组件实例的根组件实例,如果当前实例没有父实例,返回组件自己
  • $slots:返回通过插槽分发的内容,$slots.default()返回模板插槽的内容,具名slot都会在对应的$slots下找到,注意这是一个函数,需要()调用后返回结果
  • $refs:返回组件注册过ref属性的DOM元素和组件实例(与组合式API的ref没有关系)
  • $attrs:返回了父作用域中不作为组件props或自定义事件的attribute绑定和事件

h函数

h函数用来创建虚拟Dom,它会返回一个VNode,它接受三个参数:h(tag, props, children)

  • tag:必须,可以是HTML标签,也可以是异组件、异步组件或者null(返回null会渲染注释)
  • props:可选的,与attributeprops和事件对应的对象,会在模板(组件或HTML标签)中使用
  • children:可选的,可以是通过h创建的子VNode,也可以是字符串,或者是插槽对象
1
2
3
4
5
h('div', {}, ['Hello', h('h1', 'Title']);

h('div', ['Hello']);

h('div', ['Hello', h('h1', 'Title']);

约束

VNodes必须唯一

组件数的所有的VNodes必须唯一,所以下面的渲染函数是不合法的:

1
2
3
4
5
6
7
render() {
const myParagraphVNode = h('p', 'hi')
return h('div', [
// 错误 - 重复的Vnode!
myParagraphVNode, myParagraphVNode
])
}

官网文档中说明上面的渲染函数是非法的,但是实际上我尝试后发现,控制台并没有报错,而且渲染也是成功了。但是使用文档建议的工厂函数,也是可行的(2021-05-06)

改为使用工厂函数:

1
2
3
4
5
6
render() {
const myParagraphVNodes = Array.from({length: 20}).map(() => {
return h('p', 'hi');
});
return h('div', myParagraphVNodes);
}

使用JavaScript代替模板功能

v-forv-if

v-if是ioyngif...else代替,v-for使用map代替

v-model

组件的v-model需要在h的第二个参数中,传入modelValue属性和update:modelValue方法作为VNode的Prop:

1
2
3
4
5
6
7
8
props: ['modelValue'],
emits: ['update:modelValue'],
render() {
return h(SomeComponent, {
modelValue: this.modelValue,
'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
})
}

v-on

我们需要为模板的事件处理程序提供正确的Prop名称,例如@click的Prop名为onClick@input的Prop名为onInput

1
2
3
4
5
render() {
return h('div', {
onClick: $event => console.log('clicked', $event.target)
})
}

事件修饰符

对于.passive.capture.once事件修饰符,使用的时候需要已驼峰的形式,将它们凭借在事件名后面:

1
2
3
4
5
6
7
render() {
return h('input', {
onClickCapture: this.doThisInCapturingMode,
onKeyupOnce: this.doThisOnce,
onMouseoverOnceCapture: this.doThisOnceInCapturingMode
})
}

对于其他修饰符,需要在事件处理函数中自行实现(因为其他修饰符实际上就是语法糖)

插槽

使用this.$slots来访问静态插槽内容,每个插槽都是一个VNode数组

1
2
3
4
render() {
// `<div><slot></slot></div>`
return h('div', {}, this.$slots.default())
}

slots中对应的插槽名被调用时,可以传入一个对象作为传入,来实现作用域插槽的功能:

使用模板时:

1
2
3
4
5
<!-- todo-list -->
<slot message="333"></slot>

<!-- parent -->
<todo-list v-slot:default="slotProps">{{slotProps.message}}</todo-list>

使用渲染函数时:

1
2
3
4
5
render() {
return h('div', {}, this.$slots.default({
text: 333
}))
}

JSX

为了简化h函数的使用,可以使用jsx-next这个Babel插件,用于在Vue使用JSX语法

1
2
3
4
5
6
7
8
9
10
11
12
13
import AnchoredHeading from './AnchoredHeading.vue'

const app = createApp({
render() {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})

app.mount('#demo')

什么时候使用JSX

在动态性强的场景下,JSX 会有一定优势。