前提
在日常使用vue2开发的过程中,我们大多是使用文档推崇的模版语法的方式进行界面HTML结构的编写
但其实在vue文档中还介绍了另外一个编写HTML的方式:render函数(渲染函数)
由于render函数更接近vue的渲染器底层,所以它具备更好的灵活性,
同时也由于贴近底层,导致写出的代码具备可读性较差,维护难度大的特点。
看完本篇可以学到什么
- render函数在vue中何处调用
- render函数的使用方式
- 为什么需要写render函数
vue2中的render函数
接下来我们讨论的主要是vue2的(runtime-with-compiler),即是带编译器的版本。
在vue2中主要是有两种挂载节点的写法,一种是通过根节点的el属性,另外一种是通过直接调用vue实例的$mount方法。
当本质上在传入el属性时,会自动调用$mount方法进行挂载
src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// 省略。。。。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
官方文档的流程图中有对挂载到dom节点上有具体描述
可以在图中看到,在实例完成init后,即created生命周期后,在挂载之前会有一个阶段是负责处理dom结构的,主要是分为2种情况,
-
判断传入的option是否存在el属性,当存在el属性时,再去判断是否有template属性,如果有则把template属性经过编译器处理成render函数;如果没有则使用el元素的outerHTML内容作为template,与上一步一样,编译器处理后变成render函数
-
判断传入的option是否存在el属性,当不存在el属性时,则在在手动调用mount时,再去判断是否有template属性,如果有则把template属性经过编译器处理成render函数;在(runtime-with-compiler)版本的vue包中,mount函数进行了兼容性处理,如果没有template属性,则使用mount函数传入的el元素的outerHTML内容作为template,与上一步一样,编译器处理后变成render函数
这是2种vue内部处理的render函数来源
还有一种就是用户可以自定义render函数,这是优先级最高的,在我们的vue-cli创建的项目中根实例就运用了这种方式,
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
在vue源码中(src/platforms/web/entry-runtime-with-compiler.js),如果存在render函数,则不会再进去上面的模版编译阶段
可以看到在实际挂载之前,都会保证存在一个render函数,而这个渲染函数的作用是产出当前组件的vnode。
在vue原型上的_render()方法负责调用render函数并返回vnode
src/core/instance/render.js
那可以合理推断,调用_render方法的时机就是调用render函数的时机
那vue在哪里调用了这个_render方法呢
我们又回到了$mount方法
在$mount方法中,主要是调用mountComponent方法
再看看mountComponent方法
可以在mountComponent方法内发现了_render方法的踪迹,可是调用的方式有些特别
在外层还有一个_update方法,这个函数的作用有两个:一是首次渲染时,根据render函数产出的vnode去渲染页面元素;二是在非首次渲染的情况下,再次调用render函数产出新vnode,通过diff算法去比对新旧vnode,对页面元素进行更新与复用。
这里我们注意到有个Watcher类,这是vue中的侦听器,是vue中实现观察者模式重要的一环,这里就不多描述,简单介绍一下作用:这是个渲染器watcher,负责在页面响应式data更新时,触发视图更新,即上面描述的_update方法
到这里,我们主要了解到render函数在vue中的作用,也了解到render函数其实是很贴近底层机制的,页面渲染与视图更新都得依赖渲染函数。
render函数的使用
这里结合vue官网逐一介绍render函数的写法
render函数创建一个div
完整用法
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
message: 'Hello Vue!'
},
render: function (createElement) {
return createElement('div', this.message)
}
}).$mount('#app')
</script>
</body>
这里可以简化一下(后续也是使用这样的用法)
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
message: 'Hello Vue!'
},
render(h) {
return h(
'div', // 标签名称
this.message // 子节点数组
)
}
}).$mount('#app')
</script>
</body>
在调用render函数时会传入一个createElement函数提供给这个render函数消费,
在vue文档中是这样描述的:
createElement
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是createNodeDescription
,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
我们可以简单的理解createElement是用来创建一个vnode的工厂函数,通过传入不同的参数,会产出不同类型的唯一的vnode对象,这些vnode在挂载后与我们的真实dom节点一一对应
createElement参数
createElement(arg1,arg2,arg3)函数可以传入三个参数:
参数1
{String | Object | Function}
一个 HTML 标签名、组件选项对象,或者
resolve 了上述任何一种的一个 async 函数。
必填项。
简单理解就是:
-
当我们传入是一个String类型时,vue会使用该参数作为vnode的标签名tagName,在挂载时会创建一个类型的真实节点,比如createElement('div'),createElement会判断这个参数是否是普通的html标签,比如div,则会创建一个div为tagName的vnode;如果传入的是非普通html标签,则会在vue实例上去寻找是否有该名称的全局组件,如果有则使用该组件的内容创建vnode
-
当我们传入是一个Object类型时,createElement会使用该Object作为子组件的配置,会创建一个子组件vnode
-
当我们传入是一个async 函数类型时,createElement会调用该async 函数,并以它resolve返回的内容按照上面1,2两种情况进行vnode创建,这是vue中异步函数的一种写法
后续会详细介绍用法。
参数2
{Object}
一个与模板中 attribute 对应的数据对象。
可选。
简单理解就是把dom上的属性用key-value的形式传入这个vnode中,这涉及到动态属性,静态属性,绑定的事件
后续会详细介绍用法。
参数3
{String | Array}
子级虚拟节点 (VNodes),由
createElement()
构建而成, 也可以使用字符串来生成“文本虚拟节点”。可选。
-
当我们传入是一个String类型时,createElement会把它当成文本节点进行处理,文本节点也是vnode的一种
-
当我们传入是一个Array类型时,createElement会把它当成多个子vnode进行处理,在挂载时,作为dom的children进行渲染
后续会详细介绍用法。
createElement使用场景
接下来用render函数的写法,来实现我们平时写的模版语法的功能
创建普通标签的vnode
<body>
<div id="app">
<!-- <div>这是一个div</div> -->
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
},
render(h) {
return h(
'div', // 标签名称
'这是一个div'
)
}
}).$mount('#app')
</script>
</body>
创建普通标签带插值的vnode
可以在render函数中直接用this.message,这是因为vue在调用render函数的时候会修改this的指向为当前的vue实例,这样一来在render函数中就可以直接使用vue实例上的插槽,props等元素。
<body>
<div id="app">
<!-- <div>{{message}}</div> -->
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
message:'这是一个div'
},
render(h) {
return h(
'div', // 标签名称
this.message
)
}
}).$mount('#app')
</script>
</body>
创建包含全局组件的vnode
我们在Vue上先注册个全局组件,然后传入这个全局组件的name(String)
注意h('my-component')必须包裹成array的形式传入,即[h('my-component')]
因为它不是一个普通的文本vnode
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
Vue.component('my-component', {
render(h) {
return h('div', 'myComponent')
}
})
new Vue({
data: {
},
render(h) {
return h(
'div', // 标签名称
[h('my-component')]
)
}
}).$mount('#app')
</script>
</body>
创建包含局部组件的vnode
我们创建一个局部组件的option,然后传入我们的h函数的第一个参数中(可以不在component中注册)
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
let myComponent2 = {
name: 'myComponent2',
render(h) {
return h('div', 'myComponent2')
}
}
new Vue({
data: {
},
render(h) {
return h(
'div', // 标签名称
[h(myComponent2)]
)
}
}).$mount('#app')
</script>
</body>
创建包含异步组件的vnode
我们用Promise创建一个异步组件的asyncComponent,setTimeout在两秒后resolve一个异步组件的option
打开控制台可以看到,vue会先用一个注释vnode给asyncComponent占位,
这个页面在2秒后会对这个组件进行渲染,取代这个注释vnode对位置
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
function asyncComponent() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
name: 'asyncComponent',
render(h) {
return h('div', 'asyncComponent')
}
})
}, 2000)
})
}
new Vue({
data: {
},
render(h) {
return h(
'div', // 标签名称
[h(asyncComponent)]
)
}
}).$mount('#app')
</script>
</body>
创建的vnode需要保存唯一
这是render函数的一种约束
下面是官方的例子:
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
},
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 错误 - 重复的 VNode
myParagraphVNode, myParagraphVNode
])
}
}).$mount('#app')
</script>
</body>
这是因为createElement创建出来的vnode是个对象,在数组合中保存的是对象的引用,如果修改了myParagraphVNode的内容,另外一个vnode的内容也会改变,这是我们并不想要的副作用,所以我们可以用工厂函数来处理
下面是官方的例子:
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
},
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
}).$mount('#app')
</script>
</body>
设置vnode的attribute属性
属性的写法非常多样性,这是因为createElement内部对各种写法进行兼容处理
设置普通的 HTML attribute
我们通过设置第二个参数中的attrs来设置vnode的普通属性和自定义属性,
注意attrs必须是object的形式,因为h函数是通过key-value的方式设置属性的
这里提供了几种常见的写法,你们可以在控制台看看效果,
像一些图片懒加载时用的data-Src也是可以设置在这个选项内,
这里还可以看到class与style也是可以在attrs设置的,但是后面还会介绍class与style的一些特殊处理方式
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
id:Math.random(),
activeclass:Math.random(),
},
render(h) {
return h(
'div', // 标签名称
{
// 普通的 HTML attribute
attrs: {
id: 'foo',
'data-id': this.id,
'data-Src':'666',
'data-v-666':'',
class:'a1 '+this.activeclass,
style:'color:red;'
},
},
'这是一个div'
)
}
}).$mount('#app')
</script>
</body>
设置v-bind:class
vue的模版语法中对v-bind:class进行了特殊处理,在render函数中也做了相应的处理
我们通过设置第二个参数中的class来设置vnode的class属性,
注意单独设置了class属性后,attrs的class属性会失效
与 v-bind:class的 API 相同, class接受一个字符串、对象或字符串和对象组成的数组
如果传入的是对象,则必须是(class名--布尔值)的形式,当然传入其他类型会转换成布尔值处理
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
activeclass: Math.random(),
},
render(h) {
return h(
'div', // 标签名称
{
// class: ['1', '2'],
// class: 'a1 ' + this.activeclass,
class: {
foo: true,
bar: 'false'
},
},
'这是一个div'
)
}
}).$mount('#app')
</script>
</body>
设置v-bind:style
与 v-bind:style的 API 相同, 接受一个字符串、对象,或对象组成的数组
注意在style中设置的样式会对attrs中的style进行覆盖,但attrs中独有的样式会保留
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
},
render(h) {
return h(
'div', // 标签名称
{
// style: 'font-size:16px',
style: [{
fontSize: '14px'
}, {
color: 'green'
}],
// style: {
// // color: 'green',
// fontSize: '14px'
// },
},
'这是一个div'
)
}
}).$mount('#app')
</script>
</body>
设置原生DOM property
render函数中允许你绑定如 innerHTML这样的 DOM property (这会覆盖 v-html
指令)。
同时也会使用设置的子vnode失效
这是dom层面上的操作,如果是innerHTML里面设置的dom节点,虚拟dom是无法追踪到的,
无法根据diff算法去高效更新,每次都会重新渲染新的dom
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
message: '这是一个div吗?'
},
render(h) {
return h(
'div', // 标签名称
{
// DOM property
domProps: {
innerHTML: `<h1>${this.message}</h1>`
},
},
'这是一个div'
)
}
}).$mount('#app')
</script>
</body>
设置子组件的prop
与动态class动态style的方式类似,通过props对象的形式传入子组件
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
Vue.component('my-component', {
props: ['message','message2'],
render: function (createElement) {
return createElement('div', [
this.message,this.message2
])
}
})
// 需要编译器
new Vue({
render: function (createElement) {
return createElement('div', [
createElement('my-component', {
props: {
message: 'bar',
message2: 'bar2'
},
})
])
}
}).$mount('#app')
</script>
</body>
设置vnode的key与ref
需要注意的是refInFor: true这个配置
如果你在渲染函数中给多个元素都应用了相同的 ref 名,
那么$refs.myRef会变成一个数组。
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
Vue.component('my-component', {
props: ['message','message2'],
render: function (createElement) {
return createElement('div', [
this.message,this.message2
])
}
})
// 需要编译器
new Vue({
render: function (createElement) {
return createElement('div', [
createElement('my-component', {
props: {
message: 'bar',
message2: 'bar2'
},
// 设置vnode的key与ref
key: 'myKey',
ref: 'myRef',
refInFor: true
})
])
}
}).$mount('#app')
</script>
</body>
设置vnode的directive
这里我们引用一个官网的例子
与自定义指令相关的api可以查看官网
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '<br>' +
'value: ' + s(binding.value) + '<br>' +
'expression: ' + s(binding.expression) + '<br>' +
'argument: ' + s(binding.arg) + '<br>' +
'modifiers: ' + s(binding.modifiers) + '<br>' +
'vnode keys: ' + Object.keys(vnode).join(', ')
}
})
// 需要编译器
new Vue({
render: function (createElement) {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return createElement('div', {
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'demo',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
})
}
}).$mount('#app')
</script>
</body>
设置vnode绑定的事件
在模版语法中我们通过v-on来绑定事件,在render函数中,则是在第二个参数对象的on属性进行事件绑定
普通标签设置on事件
事件监听器在
on
内, 但不再支持如v-on:keyup.enter
这样的修饰器。需要在处理函数中手动检查 keyCode。
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
new Vue({
data: {
},
methods: {
clickHandler(e) {
alert(JSON.stringify(e))
},
mouseenterHandler(){
console.log('mouseenterHandler')
},
},
render(h) {
return h(
'div', // 标签名称
{
on: {
click: this.clickHandler,
mouseenter:this.mouseenterHandler
},
},
'这是一个div'
)
}
}).$mount('#app')
</script>
</body>
组件设置原生on事件
nativeOn仅用于组件,用于监听原生事件,而不是组件内部使用 vm.$emit触发的事件。
这相当于在模版语法中给事件设置native修饰符
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
Vue.component('my-component', {
render(h) {
return h('div', 'myComponent')
}
})
new Vue({
data: {
},
methods: {
clickHandler(e) {
alert(JSON.stringify(e))
},
},
render(h) {
return h(
'div',
[h('my-component', {
nativeOn: {
click: this.clickHandler
},
}),]
)
}
}).$mount('#app')
</script>
</body>
设置事件&按键修饰符
给事件设置修饰符在模版语法中是非常简单的,但是在render函数中写法不太一样
在这里直接引用vue官方文档的描述(偷懒了嘻嘻)
因为已经写得很清楚了
设置vnode的插槽相关配置
在render函数中使用插槽是比较麻烦一点的,而且也不太容易理解
这里我将根据使用场景去介绍render函数中插槽的使用方式
设置组件的默认插槽与具名插槽
与模版语法一样,可以通过this.$slots访问静态插槽的内容,每个插槽都是一个 VNode 数组:
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
Vue.component('my-component', {
render(h) {
return h('div', ['插槽内容:', this.$slots.default, 'test插槽内容:', this.$slots.test])
}
})
new Vue({
data: {
},
methods: {
},
render(h) {
return h(
'div', // 标签名称
[
'text vnode',
h('my-component',
[
h('div', '12345'),
h('div', { slot: 'test', }, ['54321','12345'])
]
)
]
)
}
}).$mount('#app')
</script>
</body>
上面的父组件render函数转换为模版语法写法如下:
<div>
<my-component>
<div>12345</div>
<div slot="test" >5432112345</div>
</my-component>
</div>
上面的子组件render函数转换为模版语法写法如下:
<div>
插槽内容:
<slot></slot>
test插槽内容:
<slot name="test"></slot>
</div>
注意当你需要指定某个vnode是子组件的具名插槽内容时,需要设置 { slot: '具名插槽name'}
设置组件的作用域插槽
在子组件中使用this.$scopedSlots,
向父组件传入插槽的vnode提供子组件的数据,
父组件使用scopedSlots{}通过render函数的形式消费子组件的数据
<body>
<div id="app"></div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
Vue.component('my-component', {
props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
})
new Vue({
render: function (createElement) {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return createElement('div', [
createElement('my-component', {
// 在数据对象中传递 `scopedSlots`
// 格式为 { name: props => VNode | Array<VNode> }
props: {
message: 'bar'
},
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
}).$mount('#app')
</script>
</body>
使用 JavaScript 代替模板功能
在模版语法中,vue给开发者提供了非常多的默认指令方便我们使用,
但是在render函数中,我们需要使用 JavaScript 手动实现这些指令的功能
v-if
在模版语法中模拟v-if的实现还是比较容易的,使用JavaScript的条件控制语句已经可以满足大部分需求了
三目运算符
render(h) {
return h('div', [this.is ? h('div', '这是1') : h('div', '这是2')])
}
if-else与工厂函数结合
render(h) {
const createEle =()=>{
if(this.is){
return h('div', '这是1')
}else{
return h('div', '这是2')
}
}
return h('div', [createEle()])
}
v-for
在模版语法中模拟v-for的实现方式也很多
主要是通过js生成一个vnode的数组即可
我们这里使用Array的map方法来模拟v-for
data() {
return {
mockList:['test','test','test','test']
}
},
render(h) {
return h('div', this.mockList.map((item,index)=>{
return h('div',`${item}的index是${index}`)
}))
}
当然还可以使用es6的拓展运算符在同一个div里模拟两次v-for
data() {
return {
mockList: ['test', 'test', 'test', 'test']
}
},
render(h) {
return h('div',
[
...this.mockList.map((item, index) => {
return h('div', `${item}的index是${index}`)
}),
...this.mockList.map((item, index) => {
return h('span', `span的index是${index}`)
})
]
)
}
v-model
对于在render函数里使用v-model,vue官方给出了这样的解决方式
渲染函数中没有与
v-model
的直接对应——你必须自己实现相应的逻辑这就是深入底层的代价,但与
v-model
相比,这可以让你更好地控制交互细节。
在render函数中需要手动去实现vue的响应式逻辑,即通过prop与emit的形式
官方的例子:
props: ['value'],
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
}
}
})
}
render函数在函数式组件的运用
在react中,函数式组件运用得常见非常多
但在使用vue的日常的业务开发中运用得比较少
但其实函数式组件在vue2中性能是由于带状态的组件的,所以我们可以用函数式组件去进行项目优化
在函数式组件中render函数的使用有所不同
下面是一个简单的例子:
<body>
<div id="app">
</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js'
Vue.component('my-component', {
functional: true,
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
console.log(context)
let slots = context.slots()
return createElement('div',[
createElement('div','message:'+context.props.message),
context.children,
slots.default,
slots.test
])
}
})
// 需要编译器
new Vue({
data: {
message: '这是一个div'
},
render(h) {
return h(
'div', // 标签名称
[
h('my-component', {
props: {
message: this.message
},
},
[
h('div', '默认插槽'),
h('div', { slot: 'test', }, '具名插槽')
]
)
]
)
}
}).$mount('#app')
</script>
</body>
可以发现,由于函数式组件没有自身的状态,所以在render函数添加了第二个参数conText,作为上下文。
conText中包含父组件传入的属性与vnode,函数式组件将根据这些参数切换自己渲染的内容。
context的内容包括:
props
:提供所有 prop 的对象children
:VNode 子节点的数组slots
:一个函数,返回了包含所有插槽的对象scopedSlots
:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。data
:传递给组件的整个数据对象,作为createElement
的第二个参数传入组件parent
:对父组件的引用listeners
:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是data.on
的一个别名。injections
:(2.3.0+) 如果使用了inject
选项,则该对象包含了应当被注入的 property。
这里需要注意一个地方就是,我们需要把父组件传递进来的参数进行透传,在vue官方文档中的例子是这样的
Vue.component('my-functional-button', {
functional: true,
render: function (createElement, context) {
// 完全透传任何 attribute、事件监听器、子节点等。
return createElement('button', context.data, context.children)
}
})
把 context.data传入第二个参数完成透传
render函数的一些运用
递归渲染树形结构
用模版语法时,单一组件内部做到递归渲染比较困难
但是在render函数里就可以做到
data: {
tree_mock: [{
name: 't1',
children: [
{
name: 't2',
children: [
{
name: 't5',
children: [
{
name: 't7',
children: [
]
},
]
},
{
name: 't6',
children: [
]
},
]
},
{
name: 't3',
children: [
]
},
{
name: 't4',
children: [
]
},
]
}, {
name: 't8',
children: [
]
}],
},
render(h) {
// 递归写法
const createTree = (treeNodeList) => {
return treeNodeList.length ? treeNodeList.map(element => {
return h('ul', [
h('li', element.name),
createTree(element.children)
])
}) : '';
}
return h(
'div',
createTree(this.tree_mock)
)
}
动态渲染标签类型
这是vue官方的一个例子,运用render函数优化了模版语法繁琐的写法
模版语法
<script type="text/x-template" id="anchored-heading-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>
</script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
render函数:
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
可见render函数在某些场景还是非常简洁高效的
element中render函数的运用(JSX语法)
纯render函数代码在可读性上不太理想
JSX则是解决这个问题的优秀解决方案
vue文档中描述
如果你写了很多
render
函数,可能会觉得这样的代码写起来很痛苦这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
将
h
作为createElement
的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 Vue 的 Babel 插件的 3.4.0 版本开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入const h = this.$createElement
,这样你就可以去掉(h)
参数了。对于更早版本的插件,如果h
在当前作用域中不可用,应用会抛错。
下面这是element中render函数的运用:
label-wrap.vue
render() {
const slots = this.$slots.default;
if (!slots) return null;
if (this.isAutoWidth) {
const autoLabelWidth = this.elForm.autoLabelWidth;
const style = {};
if (autoLabelWidth && autoLabelWidth !== 'auto') {
const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
if (marginLeft) {
style.marginLeft = marginLeft + 'px';
}
}
return (<div class="el-form-item__label-wrap" style={style}>
{ slots }
</div>);
} else {
return slots[0];
}
},
这是element中form表单的label组件
由于依赖的响应式数据比较多,但组件实际展示内容并不复杂
如果使用模版语法去编写可能会有较多的代码量
这里使用JSX结合render函数只需要不到20行代码即可完成
总结
在我们的日常开发中,可能render函数使用得比较少,但是学会它可以拓展你编写组件的能力
在一些简易的组件和函数式组件中,使用render函数也是不错的选择
尤其在引入编译-运行时vue包的普通项目中,使用render函数去代替模版语法,可以大大优化项目。