新鮮出爐的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的設計理念和目標,咱們必須作一些不一樣的取捨。

相關文章
相關標籤/搜索