Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
了解render函数的用法,可以先查看官方文档 渲染函数 & JSX。
1、首先看一个初级的示例:
这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了<slot></slot>
,在要插入锚点元素时还要再次重复。
<template>
<div>
<div v-if="level === 1">
<slot></slot>
</div>
<p v-else-if="level === 2">
<slot></slot>
</p>
<h1 v-else-if="level === 3">
<slot></slot>
</h1>
<h2 v-else-if="level === 4">
<slot></slot>
</h2>
<strong v-else-if="level === 5">
<slot></slot>
</stong>
<textarea v-else>
<slot></slot>
</textarea>
</div>
</template>
<script>
export default {
props: {
level: {
type: Number,
required: true
}
}
};
</script>
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render
函数重写上面的例子:
<template>
<div>
<child :level="level">Hello world!</child>
</div>
</template>
<script>
import Vue from 'vue'
Vue.component('child', {
render(createElement) {
const tag = ['div', 'p', 'strong', 'h1', 'h2', 'textarea'][this.level-1]
return createElement(tag, this.$slots.default)
},
props: {
level: {
type: Number,
required: true
}
}
})
export default {
name: 'hehe',
data() {
return {
level: 3
}
}
}
</script>
这样看起来代码就精简多了,但是需要非常熟悉 Vue 的实例属性。在这个例子中,你需要知道,向组件中传递不带 v-slot
指令的子节点时,这些子节点被存储在组件实例中的 $slots.default
中,否则具名插槽可通过 $slots.插槽名
称来指定。
可以看到,render
函数接收一个参数createElement
,然后Vue 通过建立一个虚拟 DOM
(VNode
)来追踪自己要如何改变真实 DOM
。
createElement
函数中使用模板中的那些功能,它接受的参数如下:
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
"div",
// {Object}
// 一个与模板中属性对应的数据对象。可选。
{
// 与 `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 属性
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",
// 其它特殊顶层属性
key: "myKey",
ref: "myRef",
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true,
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
"先写一些文字",
createElement("h1", "一则头条"),
createElement(MyComponent, {
props: {
someProp: "foobar",
},
}),
]
);
2、父子template
组件通过render
方法实现:
首先初始单文本组件如下,来模拟一个简单的TODO页面:
// 父组件:Todo.vue
<template>
<div class="todo">
<input type="text" v-model="content" placeholder="接下来的计划..." />
<button @click="commit">提交</button>
<todo-list :todoList="todoList">待办事项:</todo-list>
</div>
</template>
<script>
import TodoList from "./TodoList.vue";
export default {
name: "Todo",
data() {
return {
content: "",
todoList: []
};
},
methods: {
commit() {
let id = this.todoList.length + 1;
this.todoList.push({ id: id, title: this.content });
this.content = '';
},
},
components: {
TodoList
},
};
</script>
<style lang="less" scoped>
.todo {
width: 500px;
margin: 50px auto;
input {
padding: 5px 10px;
}
button {
margin-left: 20px;
padding: 5px 10px;
}
}
</style>
// 子组件:TodoList.vue
<template>
<div class="todo-list">
<slot></slot>
<ul>
<li v-for="item in todoList" :key="item.id">{{ item.title }}</li>
</ul>
</div>
</template>
<script>
export default {
name: "TodoList",
props: ["todoList"]
};
</script>
<style lang="less" scoped>
.todo-list {
margin-top: 20px;
padding-left: 0;
li {
margin: 5px 0;
list-style: none;
}
}
</style>
通过以上是现实了如下页面:
接下来尝试使用render
函数实现以上功能页面:
- 发现如果
template
和render
函数同时存在时,Vue还是会优先使用template
中的内容。
父组件Todo.vue:
<!--<template>
<div class="todo">
<input type="text" v-model="content" placeholder="接下来的计划..." />
<button @click="commit">提交</button>
<todo-list :todoList="todoList">{{ listTitle }}</todo-list>
</div>
</template>-->
<script>
import TodoList from "./TodoList.vue";
export default {
name: "Todo",
data() {
return {
content: "",
todoList: [],
listTitle: "待办事项:",
};
},
methods: {
commit() {
let id = this.todoList.length + 1;
this.todoList.push({ id: id, title: this.content });
this.content = "";
},
},
render(createElement) {
var self = this; // 定义self保存this,使之始终指向vue示例
return createElement(
"div",
{
// 接受一个字符串、对象或字符串和对象组成的数组
class: {
todo: true, // 通过对象定义class是否启用
}
},
[
createElement("input", {
// DOM 属性
domProps: {
type: "text",
placeholder: "接下来的计划...",
value: self.content, // 将content属性值赋值给输入框
},
// 普通的 HTML attribute
// attrs: {
// type: "text",
// placeholder: "接下来的计划...",
// value: self.content, //
// },
on: {
change: function(event) {
self.content = event.target.value; // 输入框值赋给content属性
},
},
}),
createElement(
"button",
{
// 给按钮添加click事件,触发commit方法
on: {
click: this.commit,
}
},
"提交"
),
createElement(
// 子组件选项对象
"todo-list",
{
// props传值给子组件
props: {
todoList: this.todoList,
}
},
this.listTitle
),
]
);
},
components: {
TodoList,
},
};
</script>
<style lang="less" scoped>
.todo {
width: 500px;
margin: 50px auto;
input {
padding: 5px 10px;
}
button {
margin-left: 20px;
padding: 5px 10px;
}
}
</style>
子组件TodoList.vue:
<!--<template>
<div class="todo-list">
<slot></slot>
<ul>
<li v-for="item in todoList" :key="item.id">{{ item.id }}. {{ item.title }}</li>
</ul>
</div>
</template>-->
<script>
export default {
name: "TodoList",
props: ["todoList"],
render(createElement) {
return createElement(
"div",
{
class: {
'todo-list': true,
}
},
[
this.$slots.default,
createElement(
"ul",
this.todoList.map(function(item) {
return createElement(
"li",
{
key: item.id,
},
`${item.id}. ${item.title}`
);
})
),
]
);
},
};
</script>
<style lang="less" scoped>
.todo-list {
margin-top: 20px;
padding-left: 0;
li {
margin: 5px 0;
list-style: none;
}
}
</style>
3、小结
-
createElement
createElement
,是 Vue 虚拟DOM
的概念,创建出来的并不是html
节点,而是VNode
的一个类,类似DOM
结构的一个结构,并存在内存中,它会和真正的DOM
进行对比,若发现需要更新的DOM
,才会去转换这部分DOM
内容,并填到真正的DOM
中,从而提高性能; -
nativeOn 与 on 的区别
对于nativeOn
,官方的解释是:仅对于组件,用于监听原生事件,而不是组件内部使用vm.$emit
触发的事件。
解释比较抽象,个人理解:
父组件要在子组件上使用click
事件,就像使用正常的html
标签那样使用click
,我们知道在Vue中,普通html
标签中这样写click
事件是没问题:<h @click="doSomething()"></h>
但假如我们有一个组件叫comA,直接使用
click
是不行的(除非子组件里面做了处理),加上了.native
就可以生效:<comA @click="doSomething()"></comA> // 无效 <comA @click.native="doSomething()"></comA> // 有效
所以,仅用于组件这句话意思应该是:
createElement()
里面使用nativeOn
创建的不可以是原生html
元素而是组件,比如:createElement("p", { nativeOn: { click: function() {} } })
这个时候
nativeOn
就没有意义,而下面写法就会有意义:createElement("组件名称", { nativeOn: { click: function() {} } })
在该组件
根节点
上发生了点击事件会触发nativeOn
里面的click
事件。