Vue函数式组件及Debounce组件实践

175 阅读3分钟

今天在看Vue源码的时候,看见了一个新的名词, 函数式组件。自学Vue也有一些时间了,但是在我的印象中,好像并没有听说过这个名词,于是乎google一下一圈,发现还是Vue的官方文档写得清楚明白。

之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。

即是说,如果你的组件比较简单,没有进行任何的状态管理,没有生命周期,只接收一些props参数,那么,我们可以将这个组件标记为 functional,该组件既无状态,也无实例(即this),这样的一个组件,我们便称之为函数式组件

在js文件中,我们可以像下面这样创建函数式组件

Vue.Component("functional-com",{
    functional:true, // 标记为函数式组件
    props:[],
    render(h,context){
    ...
    }
})

如果在.vue的单文件组件中,则我们可以这样做

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

在 2.3.0 之前的版本中,如果一个函数式组件想要接收 prop,则 props 选项是必须的。在 2.3.0 或以上的版本中,你可以省略 props 选项,所有组件上的 attribute 都会被自动隐式解析为 prop。

组件需要的一切都是通过 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中,上述变量可以直接访问到。

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

在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:

  • 程序化地在多个组件中选择一个来代为渲染;
  • 在将 childrenpropsdata 传递给子组件之前操作它们。

上面提到了函数式组件的两个优点,一是渲染开销的低,二是可对子组件进行增强。

以下代码是element-uidivider 组件的源码,我们可以看见也是使用了函数式组件。

<template functional>
  <div
    v-bind="data.attrs"
    v-on="listeners"
    :class="[data.staticClass, 'el-divider', `el-divider--${props.direction}`]"
  >
    <div
      v-if="slots().default && props.direction !== 'vertical'"
      :class="['el-divider__text', `is-${props.contentPosition}`]"
     >
      <slot />
    </div>
  </div>
</template>

<script>
export default {
  name: 'ElDivider',
  props: {
    direction: {
      type: String,
      default: 'horizontal',
      validator(val) {
        return ['horizontal', 'vertical'].indexOf(val) !== -1;
      }
    },
    contentPosition: {
      type: String,
      default: 'center',
      validator(val) {
        return ['left', 'center', 'right'].indexOf(val) !== -1;
      }
    }
  }
};

</script>

不仅仅是可以降低渲染开销,还可以增强子组件的功能,比如下面的这样一个支持防抖的组件

<script>
/**
 * 防抖函数
 * @param {Funtion} target 需要进行防抖处理的函数
 * @param {number} delay 延时时间,ms
 * @returns 处理后的函数
 */
function debounce(target, delay) {
  if (typeof target !== "function") {
    throw TypeError("the first argument must be a Function");
  }
  if (typeof delay !== "number" || delay < 0) {
    delay = 500;
  }
  let id;
  // 此处不能使用箭头函数,否则 argument 失效
  return function () {
    clearTimeout(id);
    id = setTimeout(() => {
      target(...arguments);
    }, delay);
  };
}

export default {
  name: "debounce-com",
  functional: true,
  props: {
    time: {
      type: Number,
      default: 500,
    },
    event: {
      type: String,
      default: "click",
    },
  },
  render(h, context) {
    let { time, event } = context.props;
    const childList = context.slots().default;
    const vnode = childList[0] || null;
    if (vnode === null) {
      console.error("<debounce-com>必须拥有子元素");
      return null;
    }
    const func = vnode.data.on[event];
    if (func === undefined || func === null) {
      console.error(`you must give me the ${event} method`);
    }
    vnode.data.on[event] = debounce(func, time);
    return vnode;
  },
};
</script>

在该组件中,我们可以获取到子组件所注册的事件,并进行增强,即进行防抖。同理,我们最终也可以使用相同的方式进行节流组件的编写。

Debounce组件全部代码见下面:

没啥可总结的,简单的笔记,如果文中有什么不对的地方欢迎指正,也欢迎大家评论区交流。