前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用作用域插槽和偏函数编写高复用 Vue 组件

用作用域插槽和偏函数编写高复用 Vue 组件

作者头像
FairyEver
发布2019-07-26 14:57:34
1.1K0
发布2019-07-26 14:57:34
举报
文章被收录于专栏:今日前端今日前端

引言

作用域插槽是 Vue 2.1 之后引入的一种组件复用工具。其原理类似 React 里面的 Render Props 组件设计模式。如果你使用过 Render Props,那么你不仅可以很快理解作用域插槽,也能明白其实现原理。没有使用过也没关系,Vue 简明的语法足以让你短时间内掌握作用域插槽的用法。

偏函数(Partial Application)是一种函数复用和函数组合的技巧。举个简单的例子。

代码语言:javascript
复制
const add = x => y => x + y;

你可以将 add 看成柯里化函数,也可以把它看成偏函数,这里就不展开讲了。重点是,基于 add可以扩展出很多新函数。比如:

代码语言:javascript
复制
const add5 = add(5);
add5(5); // => 10

const add10 = add(10);
add10(5); // => 15

基于上面简单的例子再扩展下,把普通函数转化成偏函数:

代码语言:javascript
复制
function partial(func, argArr) {
  return function(...args) {
    const allArguments = argArr.concat(args);
    return func.apply(this, allArguments);
  };
}

const add = (x, y, z) => x + y + z;

const addTwoAndThree = partial(add, [2, 3]);

addTwoAndThree(5); // => 10

就是这样一个简单到有点无聊的函数概念,在函数复用和组合上却有着很强大的作用。

在接下来的例子中,我会把这两个概念结合起来,写一个高复用和符合 DRY (Don't repeat yourself) 原则的 Vue 组件。

需求

如上图,我们需要展示一个水果列表,列表中有每种水果的价格和库存信息。价格当然是我瞎编的。点击价格和库存表头,可根据相应标签进行排序。点击排序表头文字,第一次点击向上排序,接着点击,按上一次相反的方向排序。排序表头右边上下两个箭头,分别可点击向上向下排序。每次排序完后,对应标签的上或下标签根据排序方向高亮。

业务逻辑

列表的数据可以在组件里处理,也可以在 Vuex 里面处理,看业务需求。这里我就在 Vuex 里处理了。我们先写简单的。把 UI 需要的数据放在 state 里,然后写个 mutation 函数,根据传进来的标签和顺序,对数据进行排序。

代码语言:javascript
复制
// App.vue
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);

import { descend, ascend, sortWith, prop } from "ramda";
const sortBy = options => prop(options.sortBy);

const store = () =>
  new Vuex.Store({
    state: {
      fruits: [
        { name: "bananas", price: 12, stock: 30 },
        { name: "apples", price: 16, stock: 25 },
        { name: "pineapples", price: 15, stock: 32 },
        { name: "oranges", price: 10, stock: 34 },
        { name: "pears", price: 13, stock: 60 },
        { name: "avocado", price: 20, stock: 50 }
      ]
    },
    mutations: {
      SORT_FRUITS(state, sortOptions) {
        const sortData = sortOptions.sortAscend
          ? sortWith([ascend(sortBy(sortOptions))])
          : sortWith([descend(sortBy(sortOptions))]);
        const sortedFruits = sortData(state.fruits);
        state.fruits = [...sortedFruits];
      }
    }
  });

SORT_FRUITS 函数接受一个对象 sortOptions 为参数(注:对 Vuex 不熟的读者可能会对这部分困惑,我这里是说 mutation 在被调用的时候,只接受一个参数),这个对象包含了排序依赖的信息: sortAscend:Boolean 是否升序,和 sortBy:String 排序标签。

这里排序的逻辑我借用了 Ramda 库,这只是我的个人偏好,你也可以用原生函数写。如果你是新人,建议还是先熟悉原生 API 的写法。如果想了解更多 Ramda,可参考我另一篇文章 优雅代码指北 -- 巧用 Ramda

主要的业务逻辑写完了,接下来的任务就是让 UI 事件来调用 SORT_FRUITS,并传入相应的参数来操作数据,最后利用 Vue 的双向数据绑定来更新 UI。

原子组件

在对组件划分的认识上,我自己发明了一个概念,叫原子组件(Atomic Components)。原子组件就是可复用的,不能再继续拆分的最底层组件。原子组件有这样一些特征:

  1. 无业务逻辑,只执行传进来的方法。
  2. 不关心和它的功能不相关的信息。举个例子,一个开关(toggle)组件,它只关心它处于打开还是关闭的状态,并执行对应的回调函数,它不关心它打开和关闭的是外部的哪个元素。这是组件复用的核心部分。

在我们在写的 demo 中,排序表头就是这样一个原子组件。它的功能就是执行外面传进来的排序函数,并记住排序顺序,方便下一次排序和高亮箭头。它不关心它到底是给价格排序还是给库存排序,也不关心它该显示什么文字,这是外层组件该关心的事。

排序表头组件

先来看表头组件的 Template:

代码语言:javascript
复制
<!-- TitleWithSortingArrows.vue -->

<template>
  <div class="title">
    <div class="title--text">
      <slot :handleClick="onClickTitle"></slot>
    </div>
    <div class="title--arrows">
      <div :class="upArrowHighlighted ? 'up-arrow__highlight' : 'up-arrow'"
        @click="onClickUpArrow"></div>
      <div :class="downArrowHighlighted ? 'down-arrow__highlight' : 'down-arrow'"
        @click="onClickDownArrow"></div>
    </div>
  </div>
</template>

排序表头的文字因为是由外部定义的,所以放了个插槽。另外,由于在外部点击表头文字时,执行的方法是由排序表头状态决定的,所以通过作用域插槽把排序表头内部的方法传到外部,这个函数是 onClickTitle。模板下面的两个上下箭头用纯 CSS 写的,根据排序的状态决定是否用高亮背景色。

再看 JS 部分:

代码语言:javascript
复制
export default {
  name: "titleWithSortingArrows",
  props: ["sortMethod"],
  data() {
    return {
      sortTriggered: false,
      sortAscend: true
    };
  },
  computed: {
    upArrowHighlighted: function() {
      return this.sortTriggered && this.sortAscend;
    },
    downArrowHighlighted: function() {
      return this.sortTriggered && !this.sortAscend;
    }
  },
  methods: {
    checkIfSortTriggered() {
      if (!this.sortTriggered) {
        this.sortTriggered = true;
      }
    },
    onClickUpArrow() {
      this.sortMethod(true);
      this.sortAscend = true;
      this.checkIfSortTriggered();
    },
    onClickDownArrow() {
      this.sortMethod(false);
      this.sortAscend = false;
      this.checkIfSortTriggered();
    },
    onClickTitle() {
      this.sortMethod(!this.sortAscend);
      this.sortAscend = !this.sortAscend;
      this.checkIfSortTriggered();
    }
  }
};

可以看到组件接受一个排序方法 sortMethod 为属性,并根据自身状态,在不同部分执行排序方法时传入升序(true)还是降序(false)。 computed 部分两个变量是计算两个箭头是否应该高亮。 sortTriggered 状态默认是 false,意味着组件首次加载时箭头都是灰色。这个组件最值得注意的地方是 onClickTitle 方法,组件把父组件传进来的方法根据自身特有的属性(此时的排序顺序)进行定制化,再通过作用于插槽把定制化后的方法提供给父组件调用。

通过作用域插槽取到子组件的数据(方法)

排序表头组件通过作用域插槽向外传数据( onClickTitle 方法)后,调用它的父级组件就能通过 slot-scope 这个标签在模板里取到相关数据了。来看父级组件是怎么取作用域插槽的数据的:

代码语言:javascript
复制
<!-- TableHeader.vue -->
<template>
  <div class="header">
    <div class="header--item">
      <span>Fruits</span>
    </div>
    <div class="header--item">
      <title-with-sorting-arrows :sort-method="onClickSortPrice">
        <span slot-scope="{handleClick}" @click="handleClick">Price</span>
      </title-with-sorting-arrows>
    </div>
    <div class="header--item">
      <title-with-sorting-arrows :sort-method="onClickSortStock">
        <span slot-scope="{handleClick}" @click="handleClick">Stock</span>
      </title-with-sorting-arrows>
    </div>
  </div>
</template>

handleClick 就是从作用域插槽传来的方法。

难题:怎么将 Vuex mutation 转成偏函数

在上面的排序表头组件里,组件只关心是升序排序和降序排序,它并不关心是给哪个标签排序。那问题来了。再看下我们在 mutation 里写的排序函数 SORT_FRUITS,它需要两个排序信息才能工作:排序顺序和排序标签。如果 SORT_FRUITS 接受两个参数,那我们可以利用偏函数,先把它应用一部分参数,再传给表头。类似这样:

代码语言:javascript
复制
const sortByPrice = partial(this.SORT_FRUITS, ["price"]);

然后我们就能在父级组件给表头组件传 sortByPrice 这个函数了。

问题是, SORT_FRUITS 接受的是一个对象,不是两个参数!

考验我们 JS 基础知识的时间到了。其实只要理解了闭包和文章开头写的 partial 函数工作原理,是能很容易把接受对象为参数的函数也转成偏函数的。这样子:

代码语言:javascript
复制
// TableHeader.vue
export default {
  name: "TableHeader",
  components: { TitleWithSortingArrows },
  methods: {
    ...mapMutations({
      SORT_FRUITS: "SORT_FRUITS"
    }),
    onClickSortPrice(sortAscend) {
      const self = this;
      return (function applySortBy(sortBy) {
        self.SORT_FRUITS({ sortAscend, sortBy });
      })("price");
    },
    onClickSortStock(sortAscend) {
      const self = this;
      return (function applySortBy(sortBy) {
        self.SORT_FRUITS({ sortAscend, sortBy });
      })("stock");
    }
  }
};

onClickSortPriceonClickSortStock 函数利用闭包记住了排序标签。通过返回一个立即执行函数,这两个函数给 SORT_FRUITS 塞进了一个变量 sortBy。然后等排序表头组件执行这两个方法的时候,排序标签已经被提前填充进来了。

你可能会问,为什么不把排序标签作为属性传给排序表头组件,然后让它执行 SORT_FRUITS 时把全部参数传进去?答案是:

  1. 这违反了 DRY 原则。既然在一个排序表头里每次执行 SORT_FRUITS 方法时传的 sortBy 参数都一样,为什么不在父级就把这个参数填充了?而且,想象一下,如果 SORT_FRUITS 方法执行很多次,一直复制粘贴同一个参数,看起来实在乱。
  2. 给外部哪个数据排序,不是表头组件该关心的。它只关心是升序还是降序。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 今日前端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 需求
  • 业务逻辑
  • 原子组件
  • 排序表头组件
  • 通过作用域插槽取到子组件的数据(方法)
  • 难题:怎么将 Vuex mutation 转成偏函数
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档