帶你瞭解 vue-next(Vue 3.0)之 小試牛刀【實踐】
![帶你瞭解 vue-next(Vue 3.0)之 小試牛刀【實踐】](http://p2.ttnews.xyz/loading.gif)
作者:大轉轉FE
轉發連接:https://mp.weixin.qq.com/s/gii93neiVqGGChNWOvkoiQ
前言
看完上一章 帶你瞭解 vue-next(Vue 3.0)之 初入茅廬【實踐】之後,相信大家已經對vue-next(Vue 3.0)有所瞭解了。本章帶你掌握 vue-next 函數式的API,瞭解這些的話,無論是對於源碼的閱讀,還是當正式版發佈時開始學習,應該都會有起到一定的輔助作用。
基本例子
直接拷貝下面代碼,去運行看效果吧。推薦使用高版本的chrome瀏覽器,記得打開F12調試工具哦!
<code> Title /<code>
設計動機
邏輯組合與複用
組件 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 來聲明,如下:
<code>const App= { setup(props, context) { const msg = ref('hello') function appendName() { msg.value =`hello $ {props.name}` } return {appendName,msg} }, template: ` < div@click = "appendName" > {{msg}}/<code>
` }
我們知道在 JavaScript 中,原始值類型如 string 和 number 是隻有值,沒有引用的。如果在一個函數中返回一個字符串變量,接收到這個字符串的代碼只會獲得一個值,是無法追蹤原始變量後續的變化的。
因此,包裝對象的意義就在於提供一個讓我們能夠在函數之間以引用的方式傳遞任意類型值的容器。這有點像 ReactHooks 中的 useRef —— 但不同的是 Vue 的包裝對象同時還是響應式的數據源。有了這樣的容器,我們就可以在封裝了邏輯的組合函數中將狀態以引用的方式傳回給組件。組件負責展示(追蹤依賴),組合函數負責管理狀態(觸發更新):
<code>setup(props, context) { // x,y 可能被 useMouse() 內部的代碼修改從而觸發更新 const {x,y} = useMouse(); return {x,y} }/<code>
包裝對象也可以包裝非原始值類型的數據,被包裝的對象中嵌套的屬性都會被響應式地追蹤。用包裝對象去包裝對象或是數組並不是沒有意義的:它讓我們可以對整個對象的值進行替換 —— 比如用一個 filter 過的數組去替代原數組:
<code>const numbers = ref([1, 2, 3]) // 替代原數組,但引用不變 numbers.value = numbers.value.filter(n => n > 1)/<code>
這裡補充一下,在 基礎類型 第一個例子中你可能注意到了,雖然 setup() 返回的 msg是一個包裝對象,但在模版中我們直接用了 {{msg}}這樣的綁定,沒有用 .value。這是因為當包裝對象被暴露給模版渲染上下文,或是被嵌套在另一個響應式對象中的時候,它會被自動展開 (unwrap)為內部的值。
引用類型
引用類型除了可以使用 ref 來聲明,也可以直接使用 reactive,如下:
<code>const App = { setup(props, context){ const state = reactive({name:'zhuanzhuan'}); function changeName(){ state.name = '轉轉'; } return {state, changeName, msg} },template:`` }/<code>
接收 props 數據
- 在 props 中定義當前組件允許外界傳遞過來的參數名稱:
<code>props: { age: Number}/<code>
- 通過 setup 函數的第一個形參,接收 props 數據:
<code>setup(props) { console.log('props.age', props.age) watch(() = >props.age, (value, oldValue) = >console.log(`watch props.age value: $ {value}oldValue: $ {oldValue}`)) }/<code>
除此之外,還可以直接通過 watch 方法來觀察某個 prop 的變動,這是為什麼呢?答案非常簡單,就是 props本身在源碼中,也是一個被 reactive 包裹後的對象,因此它具有響應性,所以在 watch 方法中的回調函數會自動收集依賴,之後當 age 變動時,會自動調用這些回調邏輯。
context
setup 函數的第二個形參是一個上下文對象,這個上下文對象中包含了一些有用的屬性,這些屬性在 vue2.x 中需要通過 this 才能訪問到,那我想通過 this 像在 vue2 中訪問一些內置屬性,怎麼辦?比如 attrs 或者 emit。我們可以通過 setup 的第二個參數,在 vue-next 中,它們的訪問方式如下:
<code>const MyComponent = { setup(props, context) { context.attrs context.slots context.parent context.root context.emit context.refs } }/<code>
注意:==在 setup() 函數中無法訪問到 this==
reactive() 函數
reactive() 函數接收一個普通對象,返回一個響應式的數據對象。
基本語法
等價於 vue2.x 中的 Vue.observable()函數, vue3.x 中提供了 reactive() 函數,用來創建響應式的數據對象,基本代碼示例如下:
<code>// 創建響應式數據對象,得到的 state 類似於 vue 2.x 中 data() 返回的響應式對象 const state = reactive({name:'zhuanzhuan'});/<code>
定義響應式數據供 template 使用
- 按需導入 reactive 函數:
<code>const { reactive } = Vue/<code>
- 在 setup() 函數中調用 reactive() 函數,創建響應式數據對象:
<code>const {reactive} = Vuesetup(props, context) { const state = reactive({ name: 'zhuanzhuan' }); return state }/<code>
- 在 template 中訪問響應式數據:
<code>template:``/<code>
Value Unwrapping(包裝對象的自動展開)
ref() 函數
ref() 函數用來根據給定的值創建一個響應式的數據對象, ref() 函數調用的返回值是一個對象,這個對象上只包含一個 .value 屬性。
基本語法
<code>const { ref } = Vue // 創建響應式數據對象 age,初始值為 3 const age = ref(3) // 如果要訪問 ref() 創建出來的響應式數據對象的值,必須通過 .value 屬性才可以 console.log(age.value) // 輸出 3 // 讓 age 的值 +1 age.value++ // 再次打印 age 的值 console.log(age.value) // 輸出 4/<code>
在 template 中訪問 ref 創建的響應式數據
- 在 setup() 中創建響應式數據:
<code>setup() { const age = ref(3) return {age,name: ref('zhuanzhuan') } }/<code>
- 在 template 中訪問響應式數據:
<code>template:`名字是:{{name}},年齡是{{age}}
`/<code>
在 reactive 對象中訪問 ref 創建的響應式數據
當把 ref() 創建出來的響應式數據對象,掛載到 reactive() 上時,會自動把響應式數據對象展開為原始的值,不需通過 .value 就可以直接被訪問。
換句話說就是當一個包裝對象被作為另一個響應式對象的屬性引用的時候也會被自動展開例如:
<code>const age = ref(3) const state = reactive({age}) console.log(state.age) // 輸出 3 state.age++ // 此處不需要通過 .value 就能直接訪問原始值 console.log(age) // 輸出 4/<code>
以上這些關於包裝對象的細節可能會讓你覺得有些複雜,但實際使用中你只需要記住一個基本的規則:只有當你直接以變量的形式引用一個包裝對象的時候才會需要用 .value 去取它內部的值 —— 在模版中你甚至不需要知道它們的存在。
==注意:新的 ref 會覆蓋舊的 ref,示例代碼如下:==
<code>const age = ref(3) const state = reactive({age}) console.log(state.age) // 輸出 3 state.age++ // 此處不需要通過 .value 就能直接訪問原始值 console.log(age) // 輸出 4/<code>
isRef() 函數
isRef() 用來判斷某個值是否為 ref() 創建出來的對象;應用場景:當需要展開某個可能為 ref() 創建出來的值的時候,例如:
<code>const { isRef } = Vue const unwrapped = isRef(foo) ? foo.value : foo/<code>
toRefs() 函數
<code>const { toRefs } = Vuesetup() { // 定義響應式數據對象 const state = reactive({ age: 3 }) // 定義頁面上可用的事件處理函數 const increment = () => { state.age++ } // 在 setup 中返回一個對象供頁面使用 // 這個對象中可以包含響應式的數據,也可以包含事件處理函數 return { // 將 state 上的每個屬性,都轉化為 ref 形式的響應式數據 ...toRefs(state), // 自增的事件處理函數 increment }}/<code>
頁面上可以直接訪問 setup() 中 return 出來的響應式數據:
<code>template:`當前的age值為:{{age}}
/<code>
computed() 函數
computed() 用來創建計算屬性, computed() 函數的返回值是一個 ref 的實例。使用 computed 之前需要按需導入:
<code>const { computed } = Vue/<code>
創建只讀的計算屬性
<code>const { computed } = Vue // 創建一個 ref 響應式數據 const count = ref(1) // 根據 count 的值,創建一個響應式的計算屬性 plusOne // 它會根據依賴的 ref 自動計算並返回一個新的 refconst plusOne = computed(() => count.value + 1)console.log(plusOne.value) // 輸出 2plusOne.value++ // error/<code>
創建可讀可寫的計算屬性
在調用 computed() 函數期間,傳入一個包含 get 和 set 函數的對象,可以得到一個可讀可寫的計算屬性,示例代碼如下:
<code>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/<code>
watch() 函數
watch() 函數用來監視某些數據項的變化,從而觸發某些特定的操作,使用之前需要按需導入:
<code>const { watch } = Vue/<code>
基本用法
<code>const { watch } = Vueconst count = ref(0) // 定義 watch,只要 count 值變化,就會觸發 watch 回調 // watch 會在創建時會自動調用一次 watch(() => console.log(count.value)) // 輸出 0setTimeout(() => { count.value++ // 輸出 1 }, 1000)/<code>
監視指定的數據源
監視 reactive 類型的數據源:
<code>const { watch, reactive } = Vue const state = reactive({name:'zhuanzhuan'}); watch(() => state.name, (value, oldValue) => { /* ... */ })/<code>
監視 ref 類型的數據源:
<code>const { watch, ref } = Vue // 定義數據源 const count = ref(0) // 指定要監視的數據源 watch(count, (value, oldValue) => { /* ... */ })/<code>
監視多個數據源
監視 reactive 類型的數據源:
<code>const { reactive, watch, ref } = Vue onst 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)/<code>
清除監視
在 setup() 函數內創建的 watch 監視,會在當前組件被銷燬的時候自動停止。如果想要明確地停止某個監視,可以調用 watch() 函數的返回值即可,語法如下
<code>// 創建監視,並得到 停止函數 const stop = watch(() => { /* ... */ }) // 調用停止函數,清除對應的監視 stop()/<code>
在 watch 中清除無效的異步任務
有時候,當被 watch 監視的值發生變化時,或 watch 本身被 stop 之後,我們期望能夠清除那些無效的異步任務,此時, watch 回調函數中提供了一個 cleanup registratorfunction 來執行清除的工作。這個清除函數會在如下情況下被調用:
- watch 被重複執行了
- watch 被強制 stop 了
Template 中的代碼示例如下:
<code>/* template 中的代碼 */ /<code>
Script 中的代碼示例如下:
<code>// 定義響應式數據 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}/<code>
之所以要用傳入的註冊函數來註冊清理函數,而不是像 React 的 useEffect 那樣直接返回一個清理函數,是因為 watcher 回調的返回值在異步場景下有特殊作用。我們經常需要在 watcher 的回調中用 asyncfunction 來執行異步操作:
<code>const data = ref(null) watch(getId, async (id) => { data.value = await fetchData(id) })/<code>
我們知道 asyncfunction 隱性地返回一個 Promise - 這樣的情況下,我們是無法返回一個需要被立刻註冊的清理函數的。除此之外,回調返回的 Promise 還會被 Vue 用於內部的異步錯誤處理。
watch 回調的調用時機
默認情況下,所有的 watch 回調都會在當前的 renderer flush 之後被調用。這確保了在回調中 DOM 永遠都已經被更新完畢。如果你想要讓回調在 DOM 更新之前或是被同步觸發,可以使用 flush 選項:
<code>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 } )/<code>
全部的 watch 選項(TS 類型聲明)
<code>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' }/<code>
- lazy與 2.x 的 immediate 正好相反
- deep與 2.x 行為一致
- onTrack 和 onTrigger 是兩個用於 debug 的鉤子,分別在 watcher - 追蹤到依賴和依賴發生變化的時候被調用,獲得的參數是一個包含了依賴細節的 debugger event。
LifeCycle Hooks 生命週期函數
所有現有的生命週期鉤子都會有對應的 onXXX 函數(只能在 setup() 中使用):
<code>const { onMounted, onUpdated, onUnmounted } = Vue const MyComponent = { setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) // destroyed 調整為 unmounted onUnmounted(() => { console.log('unmounted!') }) }}/<code>
下面的列表,是 vue2.x 的生命週期函數與新版 CompositionAPI 之間的映射關係:
- beforeCreate -> setup()
- created -> setup()
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- updated -> onUpdated
- beforeDestroy -> onBeforeUnmount
- destroyed -> onUnmounted
- errorCaptured -> onErrorCaptured
provide & inject
provide() 和 inject() 可以實現嵌套組件之間的數據傳遞。這兩個函數只能在 setup() 函數中使用。父級組件中使用 provide() 函數向下傳遞數據;子級組件中使用 inject() 獲取上層傳遞過來的數據。
共享普通數據
App.vue 根組件:
<code>
App 根組件
/<code>
LevelOne.vue 組件:
<code>
Level One
/<code>
LevelTwo.vue 組件:
<code>
Level Two
/<code>
共享 ref 響應式數據
如下代碼實現了點按鈕切換主題顏色的功能,主要修改了 App.vue 組件中的代碼, LevelOne.vue 和 LevelTwo.vue 中的代碼不受任何改變:
<code>
App 根組件
/<code>
template refs
通過 ref() 還可以引用頁面上的元素或組件。
元素的引用
示例代碼如下:
<code>
TemplateRefOne
/<code>
組件的引用
TemplateRefOne.vue 中的示例代碼如下:
<code>
TemplateRefOne
/<code>
TemplateRefTwo.vue 中的示例代碼:
<code>
TemplateRefTwo --- {{count}}
/<code>
createComponent
這個函數不是必須的,除非你想要完美結合 TypeScript 提供的類型推斷來進行項目的開發。
這個函數僅僅提供了類型推斷,方便在結合 TypeScript 書寫代碼時,能為 setup() 中的 props 提供完整的類型推斷。
<code>import { createComponent } from 'vue' export default createComponent({ props: { foo: String }, setup(props) { props.foo // /<code>
參考
- 嚐鮮 vue3.x 新特性 - CompositionAPI:https://www.cnblogs.com/liulongbinblogs/p/11649393.html
- Vue 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)之爐火純青 帶你解密。
作者:大轉轉FE
轉發連接:https://mp.weixin.qq.com/s/gii93neiVqGGChNWOvkoiQ
推薦Vue學習資料文章:
《帶你瞭解 vue-next(Vue 3.0)之 初入茅廬【實踐】》
《一篇文章教你並列比較React.js和Vue.js的語法【實踐】》
《深入淺出通過vue-cli3構建一個SSR應用程序【實踐】》
《聊聊昨晚尤雨溪現場針對Vue3.0 Beta版本新特性知識點彙總》
《【新消息】Vue 3.0 Beta 版本發佈,你還學的動麼?》
《Vue + Koa從零打造一個H5頁面可視化編輯器——Quark-h5》
《深入淺出Vue3 跟著尤雨溪學 TypeScript 之 Ref 【實踐】》
《手把手教你深入淺出vue-cli3升級vue-cli4的方法》
《Vue 3.0 Beta 和React 開發者分別槓上了》
《手把手教你用vue drag chart 實現一個可以拖動 / 縮放的圖表組件》
《Vue3 嚐鮮 》
《手把手讓你成為更好的Vue.js開發人員的12個技巧和竅門【實踐】》
《2020 年,Vue 受歡迎程度是否會超過 React?》
《
Vue合理配置WebSocket並實現群聊》《手把手教你Vue解析pdf(base64)轉圖片【實踐】》
《手把手教你Vue之父子組件間通信實踐講解【props、$ref 、$emit】》
《深入淺出Vue3 的響應式和以前的區別到底在哪裡?【實踐】》
《乾貨滿滿!如何優雅簡潔地實現時鐘翻牌器(支持JS/Vue/React)》
《基於Vue/VueRouter/Vuex/Axios登錄路由和接口級攔截原理與實現》
《手把手教你D3.js 實現數據可視化極速上手到Vue應用》
《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【上】》
《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【中】》
《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【下】》
收藏
舉報