在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力,这时渲染函数就派上用场了。
一、创建 VNode
VNode 是 Vue 对页面 DOM 节点的描述, 其是一个 Object 类型
Vue 提供了一个 h()
函数用于创建 VNode:
js
import { h } from 'vue'
const vnode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
[
/* children */
]
)
h()
是 hyperscript 的简称------意思是"能生成 HTML (超文本标记语言) 的 JavaScript"。一个更准确的名称应该是 createVnode()
,但当你需要多次使用渲染函数时,一个简短的名字会更省力。
h()
函数的使用方式非常的灵活:
js
// 除了类型必填以外,其他的参数都是可选的
h('div')
h('div', { id: 'foo' })
// attribute 和 property 都能在 prop 中书写
// Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' })
// 像 `.prop` 和 `.attr` 这样的的属性修饰符
// 可以分别通过 `.` 和 `^` 前缀来添加
h('div', { '.name': 'some-name', '^width': '100' })
// 类与样式可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')
// 没有 props 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])
// children 数组可以同时包含 vnodes 与字符串
h('div', ['hello', h('span', 'hello')])
h()
函数接收三个参数
js
// @returns {VNode}
h(
// {String | Object | Function | null} tag
// 一个 HTML 标签名、一个组件、一个异步组件,或者 null。
// 使用 null 将会渲染一个注释。
//
// 必需的。
'div',
// {Object} props
// 与 attribute、prop 和事件相对应的对象。
// 我们会在模板中使用。
//
// 可选的。
{},
// {String | Array | Object} children
// 子 VNodes, 使用 `h()` 构建,
// 或使用字符串获取 "文本 Vnode" 或者
// 有插槽的对象。
//
// 可选的。
[
'Some text comes first.',
h('h1', 'A headline'),
h(MyComponent, {
someProp: 'foobar'
})
]
)
二、声明渲染函数
当组合式 API 与模板一起使用时,setup()
钩子的返回值是用于暴露数据给模板。然而当我们使用渲染函数时,可以直接把渲染函数返回:
js
import { ref, h } from 'vue'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// 返回渲染函数
return () => h('div', props.msg + count.value)
}
}
在 setup()
内部声明的渲染函数天生能够访问在同一范围内声明的 props 和许多响应式状态。
除了返回一个 vnode,你还可以返回字符串或数组:
js
export default {
setup() {
return () => 'hello world!'
}
}
js
import { h } from 'vue'
export default {
setup() {
// 使用数组返回多个根节点
return () => [
h('div'),
h('div'),
h('div')
]
}
}
下面来看一个简单的例子:
js
<script>
import { defineComponent, h } from "vue";
export default defineComponent({
render() {
const props = { style: { color: "red" } };
const small = h("small", "副标题");
return h("h2", props, ["123456789", small]);
},
});
</script>
注意:组件树中的 vnodes 必须是唯一的,下面是错误示范:
js
function render() {
const p = h('p', 'hi')
return h('div', [
// 啊哦,重复的 vnodes 是无效的
p,
p
])
}
如果想在页面上渲染多个重复的元素或者组件,可以使用一个工厂函数来做这件事
js
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
三、渲染组件并实现双向绑定
h()
函数还可以渲染 "组件", 假设我们有一个 switch 组件, 其支持 <switch v-model:checked="checked"/>
.
js
<script>
import { ref, h } from 'vue'
import ASwitch from "../components/ASwitch.vue";
export default {
props: {
/* ... */
},
setup(props) {
const checked = ref(false)
// 返回渲染函数
return () => h(ASwitch)
}
}
</script>
此时会发现点击切换不了,这是因为没有像在模板中那样使用 v-model
,需要在 "h" 中通过第2 个参数传入 checked
属性和 onUpdate:checked
事件实现 v-model
的等同操作
js
<script>
import { ref, h } from 'vue'
import ASwitch from "../components/ASwitch.vue";
export default {
props: {
/* ... */
},
setup(props) {
const checked = ref(false)
// 返回渲染函数
return () => h(ASwitch, {
checked: checked.value,
["onUpdate:checked"]: (checked) => {
checked.value = checked;
},
});
},)
}
}
</script>
四、渲染函数官网案例
1. v-if
js
<div>
<div v-if="ok">yes</div>
<span v-else>no</span>
</div>
// 等同于
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
2. v-for
js
<ul>
<li v-for="{ id, text } in items" :key="id">
{{ text }}
</li>
</ul>
// 等同于
h(
'ul',
// assuming `items` is a ref with array value
items.value.map(({ id, text }) => {
return h('li', { key: id }, text)
})
)
3. v-on
以 on
开头,并跟着大写字母的 props 会被当作事件监听器。比如,onClick
与模板中的 @click
等价。
js
h(
'button',
{
onClick(event) {
/* ... */
}
},
'click me'
)
4. 渲染插槽
js
export default {
props: ['message'],
setup(props, { slots }) {
return () => [
// 默认插槽:
// <div><slot /></div>
h('div', slots.default()),
// 具名插槽:
// <div><slot name="footer" :text="message" /></div>
h(
'div',
slots.footer({
text: props.message
})
)
]
}
}
5. 传递插槽
js
// 单个默认插槽
h(MyComponent, () => 'hello')
// 具名插槽
// 注意 `null` 是必需的
// 以避免 slot 对象被当成 prop 处理
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
五、实例:Modal.confirm 中使用渲染函数
下面来看一下我在公司的管理后台项目中遇到的一个问题。首先在 ant-design-vue 组件库中的对话框组件有使用到 createVNode
,代码如下:
js
<template>
<div>
<a-button @click="showConfirm">Confirm</a-button>
</div>
</template>
<script lang="ts">
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { createVNode, defineComponent } from 'vue';
import { Modal } from 'ant-design-vue';
export default defineComponent({
setup() {
const showConfirm = () => {
Modal.confirm({
title: () => 'Do you Want to delete these items?',
icon: () => createVNode(ExclamationCircleOutlined),
content: () => createVNode('div', { style: 'color:red;' }, 'Some descriptions'),
onOk() {
console.log('OK');
},
onCancel() {
console.log('Cancel');
},
class: 'test',
});
};
return {
showConfirm
};
},
});
</script>
下面我们公司新增了一个需求,需要在确认框中放入一个 select
下拉框,其实我们可以改造下确认框,直接使用 a-modal
即可,这样会方便许多;那么在这里我将使用 createVNode
来完成插入下拉框的需求,代码上会复杂许多,但是能够提高对渲染函数的理解。
js
Modal.confirm({
title: () => '修改所有用户的属性',
closable: true,
centered: true,
content: (h) => {
let Option = []
for (let i in self.addOption) {
Option.push(h('a-select-option', {
props: {
value: self.addOption[i].type
}
}, self.addOption[i].title))
}
let Select = h('div', {style: {padding: '10px 0'}}, [h('a-select', {
props: {
value: self.form.addType,
},
style: {
width: '100%'
},
on: {
input: (val) => {
self.form.addType = val
},
change: (v) => {
self.form.addType = v
}
}
}, Option)])
let Input = h('div', {style: {padding: '10px 0'}}, [h('a-input', {
props: {
value: self.form.editValue,
placeholder: '请输入值'
},
style: {
width: '100%'
},
on: {
input: (val) => {
self.form.editValue = val
},
change: (v) => {
self.form.editValue = v
}
}
})])
return h('div', {style: {padding: '10px 0'}}, [Select, Input])
},
onOk() {
console.log('OK');
},
onCancel() {
console.log('Cancel');
},
class: 'test',
});
最终效果如下: