【Vue2】Render函数初探

4,164 阅读5分钟

题记

今天从后端传来一个需求,需要在表格中渲染一个select组件,点击切换时调接口修改数据,大概就是这样子。

image.png

就以这个例子来看一下vue的render函数吧!

深入了解vue组件

我们都知道vue的单文件组件,就是template、script、style,写在一个.vue结尾的文件中,然后通过打包编译,变成js,但其实,vue也有类似与react的组件写法,那就叫functional组件。

functional组件其实有2种写法,只有一个template或一个对象.

functional template

这个其实在文档中没有展现,在vue-loader的文档中有写.链接

<template functional>
  <div>{{ props.foo }}</div>
</template>

当你使用这一种写法时,vue文件中无需写script,这种适用于只有简单样式,简单的dom的受控组件,或者仅仅是用来展示的组件.很多方法需要通过listenear和parent、props去获取.

functional Component

这次我是用的第二种,一个对象表示的组件。它的基本结构如下。

export default {
    name: 'xxx', //必须有
    functional:true,
    props:{}, // 非必须
    render:function(h,ctx){
        return h('div')
    }
}

这里的h其实就是真正的render函数。下面我就以我的需求为例,讲一下这个render函数的使用。

初探render

h其实就是vue的createElement,在文档这样写

createElement( 'div', //参数1
// {String | Object | Function}  
// 一个 HTML 标签名、组件选项对象,或者 
// resolve 了上述任何一种的一个 async 函数。必填项。  
{},//参数2
// {Object}  // 一个与模板中 attribute 对应的数据对象。可选。
[] //参数3
// {String | Array}  

其中:

参数1是你要渲染的dom,这里可以是一个标签,例如div,也可以是一个vnode,或者应该render函数

参数2,是这个dom的一些属性,常用的大概有这些

{
    class:{ a:true,b:false}, // class名
    style: {color:'red'}, // style
    attrs:{} // 标签上的一些属性,例如id,src
    domProps:{innerHTML:'xxx'},// dom原生属性
    props:{} // 这个就不用多说了
    on:{click(){}}, //事件,
    nativeOn:{} // 原生dom事件以及$emit
    directives:[] // 指令,
    scopedSlots
    slot
    key
    ref
    refInFor
}

参数3则是需要渲染children

解决这个需求

我们都知道,el-select应该这样使用,因此我们也要这样去渲染相应dom.

<template>
    <el-select v-model="value" placeholder="请选择">
        <el-option value="1" label="待处理" />
        <el-option value="2" label="已处理" />
    </el-select>
</template>

渲染第一层dom

首先,我们先渲染一个el-select,并通过props传入一个value。当然,我这个项目是后台管理,已经全局引入了elementUI,因此这里可以直接使用标签渲染.

 function(h,ctx){
    return h('el-select',{props:{value:'a'}})
 }

这是页面就会显示一个select组件,里面是你传入的值,这时候需要渲染children,需要注意的是,每个children本质上还有一个render函数。从原理上讲,render函数就是递归去渲染dom,一直到没有children为止。

渲染第二层dom

因为我这里有2个选项,因此children是一个数组.这里其实可以单独写,也可以使用循环,就相当于v-for。我这里直接使用了map方法.

function(h,ctx){
      const list = [{ value: 1, label: '待处理' }, { value: 2, label: '已处理' }]
    const options = list.map(e=>h('el-option', { props: { value, label }}))

    return h('el-select',{props:{value:'a'}},options)
 }

传入value

但是这个是渲染到表格中的,因此需要传入表格中的数据。

function(h,ctx){
    const { props: { row:{status} }} = ctx
     const list = [{ value: 1, label: '待处理' }, { value: 2, label: '已处理' }]
    const options = list.map(e=>h('el-option', { props: { value, label }}))
    return h('el-select',{props:{value:status}},options)}

添加事件

function(h,ctx){
    const { props: { row:{status} }} = ctx
     const list = [{ value: 1, label: '待处理' }, { value: 2, label: '已处理' }]
    const options = list.map(e=>h('el-option', { props: { value, label }}))
    return h('el-select',{
        props:{value:status},
        on:{
            change:(currentValue)=>{}
        }
    },options)
 }

但是怎么样让他触发父组件的事件,或者给父组件传值呢?我选择的方法是把函数当作proprs传入进去

function(h,ctx){
    const { props: { emitChange,row:{status} }} = ctx
     const list = [{ value: 1, label: '待处理' }, { value: 2, label: '已处理' }]
    const options = list.map(e=>h('el-option', { props: { value, label }}))
    
    return h('el-select',{
        props:{value:status},
        on:{
            change:(currentValue)=>{emitChange(currentValue)}
        }
    },options)
 }

界面响应问题

这时候其实已经能完成大部分功能了,但是这里会出现一个问题,props其实是单向数据流,也就意味着props并不会响应数据变化,导致当选中变化的时候,界面显示并不会改变。

为了解决这个问题,我采用了一个比较投机取巧的方法。props中我不再传入status,而是row.status,由于row是一个引用类型,当我改变row.status就可以被响应。由于emitChange是一个异步方法,因此我把改变status放到这个方法执行成功之后.

function(h,ctx){
    const { props: { emitChange,row }} = ctx
    const list = [{ value: 1, label: '待处理' }, { value: 2, label: '已处理' }]
    const options = list.map(e=>h('el-option', { props: { value, label }}))
    
    return h('el-select',{
        props:{value:row.status},
        on:{
            change:(currentValue)=>{
                return emitChange(currentValue)
                    .then(()=>{
                        row.status = currentValue
                    })
            }
        }
    },options)
 }

完整代码

const StatusSelect = {
  name: 'StatusSelect',
  functional: true,
  render(h, ctx) {
    const { props: { emitChange, row }} = ctx
    const list = [{ value: 1, label: '待处理' }, { value: 2, label: '已处理' }]
    const options = list.map(e=>h('el-option', { props: { value, label }}))
    return h('el-select',
      {
        props: { value: row.status },
        on: {
          change: (currentValue) => {
            // row.status = currentValue
            return emitChange(currentValue, row)
              .then(() => { row.status = currentValue })
          }
        }
      },
      options)
    )
  }
}

小技巧

真正使用的时候,其实我们可以不用import引入,直接将这个对象写到components里,这样会更加优化,减少一次编译。

image.png

总结

其实vue2的render函数还是一个很有用的技巧,从性能上讲functional组件减少编译,性能更好。当然,我对render函数的使用也不多,所以这篇文章才叫初探。听说vue3的render函数会更加灵活,更加接近底层,后续我也会继续研究。