利用函数式组件做二次封装

3,716 阅读3分钟

前言

随着技术的快速发展,前端为了快速开发,我们一般会接入像elementui这样的库,以element为例,一些组件无法满足我们的需求,就需要做二次封装。今天想着尝试利用vue的函数式组件做一下二次封装。

先来看一个最简单的demo来补充点基础知识

// demo.vue
<template>
  <div class="demo">
    <DeInput @debounce="debounce" maxlength='5' @blur="inputBlur"/>
  </div>
</template>
<script>
import DeInput from './DeInput'
export default {
  name: 'Demo',
  components: {
    DeInput
  },
  methods: {
    debounce(value) {
      console.log('防抖后:', value)
    },
    inputBlur() {
      console.log('失去焦点')
    }
  }
}

// deinput.vue
<template>
    <div>
        <el-input v-model="inputValue" @input="deInput"></el-input>
    </div>
</template>
<script>
export default {
    data() {
        return {
            inputValue: ''
        }
    },
    methods: {
        deInput() {
            this.$emit('debounce', this.inputValue)
        }
    }
}

如果去运行这段代码就会发现inputBlur这个函数根本没有执行,maxlength这个属性也没有生效,这是因为@blur和 maxlength是el-input内部方法和属性。如果想要调用,就需要做透传,换句话说就是让el-input知道它的方法或者属性被调用。其实只要vue提供的\$attrs和$listeners属性即可。

  • $attrs:包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

我们来加一下这两个属性, 再次去执行的时候发现inputBlur这个函数已经可以被调用了,maxlength也生效了,由于太过简单,就不做过多解释

// deinput.vue
<el-input v-model="inputValue" @input="deInput" v-bind="$attrs" v-on="$listeners"></el-input>

其实这已经给我们提供了大部分的思路,接下来我们试试用函数式组件的思路是否能满足我们的需求

函数式组件

定义:我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。一个函数式组件就像这样:

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

为什么要用函数式组件?

  • 因为函数式组件只是函数,所以渲染开销也低很多。

试着函数式组件的封装一个可以防抖的input标签

// debouce.js
const debounce = (fn, delay=500, Switch=true) => {
    let timeout = null;
    return (params) => {
        clearTimeout(timeout)

        if (!Switch) {
            return fn(params)
        }

        timeout = window.setTimeout(() => {
            fn(params)
        }, Number(delay))
    }
}

export default {
    functional: true,
    render: function(createElement, context) {
        const vNodeLists = context.slots().default // 这里其实可以替换为context.children
        const time = context.props.time
        const Switch = context.props.Switch

        if (!vNodeLists) {
            console.warn('必须要有一个子元素')
            return null
        }

        const vNode = vNodeLists[0] || {}

        // 我们获取到其input方法进行二次封装
        if (vNode.tag && vNode.tag === 'input') {
            const funDefault = vNode.data.on && vNode.data.on.input
            if (!funDefault) {
                console.warn('请传入input方法(@input)')
                return null
            }
            const fun = debounce(funDefault, time, Switch)

            vNode.data.on.input = fun
        } else {
            console.warn('仅支持input')
            return null
        }
        return vNode
    }
}

看一下这个组件如何被使用

<template>
  <div class="home">
    <Debounce time='1000' :Switch='true'>
      <input type="text" @input="debounce"/>
    </Debounce>
  </div>
</template>

<script>
import Debounce from '../components/debounce'

export default {
  components: {
    Debounce
  },
  methods: {
    debounce(e) {
      console.log('防抖后:', e.target.value)
    }
  }
}
</script>

我们再来尝试封装一个elementui的el-button组件

// debounce.js 关键代码
if (vNode.componentOptions && vNode.componentOptions.tag === 'el-button') {
      const funDefault = vNode.componentOptions.listeners && vNode.componentOptions.listeners.click
      if (!funDefault) {
          console.warn('请传入click方法(@click)')
          return null
      }
      const fun = debounce(funDefault, time, Switch)

      vNode.componentOptions.listeners.click = fun
  }

我们elementui的组件和原生标签的区别是需要通过vNode.componentOptions获取,接下来贴出完整的代码

const debounce = (fn, delay=500, Switch=true) => {
    let timeout = null;
    return (params) => {
        clearTimeout(timeout)

        if (!Switch) {
            return fn(params)
        }

        timeout = window.setTimeout(() => {
            // el-button获取到的是数组,input获取到的是function
            if (!Array.isArray(fn)) {
                fn = [fn]
            }

            fn[0](params)
        }, 1000)
    }
}

export default {
    functional: true,
    render: function(createElement, context) {
        const vNodeLists = context.slots().default
        const time = context.props.time
        const Switch = context.props.Switch

        if (!vNodeLists) {
            console.warn('必须要有一个子元素')
            return null
        }

        const vNode = vNodeLists[0] || {}

        if (vNode.componentOptions && vNode.componentOptions.tag === 'el-button') {
            const funDefault = vNode.componentOptions.listeners && vNode.componentOptions.listeners.click
            if (!funDefault) {
                console.warn('请传入click方法(@click)')
                return null
            }
            const fun = debounce(funDefault, time, Switch)

            vNode.componentOptions.listeners.click = fun
        } else if (vNode.tag && vNode.tag === 'input') {
            const funDefault = vNode.data.on && vNode.data.on.input
            if (!funDefault) {
                console.warn('请传入input方法(@input)')
                return null
            }
            const fun = debounce(funDefault, time, Switch)

            vNode.data.on.input = fun
        } else {
            console.warn('仅支持input和el-button')
            return null
        }
        return vNode
    }
}

简单看一下效果吧

思考:以这种方式封装el-input是否会有问题?

本文使用 mdnice 排版