开始使用函数式组件
发布于 5天前 作者 lzy1043 122 次浏览 来自 分享

开始使用vue 函数式组件

最近从公司其他的项目中接了一个锅,项目初期的时候任务比较多,项目中存在太多重复的代码,维护起来很费时,每次修改的时候都不能保证没有遗漏。趁着有时间把其中的订单列表部分重构了一下,但是还没有重构完成,只简单的分离出一些功能没有耦合的组件。

项目背景

vue 全家桶 + elementUI 组件库

为什么会用到函数式组件

项目中的角色比较多,而订单列表这一块,列表的表头大部分类似,但是可能不同的角色对应着不同的操作和一些特殊的列。我再重构的时候就将表格做成单独的组件,表格的每一列和对应的数据通过 props 传递到组件中,这样就可以避免每次都copy表格的代码。重构之后,发现部分表格有一些特殊的需求,比如:

1.png

这里标记的部分是一些特殊的列,由于这里表格被做成单独的组件会在很多地方使用,,,所以不可能在表格组件中去做判断,这样会导致表格组件特别臃肿,而且谁知道还会有其他什么样的需求。所以这里选择使用渲染函数配合函数式组件实现,增强复用性和通用性。

函数式组件和渲染函数

Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力,这就是 render 函数,它比 template 更接近编译器。

这里引用一下官网的例子:

    Vue.component('anchored-heading', {
      render: function (createElement) {
        return createElement(
          'h' + this.level,   // tag name 标签名称
          this.$slots.default // 子组件中的阵列
        )
      },
      props: {
        level: {
          type: Number,
          required: true
        }
      }
    })

这里就是利用render创建了一个组件。在使用render函数之前需要了解一些基础的东西,包括节点、树和虚拟DOM,这些在官网上可以直接查看,这里不再赘述。vue官网。主要在这里介绍一下createElement函数以及相关的参数。

createElement 参数

createElement() 方法接受三个参数:

  • { String | Object | Function}:第一个参数是必要参数,可以使一个HTML标签字符串,组件选项对象或者是返回值为HTML标签字符串或者组件选项对象的async异步函数。通常使用HTML标签名。

  • { Object }: 第二个参数是包含模板相关属性的数据对象,包括class,style,attrs, props以及事件相关等。这一步分内容比较多,后面详细来说。这个参数为可选参数。

  • { String | Array }:第三个参数是子节点,可以是createElement 创建出来的虚拟节点,也可以是使用字符串生成的文本节点。

示例:

createElement('div', {
  class: {
    logo: true
  }
}, [
  '先写一些文字',
  createElement('h1', '这里是标题'),
  createElement('div', '这里是内容')
])


深入data 对象

模板相关属性的数据对象中包含了组件的一些相关属性。所有的属性如下:

  {
    // 和`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
    },
    // 自定义指令。注意,你无法对 `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'
  }

另一个例子:

Vue.component('btn-group', {
  render: (h) => {
    return h('div', [
      h('el-button', {
        props: {
          type: 'primary',
          size: 'mini'
        },
        class: {
          'edit-btn': true
        },
        on: {
          click: () => {
            this.handleClickEdit(params)
          }
        }
      }, '编辑')
    ])
  }
})

这个示例使用了element-ui中的button组件,通过渲染函数进行创建,向组件内部传递了type属性和size属性,并且添加了class和事件监听。

在渲染函数中,v-if, v-for 和 v-model以及插槽、按键修饰符的具体实现可以查看官网文档

函数式组件

在使用渲染函数的时候,标记组件为 functional,这意味它是无状态 (没有响应式数据),无实例 (没有 this 上下文),此时创建的组件为函数式组件。

函数式组件需要的一切都是通过上下文传递,包括:

props:提供所有 prop 的对象 children: VNode 子节点的数组 slots: 返回所有插槽的对象的函数 data:传递给组件的数据对象,并将这个组件作为第二个参数传入 createElement parent:对父组件的引用 listeners: (2.3.0+) 一个包含了所有在父组件上注册的事件侦听器的对象。这只是一个指向 data.on 的别名。 injections: (2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的属性。 关于injections 暂时不了解,放在后面来说。

在这里讲一下data, 就是传递给组件的数据对象

在这里先做一个示例:

<el-button class="add_btn">新增</el-button>

解析出来之后我们会发现, class会自动添加上,一直以为是组件内部自己接受了props传递的class,其实这里是Vue内部实现的

在普通组件中,没有被定义为 prop 的特性会自动添加到组件的根元素上,将现有的同名特性替换或与其智能合并

在函数式组件中这个就要我们自己实现:所有传递给函数式组件的非props属性,会放在data中接受,包括事件等。看一个例子,

//  定义一个函数式组件
export default {
  name: 'Render02',
  functional: true,
  render: (h, ctx) => {
    return h('el-button', ctx.data, '点击')
  }
}


// 使用

<Render02 id="Render02" @click="handleClick"></Render02>

我们这里默认是将传递进组件中所有的非props的属性,添加到组件的根元素上。查看渲染出来的结果可以发现,在组件的根元素上添加了属性 id="Render02", 并且点击事件也绑定上了。

在单文件组件中使用函数式组件

我们也可以在单文件组件中使用函数式组件,只需要在

<template functional></template>

这样该单文件就被定义成为函数式组件。

在项目中的使用

在项目中使用函数是组件的方式是配合渲染函数一起使用的,直接上代码, 这里是结合element-ui的table组价使用的


//  定义一个函数式组件

export default {
  name: 'Render',
  functional: true,
  props:{
    params: Object,   //  params 是组件需要的一些数据
    render: Function  // render 是一个渲染函数
  },
  render: (h, ctx) => {
    return ctx.props.render(h, ctx.props.params)
  }
}


//  使用

<template>
  <el-table :data="data">
    <template v-for="(c, i) in columns">
      
      <el-table-column v-if="c.render">
        <template slot-scope="scope">
          <Render :render="c.render" :params="scope.row" />
        </template>
      </el-table-column>

      <el-table-column
        :prop="c.prop"
        :label="c.label"
        v-else></el-table-column>
    </template>
  </el-table>
</template>
<script>
  import Render from './Render'
  export default {
    name: 'TheTable',
    props: {
      columns: Array,
      data: Array
    },
    components: {
      Render
    }
  }
</script>

//  在定义columns时, 传递render函数


[
...,
{
  label: '操作',
  render: (h, params) => {
    return h('div', [
      h('el-button', {
        props: {
          type: 'primary',
          size: 'mini'
        },
        class: {
          'edit-btn': true
        },
        on: {
          click: () => {
            this.handleClickEdit(params)
          }
        }
      }, '编辑'),
    ])
  }
},
...
]


这样就可以根据需求的不同,为不同的列创建不同的内容。

参考文档

vuejs官网 element-ui中的table组件

相关代码

github地址

回到顶部