vlambda博客
学习文章列表

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

前言

随着技术的快速发展,前端为了快速开发,我们一般会接入像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是否会有问题?