新鲜出炉的Composition API中文翻译, 以表期待Vue 3.0的到来!

本人声明:本文属于译文,原文出处于Vue Composition API RFCjavascript

为了避免混淆做者本意、以及方便用户对比查阅的宗旨,本译文尽量的直译,而且与原文的段落结构、风格保持一致,有些时候也会将原文放在译文后方便用户参考理解。html

但也存在部分结合语境意译之处,包含但不限于如下:vue

  • Composition API/function <=====> 组合API/组合函数、合成API/合成函数
  • primitive type <=====> 基本类型、原始类型、标量类型
  • reactive value/state <=====> 可反应值/状态

总之但愿你们能学有所获,加油!java

Composition API RFC

  • 开始日期:2019-07-10
  • 主要目标版本:2.x / 3.x
  • Issues索引:#42
  • 实现的PR<置空>

1. 前言

这是关于 合成API(Composition API) 的介绍:一组新增的、基于函数式的、容许弹性组合组件逻辑的 APIs。react

2. 基础示例

<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>

<script> import { reactive, computed } from 'vue' export default { setup() { const state = reactive({ count: 0, double: computed(() => state.count * 2) }) function increment() { state.count++ } return { state, increment } } } </script>
复制代码

3. 动机

3.1 逻辑复用 & 代码组织

咱们都由于Vue很是容易上手、构建中小型应用很是简单而爱上它,但现在随着Vue的使用率增加,大量用户也正在使用Vue构建大型的项目 - 好比有些项目须要多人协做开发的团队花很长的时间迭代和维护。过去这些年,咱们亲眼目击了这些项目的一部分因为Vue现有的API遇到了编程模型的限制。问题汇总起来主要有两大类:git

  1. 随着新功能和特性的开发迭代,复杂组件的代码会变得愈来愈难以推理。 尤为是开发者遇到不是本身写的代码的时候。根本的缘由是Vue现有的API是经过选项来组织的;但有的时候,经过关注逻辑来组织代码会更有意义。
  2. 缺乏从多个组件中优雅的提取、复用逻辑的机制

这篇 RFC 里所提议的APIs为用户在组织组件代码时提供了更多的灵活性。相比于以前一直经过选项来组织代码,如今咱们能够针对某个功能像函数同样的处理方式来组织代码。这些APIs还使得在组件之间甚至外部组件之间提取和复用逻辑更加简单明了。咱们将在 细节设计 章节描述如何实现这些目标。github

3.2 更友好的类型推断

为大型项目服务的开发者还有一个共同的特性诉求:更好的 TypeScript 支持。 将Vue现有的API在与Typescript集成时的确有一些挑战,主要是由于Vue依赖于 this 上下文来抛出属性;并且相较于纯JS,this 在Vue组件中有不少魔法性 (好比:嵌套在 methods 下的函数里的 this 指向的是组件实例,而不是 methods 对象)。再者,Vue现有的API在设计时就根本没有考虑类型推论,在想着尝试优雅的集成 Typescript 时就致使增长了不少复杂性。ajax

现在大部分用户在 Vue 中集成使用 Typescript 时都会使用 vue-class-component 库,这个库实现了使用 Typescript 的 classes 配合装饰器来书写组件。在设计3.0的时候,咱们在 上一个RFC (已删除) 中尝试提供一个内置的 Class API 来解决类型问题。可是在设计时通过讨论和迭代,咱们注意到为了让Class API可以解决类型问题,就必须依赖装饰器,而装饰器当前处在很是不稳定的 stage 2 提案,在实现细节方面还有不少不肯定性,这就致使未来会有很大的风险。vue-cli

相比之下,这篇RFC里的APIs主要使用了一些天生类型友好的变量和函数。使用提议的这些APIs来写代码能够充分享受类型推论,无需手动输入类型提示;并且代码看上去会和Typescript、纯JS几乎一致。因此,即时非Typescript用户也从类型中得到更友好的IDE支持。编程

4. 细节设计

4.1 API介绍

这里提出的APIs更多的是将Vue的核心功能做为独立功能展开,而不是引入新的概念 - 例如建立并观测反应性状态。咱们将介绍一些最基本的APIs,以及如何使用它们替代2.x版本中的选项来描述组件逻辑。注意,本章将着重介绍基本的思路想法,因此不会很深刻每个API的细节。更多APIs规范能够在API 索引章节找到

4.1.1 可反应状态 & 反作用(Reactive State and Side Effect)

让咱们从一个简单的任务开始:声明一些可反应状态。

import { reactive } from 'vue'

// reactive state
const state = reactive({
    count: 0
})
复制代码

reactive 等价于2.x版本中的 Vue.observable() API,重命名是为了不与RxJS的 observables 混淆。如上,返回的 state 如今已是Vue用户都很熟悉的可反应对象了。

在Vue中,可反应状态的基本应用场景是咱们能够在整个渲染期间使用它。感谢依赖追踪的机制,当可反应状态的值变动后,视图将会自动更新。在DOM中渲染某些内容会被视为反作用:咱们的程序正在修改程序自己(the DOM)的一些状态。

为了应用、并根据可反应状态自动从新应用反作用,咱们可使用 watch API:

import { reactive, watch } from 'vue'

const state = reactive({
    count: 0
})

watch(() => {
    document.body.innerHTML = `count is ${state.count}`
})
复制代码

watch 接收一个函数做为参数,这个函数体内部是指望将被应用的反作用(在上例中是设置了 innerHTML )。它将自动执行函数,而且将追踪整个执行过程所使用的可反应状态做为依赖。在上例中,侦听器在初次执行后,state.count 将做为依赖被追踪。当 state.count 在将来某个时间刻发生更改时,函数内部将会再次执行。

这是Vue的可反应系统机制的本质。当你在一个组件的 data() 中返回一个对象时,将会被 reactive() 这个API内部转化成可反应的。模板会被编译成使用这些可反应状态属性的渲染函数(render function, 能够当作更高效的 innerHTML)。

继续上面的例子,下面是如何处理用户输入:

function increment() {
    state.count++
}

document.body.addEventListener('click', increment)
复制代码

可是借助Vue的模板系统,咱们不须要去纠结 innerHTML 或者手动添加事件监听器。为了更加的关注可反应性,如今咱们使用一个伪代码 renderTemplate 来简化这个例子,

import { reactive, watch } from 'vue'

const state = reactive({
    count: 0
})

function increment() {
    state.count++
}

const renderContext = {
    state,
    increment
}

watch(() => {
    // 假设的内部代码,不是真实的API
    renderTemplate(
    	`<button @click="increment">{{ state.count }}</button>`,
    	renderContext
    )
})
复制代码

4.1.2 计算状态 & 引用(Computed State and Refs)

有些时候,咱们须要一些状态依赖于其余状态,在Vue中可使用 computed 属性来处理。为了直接建立一个计算值,咱们可使用 computed API:

import { reactive, computed } from 'vue'

const state = reactive({
	count: 0
})

const double = computed(() => {
	return state.count * 2
})
复制代码

这里的 computed 返回什么呢?若是咱们构思一下它内部的实现的话,可能会想到以下:

// 简单的伪代码
function computed(getter) {
	let value
	watch(() => {
		value = getter()
	})
	
	return value
}
复制代码

可是咱们都知道这是行不通的,由于 value 值若是是一个基本类型,那么它在 computed 内部与更新逻辑的链接将在返回值后马上失去联系。这是由于JavaScript的基本类型传递的是值,而不是引用:

引用和值的区别

将值做为属性传递给对象也会发生一样的问题。若是在赋值操做过程当中或者从一个函数中返回时,一个可反应值不能保持它的反应状态则没有什么实际意义。为了从计算属性中可以一直读取到最新的值,咱们须要将真实的值包装在一个对象中,而后返回这个对象:

// 简单的伪代码
function computed(getter) {
	const ref = {
		value: null
	}
	watch(() => {
		ref.value = getter()
	})
	return ref
}
复制代码

另外,为了执行依赖追踪和更改通知,咱们还须要拦截这个对象的 .value 属性的读 / 写操做(简单起见,此处省略了代码)。如今咱们能够按引用传递计算值,而无需担忧失去反应性。不过为了取到最新值,咱们如今须要使用 .value 来访问:

const double = computed(() => state.count * 2)

watch(() => {
	console.log(double.value)
}) // output: 0

state.count++ // output: 2
复制代码

在上面的例子中 double 是一个对象,咱们称之为 引用( ref ),由于它为内部持有的值提供可反应引用。

你可能注意到了Vue已经有了 "refs" 的概念,可是它只适用于在模板中引用DOM元素或者组件实例。查看这里以了解新的引用系统(refs system)如何在逻辑状态值和模板引用中均可以使用。

除了计算引用( computed refs )外,咱们还能够经过 ref API直接建立单纯的可变引用:

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1
复制代码

4.1.3 引用展开

咱们能够将一个引用做为渲染上下文的属性公开。在内部,Vue将会对在渲染上下文中遇到的全部引用执行特殊对待,上下文直接展开引用内部的值。这意味着咱们能够直接写 {{ count }} ,而不须要写成 {{ count.value }}

下面这个例子是使用 ref 代替 reactive 实现与上面计算器相同的例子:

import { ref, watch } from 'vue'

const count = ref(0)

function increment() {
	count.value++
}

const renderContext = {
	count,
	increment
}

watch(() => {
	renderTemplate(
		`<button @click="increment">{{ count }}</button>`,
		renderContext
	)
})
复制代码

除此以外,当一个引用做为属性嵌套一个可反应对象下时,在访问时也会自动展开:

const state = reactive({
	count: 0,
	double: computed(() => state.count * 2)
}) 

// 不须要使用 `state.double.value`
console.log(state.double)
复制代码

4.1.4 组件中的用法

到目前为止,咱们的代码已经提供了可以根据用户输入而更新的有效UI,可是这份代码只运行了一次而且不可重复使用。若是咱们但愿复用这些逻辑,将它们包装成一个函数看上去彷佛是合理的下一步:

import { reactive, computed, watch } from 'vue'

function setup() {
	const state = reactive({
		count: 0,
		double: computed(() => state.count * 2)
	})
	
	function increment() {
		state.count++
	}
	
	return {
		state,
		increment
	}
}

const renderContext = setup()

watch(() => {
	renderTemplate(
		`<button @click="increment"> Count is: {{ state.count }}, double is: {{ state.double }} </button>`,
		renderContext
	)
})
复制代码

注意上面是如何不依赖与组件实例而组织代码的。实际上,到目前为止所介绍的APIs均可以在组件上下文以外所使用,这就意味着咱们能够在更多的场景下使用Vue的反应系统。

在框架的加持之下,如今咱们无需调用 setup()、建立侦听器、渲染模板。定义一个组件,咱们只须要 setup() 函数和模板:

<template>
	<button @click="increment">
		count is: {{ state.count }}, double is: {{ state.double }}
	</button>
</template>

<script> import { reactive, computed } from 'vue' export default { setup() { const state = reactive({ count: 0, double: computed(() => state.count * 2) }) function increment() { state.count++ } return { state, increment } } } </script>
复制代码

这是咱们很是熟悉的单文件组件的格式,只有逻辑部分( <script> )的表述格式变了。模板语法彻底和以前同样保留,这里省略了 <style>,可是依旧和你所熟悉的同样。

4.1.5 生命周期钩子(Lifecycle Hooks)

目前为止,咱们的介绍已经包含了一个组件的纯状态方面:用户输入的可反应状态( reactive state )、计算状态( computed state )、可变状态( mutating state )。可是一个组件可能也须要执行反作用,好比打印日志、发送ajax请求、或者在 window 上创建一个事件监听。这些反作用一般在如下时间节点执行:

  • 当一些状态变动时;
  • 当组件渲染完成( mounted )、更新( updated )、或者卸载( unmounted )时(生命周期的钩子函数)

咱们都知道能够基于状态变动使用 watch API来应用反作用。而在不用的生命周期钩子中执行反作用,咱们可使用指定的 onXXX APIs(与现有的生命周期选项一一对应):

import { onMounted } from 'vue'

export default {
	setup() {
		onMounted(() => {
			console.log('mounted...')
		})
	}
}
复制代码

这些生命周期方法只能在 setup 钩子中注册调用。它会经过使用内部全局状态值自动判断当前调用 setup 的实例。之因此这样设计是为了减小咱们提取逻辑到外部函数中时的迷惑。

更多关于这些APIs的细节能够在API 索引中找到。可是,建议在深刻细节以前先完成后续的章节阅读。

4.2 代码组织

在这以前,咱们已经经过结合导入函数( imported function )复制实现了组件现有的API,但这么作是为了什么呢?

明明经过选项定义组件、看上去比把全部东西都混合在一个大函数中要有组织的多!!!

这些想法是能够理解的,但正如在 动机 章节中所描述的,咱们相信合成API( Composition API )真的可以更好的组织代码,尤为是在复杂的组件中。接下来将尝试解释为何。

4.2.1 什么是“组织代码”?

让咱们回过头从新思考一下,当咱们在讨论“组织代码”的时候,咱们真正想说的是什么。使代码有组织性的终极目标应该是让代码更加容易阅读和理解,那咱们说的“理解代码”的本质是什么?咱们真的能够仅仅由于知道某个组件包含哪些选项,而声称咱们“理解”了这个组件吗?你是否深刻过一个由其余开发者写的超大组件(这里就有一个例子)、而且发现很难将其紧紧掌握?

思考一下你会如何向你的开发者朋友阐述介绍相似上面连接中的超大组件。你极可能青睐于从 “这个组件处理了X, Y和Z” 来开始介绍,而不是 “这个组件有这些data属性,这些computed属性和一些方法。” 当咱们尝试理解一个组件时,咱们更多关心的是 “这个组件正在尝试作什么” ,而不是 “这个组件中使用了哪些选项”。使用现有的基于选项的API来撰写的代码很天然的就能解释后者,可是在描述前者时表达的很不友好,甚至是差劲。

4.2.2 逻辑关注点 vs 选项类型(Logical Concerns vs. Option Types)

咱们将组件处理的 “X, Y和Z” 定义为逻辑关注点。小而功能单一的组件中通常不存在可读性的问题,由于整个组件都聚焦在单一的逻辑处理。总之越是高级的使用场景,这个问题越突出。就以 Vue CLI file for explorer 为例,这个组件不得不处理大量的逻辑关注点:

  • 跟踪当前目录状态并展现它的内容
  • 处理目录导航(打开、关闭、刷新)
  • 处理新目录的建立
  • 切换显示收藏夹
  • 切换显示隐藏的文件夹
  • 处理当前工做目录的变动、切换

您能经过阅读这些基于选项的代码,马上知道并区分这些逻辑关注点吗?有点难顶!你会发现与某一个特定逻辑关注点相关联的代码一般分散在各处。例如,"建立新目录"(crete new folder) 功能使用了两个data属性一个方法。注意观察:文件中这个方法的定义和data属性定义的距离超过了100行。

若是咱们对每一个逻辑关注点的代码进行着色,咱们会发现当使用基于选项的方式书写组件时,这些逻辑关注点的代码是有多分散:

逻辑关注点代码着色

正是这些代码的分散和碎片化、以及选项的强制分离使得逻辑关注点变得模糊而致使的一个复杂组件难以理解和维护。另外,当咱们关注一个逻辑点时,会不得不常常的在不一样的选项中跳来跳去——只为了查找和它相关的另外一个逻辑。

注意:源码可能会在几个地方改进,可是在撰写本文时展现的是最新提交的版本,无需任何修改就能够提供一个咱们本身可能会在真实生产环境写的案例。

若是咱们可以将逻辑关注点相关联的代码放在一块儿,那就再好不过了。这正是 合成API( Composition API ) 赋予的能力。上面“建立新目录”的功能能够经过这种方式书写:

function useCreateFolder(openFolder) {
	// 初始data属性
	const showNewFolder = ref(false)
	const newFolderName = ref('')
	
	// 初始computed属性
	const newFolderValid = computed(() => isValidMultiName(newFolderName.value))
	
	// 初始一个方法
	async function createFolder() {
		if (!newFolderValid.value) return
		const result = await mutate({
			mutation: FOLDER_CREATE,
			variables: {
				name: newFolderName.value
			}
		})
		openFolder(result.data.folderCreate.path)
		newFolderName.value = ''
		showNewFolder.value = false
	}
	return {
		showNewFolder,
		newFolderName,
		newFolderValid,
		createFolder
	}
}
复制代码

能够注意到咱们是如何将"建立新目录"功能相关的全部逻辑都放在一块儿并封装在一个函数中的。因为其语义化的名称,这个函数某种程度上也是自带文档(self-documenting)的。在命名函数时,建议约定以 use 开始,以指明这是一个合成函数。这个模式能够被应用到组件中的全部逻辑关注点,进而更好的对功能解耦:

选项模式和合成模式

上图的对比排除了 import 语句和 setup() 函数。此功能组件使用合成API( Composition API )的从新实现可点击这里查看

如今:

  • 每一个逻辑关注点的代码都放置在一个合成函数中 => 现有的在组件的选项之间频繁的"跳转"动做显著减小了
  • 合成函数在编辑器中能够被折叠 => 组件代码更加清晰明了
export default {
	setup() {
		// ...some code here
	}
}

function useCurrentFolderData(nextworkState) { // ...
}

function useFolderNavigation({ nextworkState, currentFolderData }) { // ...
}

function useFavoriteFolder(currentFolderData) { // ...
}

function useHiddenFolders() { // ...
}

function useCreateFolder(openFolder) { // ...
}
复制代码

如今 setup() 函数主要做为全部合成函数被调用的入口:

export default{
	setup() {
		// Network
    	const { networkState } = useNetworkState()

    	// Folder
    	const { folders, currentFolderData } = useCurrentFolderData(networkState)
    	const folderNavigation = useFolderNavigation({ networkState, currentFolderData })
    	const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData)
    	const { showHiddenFolders } = useHiddenFolders()
    	const createFolder = useCreateFolder(folderNavigation.openFolder)

    	// Current working directory
    	resetCwdOnLeave()
    	const { updateOnCwdChanged } = useCwdUtils()

    	// Utils
    	const { slicePath } = usePathUtils()

    	return {
      		networkState,
      		folders,
      		currentFolderData,
      		folderNavigation,
      		favoriteFolders,
      		toggleFavorite,
      		showHiddenFolders,
      		createFolder,
      		updateOnCwdChanged,
      		slicePath
    	}
	}
}
复制代码

固然,当咱们使用选项模式的API时,咱们不须要写上面这些代码。可是注意观察,setup 这个函数读起来就好像是口头描述这个组件开始尝试作什么同样 —— 这是基于选项模式彻底没有的。您还能够根据传递的参数清楚的看到各个合成函数之间的依赖关系流。最后,return 语句做为惟一的出口、能够检查暴露给模板的属性有哪些。

对于给定的功能,经过选项模式书写的组件和经过合成函数书写的组件基于同一个逻辑、表现出了两种不一样的组织方式。选项模式强制咱们基于 选项类型(option types) 组织代码,而合成API( Composition API )容许咱们基于各个逻辑关注点组织代码。

4.3 逻辑提取 & 复用(Logic Extraction and Reuse)

当涉及到跨组件之间提取、复用逻辑时,Compositon API很是的灵活。一个合成函数只依赖于它的参数和全局引入的Vue APIs,而不是充满魔法的 this 上下文。只须要将组件中你想复用的那部分代码,简单的将它导出为函数就能够了。你甚至能够经过导出组件的整个 setup 函数实现和 extends 等价的功能。

如今咱们来看一个例子:追踪鼠标位置。

import { ref, onMounted, onUnMounted } from 'vue'

export function useMouseTracking() {
	const x = ref(0)
	const y = ref(0)
	
	function update(e) {
		x.value = e.clientX
		y.value = e.clientY
	}
	
	onMounted(() => {
		window.addEventListener('mousemove', update)
	})
	
	onUnMounted(() => {
		window.removeEventListener('mousemove', update)
	})
	
	return {
		x, y
	}
}
复制代码

在其余组件中引用:

import { useMouseTracking } from '/path/to/useMouseTracking'

export default {
	setup() {
		const { x, y } = useMouseTracking()
		// ...其余的逻辑代码
		return {
			x, y
		}
	}
}
复制代码

在上面的合成API版本的文件访问例子中,咱们已经提取了一些实用代码(例如 usePathUtilsuseCwdUtils)到一个外部文件中,正是由于咱们发现对于其余的组件,它们一样颇有用。

相似的逻辑复用也能够经过现有的方法来实现,好比 mixins 、高阶组件或者无渲染组件(经过 scoped slots)。网上有不少关于这些方法的如何使用的解释,此处就再也不过多介绍了。高阶层次的想法是,这些方法模式中每个对比合成函数都有缺点:

  • 在渲染上下文中,暴露的属性来源不清晰。例如,当咱们阅读使用了多个 mixins 的组件模板时,很难判断出某个特定属性是由哪一个 mixin 注入的
  • 命名空间冲突。Mixins 潜在的与属性名、方法名冲突,而高阶组件可能会与预期的 prop 名称冲突。
  • 性能。高阶组件和无渲染组件须要额外有状态的组件实例配合实现,会形成必定的性能消耗。

相比之下,使用合成API( Composition API ):

  • 暴露给模板的属性值因为是由合成函数返回的,因此它们有清晰的来源
  • 合成函数的返回值能够被任意命名,因此不会发生命名冲突
  • 为了逻辑复用,没有建立没必要要的组件实例

4.4 和现有API结合使用(Usage Alongside Exisiting API)

合成API可以与现有基于选项模式的API结合使用。

  • 合成API已经在2.x选项( data, computedmethods )以前完成,而且没有权限访问由这些选项定义的属性
  • setup() 函数返回的属性将会挂载到 this 上,而且在2.x的选项中能够访问

4.5 插件开发(Plugin Development)

现在,不少Vue插件都将属性挂载到 this 上。例如: Vue Router 会注入 this.$routethis.$router, Vuex 注入了 this.$store。因为每一个插件都要求用户为注入的属性增长Vue类型,致使类型推断变得有点棘手。

当使用合成API时,不可使用 this。替而代之的,插件将利用内置的 provideinject 并抛出一个合成函数。下面是一个插件的伪代码:

const StoreSymbol = Symbol()

export function provideStore(store) {
	provide(StoreSymbol, store)
}

export function useStore() {
	const store = inject(StoreSymbol)
	if(!store) {
		// throw error, no store provided
	}
	return store
}
复制代码

接下来是如何使用:

// provide store at component root

const App = {
	setup() {
		provideStore(store)
	}
}

const Child = {
	setup() {
		const store = useStore()
		// use the store
	}
}
复制代码

请注意,store 也能够经过 Global API change RFC 中所建议的,经过应用程序级别来提供,但 useStore API在消费者组件中就将是同样的。

不足之处(Drawbacks)

5.1 Refs的开销

从技术上来说,Ref是本提案惟一的一个新概念。以前也介绍过,它的做用是为了将可反应的值做为变量传递,而且不要依赖于 this。这样作的缺点在于:

  1. 当使用这个合成API时,咱们须要不断的从单纯的值和引用对象之间区分refs,进而会增大理解上的精神负担。不过咱们能够经过命名约定大幅减小这种精神负担,例如:为全部引用变量(ref variables)加上后缀 xxxRef,或者依赖于类型系统。另一方面,因为在代码组织上提高了灵活性,组件的逻辑将会被切割成不少的小函数,这些函数的本地上下文很简单,引用的开销也很容易管理。
  2. 相比于单纯的值,refs的读取和变动显得要冗长些,由于须要经过 .value 访问。有些人建议经过编译时语法糖(相似Svelte 3)来解决这个问题。尽管在技术上来讲是可行的,但咱们认为在Vue中这样给予默认值没有什么意义(正如在与Svelte相比较中所讨论的)。换句话说,若是将它做为一个Babel插件处理这个问题,从技术上是可行的。

咱们已经讨论了是否有可能彻底禁用Ref概念、而且只使用可反应对象,但不管如何有些状况须要考虑:

  • 计算属性的getters可以返回基本类型,因此相似Ref的容器不可避免的须要使用;
  • 出于反应性的考虑,合成函数指望或者返回的只有基本类型值时,也须要将值包裹在对象中。若是框架没有提供标准的实现,用户颇有可能会实现他们本身喜欢的Ref模式(这样会致使生态系统碎片化)。

5.2 引用 vs 反应性(Ref vs. Reactive)

能够预测获得,用户可能会对介于 refreactive 使用哪个感到困惑。首先你须要知道的是,你必需要理解二者,以更有效的的使用合成API。只使用其中一个的话,可能会致使一些神奇的问题(esoteric workarounds)或者从新造了个轮子。

使用 refreactive 之间的区别,部分取决于与你会如何书写你的逻辑代码:

// 第一种:独立的变量
let x = 0
let y = 0

function updatePosition(e) {
	x = e.pageX
	y = e.pageY
}

// 第二种:一个对象
let pos = {
	x: 0,
	y: 0
}
function updatePosition(e) {
	pos.x = e.pageX
	pos.y = e.pageY
}
复制代码
  • 若是使用 ref, 咱们主要使用refs将第一种转换成更冗长的等式(就为了让基本类型的值具备反应性)
  • 若是使用 reactive,咱们的代码将会和第二种几乎同样,只须要使用 reactive 建立对象就能够了

总之,只使用 reactive 的问题主要是:合成函数的消费者必须一直与函数的返回值保持引用关联,以保持反应性。这个对象不能够被解构或者被展开:

// 合成函数
function useMousePosition() {
	const pos = reactive({
		x: 0,
		y: 0
	})
	// ...
	return pos
}

// 使用合成函数的组件
export default {
	setup() {
		// 反应性丢失!
		const { x, y } = useMousePosition()
		return {
			x, y
		}
		
		// 反应性丢失!
		return {
			...useMousePosition()
		}
		
		// 只有这样才会保持反应性
		// 你必须原样返回 `pos`, 在模板中使用 `pos.x` 和 `pos.y`
		return {
			post: useMousePosition()
		}
	}
}
复制代码

toRefs API能够被用来处理这种约束,它将每一个可反应对象转换为相应的引用:

function useMousePosition() {
	const pos = reactive({
		x: 0,
		y: 0
	})
	// ...
	
	return toRefs(pos)
}

// x & y 如今是refs了!
const { x, y } = useMousePosition()
复制代码

总结起来,有两种可行的风格:

  1. 同时使用 refreactive 就好像在普通的JS中,如何声明基本类型的变量和对象变量同样。当使用这种风格时建议使用一个IDE支持的类型系统。
  2. 尽量使用 reactive, 而且在合成函数中返回反应性对象时记得使用 toRefs。这会减小一些花在 refs 上的精力,但并不意味着你不须要去了解这个API。

在本阶段,咱们以为为 ref vs. reactive 制定最佳实践还为时过早。咱们建议您从上面的两种风格中衡量一下,并选择适合本身预期的模型。咱们会收集全世界用户的反馈,并就这个话题提供一个更加明确的指引。

5.3 Return语句的冗长(Verbosity of the Return Statement)

一些用户已经提出了关于 setup() 函数中的 return 语句冗长、并且跟样板同样的担心。

咱们认为一个明确的 return 语句有益于维护性。它让咱们能够精确的控制有哪些属性暴露给了模板,而且当咱们想知道模板中用到的一个属性是在组件中哪里定义的时候,能够做为一个入口点进行追踪。

过去有一些建议说,自动暴露在 setup() 中声明的变量,return 语句做为可选项。再说一次,因为这个违背了标准JavaScript的直觉,咱们不认为它应该是个默认选项。可是站在用户的角度,有几个方案能够作到这些:

  • IDE扩展基于 setup() 中声明的变量,自动生成 return 语句
  • Babel插件隐式生成并插入 return 语句

5.4 越灵活,就越要有纪律(More Flexibility Requires More Discipline)

不少用户指出,当合成API提供了更加灵活的代码组织方式时,为了让开发者作正确的事,同时也得要求更多规矩。一些人担忧经验不足的开发者会写出意大利面条式代码。换句话说,尽管合成API提升了代码质量的上限,但它同时也下降了质量下限。

某种程度上咱们赞成上面的观点。但咱们认为:

  1. 上限的收益远大于下限的损失;
  2. 经过相应的文档和社区的指导,咱们能够有效的解决代码组织的问题。

有些用户使用 Angular 1 的 controllers 做为(很差的)设计会致使很差的代码的示例。在合成API和Angular 1的控制器之间,它们最大的区别是它不依赖于一个共享的局部上下文。这样就能够很容易的将逻辑拆分到函数中,这也是JavaScript代码组织的核心机制。

任何JavaScript程序都从一个入口开始(就比如 setup())。咱们基于逻辑关注点、经过将它们分离到函数和模块中来构建程序,而合成API赋予咱们为Vue组件的代码作一样的事情的能力(The Composition API enables us to do the same for Vue component code)。换句话说,当使用合成API时,书写优雅的JavaScript代码的技能 ====直接转换成====> 书写优雅的Vue代码的技能。

6. 采用策略

不会影响、也不会放弃任何现有的 2.x APIs,由于合成API彻底是新增、独立的。经过@vue/composition 库做为一个插件已经能够在2.x中使用了。这个库的主要目标是提供API实验和收集用户反馈。当前的实现进度和本提案是保持同步更新的,但可能会因为插件技术的限制,可能会包含一些不同的地方。也可能会因为本提案的更新出现破坏性的变动,因此在当前阶段咱们不建议在生成环境中使用。

咱们尝试将这些API内置在3.0中,与2.x的选项能够一块儿使用。

对于只选择使用合成API的用户,能够提供一个编译时期的flag以用来丢弃适用于处理2.x选项的代码,这样作能够减少包的体积。不管如何,这些都是能够选择的。

这些API将被做为高级功能,由于它处理的问题主要出如今大型的应用程序里。咱们不会尝试将它做为默认文档,相反在文档中将会有专门的章节来介绍它。

7. 附录

7.1 Class API的类型问题(Type Issues with Class API)

曾经引入Class API的主要目的,是想为了获取更好的Typescript推论支持找到一个替代的API方案。但事实上Vue的组件须要合并来自多个源对象的属性挂载到单一的 this 上下文,而这会产生不少挑战 - 即使是基于Class API。

一个例子是为 props 声明类型。为了将 props 合并到 this 上,咱们必须在组件类上使用一个泛型参数,或者使用装饰器。

如下是个使用了泛型参数的例子:

interface Props {
	message: string
}

class App extends Component<Props> {
	static props = {
		message: String
	}
}
复制代码

尽管将接口声明传递给了泛型参数,用户仍然须要为 this 上的 props 代理行为提供一个运行时的 props 声明。这种双重声明很不必。

咱们也考虑过使用装饰器来代替:

class App extends Component<Props> {
	@prop message: string
}
复制代码

使用装饰器会致使依赖于一个有不少不肯定性的、处在stage-2的提案,并且Typescript如今的实现和TC39的提案彻底不一样步。另外,没法将使用装饰器实现的props的类型声明暴露给 this.$props,这会破坏TSX的支持。用户可能也会猜测能够为 prop 声明一个默认值,例如 @prop message: string = 'foo',可从实际技术出发,它们并无按预期工做。

除此以外,目前尚无办法为类的方法参数使用上下文类型 - 这就意味着传递给一个类的render函数的参数没法基于类的其余属性使用类型推断。

7.2 与React Hooks的比较

这种基于函数的API提供了与React Hooks相同级别的逻辑组合能力,但也有一些很重要的不一样之处。和React Hooks不一样,setup() 方法只会被调用一次,这意味着使用了Vue的合成API的代码:

  • 通常状况下更符合经常使用的JavaScript代码直觉
  • 对调用顺序不敏感,也能够有条件的执行
  • 不会在每次渲染时都重复调用,并产生相对较小的垃圾回收机制压力
  • 没必要考虑为了防止内联处理程序致使的子组件过渡从新渲染,而处处须要使用 useCallback 的问题
  • 没必要考虑若是用户忘记传递正确的依赖数组项、而致使的 useEffectuseMemo 可能捕获过失的变量的问题,Vue的自动依赖想追踪功能会确保侦听器和计算属性值一直保持正确不过时。(原文:ensures watchers and computed values are always correctly invalidated。最后的 invalidated 多是做者输错了)

咱们很是承认React Hooks的创造价值,它也是本提案的重要灵感来源之一。总之,上面提到的问题真实存在于它的设计中,咱们注意到Vue的可反应模型正好提供了解决方案。

7.3 与Svelte的比较

尽管采用的路线大相径庭,可是合成API和Svelte 3基于编译期的方法从概念上讲,实际上有很大的通性。下面是个例子

Vue:

<script> import { ref, watch, onMounted } from 'vue' export default { setup() { const count = ref(0) function increment() { count.value++ } watch(() => { console.log(count.value) }) onMounted(() => { console.log('mounted') }) return { count, increment } } } </script>
复制代码

Svelte:

<script> import { onMount } from 'svelte' let count = 0 function increment() { count++ } $: console.log(count) onMount(() => { console.log('mounted') }) </script>
复制代码

Svelte的代码看上去简洁不少主要是由于它在编译时期作了以下的事情:

  • 隐式的将整个 <script> 块(除去 import 语句)包装到被每一个组件实例调用的函数中(而不是只被执行一次)
  • 隐式的为可变的变量注册了反应性(原文:Implicitly registers reactivity on variable mutations)
  • 隐式的将局部变量暴露到渲染上下文中
  • $ 语句将被编译成从新执行的代码

从技术上来说,在Vue中咱们能够作一样的事情(用户也能够经过Babel插件)。咱们没有这样作的主要缘由是 和标准的JavaScript 保持一致。若是你从一个Vue文件中提取了 <script> 的代码,咱们但愿它和标准的ES Moudle同样。Svelte的 <script> 块内部的代码内容,某种方面来看,从技术上根本不是标准的JavaScript。这种基于编译期的方法存在不少问题:

  1. Code works differently with/without compilation(大体意思是有无编译流程,代码工做表现的不一致,后文也是)。做为一个渐进式的框架,不少Vue用户可能指望、须要、必须在没有构建流程的状况下使用它,因此“须要构建”不能做为默认项。另外一方面,Svelte将本身做为一个编译器,而且只能配合构建流程使用。这是两个框架在有意识的作出取舍。
  2. Code works differently inside/outside components。当尝试从一个Svelte组件中,提取出逻辑并放到一个标准的JavaScript文件中时,会失去具备魔法性的简洁语法,并且必须使用更加冗长低级API
  3. Svelte的自动增长反应性只对顶层变量有效 - 不处理函数内部声明建立的变量,所以咱们没法将反应性状态封装在组件内部声明的函数中
  4. 不标准的语义致使与Typescript集成时会有问题。

这些毫不是在说Svelte 3的想法很烂 - 事实上,这些都是颇有创意的实现,咱们很是承认Rich的工做。可是基于Vue的设计理念和目标,咱们必须作一些不一样的取舍。

相关文章
相关标签/搜索