composition-api是框架的革命还是函数式编程的胜利?

2,035 阅读6分钟

目录:

  1. 回顾:我们现在的编程模式。
  2. 一道惊雷:hooks 应运而生。
  3. Vue 的 hooks,composition-api 的基本使用。
  4. 使用体会与心得,我是怎么理解 composition-api 的。
  5. 编程范式:面向对象与函数式编程。
  6. 最后,我听到的值得分享的前辈的建议。

1、 回顾:我们现在的编程模式

首先这里有一个问题,我们现在的框架不是已经很完善了吗?为什么要学习 composition-api ?

回答这个问题之前就得先看看我们现在的编程模式,我们现在的模式编程有什么特点?以及开发中会遇到哪些问题?

要回答这个问题,就得回到我们现在框架上来,去了解它,让我们回到它最初的起点,看看当我们要创建一个应用的时候:

// main.js

// 我们实例化了一个 Vue 类

new Vue({

  render: (h) => h(App),

}).$mount('#app');

当我们编写一个组件时

<template>

  <div></div>

</template>

<script>

// 我们返回了一个对象

export default {

  data() {

  },

  computed: {



  },

  methods: {

  },

};

</script>

可以看出我们现在的编程模式完全基于面向对象的思想进行开发,根据面向对象的编程特性,封装、继承、多态,其实这就已经满足了我们日常开发的几乎所有需求。面向对象确实很全面,但面向对象有没有缺点呢?或者说面向对象在前端开发中有没有缺点呢?

  1. 绑定关系冗长、复杂,易读性差
  2. this 的指向容易混乱,导致出错的概率大大增加

2、一道惊雷:hooks 应运而生

基于以上开发的问题,react 在 19 年 2 月 4 日发布了首个颠覆性解决方案 hooks 在 v16.8.0 正式发布,正如 react 的当初做的一样,hooks 的出现再一次颠覆了前端的编程思维,既然面向对象我不好控制,那我就不用了; 既然 this 指向不明确,那我们就把 this 给干掉,当 this 指针头颅落地的时刻,也标志着面向对象在前端的死亡,至此,大航海时代正式到来。在引爆了前端开发的模式之后,Vue 紧赶慢赶,总算在20918日正式发布了 Vue 3.0 ,one-piece终于正式面向大众!Vue 交出了自己的对 hooks 的答卷,composition-api 终于发布。随后也在不断优化更新中升级打怪,也就是在今年的 2227 日,Vue 3 终于正式更名 vue-core。果实能力终于觉醒!

3、composition-api 的基本使用

需要说明的是我们在项目中还没有使用 Vue3,而是在2.x项目中引入了@vue/composition-api 来进行composition-api 的开发,这样做的原因如下:

  1. proxy 的兼容性虽然已占了 93% , 但是另外 2% 的用户还是要考虑一下的,毕竟刚出来嘛

  1. 也是最大的原因,目前项目已经很稳定,重构代价巨大,因此,既能够使用 composition-api 又不用大型手术的方案最合理 。

下面正式开始使用,

最基础的创建一个响应式变量
// composition-api

import {

  ref, reactive, defineComponent

} from '@vue/composition-api';

export default defineComponent({

  setup() {

    const num = ref(0)

    const people = reactive({

      name: "Bob",

      age: 18

    })

    // !!! 注意:ref创建的值必须使用 .value 访问

    console.log(num.value) // 0

    console.log(people) // { name: "Bob",age: 18 }

    return {

      num,

      people

    }

  }

})
为dom元素添加事件
<template>

  <div @click="tapMe">BUTTON</div>

</template>

<script>

// composition-api

import {

  defineComponent

} from '@vue/composition-api';

export default defineComponent({

  setup() {

    const tapMe = () => {

      console.log(111)

    }

    return {

      tapMe

    }

  }

})

</script>
以及通信相关 props, computed,$emit ...
// composition-api

import {

  defineComponent, ref,computed

} from '@vue/composition-api';

export default defineComponent({

  props: {

    text: {

      type: String,

      default: '--'

    }

  },

  setup(props, { emit }) {

    const { text } = props

    

    const money = ref(0)

    const moneyText = computed(() => `${money.value}元`)

    const send = () => {

      emit('change', moneyText)

    }

    return {

      money,

      moneyText,

      send

    }

  }

})
最后获取根组件的实例
// composition-api

import {

  defineComponent

} from '@vue/composition-api';

export default defineComponent({

  setup(props, { root }) {

    const toPageIndex = () => {

      root.$router.push({

        path: '/index'

      })

    }

    return {

      toPageIndex

    }

  }

})

从篇幅就能看出来使用还是很简单的,但是问题来了,我也没看出这有啥好处啊,代码量也没减少啊,不就是写法不一样了吗?而且所有代码都挤在 setup 函数里,也太难受了,你确定 composition-api 不是为了 kpi 而诞生的?别急,我们细细来品。

4、 我对 composition-api 的理解

  1. 代码量确实没有变化,但是逻辑更清晰了,可读性也更高了

官网上其实就有了非常好的解释,下图就是一个我们之前代码的编辑之后的样子:

每个颜色代表各个业务逻辑相关逻辑,可以看到,代码全糅杂在一起,如果哪位老哥的代码量过大,写了一两千行的代码,相信也是没几个人愿意去看这样的代码了。

用代码对比就能知道,我们举个基本页面的例子

用代码说话就是,如果在之前我们会这么写

export default {

  data() {

    return {

      banner: '',

      tab: {

        index: 0

      },

      list: []

    }

  },

  computed: {



  },

  methods: {

    clickBanner() {

      console.log('clickBanner')

    },

    setBanner(value) {

      this.banner = value

    },

    changeTab(value) {

      this.tab.index = value

    },

    setList(list) {

      this.list = list

    },

    clickItem(item) {

      console.log('clickItem', item)

    }

  },

};

但是我们在使用了 composition-api 之后就会是这样

import {

  ref, reactive, defineComponent

} from '@vue/composition-api';

export default defineComponent({

  setup() {

    // banner 图相关的逻辑

    const banner = ref('')

    const clickBanner = () {

      console.log('clickBanner')

    }

    const setBanner = (value) {

      banner.value = value

    }

    // tab 相关的逻辑

    const tab = reactive({

      index: 0

    })

    const changeTab = (value) {

      tab.index = value

    }

    // list 相关的逻辑

    const list = ref([])

    const setList = (list) => {

      list.value = list

    },

    const clickItem => (item) {

      console.log('clickItem', item)

    }

    

    return {

      // banner 图相关的逻辑

      banner,

      clickBanner,

      setBanner,

      // tab 相关的逻辑

      tab,

      changeTab,

      // list 相关的逻辑

      list,

      setList,

      clickItem,

    }

  }

})

每个业务代码相关的逻辑都紧紧站在一起,这样代码是不是就很清晰易读了。那仅仅就这样就可以吗?我们把组件拆分粒度更细一点不也一样吗?我们把 banner, tabs,List, 等组件一个个拆分,细粒度化组件。

这样做确实是可以的,我们在之前的业务开发中也是这么做的,但是后来产品加需求了,比如说:

我这个banner可能是个轮播,可能是个图片

我甚至我的每个页面的 List 的 UI 长得不一样,甚至于我的 Item 都不一样,且还会有一些其它逻辑,比如风云榜需求 List 的前 3 名需要单独拿出来展示

我这个页面还要个 PC 端,banner pc端用的字段还不一样

既然要分 pc 移动端,那就得实现 UI 与 逻辑 分离,我们之前为了解决这个问题,提出了 UI 组件与业务组件的解决方案,

如上图,问题确实解决了,但是,我们会发现一个问题,业务粒度拆分越来越细,层级越来越高,组件嵌套越来越繁杂。我们好像进入了无限套娃的模式。在实际开发中发现这样会带来一个最简单的问题,如果你要将一个页面UI改造,你就得处理各种通信,组件各种拆分,太复杂了,有可能你宁愿 ctrl + cv 一套代码也不愿意把组件拆细,更愿意维护 pc、 移动端两套代码。

那 composition-api 就能解决吗?我可以肯定的告诉你,是的 !

我们稍微改造一下代码,自定义一下hooks

// hooks.js

import {

  ref, reactive

} from '@vue/composition-api';

// banner 图相关的逻辑

export function useBanner () {

    const banner = ref('')

    const clickBanner = () {

      console.log('clickBanner')

    }

    const setBanner = (value) {

      banner.value = value

    }

    return {

      banner,

      clickBanner,

      setBanner,

    }

}

// tab 相关的逻辑

export function useTab() {

    const tab = reactive({

      index: 0

    })

    const changeTab = (value) {

      tab.index = value

    }

    return {

      tab,

      changeTab

    }

}



// component.js 

import {

  defineComponent

} from '@vue/composition-api';

import { useBanner, useTab } from './hooks'



export default defineComponent({

  setup() {

    // banner 图相关的逻辑

    const {

      banner,

      clickBanner,

      setBanner,

    } = useBanner()

    // banner 图相关的逻辑

    const {

      tab,

      changeTab

    } = useTab()

    return {

      banner,

      clickBanner,

      setBanner,

      tab,

      changeTab,

      // list 相关的逻辑

      // ...

    }

  }

})

这样我们就实现了UI与组件的完全分离,并且肉眼可见的代码更清晰,而且由于是函数引用的,我们可以只引入我业务相关的代码,代码是不是就很清晰了,而且由于引入与使用明确,cli 在打包 tree-shaking 期间可以更好地压缩代码量。

5、点题:编程范式 - 面向对象与函数式编程

我们刚刚看到了 composition-api 在 UI 与逻辑分离上的精彩表现。但是为什么我们刚开始的面向对象就不行呢?这就要说回编程范式的问题了

我们在说到面向对象的时候,总是会想办法抽象出一个类,而类有两个最基本的概念,一个是属性,另外一个就是方法。将类实例化之后,返回一个个我们需要的对象,实例化返回的对象会继承继承所有类的属性,而对象绑定的方法本质是为了改变对象的状态。而到了我们的框架里面,状态变成了 data,方法变成了 methods, methods 和 data 强相关了,而 data 又和 我们的页面 UI 有着强绑定的关系,绑在一条船上,所以这就是为什么分不开的原因。

而 composition-api 是函数式编程的产物,函数式编程的特点就是只关心输入和输出。而函数天生就有着隔离性, 在 composition-api 的体现上就是反正我一定有一个返回值给你,对页面 UI 来说,只需要告诉它需要和哪个返回值绑定就行了。而 setup 就会处理好输出和页面 UI 的映射关系,这也就是为什么 composition-api为什么必须在 setup 里面的原因。

6、值得分享的前辈建议

  1. 注意身体

不是开玩笑,很多前辈都有职业病,不规范使用键盘导致鼠标手、久坐痔疮、长期低头伏案的颈椎病...。这里想提醒大家和自己:注意锻炼,记得喝水。

  1. 不必不如BAT

这句话是想说,我们有时候强行给自己设了上限,总觉得别人是我们永远无法达到的目标,比如可能认为自己的技术永远都不如 bat 的开发人员,这里我们需要一些精神胜利法,我们觉得,其实我们和 bat 的他们是一样的,他们只不过比我们多懂一点技术而已,比如他们可能会 更早使用 composition-api, 那我们就也学会;他们可能 ts 用的更好,那我就也去学会它;可能他们底层更扎实,那我们就补足,这样我们总有一天也能达到相应的水平

The End

大家互勉!