vue 同时执行两个函数 点击_【第2112期】 import { reactive } from #39;vue#39;
前言
今日早讀文章由@Anthony Fu授權(quán)分享。
@Anthony Fu,是 Vue 的 Core Team 的一員,在 Vue主要負(fù)責(zé) @vue/composition-api 這個(gè)項(xiàng)目的維護(hù)。這是一個(gè)面向 Vue 2 的插件,它在 Vue 2 中增加了 Vue 3 的 Composition API 的支持。最近也加入了 Vite 負(fù)責(zé)一些 Code Review 的工作。GitHub:@antfu
正文從這開(kāi)始~~
介紹
我這次分享的主要會(huì)和大家簡(jiǎn)單介紹一下響應(yīng)式與組合式 API,然后通過(guò)一個(gè)例子的形式介紹組合式 API 所帶來(lái)的優(yōu)勢(shì)。再來(lái),我會(huì)以一個(gè)工具庫(kù)作者的角度跟大家聊一聊如何做到 Vue 2 與 Vue 3 雙版本同時(shí)兼容的同構(gòu)。最后,我會(huì)去再介紹一下響應(yīng)式 API 的一些延伸應(yīng)用。
慶祝 Vue 3.0 One Piece 在上個(gè)禮拜正式發(fā)布!
大家知道,在 Vue 3.0 中我們使用 TypeScript 進(jìn)行了一次從零的重寫(xiě)。利用這次重寫(xiě)的機(jī)會(huì),我們對(duì)整個(gè) Repo 的結(jié)構(gòu)進(jìn)行了一些解構(gòu),把 Vue 拆分成了這幾個(gè)獨(dú)立的庫(kù)。在這一次的分享中我會(huì)主要會(huì)面向比較底層的響應(yīng)式(@vue/reactivity)和組合式(@vue/runtime-core)這兩個(gè)模塊進(jìn)行討論。
響應(yīng)式 Reactivity API
那么什么是響應(yīng)式呢?提到這個(gè)就得祭出這張非常經(jīng)典的 GIF。在一個(gè) Excel 表格里面,我們會(huì)以公示的形式去定義一個(gè)一個(gè)單元格應(yīng)該去做怎么樣的一個(gè)運(yùn)算。那么大家可以看到,在我設(shè)置好了 A3 這個(gè)格子的公式之后,我去更新 A1 的數(shù)值時(shí), A3 就會(huì)自動(dòng)更新,而我不需要再去做任何的操作。這就是響應(yīng)是能夠給我們帶來(lái)的一個(gè)非常好的幫助,依賴(lài)的自動(dòng)收集跟更新。
在 Vue 3 里面,我們對(duì)整個(gè)響應(yīng)式系統(tǒng)做了一個(gè)重新的設(shè)計(jì),同時(shí)暴露出了這幾個(gè)新的API,ref reactive computed effect。我們把原本 Vue 2 Object.defineProperty 的實(shí)現(xiàn)改成了使用 Proxy 的實(shí)現(xiàn)方式。而 Proxy 可以給我們提供對(duì)屬性更新監(jiān)控的更大的靈活性。
const reactive = (target) => new Proxy(target, {
get(target, prop, receiver) {
track(target, prop)
return Reflect.get(...arguments) // get original data
},
set(target, key, value, receiver) {
trigger(target, key)
return Reflect.set(...arguments)
}
})
const obj = reactive({
hello: 'world'
})
console.log(obj.hello) // `track()` get called
obj.hello = 'vue' // `trigger()` get called
我們可以通過(guò) get 和 set 這兩個(gè) handler 去追蹤每一個(gè)屬性的訪問(wèn)和修改,在這個(gè)例子中我們?cè)?get 里注入了 track 這個(gè)函數(shù),在 set 里注入了trigger 這個(gè)函數(shù)。那么在對(duì) reactive 這個(gè)對(duì)象的 hello 屬性進(jìn)行訪問(wèn)的時(shí)候 track 就會(huì)被執(zhí)行,在對(duì) obj.hello 進(jìn)行賦值的時(shí)候,trigger 就會(huì)被執(zhí)行。通過(guò) track 和 trigger 我們就可以進(jìn)行一些響應(yīng)式的追蹤。
Effect
effect 是在 Vue 3 里面新引入的一個(gè)API,它的作用就是去結(jié)合 track 和 trigger 這兩個(gè)功能,track 的作用是追蹤調(diào)用他的函數(shù),trigger 是去觸發(fā)綁定的依賴(lài)更新。
const targetMap = new WeakMap()
export const track = (target, key) => {
if (tacking && activeEffect)
targetMap.get(target).key(key).push(activeEffect)
}
export const trigger = (target, key) => {
targetMap.get(target).key(key).forEach(effect => effect())
}
export const effect = (fn) => {
let effect = function() { fn() }
enableTracking()
activeEffect = effect
fn()
resetTracking()
activeEffect = undefined
}
在 effect 里面我們會(huì)接受一個(gè)函數(shù)作為參數(shù),在執(zhí)行這個(gè)函數(shù)之前的我們會(huì)開(kāi)啟 tracking,然后把當(dāng)前的函數(shù)設(shè)置在一個(gè)全局變量 activeEffect,然后再去執(zhí)行這個(gè)函數(shù)。那么在這個(gè)函數(shù)的調(diào)用時(shí)間里面我們有任何的 reactive 的調(diào)用就會(huì)觸發(fā) track 這個(gè)函數(shù)。track 的主要功能就是說(shuō)我們把當(dāng)前的 activeEffect 綁定到所觸發(fā)它的這個(gè)屬性調(diào)用上。然后在數(shù)據(jù)更新的時(shí)候,我們?cè)偃フ业竭@個(gè)依賴(lài)上面所綁定的所有 effect 把他們一一調(diào)用。這樣就完成了一個(gè)最基本的響應(yīng)式的功能。
computed & watch
在 Vue 3.0 里面,computed 和 watch 都是基于 effect 的包裝,我們這邊可以看到一個(gè)簡(jiǎn)單的 computed 的實(shí)現(xiàn)
const computed = (getter) => {
let value
let dirty = true
const runner = effect(getter, {
lazy: true,
scheduler() {
dirty = true // deps changed
}
})
return {
get value() {
if (dirty) {
value = runner() // re-evaluate
dirty = false
}
return value
}
}
}
computed 接受一個(gè) getter 函數(shù),這個(gè)函數(shù)我們把它直接傳給 effect,effect會(huì)在先執(zhí)行一次進(jìn)行依賴(lài)收集,在收集完了之后,如果里面其中的依賴(lài)發(fā)生了變動(dòng),他就會(huì)觸發(fā)這個(gè) scheduler 將 dirty 設(shè)置為 true。在最后我們?cè)趯?duì) computed 進(jìn)行求值的時(shí)候,如果 dirty 為 true,我們就會(huì)重新進(jìn)行一次運(yùn)算得到新的 value 后再把 value 傳出去。在第二次調(diào)用時(shí),如果里面的依賴(lài)沒(méi)有更新,我們就可以直接用上一次計(jì)算的結(jié)果,這件可以避免掉多余重復(fù)的計(jì)算。這里有一些 延伸閱讀,大家如果有興趣去了解一些比較深入的原理的話也可以去看一看。
組合式 Composition API
那么聊完了響應(yīng)式,我們?cè)賮?lái)看看什么是組合式。
組合式其實(shí)是基于響應(yīng)式延伸出來(lái)的一套和 Vue 生命周期綁定的一套工具。它提供了 Vue 生命周期的鉤子像是 onMounted onUpdate 和 onUnmounted 等等。還有個(gè)非常重要的功能就是說(shuō)在 Vue 的 setup() 里面,所建立的類(lèi)似 computed 或者 watch 的 effect 會(huì)在組件銷(xiāo)毀的時(shí)候自動(dòng)跟隨這個(gè)組件一并銷(xiāo)毀。那么組合是最重要的作用就是它可以提供可復(fù)用的邏輯,我們可以把很多的邏輯拆分出來(lái),做成一個(gè)一個(gè)的工具。然后可以跨組件的進(jìn)行復(fù)用或甚至是把它做成一個(gè)第三方庫(kù),跨應(yīng)用地進(jìn)行復(fù)用。這個(gè)我們會(huì)在之后進(jìn)行詳細(xì)的介紹。
響應(yīng)式是跟組合式的區(qū)別,就是他們是有兩個(gè)不同的包提供的,在整個(gè) Vue 應(yīng)用的角度來(lái)看的話 ,這些 API 都會(huì)從 vue 這個(gè)包里面統(tǒng)一導(dǎo)出的。但是如果我們會(huì)我們想要使用其中的一部分的話,那么可以看到 ref reactive computed effect 是在 @vue/reactivity 這個(gè)包里導(dǎo)出的,然后像是 watch setup 和一些生命周期是在 @vue/runtime-core 這個(gè)包里導(dǎo)出的??梢宰⒁獾揭稽c(diǎn)也是非常有趣的一點(diǎn),就是 @vue/reactivity 這個(gè)包其實(shí)是可以作為一個(gè)獨(dú)立的包使用的,也就是說(shuō)我可以不依賴(lài)于 Vue,我可以基于這個(gè)自己做一個(gè)框架,甚至我可以在 Node.js,在沒(méi)有 UI 的環(huán)境下去進(jìn)行使用。這個(gè)也會(huì)在我們后面的PPT里面去做一個(gè)比較詳細(xì)的介紹。
Case Study
那我們來(lái)看一個(gè)簡(jiǎn)單的使用場(chǎng)景的一個(gè)例子,這里有一個(gè)需求,我們現(xiàn)在想給我們的網(wǎng)頁(yè)實(shí)現(xiàn)一個(gè) Dark Mode 這個(gè)功能。我希望整個(gè)頁(yè)面在默認(rèn)的情況下會(huì)隨著我系統(tǒng)的系統(tǒng)的偏好改變。然后我可能希望一個(gè)用戶有一個(gè)手動(dòng)可以修改的功能,比如說(shuō)我有一個(gè)按鈕一個(gè)直接改變 Dark Mode。
然后又希望這個(gè)這個(gè)功能是一個(gè)可持久化的,我可以保存下用戶的偏好,在網(wǎng)頁(yè)刷新后還可以還可以繼續(xù)存留用戶的上一次的修改。最后可能會(huì)希望說(shuō)在兩個(gè)模式切換的時(shí)候去執(zhí)行一些代碼,比如說(shuō)通知用戶或者是通知組件進(jìn)行一些操作之類(lèi)的。
基礎(chǔ)實(shí)現(xiàn)
那我們看一下我們?cè)趺慈?shí)現(xiàn)這樣一個(gè)功能。我們假設(shè)說(shuō) Dark Mode 已經(jīng)在CSS層面上都做好了,也就是說(shuō)我把 dark class 加上的時(shí)候,整個(gè)頁(yè)面就會(huì)變成黑暗模式。那么我再提供一個(gè)按鈕去給用戶做切換。這個(gè)就是我們提供的模板的部分
:class='{dark}'>
@click='toggleDark'>Toggle
我們?cè)賮?lái)看代碼的部分要怎么實(shí)現(xiàn)
那么在 Options API 里面,非常的簡(jiǎn)單,我們可以這樣實(shí)現(xiàn):
export default {
data() {
return {
dark: false
}
},
methods: {
toggleDark() {
this.dark = !this.dark
}
}
}
那在 Composition API 里面,我們可以把 dark 變成 ref。這個(gè) dark 會(huì)直接從setup() 里面?zhèn)鞒鋈?#xff0c;那我們同時(shí)可以在 return 里面?zhèn)饕粋€(gè)叫做 toggleDark 的函數(shù),然后我們也是一樣對(duì) dark 進(jìn)行取反。這樣我們就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的開(kāi)關(guān)的功能。
系統(tǒng)偏好
再來(lái)的話,我們希望去增加用戶系統(tǒng)偏好的更新。我們可以通過(guò)一個(gè)瀏覽器提供的 API window.matchMedia。然后再利用一個(gè) CSS 的 Query (prefers-color-scheme: dark),我們就可以知道是用戶的系統(tǒng)的顏色偏好。然后我們會(huì)我們可以對(duì)這個(gè) matchMedia 調(diào)用 addEvenetListener 進(jìn)行監(jiān)聽(tīng),那么在用戶系統(tǒng)改變的時(shí)候,我們可以隨之一起改變。
那么為了實(shí)現(xiàn)這樣一個(gè)功能的話,在 Options API 里面我們需要在需要將 media 暴露在 Vue 實(shí)例上,然后在 created 中進(jìn)行事件的綁定,同時(shí)在 destroyed 的時(shí)候再把這個(gè)事件監(jiān)聽(tīng)注銷(xiāo)。
// Options API
export default {
data() {
return {
dark: false,
media: window.matchMedia('(prefers-color-scheme: dark)')
}
},
methods: {
toggleDark() {
this.dark = !this.dark
},
update() {
this.dark = this.media.matches
}
},
created() {
this.media.addEventListener('change', this.update)
this.update()
},
destroyed() {
this.media.removeEventListener('change', this.update)
}
}
那么再來(lái)看看 Composition API 要怎么實(shí)現(xiàn)。我們直接定義這個(gè) media。
然后因?yàn)樵?Composition API 中,setup() 相當(dāng)于 Options API 的 created,我們直接可以把 addEventListener 的直接寫(xiě)在 setup() 里面,對(duì)應(yīng)的我們?cè)偻ㄟ^(guò)一個(gè)生命周期的鉤子 OnUnmounted 注銷(xiāo)事件監(jiān)聽(tīng)。
// Composition API
import { onUnmounted, ref } from 'vue'
export default {
setup() {
const media = window.matchMedia('(prefers-color-scheme: dark)')
const dark = ref(media.matches)
const update = () => dark.value = media.matches
media.addEventListener('change', update)
onUnmounted(() => {
media.removeEventListener('change', update)
})
return {
dark,
toggleDark() {
dark.value = !dark.value
}
}
}
}
用戶設(shè)置持久化
再來(lái)我們需要讓用戶的設(shè)置可以持久化,我們就需要把用戶的設(shè)置存在 localStorage 里。設(shè)置修改的時(shí)候存入 localStorage,每次頁(yè)面加載的時(shí)候再讀出來(lái)。邊代碼大家看一看就可以了,主要想讓大家看到的一點(diǎn)就是在 Options API 里面,我們給現(xiàn)有的一個(gè)組件增加功能的時(shí)候,我們會(huì)在不同的地方插入代碼。比如說(shuō)在 data 里面聲明狀態(tài),在 methods 加幾個(gè)函數(shù)。我們插入非常零碎的幾個(gè)片段去實(shí)現(xiàn)一個(gè)功能,當(dāng)這個(gè)組件的代碼變得非常的長(zhǎng)的時(shí)候我們很容易去丟失掉單一功能的上下文。
那么在 Composition API 里,我們可以我們可以很好的把代碼給組織在一起。像是這樣的一個(gè)功能,就只需要在一個(gè) Block 里面加入這些代碼,我們可以很清楚的上有上下文,也可以有 TypeScript 進(jìn)行檢查。 以我們剛剛實(shí)現(xiàn)的 Dark Mode 為例,其實(shí)相對(duì)并不是一個(gè)非常復(fù)雜的功能,而我們已經(jīng)寫(xiě)了這么多行的代碼。如果在再這個(gè)組件繼續(xù)的擴(kuò)展的時(shí)候,會(huì)導(dǎo)致代碼的整個(gè)結(jié)構(gòu)變得非常的復(fù)雜,其實(shí)就是一個(gè)不是非常好的 Smell。這也是我們希望避免的一件事情。
那么我們會(huì)可能會(huì)希望我們可以把邏輯拿出來(lái)復(fù)用,或者是我們希望 Dark Mode 的這個(gè)功能,可以在另外的一個(gè)組件去做調(diào)用,或者是我就希望讓整個(gè)代碼看起來(lái)比較的干凈。在 Options API 里面,我們是可以做到這一點(diǎn),但是現(xiàn)有的幾個(gè)方案都并不是非常的理想 (Mixin, Renderless Component, Vuex, etc.)
Mixin 問(wèn)題是會(huì)有命名空間的沖突。像是我們剛剛的例子,我們會(huì)有一個(gè) updated 的函數(shù),那么如果我們?cè)?Mixin 中使用 updated 這個(gè)函數(shù),然后用戶端在使用的時(shí)候如果沒(méi)有注意到,他也自己寫(xiě)了一個(gè) updated 函數(shù),這就會(huì)導(dǎo)致函數(shù)覆蓋,會(huì)出現(xiàn)一些不希望的情況,但是又很難去 debug。
Renderless Component 可以一定程度上解決命名空間的問(wèn)題,但是他只能在模板里面使用,組合性也有很多的局限。
Vuex 的話要做到這些就會(huì)變得更加復(fù)雜,你需要去定義 Mutations 也需要去定義 Actions。然后再綁定一些瀏覽器的事件。
但是 Composition API 的話就變得非常的簡(jiǎn)單粗暴,我只需要把 setup() 的代碼復(fù)制粘貼出去,然后用一個(gè)函數(shù)把它包裝起來(lái)。那么在這里,我就只需要去調(diào)一個(gè) use 就可以了。而且我們可以繼續(xù)在這里面寫(xiě)更多的邏輯,同時(shí)也不會(huì)導(dǎo)致找不到對(duì)應(yīng)的上下文。
進(jìn)一步復(fù)用
我們甚至可以進(jìn)行進(jìn)一步的復(fù)用。以剛剛的代碼為例,我們可以把這個(gè) useDark 里面的這個(gè) matchMedia 和用戶設(shè)置的部分把他單獨(dú)拉出來(lái),變成兩個(gè)獨(dú)立的獨(dú)立的函數(shù)。那么這些函數(shù)它就可以單獨(dú)去專(zhuān)注在解決他單一問(wèn)題上。以 useDark 的層面就只需要去在意,我在什么時(shí)候需要使用系統(tǒng)的設(shè)置和什么時(shí)候需要使用用戶的設(shè)置。這里還有一個(gè)有趣的點(diǎn),就是在這些組合工具里面他都可以使用生命周期的鉤子,它就可以做到自動(dòng)更新和自動(dòng)注銷(xiāo)。或者是說(shuō)在數(shù)據(jù)改變的時(shí)候自動(dòng)進(jìn)行保存。
那么做到這一點(diǎn)的情況下,在使用的時(shí)候就可以沒(méi)有什么負(fù)擔(dān)。我只需要去在意他每一個(gè) ref 對(duì)應(yīng)什么樣的功能,更新了之后它就可以幫我做到它應(yīng)該做到的事情。這樣對(duì)一個(gè)非常龐大的項(xiàng)目來(lái)說(shuō),可以更好的提高代碼的復(fù)用度也可以提高代碼的可讀性跟可維護(hù)性。
export function useDark() {
const system = usePreferDark()
const setting = useLocalStorage('setting-dark', 'auto')
const dark = computed({
get() {
return setting.value === 'auto'
? system.value
: setting.value === 'dark'
},
set(v) {
if (v === system.value)
setting.value = 'auto'
else
setting.value = v ? 'dark' : 'light'
},
})
return dark
}
export function usePreferDark() {
const media = window.matchMedia('(prefers-color-scheme: dark)')
const dark = ref(media.matches)
const update = () => dark.value = media.matches
media.addEventListener('change', update)
onUnmounted(() => {
media.removeEventListener('change', update)
})
return dark
}
export function useLocalStorage(key, defaultValue) {
const data = ref(localStorage.getItem(key) ?? defaultValue)
watch(data, () => localStorage.setItem(key, data.value))
return data
}
邏輯的組件
所以我覺(jué)得對(duì)于這些可以被復(fù)用的這些函數(shù)來(lái)說(shuō),它更像是一個(gè)邏輯的組件。我們平常講組件的時(shí)候,一般來(lái)說(shuō)都是指UI組件。UI 組件我們可以把它抽象成這樣一個(gè)情況,就是說(shuō) UI 組件接受一個(gè) Props,也就是從他的父組件傳進(jìn)來(lái)的一些參數(shù),然后會(huì)根據(jù)它的 State 去更新對(duì)應(yīng)的UI,再以通過(guò)事件的形式去通知父組件。
那么換到邏輯組件來(lái)說(shuō),其實(shí)就是一個(gè)函數(shù),函數(shù)可以接受一些參數(shù)。這些參數(shù)可以是普通參數(shù),也可以是響應(yīng)式的。然后在這些在這些函數(shù)里面,我們可以進(jìn)行一些生命周期的綁定,可以去做一些對(duì)監(jiān)聽(tīng)事件的銷(xiāo)毀。最后我再回傳出一些響應(yīng)式的數(shù)據(jù),這些數(shù)據(jù)可以是 ref 也可以是 reactive。同時(shí)這些響應(yīng)的數(shù)據(jù)會(huì)根據(jù)其中內(nèi)部的狀態(tài)進(jìn)行一些更新,可以達(dá)到類(lèi)似事件通知的效果。其實(shí)右邊這張圖是給 UI 組件的一張圖,但是我覺(jué)得他也同樣適用于邏輯組件。
也就是說(shuō),我可以復(fù)用底層的 useLocalStorage useQuery 去實(shí)現(xiàn)一個(gè)更高層的邏輯組件。讓每一層組件都專(zhuān)注于在做自己的事情上就好了。
現(xiàn)有邏輯組件庫(kù)
現(xiàn)有的 Vue 3 已經(jīng)可以使用的有兩個(gè)主要的邏輯的組件庫(kù),VueUse 和 vue-composable。有點(diǎn)像 React 中的 react-use 或者 ahooks 這一類(lèi)的工具。VueUse 提供了更加細(xì)粒度的 Web API 以及工具分裝。vue-composable 是由另外一個(gè) Core Team Member @pikax 做的,它提供了更多常用的邏輯封裝。例如 useI18n, useValidation 等等。這些功能直接實(shí)現(xiàn)在了這個(gè)工具里面,而不需要再去安裝另外依賴(lài)于別的庫(kù)的。
組合式 API 生態(tài)
然后和大家簡(jiǎn)單講一下組合式 API 的生態(tài)支持。在 DevTools 6.0.0-beta.2 的更新了之后,加入了 Vue 3 的支持,同時(shí)加入一個(gè)新的功能是 Timeline 這個(gè)自定義的事件的打點(diǎn),他可以去監(jiān)聽(tīng)整個(gè)應(yīng)用里面發(fā)生的各種各樣的事件,然后把它做成一個(gè)個(gè)的點(diǎn),讓你可以去以時(shí)間的維度知道發(fā)生了什么。
然后在 vue-composable 里面提供了一個(gè)非常有趣的 API 叫做 useDevtoolsInspector,你可以傳一些響應(yīng)式的數(shù)據(jù),當(dāng)這些數(shù)據(jù)更新的時(shí)候去打點(diǎn)在 Timeline。你就可以更好的知道你的這些響應(yīng)式的數(shù)據(jù)什么時(shí)候被什么時(shí)候被更新了以及更新成了什么。
import { useDevtoolsInspector } from 'vue-composable'
const counter = ref(0)
useDevtoolsInspector({ counter })
然后再來(lái)一個(gè)就是 SFC 的單文件組件的一些更新。我們給 script 標(biāo)簽加了一個(gè) setup 的 flag。那么通過(guò)
總結(jié)
以上是生活随笔為你收集整理的vue 同时执行两个函数 点击_【第2112期】 import { reactive } from #39;vue#39;的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 图元和片元_OpenGL中的顶点、 图元
- 下一篇: mac安装和卸载mysql_小白自学My