了解 Vue 的 Compsition API
在这篇文章中,我将讲讲 Vue 的 Composition API 为什么比之前的 Options API 要好,以及它是如何工作的。
Options API 有什么问题
首先,这里不是要大家放弃 Options API,如果你觉得 Options API 还不错,已经习惯了,就继续使用它。但我希望你能明白为什么 Composition API 是一种更好的选择。
当我刚开始接触 Vue 时,我喜欢它用匿名对象来构建组件的方式。简单易学,也很自然。但随着使用的时间越长,就会遇到一些很奇怪的问题。比如下面这段代码:
export default {
mounted: async function () {
this.load().then(function () {
this.name = 'Foo'
})
},
}
这里的第二个this
的问题我想很多人都遇到过。在不调试的情况下,很难知道两个this
为什么不一样。我们只知道要解决这个问题,反正得用箭头函数,例如:
mounted: async function () {
this.load().then(() => {
this.name = 'Foo';
});
}
此外,从data
函数返回成员后,Vue 会包装它们(通常使用Proxy
),以便能够监听对象的修改。但如果你尝试直接替换对象(比如数组),就可能监听不到对象修改了。比如:
export default {
data: () => {
return {
items: [],
}
},
mounted: async function () {
this.load().then((result) => {
this.items = result.data
})
},
methods: {
doSomeChange() {
this.items[0].foo = 'foo'
},
},
}
这是 Vue 最常见的问题之一。尽管可以很简单地解决这些问题,但毕竟给学习 Vue 工作原理造成了障碍。
最后,Options API 很难实现组件间的功能共享,为了解决这个问题,Vue 使用了mixins
的概念。例如,你可以创建一个 mixin 来扩展组件,如下所示:
export default {
data: function () {
return { items: [] };
},
methods: {
...
}
}
mixin 看起来很像 Options API。它只是对对象进行合并,以允许你添加到特定组件上:
import dataService from "./dataServiceMixin";
export default {
mixins: [dataService],
...
mixin 最大问题是名称冲突。
鉴于 Options API 这些不足,Composition API 诞生了。
了解 Composition API
现在来介绍一下 Composition API。Vue 2 也可以使用 Composition API。首先,你需要引入 Composition API 库:
> npm i -s @vue/composition-api
要启用 Composition API,你只需在main.js/ts
中注册一下:
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
让我们来写一个 Composition API 组件。首先,组件仍然是匿名对象:
export default {
setup() {
return {}
},
}
Composition API 的关键就是这个setup
方法,所有所需的数据都从setup
中返回。这非常类似于 Options API 的data
方法:
export default {
setup() {
const name = 'Foo'
return { name }
},
}
与其只在return
中赋值,不如在setup
里面用一个局部变量创建。为什么要这样做呢?因为 JavaScript 的闭包。让我们扩展一下,再写一个函数。
export default {
setup() {
const name = 'Foo'
function save() {
alert(`Name: ${name}`)
}
return { name, save }
},
}
该函数可以访问name
,因为它们在同一个作用域内。这就是 Composition API 的神奇之处。没有魔术对象,只有 JavaScript。这个模式可以支持更复杂的场景,更灵活,这一切的基础只是闭包,仅此而已。
在这个例子中name
是永远不会改变的。为了让 Vue 能够处理绑定,它需要知道name
的变化。在这个例子中,如果你在save()
中修改了代码来改变名称,它不会反映在 UI 中:
export default {
setup() {
const name = 'Foo'
function save() {
name = name + ' saved'
alert(`Name: ${name}`)
}
return { name, save }
},
}
响应式
要使对象具有响应式,Vue 提供了两种方法:ref
和reactive
包装器。
你可以将name
包装成一个ref
对象:
import { ref } from '@vue/composition-api'
export default {
setup() {
const name = ref('Foo')
function save() {
name.value = name.value + ' saved'
alert(`Name: ${name.value}`)
}
return { name, save }
},
}
这是一个简单的响应式,ref 对象的 value 属性才是实际的值。
这与简单对象可以很好地配合,但是具有自身状态的对象(例如类或数组),对值的更改是不够的。你真正需要的是使用一个 proxy 函数以便监听该对象内部发生的任何更改。
import { ref, reactive } from '@vue/composition-api'
export default {
setup() {
const name = ref('Foo')
const items = reactive([])
function save() {
// Change Array
items.splice(0, items.length)
name.value = name.value + ' saved'
alert(`Name: ${name.value}`)
}
return { name, save, items }
},
}
在此示例中,使用了splice
更改数组,Vue 需要了解此更改。你可以通过使用另一个称为reactive
的包装器包装该对象(在本例中为数组)来实现。
Composition API 中的 reactive 包装器与 Vue2 的 Vue.observable 包装器相同。
ref 和 react 之间的区别在于,reactive 用 proxy 包装对象,而 ref 是简单的值包装器。
因此,你不需要将返回的 reactive 对象再包装,不需要像 ref 对象一样通过 value 属性访问其值。
一旦有了 ref 和 reactive 对象,就可以观察更改。有两种方法可以做到这一点:watch
和watchEffect
。watch 允许你监听单个对象的更改:
setup() {
const name = ref("Shawn");
watch(() => name, (before, after) => {
console.log("name changes");
});
return { name };
},
该watch
函数有两个参数:将数据返回到watch
的回调以及发生更改时要调用的回调。它提供两个参数分别是修改前的值before
和修改后的值after
。
或者,你可以使用watchEffect
监听 reactive 对象的任何更改。它只需要一个回调:
watchEffect(() => {
console.log('Ch..ch...ch...changes.')
})
组合组件
有时候你希望把一个已有组件的功能组合到另一个组件,以便代码重用和数据交互。当组合组件时,Composition API 只是使用作用域和闭包解决问题。例如,你可以创建一个简单的 service 服务:
import axios from 'axios'
export let items = []
export async function load() {
let result = await axios.get(API_URL)
items.splice(0, items.length, ...result.data)
}
export function removeItem(item) {
let index = items.indexOf(item)
if (index > -1) {
items.splice(index, 1)
}
}
然后你可以按需将需要的功能(对象或函数) import 到你的组件中:
import { ref, reactive, onMounted } from '@vue/composition-api'
import { load, items, removeItem } from './dataService'
export default {
setup() {
const name = ref('Foo')
function save() {
alert(`Name: ${this.name}`)
}
return {
load, // from import
removeItem, // from import
name,
save,
items, // from import
}
},
}
你可以使用 Composition API 更明显式地组合你的组件。接下来我们来谈谈组件的使用。
使用 Props
在 Composition API 中定义 Props 的方式与 Options API 相同:
export default {
name: 'WaitCursor',
props: {
message: {
type: String,
required: false,
},
isBusy: {
type: Boolean,
required: false,
},
},
setup() {},
}
你可以通过向setup
添加可选参数来访问 Props:
setup(props) {
watch(() => props.isBusy, (b,a) => {
console.log(`isBusy Changed`);
});
}
此外,你可以添加第二个参数,可以访问emit
,slots
和attrs
对象,和 Options API 上 this 指针上的对象对应:
setup(props, context) {
watch(() => props.isBusy, (b,a) => context.emit("busy-changed", a));
}
使用组件
在 Vue 中有两种使用组件的方法。你可以全局注册组件(用作通用组件),以便可以在任何地方使用它们:
import WaitCursor from './components/WaitCursor'
Vue.component('wait-cursor', WaitCursor)
通常,你可以将组件添加到正在使用的特定组件中。在 Composition API 中,与 Options API 相同:
import WaitCursor from './components/waitCursor'
import store from './store'
import { computed } from '@vue/composition-api'
export default {
components: {
WaitCursor, // Use Component
},
setup() {
const isBusy = computed(() => store.state.isBusy)
return { isBusy }
},
}
在 Composition API 中指定了组件后,就可以使用它:
<div>
<WaitCursor message="Loading..." :isBusy="isBusy"></WaitCursor>
<div class="row">
<div class="col">
<App></App>
</div>
</div>
</div>
使用组件的方式与 Options API 中的方式没有什么不同。
在 Vue 3 中使用 Composition API
如果你使用的是 Vue 3,则无需单独引用 Composition API。Vue 3 已经集成好了。该@vue/composition-api
库仅用于 Vue 2 项目。Vue 3 的真正变化是,当你需要导入 Composition API 时,只需要直接从 Vue 获取它们:
import {
ref,
reactive,
onMounted,
watch,
watchEffect,
//from "@vue/composition-api";
} from 'vue'
其他一切都一样。只需从“ vue”导入即可。在 Vue 3 中,使用 Composition API 只是简单一些,因为它是默认行为。
Vue 3 的主要目标之一是改善 TypeScript 体验,所有内容都有类型库。但是要增加类型安全性,你必须进行一些小的更改才能使用 TypeScript。在创建组件时,需使用defineComponent
:
import { defineComponent, reactive, onMounted, ref } from "vue";
import Customer from "@/models/Customer";
export default defineComponent({
name: "App",
setup() {
const customers = reactive([] as Array<Customer>);
const customer = ref(new Customer());
const isBusy = ref(false);
const message = ref("");
return {
customers, customer, isBusy, message
}
}
});
在这些示例中,变量被推断为类型实例(例如,Customers
对象是Reactive<Customer[]>
类型的实例)。此外,使用强类型可以降低传递错误数据的机会。当然,如果你使用 TypeScript(尤其是在 Visual Studio 或 VS Code 中),则会有非常友好的智能提示。