• Vue 3.0 的 Composition API 尝鲜


    前段时间,vue 官方释出了 Composition API RFC 的文档,我也在收到消息的第一时间上手尝鲜。

    虽然 vue 3.0 尚未发布,但是其处于 RFC 阶段的 Composition API 已经可以通过插件 @vue/composition-api 进行体验了。接下来的内容我将以构建一个 TODO LIST 应用来体验 Composition API 的用法。

    一、Vue 2.x 方式构建应用。

    这个 TODO LIST 应用非常简单,仅有一个输入框、一个状态切换器、以及 TODO 列表构成:

    大家也可以在这里体验。

    借助 vue-cli 初始化项目以后,我们的项目结构如下(仅讨论 /src 目录):

    .
    ├── App.vue
    ├── components
    │   ├── Inputer.vue
    │   ├── Status.vue
    │   └── TodoList.vue
    └── main.js

    从 /components 里文件的命名不难发现,三个组件对应了 TODO LIST 应用的输入框、状态切换器,以及 TODO 列表。这三个组件的代码都非常简单就不展开讨论了,此处只讨论核心的 App.vue 的逻辑。

    • App.vue
    <template>
      <div class="main">
        <Inputer @submit="submit" />
        <Status @change="onStatusChanged" />
        <TodoList
          :list="onShowList"
          @toggle="toggleStatus"
          @delete="onItemDelete"
        />
      </div>
    </template>
    
    <script>
    import Inputer from './components/Inputer'
    import TodoList from './components/TodoList'
    import Status from './components/Status'
    
    export default {
      components: {
        Status,
        Inputer,
        TodoList
      },
    
      data () {
        return {
          todoList: [],
          showingStatus: 'all'
        }
      },
      computed: {
        onShowList () {
          if (this.showingStatus === 'all') {
            return this.todoList
          } else if (this.showingStatus === 'completed') {
            return this.todoList.filter(({ completed }) => completed)
          } else if (this.showingStatus === 'uncompleted') {
            return this.todoList.filter(({ completed }) => !completed)
          }
        }
      },
      methods: {
        submit (content) {
          this.todoList.push({
            completed: false,
            content,
            id: parseInt(Math.random(0, 1) * 100000)
          })
        },
        onStatusChanged (status) {
          this.showingStatus = status
        },
        toggleStatus ({ isChecked, id }) {
          this.todoList.forEach(item => {
            if (item.id === id) {
              item.completed = isChecked
            }
          })
        },
        onItemDelete (id) {
          let index = 0
          this.todoList.forEach((item, i) => {
            if (item.id === id) {
              index = i
            }
          })
          this.todoList.splice(index, 1)
        }
      }
    }
    </script>

    在上述的代码逻辑中,我们使用 todoList 数组存放列表数据,用 onShowList 根据状态条件 showingStatus 的不同而展示不同的列表。在 methods 对象中定义了添加项目、切换项目状态、删除项目的方法。总体来说还是非常直观简单的。

    按照 Vue 的官方说法,2.x 的写法属于 Options-API 风格,是基于配置的方式声明逻辑的。而接下来我们将使用 Composition-API 风格重构上面的逻辑。

    二、使用 Composition-API 风格重构逻辑

    下载了 @vue/composition-api 插件以后,按照文档在 main.js 引用便开启了 Composition API 的能力。

    • main.js
    import Vue from 'vue'
    import App from './App.vue'
    import VueCompositionApi from '@vue/composition-api'
    
    Vue.config.productionTip = false
    Vue.use(VueCompositionApi)
    
    new Vue({
      render: h => h(App),
    }).$mount('#app')

    回到 App.vue,从 @vue/composition-api 插件引入 { reactive, computed, toRefs } 三个函数:

    import { reactive, computed, toRefs } from '@vue/composition-api'

    仅保留 components: { ... } 选项,删除其他的,然后写入 setup() 函数:

    export default {
      components: { ... },
      setup () {}
    }

    接下来,我们将会在 setup() 函数里面重写之前的逻辑。

    首先定义数据

    为了让数据具备“响应式”的能力,我们需要使用 reactive() 或者 ref() 函数来对其进行包装,关于这两个函数的差异,会在后续的章节里面阐述,现在我们先使用 reactive() 来进行。

    在 setup() 函数里,我们定义一个响应式的 data 对象,类似于 2.x 风格下的 data() 配置项。

    setup () {
        const data = reactive({
          todoList: [],
          showingStatus: 'all',
          onShowList: computed(() => {
            if (data.showingStatus === 'all') {
              return data.todoList
            } else if (data.showingStatus === 'completed') {
              return data.todoList.filter(({ completed }) => completed)
            } else if (data.showingStatus === 'uncompleted') {
              return data.todoList.filter(({ completed }) => !completed)
            }
          })
        })
    }

    其中计算属性 onShowList 经过了 computed() 函数的包装,使得它可以根据其依赖的数据的变化而变化。

    接下来定义方法

    在 setup() 函数里面,对之前的几个操作选项的方法稍加修改即可直接使用:

        function submit (content) {
          data.todoList.push({
            completed: false,
            content,
            id: parseInt(Math.random(0, 1) * 100000)
          })
        }
        function onStatusChanged (status) {
          data.showingStatus = status
        }
        function toggleStatus ({ isChecked, id }) {
          data.todoList.forEach(item => {
            if (item.id === id) {
              item.completed = isChecked
            }
          })
        }
        function onItemDelete (id) {
          let index = 0
          data.todoList.forEach((item, i) => {
            if (item.id === id) {
              index = i
            }
          })
          data.todoList.splice(index, 1)
        }

    与在 methods: {} 对象中定义的形式所不同的地方是,在 setup() 里的方法不能通过 this 来访问实例上的数据,而是通过直接读取 data 来访问。

    最后,把刚刚定义好的数据和方法都返回出去即可:

        return {
          ...toRefs(data),
          submit,
          onStatusChanged,
          toggleStatus,
          onItemDelete,
        }

    这里使用了 toRefs() 给 data 对象包装了一下,是为了让它的数据保持“响应式”的,这里面的原委会在后续章节展开。

    重构完成后,发现其运行的结果和之前的完全一致,证明 Composition API 是可以正确运行的。接下来我们来聊聊 reactive() 和 ref() 的问题。

    三、响应式数据

    我们知道 Vue 的其中一个卖点,就是其强大的响应式系统。无论是哪个版本,这个核心功能都贯穿始终。而说到响应式系统,往往离不开响应式数据,这也是被大家所津津乐道的话题。

    回顾一下,在2.x版本中 Vue 使用了 Object.defineProperty() 方法改写了一个对象,在它的 getter 和 setter 里面埋入了响应式系统相关的逻辑,使得一个对象被修改时能够触发对应的逻辑。在即将到来的 3.0 版本中,Vue 将会使用 Proxy 来完成这里的功能。为了体验所谓的“响应式对象”,我们可以直接通过 Vue 提供的一个 API Vue.observable() 来实现:

    const state = Vue.observable({ count: 0 })
    
    const Demo = {
      render(h) {
        return h('button', {
          on: { click: () => { state.count++ }}
        }, `count is: ${state.count}`)
      }
    }
    上述代码引用自官方文档

    从代码可以看出,通过 Vue.observable() 封装的 state,已经具备了响应式的特性,当按钮被点击的时候,它里面的 count 值会改变,改变的同时会引起视图层的更新。

    回到 Composition API,它的 reactive() 和 ref() 函数也是为了实现类似的功能,而 @vue/composition-api 插件的核心也是来自 Vue.observable():

    function observe<T>(obj: T): T {
      const Vue = getCurrentVue();
      let observed: T;
      if (Vue.observable) {
        observed = Vue.observable(obj);
      } else {
        const vm = createComponentInstance(Vue, {
          data: {
            $$state: obj,
          },
        });
        observed = vm._data.$$state;
      }
    
      return observed;
    }
    节选自插件源码

    在理解了 reactive() 和 ref() 的目的之后,我们就可以去分析它们的区别了。

    首先我们来看两段代码:

    // style 1: separate variables
    let x = 0
    let y = 0
    
    function updatePosition(e) {
      x = e.pageX
      y = e.pageY
    }
    
    // --- compared to ---
    
    // style 2: single object
    const pos = {
      x: 0,
      y: 0
    }
    
    function updatePosition(e) {
      pos.x = e.pageX
      pos.y = e.pageY
    }

    假设 x 和 y 都是需要具备“响应式”能力的数据,那么 ref() 就相当于第一种风格,单独地为某个数据提供响应式能力;而 reactive() 则相当于第二种风格,给一整个对象赋予响应式能力。

    但是在具体的用法上,通过 reactive() 包装的对象会有一个坑。如果想要保持对象内容的响应式能力,在 return 的时候必须把整个 reactive() 对象返回出去,同时在引用的时候也必须对整个对象进行引用而无法解构,否则这个对象内容的响应式能力将会丢失。这么说起来有点绕,可以看看官网的例子加深理解:

    // composition function
    function useMousePosition() {
      const pos = reactive({
        x: 0,
        y: 0
      })
    
      // ...
      return pos
    }
    
    // consuming component
    export default {
      setup() {
        // reactivity lost!
        const { x, y } = useMousePosition()
        return {
          x,
          y
        }
    
        // reactivity lost!
        return {
          ...useMousePosition()
        }
    
        // this is the only way to retain reactivity.
        // you must return `pos` as-is and reference x and y as `pos.x` and `pos.y`
        // in the template.
        return {
          pos: useMousePosition()
        }
      }
    }

    举一个不太恰当的例子。“对象的特性”是赋予给整个“对象”的,它里面的内容如果也想要拥有这部分特性,只能和这个对象捆绑在一块,而不能单独拎出来。

    但是在具体的业务中,如果无法使用解构取出 reactive() 对象的值,每次都需要通过 . 操作符访问它里面的属性会是非常麻烦的,所以官方提供了 toRefs() 函数来为我们填好这个坑。只要使用 toRefs() 把 reactive() 对象包装一下,就能够通过解构单独使用它里面的内容了,而此时的内容也依然维持着响应式的特性。

    至于何时使用 reactive() 和 ref(),都是按照具体的业务逻辑来选择。对于我个人来说,会更倾向于使用 reactive() 搭配 toRefs() 来使用,因为经过 ref() 封装的数据必须通过 .value 才能访问到里面的值,写法上要注意的地方相对更多一些。

    广州品牌设计公司https://www.houdianzi.com PPT模板下载大全https://redbox.wode007.com

    四、Composition API 的优势及扩展

    Vue 其中一个被人诟病得很严重的问题就是逻辑复用。随着项目越发的复杂,可以抽象出来被复用的逻辑也越发的多。但是 Vue 在 2.x 阶段只能通过 mixins 来解决(当然也可以非常绕地实现 HOC,这里不再展开)。mixins 只是简单地把代码逻辑进行合并,如果需要对逻辑进行追踪将会是一个非常痛苦的过程,因为繁杂的业务逻辑里面往往很难一眼看出哪些数据或方法是来自 mixins 的,哪些又是来自当前组件的。

    另外一点则是对 TypsScript 的支持。为了更好地进行类型推断,虽然 2.x 也有使用 Class 风格的 ts 实现方案,但其冗长繁杂和依赖不稳定的 decorator 的写法,并非一个好的解决方案。受到 React Hooks 的启发,Vue Composition API 以函数组合的方式完成逻辑,天生就适合搭配 TypeScript 使用。

    至于 Options API 和 Composition API 孰优孰劣的问题,在本文所展示的例子中其实是比较难区分的,原因是这个例子的逻辑实在是太过简单。但是如果深入思考的话不难发现,如果项目足够复杂,Composition API 能够很好地把逻辑抽离出来,每个组件的 setup() 函数所返回的值都能够方便地被追踪(比如在 VSCode 里按着 cmd 点击变量名即可跳转到其定义的地方)。这样的能力在维护大型项目或者多人协作项目的时候会非常有用,通用的逻辑也可以更细粒度地共享出去。

    关于 Composition API 的设计理念和优势可以参考官网的 Motivation 章节。

    如果脑洞再开大一点,Composition API 可能还有更酷的玩法。

    对于一些第三方组件库(如 element-ui),除了可以提供包含了样式、结构和逻辑的组件之外,还可以把部分逻辑以 Composition API 的方式提供出来,其可定制化和玩法将会更加丰富。

    reactive() 方法可以把一个对象变得响应式,搭配 watch() 方法可以很方便地处理 side effects:

    import { reactive, watch } from 'vue'
    
    const state = reactive({
      count: 0
    })
    
    watch(() => {
      document.body.innerhtml = `count is ${state.count}`
    })

    上述例子中,当响应式的 state.count 被修改以后,会触发 watch() 函数里面的回调。基于此,也许我们可以利用这个特性去处理其他平台的视图更新问题。微信小程序开发框架 mpvue 就是通过魔改 Vue 的源码来实现小程序视图的数据绑定及更新的,如果拥有了 Composition API,也许我们就可以通过 reactive() 和 watch() 等方法来实现类似的功能,此时 Vue 将会是位于数据视图中间的一层,数据的绑定放在 reactive(),而视图的更新则统一放在 watch() 当中进行。

    五、小结

    本文通过一个 TODO LIST 应用,按照官网的指导完成一次对 Composition API 的尝鲜式探索,学习了新的 API 的用法并讨论了当中的一些设计理念,分析了当中的一些问题,最后脑洞大开对立面的用法进行了探索。由于相关资料较少且 Composition API 仍在 RFC 阶段,所以文章当中可能会有难以避免的谬误,如果有任何的意见和看法都欢迎和我交流。

  • 相关阅读:
    在响应式项目中连接设计与开发
    社交APP的痛点及九大流派解析,微信陌陌,咱还约吗
    网传奶茶妹将进入红杉资本
    OLE、OCX和ActiveX控件之间的比较
    Unity3D网络游戏实战(第2版)
    离婚?在Facebook上把已婚状态改为单身就可以!
    同样酷炫但却失败了的谷歌眼镜,能否给Apple Watch一些前车之鉴?
    Apple Watch首批评测放出:有吐槽、有体贴……毒哭了,暖哭了
    如何培养战略领导力,赢得“长久游戏”?
    博客社交已死,数据社交“永生”
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/14119908.html
Copyright © 2020-2023  润新知