Composition API 一組基於功能的附加API,容許靈活地組成組件邏輯。php
觀看Vue Mastery的Vue 3基礎課程。下載Vue 3備忘單html
基本示例vue
<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>
邏輯重用和代碼組織react
咱們都喜歡Vue很是容易上手,並使中小型應用程序的構建變得垂手可得。可是現在,隨着Vue的採用率增加,許多用戶也正在使用Vue來構建大型項目,這些項目是由多個開發人員組成的團隊在很長的時間內進行迭代和維護的。多年來,咱們目擊了其中一些項目遇到了Vue當前API所帶來的編程模型的限制。這些問題能夠歸納爲兩類:ajax
隨着功能的增加,複雜組件的代碼變得愈來愈難以推理。這種狀況尤爲發生在開發人員正在閱讀本身未編寫的代碼時。根本緣由是Vue的現有API經過選項強制執行代碼組織,可是在某些狀況下,經過邏輯考慮來組織代碼更有意義。編程
缺少用於在多個組件之間提取和重用邏輯的乾淨且免費的機制。(有關邏輯提取和重用的更多詳細信息)api
該RFC中提出的API在組織組件代碼時爲用戶提供了更大的靈活性。如今能夠將代碼組織爲每一個函數都處理特定功能的函數,而沒必要老是經過選項來組織代碼。API還使在組件之間甚至外部組件之間提取和重用邏輯變得更加簡單。咱們將在「 詳細設計」部分中說明如何實現這些目標瀏覽器
更好的類型推斷框架
開發人員在大型項目上的另外一個常見功能要求是更好的TypeScript支持。Vue當前的API在與TypeScript集成時提出了一些挑戰,這主要是由於Vue依賴單個this上下文來公開屬性,而且this在Vue組件中使用比普通JavaScript更具魔力(例如this嵌套在methods指向組件實例而不是methods對象的點下方的內部函數)。換句話說,Vue現有的API在設計時就沒有考慮類型推斷,而且在嘗試使其與TypeScript完美配合時會產生不少複雜性。async
今天vue-class-component,大多數將Vue與TypeScript一塊兒使用的用戶正在使用,該庫容許將組件編寫爲TypeScript類(在裝飾器的幫助下)。在設計3.0時,咱們試圖提供一個內置的Class API,以更好地解決之前的RFC(已刪除)中的鍵入問題。可是,當咱們在設計上進行討論和迭代時,咱們注意到,要使Class API解決類型問題,它必須依賴裝飾器-這是一個很是不穩定的第2階段提案,在實現細節方面存在不少不肯定性。這使其成爲一個至關危險的基礎。(有關類API類型問題的更多詳細信息,請點擊此處)
相比之下,此RFC中提議的API大多使用普通的變量和函數,它們天然是類型友好的。用建議的API編寫的代碼能夠享受完整的類型推斷,幾乎不須要手動類型提示。這也意味着用提議的API編寫的代碼在TypeScript和普通JavaScript中看起來幾乎相同,所以,即便非TypeScript用戶也能夠從鍵入中受益,以得到更好的IDE支持。
API簡介
這裏提出的API並無引入新的概念,而是更多地將Vue的核心功能(例如建立和觀察響應狀態)公開爲獨立功能。在這裏,咱們將介紹一些最基本的API,以及如何使用它們代替2.x選項來表達組件內邏輯。請注意,本節重點介紹基本概念,所以不會詳細介紹每一個API。完整的API規範可在「 API參考」部分中找到。
#反應狀態和反作用
讓咱們從一個簡單的任務開始:聲明一些反應狀態。
import { reactive } from 'vue'// reactive stateconst state = reactive({ count: 0})
reactive與Vue.observable()2.x 中的當前API 等效,已重命名以免與RxJS observables混淆。在這裏,返回的state是全部Vue用戶都應該熟悉的反應性對象。
Vue中反應性狀態的基本用例是咱們能夠在渲染期間使用它。因爲依賴關係跟蹤,當反應性狀態更改時,視圖會自動更新。在DOM中渲染某些內容被視爲「反作用」:咱們的程序正在修改程序自己(DOM)外部的狀態。要應用並根據反應狀態自動從新應用反作用,咱們可使用watchEffectAPI:
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還提供了watch與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( `<button @click="increment">{{ state.count }}</button>`, renderContext )})
計算狀態和引用
有時咱們須要依賴於其餘狀態的狀態-在Vue中,這是經過計算屬性來處理的。要直接建立一個計算值,咱們可使用computedAPI:
import { reactive, computed } from 'vue'const state = reactive({ count: 0})const double = computed(() => state.count * 2)
computed這裏返回什麼?若是咱們猜想如何computed在內部實現,咱們可能會想到如下內容:
// simplified pseudo codefunction computed(getter) { let value watchEffect(() => { value = getter() }) return value }
可是咱們知道這value是行不通的:若是是相似的原始類型number,computed則一旦返回,它與內部更新邏輯的鏈接將丟失。這是由於JavaScript基本類型是經過值而不是經過引用傳遞的
將值分配給對象做爲屬性時,也會發生相同的問題。若是一個反應性值在分配爲屬性或從函數返回時不能保持其反應性,那麼它將不是頗有用。爲了確保咱們始終能夠讀取計算的最新值,咱們須要將實際值包裝在一個對象中,而後返回該對象:
// simplified pseudo codefunction 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」的對象,由於它用做對其持有的內部值的反應性引用。
您可能會意識到Vue已經有了「引用」的概念,可是僅用於引用模板(「模板引用」)中的DOM元素或組件實例。看看這個,看看新裁判系統如何可以同時用於邏輯狀態和模板裁判。
除了計算的引用外,咱們還可使用refAPI 直接建立普通的可變引用:
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( `<button @click="increment">{{ count }}</button>`, renderContext )})
另外,當引用做爲屬性嵌套在反應對象下時,它也將在訪問時自動展開:
const state = reactive({ count: 0, double: computed(() => state.count * 2)})// no need to use `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( `<button @click="increment"> Count is: {{ state.count }}, double is: {{ state.double }} </button>`, renderContext )})
注意上面的代碼如何不依賴於組件實例的存在。實際上,到目前爲止引入的API均可以在組件上下文以外使用,從而使咱們可以在更普遍的場景中利用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>被省略,但也將徹底相同。
#生命週期掛鉤
到目前爲止,咱們已經涵蓋了組件的純狀態方面:用戶輸入上的反應狀態,計算狀態和變異狀態。可是組件可能還須要執行一些反作用-例如,登陸到控制檯,發送ajax請求或在上設置事件監聽器window。這些反作用一般在如下時間執行:
當某些狀態改變時;
安裝,更新或卸載組件時(生命週期掛鉤)。
咱們知道咱們可使用watchEffect和watchAPI根據狀態變化來應用反作用。至於在不一樣的生命週期掛鉤中執行反作用,咱們可使用專用的onXXXAPI(直接反映現有的生命週期選項):
import { onMounted } from 'vue'export default { setup() { onMounted(() => { console.log('component is mounted!') }) }}
這些生命週期註冊方法只能在setup鉤子調用期間使用。它會自動找出setup使用內部全局狀態調用鉤子的當前實例。有意設計這種方式來減小將邏輯提取到外部功能時的摩擦。
有關這些API的更多詳細信息,請參見《API參考》。可是,咱們建議在深刻研究設計細節以前先完成如下幾節
代碼組織
至此,咱們已經使用導入的函數複製了組件API,可是該作什麼呢?用選項定義組件彷佛要比將全部功能混合在一塊兒來使功能更有組織性!
這是能夠理解的第一印象。可是,正如動機部分所述,咱們認爲Composition API實際上能夠帶來更好的組織代碼,尤爲是在複雜的組件中。在這裏,咱們將嘗試解釋緣由。
#什麼是「組織機構代碼」?
讓咱們退後一步,考慮當咱們談論「組織代碼」時的真正含義。保持代碼層次分明的最終目的應該是使代碼更易於閱讀和理解。「理解」代碼是什麼意思?咱們真的能夠僅僅由於知道組件包含哪些選項而聲稱本身「瞭解」了組件嗎?您是否遇到過由另外一位開發人員創做的大型組件(例如this),而且很難將其包裹住?
想想咱們將如何引導同一個開發人員經過一個大型組件,如上面連接的組件。您極可能從「此組件正在處理X,Y和Z」開始,而不是「此組件具備這些數據屬性,這些計算的屬性和這些方法」。在理解組件時,咱們更關心「組件正在嘗試作什麼」(即代碼背後的意圖),而不是「組件碰巧使用了哪些選項」。雖然使用基於選項的API編寫的代碼天然能夠回答後者,但在表達前者方面作得至關差。
#邏輯問題與選項類型
讓咱們將組件要處理的「 X,Y和Z」定義爲邏輯關注點。小型單一用途的組件一般不存在可讀性問題,由於整個組件只處理一個邏輯問題。可是,在高級用例中,這個問題變得更加突出。以Vue CLI UI文件瀏覽器爲例。該組件必須處理許多不一樣的邏輯問題:
跟蹤當前文件夾狀態並顯示其內容
處理文件夾導航(打開,關閉,刷新...)
處理新文件夾的建立
僅切換顯示收藏夾
切換顯示隱藏文件夾
處理當前工做目錄更改
您是否能夠經過閱讀基於選項的代碼當即識別並區分這些邏輯問題?這確定是困難的。您會注意到,與特定邏輯問題相關的代碼一般會分散在各處。例如,「建立新文件夾」功能使用了兩個數據屬性,一個計算屬性和一個方法 -其中在距數據屬性一百行的位置定義了該方法。
若是咱們對這些邏輯問題中的每個進行彩色編碼,咱們會注意到在使用組件選項表示它們時有多分散:
正是這種碎片化使得難以理解和維護複雜的組件。經過選項的強制分隔使基本的邏輯問題變得模糊。另外,當處理單個邏輯關注點時,咱們必須不斷地「跳動」選項塊,以查找與該關注點相關的部分。
注意:原始代碼可能會在幾個地方進行改進,可是咱們正在展現最新提交(在撰寫本文時),而沒有進行修改,以提供咱們本身編寫的實際生產代碼示例。
若是咱們能夠並置與同一邏輯問題相關的代碼,那就更好了。這正是Composition API使咱們可以執行的操做。能夠經過如下方式編寫「建立新文件夾」功能:
function useCreateFolder (openFolder) { // originally data properties const showNewFolder = ref(false) const newFolderName = ref('') // originally computed property const newFolderValid = computed(() => isValidMultiName(newFolderName.value)) // originally a method 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 }}
請注意,如今如何將與「建立新文件夾」功能相關的全部邏輯並置並封裝在一個函數中。因爲其描述性名稱,該功能在某種程度上也是自記錄的。這就是咱們所說的合成函數。建議使用約定以函數名稱開頭,use以代表它是組合函數。這種模式能夠應用於組件中的全部其餘邏輯問題,從而產生了許多很好的解耦功能:
此比較不包括導入語句和setup()函數。使用Composition API從新實現的完整組件能夠在此處找到。
如今,每一個邏輯關注點的代碼在組合函數中並置在一塊兒。當在大型組件上工做時,這大大減小了對恆定「跳躍」的需求。合成功能也能夠在編輯器中摺疊,以使組件更易於掃描
export default { setup() { // ... }}function useCurrentFolderData(networkState) { // ...}function useFolderNavigation({ networkState, 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 } }}
固然,這是咱們使用options API時無需編寫的代碼。可是請注意,該setup功能幾乎讀起來像是對該組件要執行的操做的口頭描述-這是基於選項的版本中徹底缺乏的信息。您還能夠根據傳遞的參數清楚地看到組合函數之間的依賴關係流。最後,return語句是檢查模板暴露內容的惟一位置。
給定相同的功能,經過選項定義的組件和經過組合函數定義的組件會表現出兩種表達同一基本邏輯的不一樣方式。基於選項的API迫使咱們根據選項類型組織代碼,而Composition API使咱們可以基於邏輯關注點組織代碼。
#邏輯提取和重用
當涉及跨組件提取和重用邏輯時,Composition API很是靈活。this合成函數不依賴魔術上下文,而僅依賴於其參數和全局導入的Vue API。您能夠經過簡單地將其導出爲函數來重用組件邏輯的任何部分。您甚至能夠extends經過導出setup組件的所有功能來達到等效的效果。
讓咱們看一個例子:跟蹤鼠標的位置。
import { ref, onMounted, onUnmounted } from 'vue'export function useMousePosition() { const x = ref(0) const y = ref(0) function update(e) { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y }}
這是組件能夠利用功能的方式:
import { useMousePosition } from './mouse'export default { setup() { const { x, y } = useMousePosition() // other logic... return { x, y } }}
在文件資源管理器示例的Composition API版本中,咱們將一些實用程序代碼(例如usePathUtils和useCwdUtils)提取到了外部文件中,由於咱們發現它們對其餘組件頗有用。
使用現有模式(例如混合,高階組件或無渲染組件)(經過做用域插槽),也能夠實現相似的邏輯重用。互聯網上有大量信息解釋這些模式,所以在此咱們將再也不重複詳細說明。高層次的想法是,與組合函數相比,這些模式中的每個都有各自的缺點:
渲染上下文中公開的屬性的來源不清楚。例如,當使用多個mixin讀取組件的模板時,可能很難肯定從哪一個mixin注入了特定的屬性。
命名空間衝突。Mixins可能會在屬性和方法名稱上發生衝突,而HOC可能會在預期的prop名稱上發生衝突。
性能。HOC和無渲染組件須要額外的有狀態組件實例,這會下降性能。
相比之下,使用Composition API:
暴露給模板的屬性具備明確的來源,由於它們是從合成函數返回的值。
合成函數返回的值能夠任意命名,所以不會發生名稱空間衝突。
沒有建立僅用於邏輯重用的沒必要要的組件實例。
#與現有API一塊兒使用
Composition API能夠與現有的基於選項的API一塊兒使用。
Composition API在2.x選項(data,computed&methods)以前已解決,而且沒法訪問由這些選項定義的屬性。
返回的屬性setup()將this在2.x選項中公開並能夠訪問。
#插件開發
現在,許多Vue插件都將屬性注入this。例如,Vue Router注入this.$route和this.$router,而Vuex注入this.$store。因爲每一個插件都要求用戶增長注入屬性的Vue類型,這使得類型推斷變得棘手。
使用Composition API時,沒有this。相反,插件將充分利用="https://composition-api.vuejs.org/api.html#dependency-injection">provide並inject在內部和公開的組成功能。如下是插件的假設代碼:
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 }}
請注意,也能夠經過全局API更改RFC中建議的應用程序級提供來提供商店,可是useStore使用組件中的樣式API相同。
#缺點
#介紹引用的開銷
從技術上講,Ref是此提案中引入的惟一「新」概念。引入它是爲了將反應性值做爲變量傳遞,而無需依賴對的訪問this。缺點是:
使用Composition API時,咱們將須要不斷將ref與純值和對象區分開來,從而增長了使用API時的精神負擔。經過使用命名約定(例如,將全部ref變量後綴爲xxxRef)或使用類型系統,能夠大大減輕精神負擔。另外一方面,因爲提升了代碼組織的靈活性,所以組件邏輯將更多地被隔離爲一些小的函數,這些函數的局部上下文很簡單,引用的開銷很容易管理。
因爲須要,讀取和變異refs比使用普通值更冗長.value。一些人建議使用編譯時語法糖(相似於Svelte 3)來解決此問題。儘管從技術上講這是可行的,但咱們認爲將其做爲Vue的默認值是不合理的(如在與Svelte的比較中所討論的)。就是說,這在用戶領域做爲Babel插件在技術上是可行的。
咱們已經討論了是否有可能徹底避免使用Ref概念並僅使用反應性對象,可是:
計算的獲取器能夠返回原始類型,所以不可避免地要使用相似Ref的容器。
僅出於反應性的考慮,僅指望或返回原始類型的組合函數也須要將值包裝在對象中。若是框架沒有提供標準的實現,那麼用戶頗有可能最終會發明本身的Ref like模式(並致使生態系統碎片化)。
#參考與反應
能夠理解,用戶可能會對ref和之間使用哪一個感到困惑reactive。首先要知道的是,您將須要瞭解二者纔能有效地使用Composition API。獨家使用一個極有可能致使神祕的解決方法或從新發明輪子。
使用ref和之間的區別reactive能夠與您編寫標準JavaScript邏輯的方式進行比較:
// style 1: separate variableslet x = 0let y = 0function updatePosition(e) { x = e.pageX y = e.pageY }// --- compared to ---// style 2: single objectconst pos = { x: 0, y: 0}function updatePosition(e) { pos.x = e.pageX pos.y = e.pageY }
若是使用ref,則使用ref在很大程度上將樣式(1)轉換爲更冗長的等效項(以使原始值具備反應性)。
使用reactive幾乎與樣式(2)相同。咱們只須要使用建立對象便可reactive。
可是,reactive僅運行的問題在於,複合函數的使用者必須始終保持對返回對象的引用,以保持反應性。該對象不能被破壞或散佈:
// composition functionfunction useMousePosition() { const pos = reactive({ x: 0, y: 0 }) // ... return pos }// consuming componentexport 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() } }}
該toRefsAPI被提供給處理該約束-它的每一個屬性轉換反應性對象到對應的REF上:
function useMousePosition() { const pos = reactive({ x: 0, y: 0 }) // ... return toRefs(pos)}// x & y are now refs!const { x, y } = useMousePosition()
總結起來,有兩種可行的樣式:
使用ref和reactive隨便怎麼樣你在正常的JavaScript聲明基本類型變量和對象變量。使用這種樣式時,建議使用具備IDE支持的類型系統。
reactive儘量使用,記住toRefs從組合函數返回反應對象時使用。這減小了裁判的精神開銷,但並無消除對這個概念熟悉的須要。
在現階段,咱們認爲在refvs. 上強制採用最佳作法爲時尚早reactive。咱們建議您從上面的兩個選項中選擇與您的心理模型相符的樣式。咱們將收集現實世界中的用戶反饋,並最終提供有關此主題的更多肯定性指導。
#返回聲明的詳細程度
一些用戶對於return語句的setup()冗長和彷彿樣板感到擔心。
咱們認爲明確的退貨聲明有利於可維護性。它使咱們可以顯式控制暴露給模板的內容,而且能夠做爲跟蹤在組件中定義模板屬性的起點。
有人建議自動公開在中setup()聲明的變量,從而使return語句成爲可選的。一樣,咱們不認爲這應該是默認設置,由於它違背了標準JavaScript的直覺。可是,有一些方法能夠減小用戶空間中的雜事:
IDE擴展,該擴展基於在中聲明的變量自動生成return語句 setup()
Babel插件隱式生成並插入return語句。
#更多的靈活性須要更多的紀律
許多用戶指出,儘管Composition API在代碼組織方面提供了更大的靈活性,但它也須要開發人員更多的紀律才能「正確執行」。有些人擔憂該API會致使經驗不足的意大利麪條式代碼。換句話說,儘管Composition API提升了代碼質量的上限,但同時也下降了代碼質量的下限。
咱們在必定程度上贊成這一點。可是,咱們認爲:
上限的收益遠大於下限的損失。
經過適當的文檔和社區指導,咱們能夠有效地解決代碼組織問題。
一些用戶使用Angular 1控制器做爲設計可能致使編寫不良代碼的示例。Composition API和Angular 1控制器之間的最大區別是,它不依賴於共享範圍上下文。這使得將邏輯分紅單獨的功能變得很是容易,這是JavaScript代碼組織的核心機制。
任何JavaScript程序都以入口文件開頭(將其視爲setup()程序的)。咱們根據邏輯關注點將程序分爲功能和模塊來組織程序。Composition API使咱們可以對Vue組件代碼執行相同的操做。換句話說,使用Composition API時,編寫層次分明的JavaScript代碼的技能會直接轉化爲編寫層次分明的Vue代碼的技能。
#採用策略
Composition API純粹是添加的,不會影響/棄用任何現有的2.x API。它已經過@vue/composition庫做爲2.x插件提供。該庫的主要目標是提供一種試驗API並收集反饋的方法。當前的實現是此提案的最新版本,可是因爲做爲插件的技術限制,可能包含一些不一致性。隨着該提案的更新,它可能還會收到制動變化,所以咱們不建議在此階段在生產中使用它。
咱們打算將API內置在3.0中。它將與現有的2.x選項一塊兒使用。
對於選擇僅在應用程序中使用Composition API的用戶,能夠提供編譯時標誌,以刪除僅用於2.x選項的代碼並減少庫的大小。可是,這是徹底可選的。
該API將被定位爲高級功能,由於它旨在解決的問題主要出如今大規模應用程序中。咱們無心修改文檔以將其用做默認文檔。相反,它將在文檔中有其本身的專用部分。
#附錄
#類API的類型問題
引入類API的主要目的是提供一種具備更好TypeScript推理支持的替代API。可是,this即便使用基於類的API ,Vue組件也須要將從多個源聲明的屬性合併到單個上下文中,這一事實帶來了一些挑戰。
一個例子是道具的打字。爲了將props合併到this,咱們必須對組件類使用通用參數,或使用裝飾器。
這是使用通用參數的示例:
interface Props { message: string }class App extends Component<Props> { static props = { message: String }}
因爲傳遞給泛型參數的接口僅處於類型區域,所以用戶仍然須要爲上的props代理行爲提供運行時props聲明this。該雙重聲明是多餘且笨拙的。
咱們已經考慮過使用裝飾器做爲替代:
class App extends Component<Props> { @prop message: string }
使用裝飾器會產生對第二階段規範的依賴,存在不少不肯定性,尤爲是當TypeScript的當前實現與TC39提案徹底不一樣步時。此外,沒法公開使用裝飾器聲明的道具類型this.$props,這會破壞TSX的支持。用戶還能夠假定他們能夠@prop message: string = 'foo'在技術上沒法按預期方式使用時聲明道具的默認值。
另外,當前沒有辦法利用上下文類型做爲類方法的參數-這意味着傳遞給Class render函數的參數不能具備基於Class其餘屬性的推斷類型。
#與React Hooks的比較
基於函數的API提供了與React Hooks相同級別的邏輯組合功能,但有一些重要的區別。與React鉤子不一樣,該setup()函數僅被調用一次。這意味着使用Vue的Composition API的代碼爲:
總的來講,它更符合慣用的JavaScript代碼的直覺;
對呼叫順序不敏感,能夠有條件;
在每次提煉中不反覆調用,併產生較小的GC壓力;
無需考慮useCallback幾乎老是須要在哪兒防止內聯處理程序致使子組件的過分渲染;
不受地方的問題useEffect,並useMemo能夠捕捉陳舊的變量,若是用戶忘記傳遞正確的依賴陣列。Vue的自動依賴關係跟蹤確保觀察者和計算值始終正確無效。
咱們承認React Hooks的創造力,這是該建議的主要靈感來源。可是,上面提到的問題確實存在於設計中,咱們注意到Vue的反應性模型提供瞭解決這些問題的方法。
#與Svelte的比較
儘管採用的路線大相徑庭,可是Composition API和Svelte 3的基於編譯器的方法實際上在概念上有不少共通之處。這是一個並行的示例:
<script>import { ref, watchEffect, onMounted } from 'vue'export default { setup() { const count = ref(0) function increment() { count.value++ } watchEffect(() => console.log(count.value)) onMounted(() => console.log('mounted!')) return { count, increment } }}</script>
<script>import { onMount } from 'svelte'let count = 0function increment() { count++} $: console.log(count)onMount(() => console.log('mounted!'))</script>
velte代碼看起來更簡潔,由於它在編譯時執行如下操做:
隱式地將整個<script>塊(import語句除外)包裝到爲每一個組件實例調用的函數中(而不是僅執行一次)
隱式註冊對可變突變的反應性
隱式地將全部做用域內的變量暴露給渲染上下文
將$語句編譯成從新執行的代碼
從技術上講,咱們能夠在Vue中作一樣的事情(能夠經過userland Babel插件來完成)。咱們不這樣作的主要緣由是與標準JavaScript保持一致。若是您從<script>Vue文件的塊中提取代碼,咱們但願它與標準ES模塊徹底同樣地工做。<script>另外一方面,Svelte 塊中的代碼在技術上再也不是標準的JavaScript。這種基於編譯器的方法存在不少問題:
不管是否編譯,代碼的工做方式都不一樣。做爲一個漸進式框架,許多Vue用戶可能但願/須要/必須在沒有構建設置的狀況下使用它,所以,編譯後的版本不能成爲默認版本。另外一方面,Svelte將自身定位爲編譯器,而且只能與構建步驟一塊兒使用。這是兩個框架在有意識地作出的折衷。
代碼在內部/外部組件中的工做方式不一樣。當嘗試從Svelte組件中提取邏輯並將其提取到標準JavaScript文件中時,咱們將失去神奇的簡潔語法,而不得不使用更爲冗長的低級API。
Svelte的反應性編譯僅適用於頂級變量-它不涉及在函數內部聲明的變量,所以咱們沒法在組件內部聲明的函數中封裝反應性狀態。這對具備功能的代碼組織施加了不小的限制-正如咱們在RFC中所展現的那樣,這對於保持大型組件的可維護性很是重要。
非標準語義使與TypeScript集成成爲問題。
這毫不是說Svelte 3是一個壞主意-實際上,這是一種很是創新的方法,咱們很是尊重Rich的工做。可是基於Vue的設計約束和目標,咱們必須作出不一樣的權衡。