「学习记录」vue2的render函数你用过吗?

2,504 阅读10分钟

前提

在日常使用vue2开发的过程中,我们大多是使用文档推崇的模版语法的方式进行界面HTML结构的编写

但其实在vue文档中还介绍了另外一个编写HTML的方式:render函数(渲染函数)

由于render函数更接近vue的渲染器底层,所以它具备更好的灵活性,

同时也由于贴近底层,导致写出的代码具备可读性较差,维护难度大的特点。

看完本篇可以学到什么

  1. render函数在vue中何处调用
  2. render函数的使用方式
  3. 为什么需要写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节点上有具体描述

image.png

可以在图中看到,在实例完成init后,即created生命周期后,在挂载之前会有一个阶段是负责处理dom结构的,主要是分为2种情况,

  1. 判断传入的option是否存在el属性,当存在el属性时,再去判断是否有template属性,如果有则把template属性经过编译器处理成render函数;如果没有则使用el元素的outerHTML内容作为template,与上一步一样,编译器处理后变成render函数

  2. 判断传入的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函数,则不会再进去上面的模版编译阶段

image.png

可以看到在实际挂载之前,都会保证存在一个render函数,而这个渲染函数的作用是产出当前组件的vnode。

在vue原型上的_render()方法负责调用render函数并返回vnode

src/core/instance/render.js

image.png

那可以合理推断,调用_render方法的时机就是调用render函数的时机

那vue在哪里调用了这个_render方法呢

我们又回到了$mount方法

在$mount方法中,主要是调用mountComponent方法

image.png

再看看mountComponent方法

image.png

可以在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 函数。

必填项。

简单理解就是:

  1. 当我们传入是一个String类型时,vue会使用该参数作为vnode的标签名tagName,在挂载时会创建一个类型的真实节点,比如createElement('div'),createElement会判断这个参数是否是普通的html标签,比如div,则会创建一个div为tagName的vnode;如果传入的是非普通html标签,则会在vue实例上去寻找是否有该名称的全局组件,如果有则使用该组件的内容创建vnode

  2. 当我们传入是一个Object类型时,createElement会使用该Object作为子组件的配置,会创建一个子组件vnode

  3. 当我们传入是一个async 函数类型时,createElement会调用该async 函数,并以它resolve返回的内容按照上面1,2两种情况进行vnode创建,这是vue中异步函数的一种写法

后续会详细介绍用法。

参数2

{Object}

一个与模板中 attribute 对应的数据对象。

可选。

简单理解就是把dom上的属性用key-value的形式传入这个vnode中,这涉及到动态属性,静态属性,绑定的事件

后续会详细介绍用法。

参数3

{String | Array}

子级虚拟节点 (VNodes),由 createElement() 构建而成, 也可以使用字符串来生成“文本虚拟节点”。

可选。

  1. 当我们传入是一个String类型时,createElement会把它当成文本节点进行处理,文本节点也是vnode的一种

  2. 当我们传入是一个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函数去代替模版语法,可以大大优化项目。