Composition API简介:一组基于函数的附加API,能够灵活地组成组件逻辑
我们都喜欢Vue非常容易拿来使用,并使构建中小型应用程序变得轻而易举。但是今天,随着Vue的采用率增长,许多用户还使用Vue来构建大型项目-这些项目需要长期反复由多个开发人员组成的团队进行和维护。多年来,我们目睹了其中一些项目遇到了Vue当前API所带来的编程模型的限制,这些问题可以概括为两类:
该RFC中提出的API在组织组件代码时为用户提供了更大的灵活性。现在可以将代码组织为每个函数都处理特定功能的函数,而不必总是通过选项来组织代码。API还使在组件之间甚至外部组件之间提取和重用逻辑变得更加简单。
开发人员在大型项目上的另一个常见功能要求是更好的TypeScript支持。Vue当前的API在与TypeScript集成时提出了一些挑战,这主要是由于Vue依靠单个this上下文来公开属性,并且在Vue组件中使用此API比普通JavaScript更具魔力。(例如,嵌套在方法下的内部函数指向组件实例,而不是方法对象)。换句话说,Vue现有的API根本就没有考虑类型推断,在尝试使其与TypeScript完美配合时会带来很多复杂性。
今天,大多数将Vue与TypeScript一起使用的用户正在使用vue-class-component,这是一个库,可将组件编写为TypeScript类(在装饰器的帮助下)。在设计3.0时,我们试图提供一个内置的Class API,以更好地解决先前(已删除)RFC中写代码的问题。但是,当我们在设计上进行讨论和迭代时,我们注意到,要使Class API解决类型问题,它必须依赖装饰器-这是一个非常不稳定的第2阶段提案,在实现细节方面存在很多不确定性。这使得在此基础上建立一个相当冒险的基础。
相比之下,此RFC中提议的API大多使用普通的变量和函数,它们自然是类型友好的。用建议的API编写的代码可以享受完整的类型推断,几乎不需要手动类型提示。这也意味着用建议的API编写的代码在TypeScript和普通JavaScript中看起来几乎相同,因此,即使非TypeScript用户也可以从写代码中受益,以获得更好的IDE支持。
这里提出的API并没有引入新的概念,而是更多地将Vue的核心功能(例如创建和观察响应状态)公开为独立的函数。在这里,我们将介绍一些最基本的API,以及如何使用它们代替2.x选项来表达组件内逻辑。请注意,本节重点介绍基本概念,因此不会详细介绍每个API。
反应状态和副作用
让我们从一个简单的任务开始:声明一些反应状态。
import { reactive } from 'vue'
// reactive state
const state = reactive({
count: 0
})
reactive 等效于2.x中当前的Vue.observable() API,已重命名以避免与RxJS observables混淆。在这里,返回 state 是所有Vue用户都应该熟悉的反应性对象。
Vue中反应性状态的基本用例是我们可以在渲染期间使用它。由于依赖关系跟踪,当反应性状态更改时,视图会自动更新。在DOM中渲染某些内容被视为“副作用”:我们的程序正在修改程序本身(DOM)外部的状态。要应用并根据反应状态自动重新应用副作用,我们可以使用watchEffect API:
import { reactive, watchEffect } from 'vue'
const state = reactive({
count: 0
})
watchEffect(() => {
document.body.innerHTML = `count is ${state.count}`
})
watchEffect需要一个函数,该函数可以应用所需的副作用(在这种情况下,请设置innerHTML)。它立即执行该函数,并跟踪其在执行期间用作依赖项的所有反应状态属性。在此,在初始执行之后,state.count将作为该监视程序的依赖项进行跟踪。将来改变state.count时,内部函数将再次执行。
这是Vue反应系统的本质。当您从组件中的data() 返回对象时,该对象会在内部由reactive() 使成为反应性的。模板被编译为使用这些反应性属性的呈现函数(将其视为更有效的innerHTML)。
watchEffect与2.x watch选项类似,但是它不需要分离被监视的数据源和副作用回调。Composition API还提供了一个监视函数,其行为与2.x选项完全相同。
function increment() {
state.count++
}
document.body.addEventListener('click', increment)
但是使用Vue的模板系统,我们不需要与innerHTML纠缠或手动附加事件侦听器。让我们使用一个假设的renderTemplate方法简化该示例,以便我们专注于反应性方面:
import { reactive, watchEffect } from 'vue'
const state = reactive({
count: 0
})
function increment() {
state.count++
}
const renderContext = {
state,
increment
}
watchEffect(() => {
// hypothetical internal code, NOT actual API
renderTemplate(
``,
renderContext
)
})
有时我们需要依赖于其他状态的状态-在Vue中,这是通过计算属性来处理的。要直接创建计算值,我们可以使用计算的API:
import { reactive, computed } from 'vue'
const state = reactive({
count: 0
})
const double = computed(() => state.count * 2)
返回到这里的 computed 是多少?如果我们猜测如何在内部实现 computed,我们可能会想到以下内容:
// 简化的伪代码
function computed(getter) {
let value
watchEffect(() => {
value = getter()
})
return value
}
但是我们知道这是行不通的:如果value是数字之类的原始类型,则返回值后,它与计算的内部更新逻辑的连接将丢失。这是因为JavaScript基本类型是通过值而不是通过引用传递的:
将值分配给对象作为属性时,也会发生相同的问题。如果一个反应性值在分配为属性或从函数返回时不能保持其反应性,那么它将不是很有用。为了确保我们始终可以读取计算的最新值,我们需要将实际值包装在一个对象中,然后返回该对象:
// 简化的伪代码
function computed(getter) {
const ref = {
value: null
}
watchEffect(() => {
ref.value = getter()
})
return ref
}
另外,我们还需要拦截对对象的.value属性的读/写操作,以执行依赖关系跟踪和更改通知(为简单起见,此处省略了代码)。现在,我们可以按引用传递计算所得的值,而不必担心失去反应性。权衡是为了获取最新值,我们现在需要通过.value访问它:
const double = computed(() => state.count * 2)
watchEffect(() => {
console.log(double.value)
}) // -> 0
state.count++ // -> 2
在这里,double是一个我们称为“ ref”的对象,因为它用作对其持有的内部值的反应性引用。
除了计算的引用外,我们还可以使用 ref API直接创建普通的可变引用:
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
我们可以将ref公开为渲染上下文的属性。在内部,Vue将对ref进行特殊处理,以便在渲染上下文中遇到ref时,该上下文直接公开其内部值。这意味着在模板中,我们可以直接编写{{count}}而不是{{count.value}}。
这是同一计数器示例的版本,使用ref而不是 reactive:
import { ref, watch } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
const renderContext = {
count,
increment
}
watchEffect(() => {
renderTemplate(
``,
renderContext
)
})
另外,当引用作为属性嵌套在反应对象下时,它也将在访问时自动展开:
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
// 无需使用 `state.double.value`
console.log(state.double)
到目前为止,我们的代码已经提供了可以根据用户输入进行更新的工作UI,但是该代码仅运行一次且不可重用。如果我们想重用逻辑,那么合理的下一步似乎是将其重构为一个函数:
import { reactive, computed, watchEffect } from 'vue'
function setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
const renderContext = setup()
watchEffect(() => {
renderTemplate(
``,
renderContext
)
})
注意上面的代码如何不依赖于组件实例的存在。实际上,到目前为止引入的API都可以在组件上下文之外使用,从而使我们能够在更广泛的场景中利用Vue的反应系统。
现在,如果我们离开了调用setup(),创建监视程序并将模板呈现到框架的任务,我们可以仅使用setup() 函数和模板来定义组件:
这是我们熟悉的单文件组件格式,只有逻辑部分(