前段时间,Vue 官方释出了 Composition API RFC 的文档,我也在收到消息的第一时间上手尝鲜。javascript
虽然 Vue 3.0 还没有发布,可是其处于 RFC 阶段的 Composition API 已经能够经过插件 @vue/composition-api 进行体验了。接下来的内容我将以构建一个 TODO LIST 应用来体验 Composition API 的用法。vue
本文示例的代码: https://github.com/jrainlau/v...
这个 TODO LIST 应用很是简单,仅有一个输入框、一个状态切换器、以及 TODO 列表构成:java
你们也能够在这里体验。react
借助 vue-cli
初始化项目之后,咱们的项目结构以下(仅讨论 /src
目录):git
. ├── App.vue ├── components │ ├── Inputer.vue │ ├── Status.vue │ └── TodoList.vue └── main.js
从 /components
里文件的命名不难发现,三个组件对应了 TODO LIST 应用的输入框、状态切换器,以及 TODO 列表。这三个组件的代码都很是简单就不展开讨论了,此处只讨论核心的 App.vue
的逻辑。github
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-cli
按照 Vue 的官方说法,2.x 的写法属于 Options-API 风格,是基于配置的方式声明逻辑的。而接下来咱们将使用 Composition-API 风格重构上面的逻辑。element-ui
下载了 @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
才能访问到里面的值,写法上要注意的地方相对更多一些。
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 可能还有更酷的玩法。
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 阶段,因此文章当中可能会有难以免的谬误,若是有任何的意见和见解都欢迎和我交流。