基本例子
直接拷貝下面程式碼,去執行看效果吧。推薦使用高版本的chrome瀏覽器,記得開啟F12除錯工具哦!
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Title</title></head><body><script src="https://s1.zhuanstatic.com/common/js/vue-next-3.0.0-alpha.0.js"></script><div id="app"></div><script> const { ref, reactive, createApp, watch, effect } = Vuefunction useMouse() { const x = ref(0) const y = ref(0) const update = e => { x.value = e.pageX y.value = e.pageY } Vue.onMounted(() => { window.addEventListener('mousemove', update) }) Vue.onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y }}const App = { props: { age: Number },// Composition API 使用的入口 setup(props, context){ console.log('props.age', props.age) // 定義響應資料 const state = reactive({name:'zhuanzhuan'}); // 使用公共邏輯 const {x,y} = useMouse(); Vue.onMounted(()=>{ console.log('當組掛載完成') }); Vue.onUpdated(()=>{ console.log('資料發生更新') }); Vue.onUnmounted(()=>{ console.log('元件將要解除安裝') }) function changeName(){ state.name = '轉轉'; } // 建立監視,並得到 停止函式 const stop = watch(() => console.log(`watch state.name:`, state.name)) // 呼叫停止函式,清除對應的監視 // stop() // 觀察包裝物件 watch(() => state.name, (value, oldValue) => console.log(`watch state.name value:${value} oldValue:${oldValue}`)) effect(() => { console.log(`effect 觸發了! 名字是:${state.name},年齡:${props.age}`) }) // 返回上下文,可以在模板中使用 return { // state: Vue.toRefs(state), // 也可以這樣寫,將 state 上的每個屬性,都轉化為 ref 形式的響應式資料 state, x, y, changeName, } }, template:`<button @click="changeName">名字是:{{state.name}} 滑鼠x: {{x}} 滑鼠: {{y}}</button>` } createApp().mount(App, '#app', {age: 123});</script></body></html>
設計動機邏輯組合與複用元件 API 設計所面對的核心問題之一就是如何組織邏輯,以及如何在多個元件之間抽取和複用邏輯。基於 Vue 2.x 目前的 API 有一些常見的邏輯複用模式,但都或多或少存在一些問題。這些模式包括:
Mixins高階元件 (Higher-order Components, aka HOCs)Renderless Components (基於 scoped slots / 作用於插槽封裝邏輯的元件)網路上關於這些模式的介紹很多,這裡就不再贅述細節。總體來說,以上這些模式存在以下問題:
模版中的資料來源不清晰。舉例來說,當一個元件中使用了多個 mixin 的時候,光看模版會很難分清一個屬性到底是來自哪一個 mixin。 HOC 也有類似的問題。名稱空間衝突。由不同開發者開發的 mixin 無法保證不會正好用到一樣的屬性或是方法名。 HOC 在注入的 props 中也存在類似問題。效能。 HOC 和 RenderlessComponents 都需要額外的元件例項巢狀來封裝邏輯,導致無謂的效能開銷。從以上 useMouse例子中可以看到:
暴露給模版的屬性來源清晰(從函式返回);返回值可以被任意重新命名,所以不存在名稱空間衝突;沒有建立額外的元件例項所帶來的效能損耗。型別推導vue-next 的一個主要設計目標是增強對 TypeScript 的支援。原本期望通過 ClassAPI 來達成這個目標,但是經過討論和原型開發,認為 Class 並不是解決這個問題的正確路線,基於 Class 的 API 依然存在型別問題。
基於函式的 API 天然對型別推導很友好,因為 TS 對函式的引數、返回值和泛型的支援已經非常完備。更值得一提的是基於函式的 API 在使用 TS 或是原生 JS 時寫出來的程式碼幾乎是完全一樣的。
setup() 函式我們將會引入一個新的元件選項, setup()。顧名思義,這個函式將會是我們 setup 我們元件邏輯的地方,它會在一個元件例項被建立時,初始化了 props 之後呼叫。它為我們使用 vue-next 的 CompositionAPI 新特性提供了統一的入口。
執行時機setup 函式會在 beforeCreate 之後、 created 之前執行。
state宣告 state 主要有以下幾種型別。
基礎型別基礎型別可以通過 ref 這個 api 來宣告,如下:
const App={ setup(props, context) { const msg = ref('hello') function appendName() { msg.value =`hello $ {props.name}` } return {appendName,msg} }, template: ` < div@click = "appendName" > {{msg}}</div>`}
我們知道在 JavaScript 中,原始值型別如 string 和 number 是隻有值,沒有引用的。如果在一個函式中返回一個字串變數,接收到這個字串的程式碼只會獲得一個值,是無法追蹤原始變數後續的變化的。
因此,包裝物件的意義就在於提供一個讓我們能夠在函式之間以引用的方式傳遞任意型別值的容器。這有點像 ReactHooks 中的 useRef —— 但不同的是 Vue 的包裝物件同時還是響應式的資料來源。有了這樣的容器,我們就可以在封裝了邏輯的組合函式中將狀態以引用的方式傳回給元件。元件負責展示(追蹤依賴),組合函式負責管理狀態(觸發更新):
setup(props, context) {\t// x,y 可能被 useMouse() 內部的程式碼修改從而觸發更新\tconst {x,y} = useMouse();\treturn {x,y}}
包裝物件也可以包裝非原始值型別的資料,被包裝的物件中巢狀的屬性都會被響應式地追蹤。用包裝物件去包裝物件或是陣列並不是沒有意義的:它讓我們可以對整個物件的值進行替換 —— 比如用一個 filter 過的陣列去替代原陣列:
const numbers = ref([1, 2, 3])// 替代原陣列,但引用不變numbers.value = numbers.value.filter(n => n > 1)
這裡補充一下,在 基礎型別 第一個例子中你可能注意到了,雖然 setup() 返回的 msg是一個包裝物件,但在模版中我們直接用了 {{msg}}這樣的繫結,沒有用 .value。這是因為當包裝物件被暴露給模版渲染上下文,或是被巢狀在另一個響應式物件中的時候,它會被自動展開 (unwrap)為內部的值。
引用型別引用型別除了可以使用 ref 來宣告,也可以直接使用 reactive,如下:
const App = { setup(props, context){ const state = reactive({name:'zhuanzhuan'}); function changeName(){ state.name = '轉轉'; } return {state, changeName, msg} },template:`<button @click="changeName">名字是:{{state.name}}</button>`}
接收 props 資料在 props 中定義當前元件允許外界傳遞過來的引數名稱:
props: { age: Number}
通過 setup 函式的第一個形參,接收 props 資料:
setup(props) {\tconsole.log('props.age', props.age) watch(() = >props.age, (value, oldValue) = >console.log(`watch props.age value: $ {value}oldValue: $ {oldValue}`))}
除此之外,還可以直接通過 watch 方法來觀察某個 prop 的變動,這是為什麼呢?答案非常簡單,就是 props本身在原始碼中,也是一個被 reactive 包裹後的物件,因此它具有響應性,所以在 watch 方法中的回撥函式會自動收集依賴,之後當 age 變動時,會自動呼叫這些回撥邏輯。
contextsetup 函式的第二個形參是一個上下文物件,這個上下文物件中包含了一些有用的屬性,這些屬性在 vue2.x 中需要通過 this 才能訪問到,那我想通過 this 像在 vue2 中訪問一些內建屬性,怎麼辦?比如 attrs 或者 emit。我們可以通過 setup 的第二個引數,在 vue-next 中,它們的訪問方式如下:
const MyComponent = {\tsetup(props, context) {\t\tcontext.attrs context.slots context.parent context.root context.emit context.refs\t}}
注意:==在 setup() 函式中無法訪問到 this==
reactive() 函式reactive() 函式接收一個普通物件,返回一個響應式的資料物件。
基本語法等價於 vue2.x 中的 Vue.observable()函式, vue3.x 中提供了 reactive() 函式,用來建立響應式的資料物件,基本程式碼示例如下:
// 建立響應式資料物件,得到的 state 類似於 vue 2.x 中 data() 返回的響應式物件const state = reactive({name:'zhuanzhuan'});
定義響應式資料供 template 使用按需匯入 reactive 函式:const { reactive } = Vue
在 setup() 函式中呼叫 reactive() 函式,建立響應式資料物件:
const {reactive} = Vuesetup(props, context) {\tconst state = reactive({\t\tname: 'zhuanzhuan'\t});\treturn state}
在 template 中訪問響應式資料:
template:`<button>名字是:{{name}} </button>`
Value Unwrapping(包裝物件的自動展開)
ref() 函式ref() 函式用來根據給定的值建立一個響應式的資料物件, ref() 函式呼叫的返回值是一個物件,這個物件上只包含一個 .value 屬性。
基本語法const { ref } = Vue// 建立響應式資料物件 age,初始值為 3const age = ref(3)// 如果要訪問 ref() 創建出來的響應式資料物件的值,必須通過 .value 屬性才可以console.log(age.value) // 輸出 3// 讓 age 的值 +1age.value++// 再次列印 age 的值console.log(age.value) // 輸出 4
在 template 中訪問 ref 建立的響應式資料在 setup() 中建立響應式資料:setup() { const age = ref(3) \t\t\treturn {age,name: ref('zhuanzhuan') } }
在 template 中訪問響應式資料:
template:`<p>名字是:{{name}},年齡是{{age}}</p>`
在 reactive 物件中訪問 ref 建立的響應式資料
當把 ref() 創建出來的響應式資料物件,掛載到 reactive() 上時,會自動把響應式資料物件展開為原始的值,不需通過 .value 就可以直接被訪問。
換句話說就是當一個包裝物件被作為另一個響應式物件的屬性引用的時候也會被自動展開例如:
const age = ref(3)const state = reactive({age})console.log(state.age) // 輸出 3state.age++ // 此處不需要通過 .value 就能直接訪問原始值console.log(age) // 輸出 4
以上這些關於包裝物件的細節可能會讓你覺得有些複雜,但實際使用中你只需要記住一個基本的規則:只有當你直接以變數的形式引用一個包裝物件的時候才會需要用 .value 去取它內部的值 —— 在模版中你甚至不需要知道它們的存在。
==注意:新的 ref 會覆蓋舊的 ref,示例程式碼如下:==
const age = ref(3)const state = reactive({age})console.log(state.age) // 輸出 3state.age++ // 此處不需要通過 .value 就能直接訪問原始值console.log(age) // 輸出 4
isRef() 函式
isRef() 用來判斷某個值是否為 ref() 創建出來的物件;應用場景:當需要展開某個可能為 ref() 創建出來的值的時候,例如:
const { isRef } = Vueconst unwrapped = isRef(foo) ? foo.value : foo
toRefs() 函式
const { toRefs } = Vuesetup() { // 定義響應式資料物件 const state = reactive({ age: 3 }) // 定義頁面上可用的事件處理函式 const increment = () => { state.age++ } // 在 setup 中返回一個物件供頁面使用 // 這個物件中可以包含響應式的資料,也可以包含事件處理函式 return { // 將 state 上的每個屬性,都轉化為 ref 形式的響應式資料 ...toRefs(state), // 自增的事件處理函式 increment }}
頁面上可以直接訪問 setup() 中 return 出來的響應式資料:
template:`<div> \t\t\t\t<p>當前的age值為:{{age}}</p> \t\t\t<button @click="increment">+1</button></div>`
computed() 函式
computed() 用來建立計算屬性, computed() 函式的返回值是一個 ref 的例項。使用 computed 之前需要按需匯入:
const { computed } = Vue
建立只讀的計算屬性
const { computed } = Vue// 建立一個 ref 響應式資料const count = ref(1)// 根據 count 的值,建立一個響應式的計算屬性 plusOne// 它會根據依賴的 ref 自動計算並返回一個新的 refconst plusOne = computed(() => count.value + 1)console.log(plusOne.value) // 輸出 2plusOne.value++ // error
建立可讀可寫的計算屬性在呼叫 computed() 函式期間,傳入一個包含 get 和 set 函式的物件,可以得到一個可讀可寫的計算屬性,示例程式碼如下:
const { computed } = Vue// 建立一個 ref 響應式資料const count = ref(1)// 建立一個 computed 計算屬性const plusOne = computed({ // 取值函式 get: () => count.value + 1, // 賦值函式 set: val => { count.value = val - 1 }})// 為計算屬性賦值的操作,會觸發 set 函式plusOne.value = 9// 觸發 set 函式後,count 的值會被更新console.log(count.value)// 輸出 8
watch() 函式
watch() 函式用來監視某些資料項的變化,從而觸發某些特定的操作,使用之前需要按需匯入:
const { watch } = Vue
基本用法
const { watch } = Vueconst count = ref(0)// 定義 watch,只要 count 值變化,就會觸發 watch 回撥// watch 會在建立時會自動呼叫一次watch(() => console.log(count.value))// 輸出 0setTimeout(() => { count.value++ // 輸出 1 }, 1000)
監視指定的資料來源監視 reactive 型別的資料來源:
const { watch, reactive } = Vueconst state = reactive({name:'zhuanzhuan'});watch(() => state.name, (value, oldValue) => { /* ... */ })
監視 ref 型別的資料來源:
const { watch, ref } = Vue// 定義資料來源const count = ref(0)// 指定要監視的資料來源watch(count, (value, oldValue) => { /* ... */ })
監視多個數據源
監視 reactive 型別的資料來源:
const { reactive, watch, ref } = Vueonst state = reactive({ age: 3, name: 'zhuanzhuan' })watch( [() => state.age, () => state.name], // Object.values(toRefs(state)), ([age, name], [prevCount, prevName]) => { console.log(age) // 新的 age 值 console.log(name) // 新的 name 值 console.log('------------') console.log(prevCount) // 舊的 age 值 console.log(prevName) // 新的 name 值 }, { lazy: true // 在 watch 被建立的時候,不執行回撥函式中的程式碼 })setTimeout(() => { state.age++ state.name = '轉轉'}, 1000)
清除監視在 setup() 函式內建立的 watch 監視,會在當前元件被銷燬的時候自動停止。如果想要明確地停止某個監視,可以呼叫 watch() 函式的返回值即可,語法如下
// 建立監視,並得到 停止函式const stop = watch(() => { /* ... */ })// 呼叫停止函式,清除對應的監視stop()
在 watch 中清除無效的非同步任務
有時候,當被 watch 監視的值發生變化時,或 watch 本身被 stop 之後,我們期望能夠清除那些無效的非同步任務,此時, watch 回撥函式中提供了一個 cleanup registratorfunction 來執行清除的工作。這個清除函式會在如下情況下被呼叫:
watch 被重複執行了watch 被強制 stop 了Template 中的程式碼示例如下:
/* template 中的程式碼 */<input type="text" v-model="keywords" />
Script 中的程式碼示例如下:
// 定義響應式資料 keywordsconst keywords = ref('')// 非同步任務:列印使用者輸入的關鍵詞const asyncPrint = val => { // 延時 1 秒後列印 return setTimeout(() => { console.log(val) }, 1000)}// 定義 watch 監聽watch( keywords, (keywords, prevKeywords, onCleanup) => { // 執行非同步任務,並得到關閉非同步任務的 timerId const timerId = asyncPrint(keywords) // keywords 發生了變化,或是 watcher 即將被停止. // 取消還未完成的非同步操作。 // 如果 watch 監聽被重複執行了,則會先清除上次未完成的非同步任務 onCleanup(() => clearTimeout(timerId)) }, // watch 剛被建立的時候不執行 { lazy: true })// 把 template 中需要的資料 return 出去return { keywords}
之所以要用傳入的註冊函式來註冊清理函式,而不是像 React 的 useEffect 那樣直接返回一個清理函式,是因為 watcher 回撥的返回值在非同步場景下有特殊作用。我們經常需要在 watcher 的回撥中用 asyncfunction 來執行非同步操作:
const data = ref(null)watch(getId, async (id) => { data.value = await fetchData(id)})
我們知道 asyncfunction 隱性地返回一個 Promise - 這樣的情況下,我們是無法返回一個需要被立刻註冊的清理函式的。除此之外,回撥返回的 Promise 還會被 Vue 用於內部的非同步錯誤處理。
watch 回撥的呼叫時機預設情況下,所有的 watch 回撥都會在當前的 renderer flush 之後被呼叫。這確保了在回撥中 DOM 永遠都已經被更新完畢。如果你想要讓回撥在 DOM 更新之前或是被同步觸發,可以使用 flush 選項:
watch( () => count.value + 1, () => console.log(`count changed`), { flush: 'post', // default, fire after renderer flush flush: 'pre', // fire right before renderer flush flush: 'sync' // fire synchronously })
全部的 watch 選項(TS 型別宣告)interface WatchOptions { lazy?: boolean deep?: boolean flush?: 'pre' | 'post' | 'sync' onTrack?: (e: DebuggerEvent) => void onTrigger?: (e: DebuggerEvent) => void} interface DebuggerEvent { effect: ReactiveEffect target: any key: string | symbol | undefined type: 'set' | 'add' | 'delete' | 'clear' | 'get' | 'has' | 'iterate' }
lazy與 2.x 的 immediate 正好相反deep與 2.x 行為一致onTrack 和 onTrigger 是兩個用於 debug 的鉤子,分別在 watcher - 追蹤到依賴和依賴發生變化的時候被呼叫,獲得的引數是一個包含了依賴細節的 debugger event。LifeCycle Hooks 生命週期函式所有現有的生命週期鉤子都會有對應的 onXXX 函式(只能在 setup() 中使用):
const { onMounted, onUpdated, onUnmounted } = Vueconst MyComponent = { setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) // destroyed 調整為 unmounted onUnmounted(() => { console.log('unmounted!') }) }}
下面的列表,是 vue2.x 的生命週期函式與新版 CompositionAPI 之間的對映關係:
beforeCreate -> setup()created -> setup()beforeMount -> onBeforeMountmounted -> onMountedbeforeUpdate -> onBeforeUpdateupdated -> onUpdatedbeforeDestroy -> onBeforeUnmountdestroyed -> onUnmountederrorCaptured -> onErrorCapturedprovide & injectprovide() 和 inject() 可以實現巢狀元件之間的資料傳遞。這兩個函式只能在 setup() 函式中使用。父級元件中使用 provide() 函式向下傳遞資料;子級元件中使用 inject() 獲取上層傳遞過來的資料。
共享普通資料App.vue 根元件:
<template> <div id="app"> <h1>App 根元件</h1> <hr /> <LevelOne /> </div></template><script> import LevelOne from './components/LevelOne'// 1. 按需匯入 provideimport { provide } from '@vue/composition-api'export default { name: 'app', setup() { // 2. App 根元件作為父級元件,通過 provide 函式向子級元件共享資料(不限層級) // provide('要共享的資料名稱', 被共享的資料) provide('globalColor', 'red') }, components: { LevelOne }}</script>
LevelOne.vue 元件:
元素的引用示例程式碼如下:
<template> <div> <h3 ref="h3Ref">TemplateRefOne</h3> </div></template><script>import { ref, onMounted } from '@vue/composition-api'export default { setup() { // 建立一個 DOM 引用 const h3Ref = ref(null) // 在 DOM 首次載入完畢之後,才能獲取到元素的引用 onMounted(() => { // 為 dom 元素設定字型顏色 // h3Ref.value 是原生DOM物件 h3Ref.value.style.color = 'red' }) // 把建立的引用 return 出去 return { h3Ref } }}</script>
元件的引用TemplateRefOne.vue 中的示例程式碼如下:
這個函式僅僅提供了型別推斷,方便在結合 TypeScript 書寫程式碼時,能為 setup() 中的 props 提供完整的型別推斷。
import { createComponent } from 'vue'export default createComponent({ props: { foo: String }, setup(props) { props.foo // <- type: string }}
參考嚐鮮 vue3.x 新特性 - CompositionAPI:/file/2020/05/06/20200506154127_8381.jpg.html Function-based API RFC:https://zhuanlan.zhihu.com/p/68477600
以上就是 vue-next(Vue 3.0)(https://github.com/vuejs/vue-next) API,相信大家已經可以靈活運用了吧。
那麼大家一定很好奇 vue-next 響應式的原理,
下一章 vue-next(Vue3.0)之爐火純青 帶你解密。