Vue3 和 Vue2 的響應(yīng)式有很大的不同,由于 Vue3 使用 Proxy 代替了 defineProperty闻镶,使得 Vue3 比 Vue2 在響應(yīng)式數(shù)據(jù)處理方面有著更好的性能即寡,更簡潔高效的處理方式陕赃,還實現(xiàn)了諸多在 Vue2 上無法實現(xiàn)的功能鼠锈。此外 Vue3 的響應(yīng)式庫 reactivity 是一個單獨的包岭粤,它可以不依賴 Vue 運(yùn)行麸拄,意味著我們可以將它運(yùn)行在其他框架里派昧。事實上,Vue3 的響應(yīng)式庫的實現(xiàn)方式以及市面上其他的大多數(shù)響應(yīng)式庫(如 observer-util拢切,meteor 等)的實現(xiàn)方式都是類似的蒂萎,Vue 也是參考這些庫實現(xiàn)的,所以我們還是很有必要去研究一下的淮椰,畢竟咱也不能落伍了 ??五慈,那么各位小伙伴們下面就跟我一起來看下這個 @vue/reactivity
究竟是怎么實現(xiàn)的纳寂。
本文章的源碼已經(jīng)發(fā)在了我的 git 上,可以前往查看:reactivity
閱讀本文章之前你要先了解以下知識點
上面這些有不了解的同學(xué)可以直接點鏈接查看詳細(xì)的文檔泻拦,文章里面就不再解釋了毙芜。
--
我們首先看一個使用 reactivity 的例子
// 創(chuàng)建一個響應(yīng)式對象
const state = reactive({ count: 1 })
// 執(zhí)行effect
effect(() => {
console.log(state.count)
})
state.count = 2 // count改變時執(zhí)行了effect內(nèi)的函數(shù),控制臺輸出2
這個例子通過 reactive 創(chuàng)建了一個響應(yīng)式對象 state争拐,然后調(diào)用 effect 執(zhí)行函數(shù)腋粥,這個函數(shù)內(nèi)部訪問了 state 的屬性,隨后我們更改這個 state 的屬性架曹,這時灯抛,effect 內(nèi)的函數(shù)會再次執(zhí)行。
這樣一個響應(yīng)式數(shù)據(jù)的通常實現(xiàn)的方式是這樣的
- 定義一個數(shù)據(jù)為響應(yīng)式(通常通過 defineProperty 或者 Proxy 攔截 get音瓷、set 等操作)
- 定義一個副作用函數(shù)(effect),這個副作用函數(shù)內(nèi)部訪問到響應(yīng)式數(shù)據(jù)時會觸發(fā) 1 中的 getter夹抗,進(jìn)而可以在這里將 effect 收集起來
- 修改響應(yīng)式數(shù)據(jù)時绳慎,就會觸發(fā) 1 中的 setter,進(jìn)而執(zhí)行 2 中收集到的 effect 函數(shù)
關(guān)于 effect: effect 在 Vue 里通常叫做副作用函數(shù)漠烧,因為這種函數(shù)內(nèi)通常執(zhí)行組件渲染杏愤,計算屬性等其他任務(wù)。在其他庫里面可能叫觀察者函數(shù)(observe)或其他已脓,個人能理解到是什么意思就好珊楼,由于本篇文章是分析 Vue3 的,所以統(tǒng)一叫副作用函數(shù)(effect)
根據(jù)以上的思路度液,我們就可以開始動手實現(xiàn)了
reactive
首先我們需要有一個 reactive 函數(shù)來將我們的數(shù)據(jù)變?yōu)轫憫?yīng)式厕宗。
// reactive.ts
import { baseHandlers } from './handlers'
import { isObject } from './utils'
type Target = object
const proxyMap = new WeakMap()
export function reactive<T extends object>(target: T): T {
return createReactiveObject(target)
}
function createReactiveObject(target: Target) {
// 只對對象添加reactive
if (!isObject(target)) {
return target
}
// 不能重復(fù)定義響應(yīng)式數(shù)據(jù)
if (proxyMap.has(target)) {
return proxyMap.get(target)
}
// 通過Proxy攔截對數(shù)據(jù)的操作
const proxy = new Proxy(target, baseHandlers)
// 數(shù)據(jù)添加進(jìn)ProxyMap中
proxyMap.set(target, proxy)
return proxy
}
這里主要對數(shù)據(jù)做了簡單的判斷,關(guān)鍵是在const proxy = new Proxy(target, baseHandlers)
中堕担,通過 Proxy 對數(shù)據(jù)進(jìn)行處理已慢,這里的baseHandlers
就是對數(shù)據(jù)的 get,set 等攔截操作霹购,下面來實現(xiàn)下baseHandlers
get 收集依賴
首先實現(xiàn)下攔截 get 操作佑惠,使得訪問數(shù)據(jù)的某一個 key 時,可以收集到訪問這個 key 的函數(shù)(effect)齐疙,并把這個函數(shù)儲存起來膜楷。
// handlers.ts
import { track } from './effect'
import { reactive, Target } from './reactive'
import { isObject } from './utils'
export const baseHandlers: ProxyHandler<object> = {
get(target: Target, key: string | symbol, receiver: object) {
// 收集effect函數(shù)
track(target, key)
// 獲取返回值
const res = Reflect.get(target, key, receiver)
// 如果是對象,要再次執(zhí)行reactive并返回
if (isObject(res)) {
return reactive(res)
}
return res
}
}
這里我們攔截到 get 操作后贞奋,通過 track 收集依賴赌厅,track 函數(shù)做的事情就是把當(dāng)前的 effect 函數(shù)收集起來,執(zhí)行完 track 后忆矛,再獲取到 target 的 key 的值并返回察蹲,注意這里是判斷了下 res 是否是對象请垛,如果是對象的話要返回reactive(res)
,是因為考慮到可能有多個嵌套對象的情況洽议,而 Proxy 只能修改到到當(dāng)前對象宗收,并不能修改到子對象,所以在這里要處理下亚兄,下面我們需要再實現(xiàn)track
函數(shù)
// effect.ts
// 存儲依賴
type Deps = Set<ReactiveEffect>
// 通過key去獲取依賴混稽,key => Deps
type DepsMap = Map<any, Deps>
// 通過target去獲取DepsMap,target => DepsMap
const targetMap = new WeakMap<any, DepsMap>()
// 當(dāng)前正在執(zhí)行的effect
let activeEffect: ReactiveEffect | undefined
// 收集依賴
export function track(target: object, key: unknown) {
if (!activeEffect) {
return
}
// 獲取到這個target對應(yīng)的depsMap
let depsMap = targetMap.get(target)
// depsMap不存在時新建一個
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 有了depsMap后审胚,再根據(jù)key去獲取這個key所對應(yīng)的deps
let deps = depsMap.get(key)
// 也是不存在時就新建一個
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 將activeEffect添加進(jìn)deps
if (!deps.has(activeEffect)) {
deps.add(activeEffect)
}
}
注意有兩個 map 和一個 set匈勋,targetMap => depsMap => deps,這樣就可以使我們通過 target 和 key 準(zhǔn)確地獲取到這個 key 所對應(yīng)的 deps(effect)膳叨,把當(dāng)前正在執(zhí)行的 effect(activeEffect)存起來洽洁,這樣在修改target[key]
的時候,就又可以通過 target 和 key 拿到之前收集到的所有的依賴菲嘴,并執(zhí)行它們饿自,這里有個問題就是這個activeEffect
它是從哪里來的,get 是怎么知道當(dāng)前正在執(zhí)行的 effect 的龄坪?這個問題可以先放一放昭雌,我們后面再將,下面我們先實現(xiàn)這個 set健田。
實現(xiàn) set
// handlers.ts
export const baseHandlers: ProxyHandler<object> = {
get() {
//...
},
set(target: Target, key: string | symbol, value: any, receiver: object) {
// 設(shè)置value
const result = Reflect.set(target, key, value, receiver)
// 通知更新
trigger(target, key, value)
return result
}
}
我們在剛才的baseHandlers
下面再加一個 set烛卧,這個 set 里面主要就是賦值然后通知更新,通知更新通過trigger
進(jìn)行妓局,我們需要拿到在 get 中收集到的依賴总放,并執(zhí)行,下面來實現(xiàn)下 trigger 函數(shù)
// effect.ts
// 通知更新
export function trigger(target: object, key: any, newValue?: any) {
// 獲取該對象的depsMap
const depsMap = targetMap.get(target)
// 獲取不到時說明沒有觸發(fā)過getter
if (!depsMap) {
return
}
// 然后根據(jù)key獲取deps好爬,也就是之前存的effect函數(shù)
const effects = depsMap.get(key)
// 執(zhí)行所有的effect函數(shù)
if (effects) {
effects.forEach((effect) => {
effect()
})
}
}
這個 trigger 就是獲取到之前收集的 effect 然后執(zhí)行间聊。
其實除了 get 和 set,還有個常用的操作抵拘,就是刪除屬性哎榴,現(xiàn)在我們還不能攔截到刪除操作,下面我們來實現(xiàn)下
實現(xiàn) deleteProperty
export const baseHandlers: ProxyHandler<object> = {
get() {
//...
},
set() {
//...
},
deleteProperty(target: Target, key: string | symbol) {
// 判斷要刪除的key是否存在
const hadKey = hasOwn(target, key)
// 執(zhí)行刪除操作
const result = Reflect.deleteProperty(target, key)
// 只在存在key并且刪除成功時再通知更新
if (hadKey && result) {
trigger(target, key, undefined)
}
return result
}
}
我們在剛才的baseHandlers
里面再加一個deleteProperty
僵蛛,它可以攔截到對數(shù)據(jù)的刪除操作尚蝌,在這里我們需要先判斷下刪除的 key 是否存在,因為可能用戶會刪除一個并不存在 key充尉,然后執(zhí)行刪除飘言,我們只在存在 key 并且刪除成功時再通知更新,因為如果 key 不存在時驼侠,這個刪除是無意義的姿鸿,也就不需要更新谆吴,再有就是如果刪除操作失敗的話,也不需要更新苛预,最后直接觸發(fā)trigger
就可以了句狼,注意這里的第三個參數(shù)即 value 是undefined
現(xiàn)在我們已經(jīng)實現(xiàn)了get
,set
热某,deleteProperty
這三種操作的攔截腻菇,還記不記得在track
函數(shù)中的activeEffect
,那里留了個問題昔馋,就是這個activeEffect
是怎么來的筹吐?,在最開始的例子里面秘遏,我們要通過 effect 執(zhí)行函數(shù)丘薛,這個activeEffect
就是在這里設(shè)置的,下面我們來實現(xiàn)下這個effect
函數(shù)邦危。
// effect.ts
type ReactiveEffect<T = any> = () => T
// 存儲effect的調(diào)用棧
const effectStack: ReactiveEffect[] = []
export function effect<T = any>(fn: () => T): ReactiveEffect<T> {
// 創(chuàng)建一個effect函數(shù)
const effect = createReactiveEffect(fn)
return effect
}
function createReactiveEffect<T = any>(fn: () => T): ReactiveEffect<T> {
const effect = function reactiveEffect() {
// 當(dāng)前effectStack調(diào)用棧不存在這個effect時再執(zhí)行榔袋,避免死循環(huán)
if (!effectStack.includes(effect)) {
try {
// 把當(dāng)前的effectStack添加進(jìn)effectStack
effectStack.push(effect)
// 設(shè)置當(dāng)前的effect,這樣Proxy中的getter就可以訪問到了
activeEffect = effect
// 執(zhí)行函數(shù)
return fn()
} finally {
// 執(zhí)行完后就將當(dāng)前這個effect出棧
effectStack.pop()
// 把a(bǔ)ctiveEffect恢復(fù)
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect<T>
return effect
}
這里主要是通過createReactiveEffect
創(chuàng)建一個 effect 函數(shù)铡俐,fn 就是調(diào)用 effect 時傳入的函數(shù),在執(zhí)行這個 fn 之前妥粟,先通過effectStack.push(effect)
把這個 effect 推入 effectStack 棧中审丘,因為 effect 可能存在嵌套調(diào)用的情況,保存下來就可以獲取到一個完整的 effect 調(diào)用棧勾给,就可以通過上面的effectStack.includes(effect)
判斷是否存在循環(huán)調(diào)用的情況了滩报,然后再activeEffect = effect
設(shè)置 activeEffect,設(shè)置完之后再執(zhí)行 fn播急,因為這個 activeEffect 是全局唯一的脓钾,所以我們執(zhí)行 fn 的時候,如果內(nèi)部訪問了響應(yīng)式數(shù)據(jù)桩警,就可以在 getter 里拿到這個 activeEffect可训,進(jìn)而收集它。
現(xiàn)在基本上是完成了捶枢,現(xiàn)在通過我們寫的這個 reactivity 庫就可以實現(xiàn)例子中的效果了握截,但是還有一些邊界情況需要考慮,下篇文章就添加一些常見的邊界情況處理烂叔。