Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力,这就是 render 函数,它比 template 更接近编译器。
这是引用Vue官方文档的一句话,确实在很多情况下我们都需要掌握 render 函数,它更加灵活多变,但也是很容易写错(经常报错的我 o(////▽////)q ),在这里记录下自己学习render的笔记。
Vue官方文档栗子
- 使用 template 创建HTML
<script type="text/x-template" id="anchored-heading-template">
<div>
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-if="level === 2">
<slot></slot>
</h2>
<h3 v-if="level === 3">
<slot></slot>
</h3>
<h4 v-if="level === 4">
<slot></slot>
</h4>
<h5 v-if="level === 5">
<slot></slot>
</h5>
<h6 v-if="level === 6">
<slot></slot>
</h6>
</div>
</script>
- 使用 render 函数创建HTML
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name 标签名称
this.$slots.default // 子组件中的阵列
)
},
props: {
level: {
type: Number,
required: true
}
}
})
从官方的栗子上看,很明显的可以看出在某些情况下,render 函数比 template 有无法比拟的优越性。在一些结构复杂或者逻辑较多的html中,建议还是使用 render 函数来创建比较好。
必须了解的知识
slot
- Props:
- name - string, 用于命名插槽。
- Usage:
- <slot> 元素作为组件模板之中的内容分发插槽。 <slot> 元素自身将被替换。
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 “transclusion” 如果你熟悉 Angular)。Vue.js 实现了一个内容分发 API,参照了当前 Web 组件规范草案,使用特殊的 <slot> 元素作为原始内容的插槽。
slot 是在编写组件的时候最经常使用的,它的作用是什么呢?浅显的说就是占位,或者官方文档所说的插槽。就好像一个电脑主板,有很多插槽,你可以选择使用或者不使用,你也可以决定自己使用什么型号和品牌,slot 就是起到这样一个插槽的作用。
官方栗子
<!--假定 <my-component> 组件有下面模板:-->
<div>
<h2>我是子组件的标题</h2>
<slot>
只有在没有要分发的内容时才会显示。
</slot>
</div>
<!--父组件模版:-->
<div>
<h1>我是父组件的标题</h1>
<my-component>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</my-component>
</div>
<!--渲染结果:-->
<div>
<h1>我是父组件的标题</h1>
<div>
<h2>我是子组件的标题</h2>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</div>
</div>
slot元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。仍然可以有一个匿名 slot,它是默认 slot,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot,这些找不到匹配的内容片段将被抛弃。
打个比方,例如电脑主板上有一个内存条插槽,一个独立显卡插槽,你可以只使用安装一根内存条,而不使用显卡插槽,这是完全没有任何影响的。
<!--假定我们有一个 app-layout 组件,它的模板为:-->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!--父组件模版:-->
<app-layout>
<h1 slot="header">这里可能是一个页面标题</h1>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
<p slot="footer">这里有一些联系信息</p>
</app-layout>
<!--渲染结果为:-->
<div class="container">
<header>
<h1>这里可能是一个页面标题</h1>
</header>
<main>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
</main>
<footer>
<p>这里有一些联系信息</p>
</footer>
</div>
createElement 参数
createElement的参数包括3个部分
- 第一个参数是HTML 标签字符串(必需的参数)
- 第二个参数是包含模板相关属性的数据对象(可为空object)
- 第三个参数HTML的子节点(可为空。是一个createElement的数组或者是简单的使用字符串来生成“文本结点”)
createElement(
// {String | Object | Function}
// 一个 HTML 标签字符串,组件选项对象,或者一个返回值类型为String/Object的函数,必要参数
'div',
{
// {Object}
// 一个包含模板相关属性的数据对象
// 这样,您可以在 template 中使用这些属性.可选参数.
},
[
// {String | Array}
// 子节点 (VNodes),由 `createElement()` 构建而成,
// 或简单的使用字符串来生成“文本结点”。可选参数。
]
)
完整参数如下
createElement(
// {String | Object | Function}
// 一个 HTML 标签字符串 ,组件选项对象,或者一个返回值类型为String/Object的函数,必要参数
'div',
// {Object}
// 一个包含模板相关属性的数据对象
// 这样,您可以在 template 中使用这些属性.可选参数.
//这些参数就好比你在template上定义的属性
{
// 和`v-bind:class`一样的 API
'class': {
foo: true,
bar: false
},
// 和`v-bind:style`一样的 API
style: {
color: 'red',
fontSize: '14px'
},
// 正常的 HTML 特性
attrs: {
id: 'foo'
},
// 组件 props
props: {
myProp: 'bar'
},
// DOM 属性
domProps: {
innerHTML: 'baz'
},
// 事件监听器基于 `on`
// 所以不再支持如 `v-on:keyup.enter` 修饰器
// 需要手动匹配 keyCode。
on: {
click: this.clickHandler
},
// 仅对于组件,用于监听原生事件,而不是组件内部使用 `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令. 注意事项:不能对绑定的旧值设值
// Vue 会为您持续追踪
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// Scoped slots in the form of
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其他组件的子组件,需为 slot 指定名称
slot: 'name-of-slot',
// 其他特殊顶层属性
key: 'myKey',
ref: 'myRef'
},
// {String | Array}
// 子节点 (VNodes),由 `createElement()` 构建而成,
// 或简单的使用字符串来生成“文本结点”。可选参数。
[
'先写一些文字', //文本节点
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
createElement中的事件监听(on)可以使用事件 & 按键修饰符
实际开发
在平时开发中,我们更常见的是render: h => h(App)
这种形式。其实这只是将h作为createElement的别名是一个通用惯例
简单的栗子
常见的写法
Vue.component('mySelect', {
render: (h) => {
// 父节点 select
return h('select',
//select的相关属性的数据对象
{
on:{
'change':(val)=>{
}
}
},
//两个子节点option
[
h('option',
//option的相关属性的数据对象
{
domProps: {
value: 0,
innerHTML: '男'
}
}
//没有子节点,所以第三个参数为空
),
h('option', {
domProps: {
value: 1,
innerHTML: '女'
},
})
]
)
})
//mySelect的渲染结果
<select>
<option value ="0">男</option>
<option value ="1">女</option>
</select>
v-for 循环
当然,作为组件肯定不会把内容都写死,需要根据参数来决定渲染结果,那么下来看一个相当于V-for的render。 render 函数循环创建需要借助map函数,在这里需要注意的是,只用map函数才能检测数据变化,其他方法不能根据数据的改变而改变
Vue.component('myNewSelect', {
// props
props:{
items:{
type:Array,
default: []
}
},
render: (h) => {
// 父节点 select
return h('select',
Array.apply(null, this.items.map(function(item){
return h('option',
{
domProps: {
value: item.value,
innerHTML: item.name
},
})
})
)
)
}
})
//假设
items = [
{
value: 0,
name: '第一个'
},
{
value: 1,
name: '第二个'
},
{
value: 2,
name: '第三个'
},
]
//<myNewSelect :items=items></myNewSelect>的渲染结果
<select>
<option value ="0">第一个</option>
<option value ="1">第二个</option>
<option value ="3">第三个</option>
</select>
v-if 判断
下面是 v-if 的 render 函数,要实现v-if的效果其实也很简单,只需要在return前判断下。
Vue.component('myList', {
props:{
items:{
type:Array,
default: []
}
},
render: (h) => {
if(items.length > 0){
return h('ul',
Array.apply(null, this.items.map(function(item){
return h('li',
{
domProps: {
innerHTML: item.name
},
})
})
)
)
}else{
return h('p', '没有数据')
}
}
})
v-model 双向绑定
render 函数的双向绑定的方法貌似有两个(可能有更多)
- 直接借助$parent的方法操作父元素的数据或方法(不赞同使用)
- 借助父子组件中的$emit来实现, 在render 函数中监听数据的变化并通过$emit向上传递事件
下面是通过$emit实现双向绑定的栗子
Vue.component('myInput', {
props:{
value:{
type: String,
default: ''
}
},
render: (h) => {
let _this = this;
return h('input',{
domProps:{
value:_this.value
},
on: {
input: function (event) {
_this.change(event.target.value);
}
}
})
},
methods:{
change(value){
this.$emit('valueChange', value);
}
}
})
父组件
<myInput :value=value></myInput>
var app=new Vue({
el:'#app',
data:{
value: 'default'
},
methods:{
valueChange(value){
this.value = value;
}
}
})
slot 内容分发
render函数中使用slot其实跟template差不多,只需要在h()函数里为 slot 指定名称即可,更直接的直接将slot的内容拿来渲染。
在render函数中,可以通过访问$slots来获取所有的slot
vm.$slots 用来访问被 slot 分发的内容。每个具名 slot 有其相应的属性(例如:slot="foo" 中的内容将会在 vm.$slots.foo 中被找到)。default 属性包括了所有没有被包含在具名 slot 中的节点。
- 指定slot的名称
Vue.component('mySlot', {
render: (h) => {
let _this = this;
return h('div',{
slot: 'name-of-slot'
})
}
})
- 通过$slots访问(官方栗子)
Vue.component('blog-post', {
render: function (createElement) {
var header = this.$slots.header
var body = this.$slots.default
var footer = this.$slots.footer
return createElement('div', [
createElement('header', header),
createElement('main', body),
createElement('footer', footer)
])
}
})
最后
没有什么特别的需求的话,还是别折腾render函数吧,老老实实用template,render函数还是蛮折腾人的,一不留神就报错了(当然也是因为我的技术渣渣啊 (╥╯^╰╥) )