手摸手教你實現Vue3 Reactivity
前言
Vue3的響應式基於Proxy,對比Vue2中使用的Object.definedProperty的方式,使用Proxy在新增的對象以及數組的攔截上都有很好的支持。
Vue3的響應式是一個獨立的系統,可以抽離出來使用,那他到底是如何實現的呢?
都知道有Getter和Setter,那Getter和Setter中分別都進行瞭哪些主要操作才能實現響應式呢?
哼哼,帶著這些問題一起來看看吧,文章會一步一步實現一個完整的響應式系統(誤)~。
開始
observer-util這個庫使用瞭與Vue3同樣的思路編寫,Vue3中的實現更加復雜,從一個更加純粹的庫開始(我不會承認是因為Vue3中有些未看懂的,不會)。
根據官網的例子:
import { observable, observe } from '@nx-js/observer-util'; const counter = observable({ num: 0 }); const countLogger = observe(() => console.log(counter.num)); // this calls countLogger and logs 1 counter.num++;
這兩個類似Vue3裡的reactive和普通的響應式。
observable之後的對象被添加瞭代理,之後observe中添加的響應函數會在依賴的屬性改變時調用一次。
小小思考
這裡粗略思考是一個訂閱發佈的模型,被observable代理之後的對象建立一個發佈者倉庫,observe這時候會訂閱counter.num,之後訂閱的內容改變時便會一一回調。
偽代碼:
// 添加監聽 xxx.addEventListener('counter.num', () => console.log(counter.num)) // 改變內容 counter.num++ // 發送通知 xxx.emit('counter.num', counter.num)
而響應式的核心也就是這個,添加監聽與發送通知會經由observable和observe自動完成。
代碼實現
經由上面的思考,在Getter裡我們需要將observe傳過來的回調添加到訂閱倉庫中。
具體的實現中observable會為這個觀察的對象添加一個handler,在Getter的handler中有一個
registerRunningReactionForOperation({ target, key, receiver, type: 'get' }) const connectionStore = new WeakMap() // reactions can call each other and form a call stack const reactionStack = [] // register the currently running reaction to be queued again on obj.key mutations export function registerRunningReactionForOperation (operation) { // get the current reaction from the top of the stack const runningReaction = reactionStack[reactionStack.length - 1] if (runningReaction) { debugOperation(runningReaction, operation) registerReactionForOperation(runningReaction, operation) } }
這個函數會獲取出一個reaction(也就是observe傳過來的回調),並且通過registerReactionForOperation保存。
export function registerReactionForOperation (reaction, { target, key, type }) { if (type === 'iterate') { key = ITERATION_KEY } const reactionsForObj = connectionStore.get(target) let reactionsForKey = reactionsForObj.get(key) if (!reactionsForKey) { reactionsForKey = new Set() reactionsForObj.set(key, reactionsForKey) } // save the fact that the key is used by the reaction during its current run if (!reactionsForKey.has(reaction)) { reactionsForKey.add(reaction) reaction.cleaners.push(reactionsForKey) } }
這裡生成瞭一個Set,根據key,也就是實際業務中get時候的key,將這個reaction添加進Set中,整個的結構是這樣的:
connectionStore<weakMap>: { // target eg: {num: 1} target: <Map>{ num: (reaction1, reaction2...) } }
註意這裡的reaction,const runningReaction = reactionStack[reactionStack.length – 1] 通過全局變量reactionStack獲取到的。
export function observe (fn, options = {}) { // wrap the passed function in a reaction, if it is not already one const reaction = fn[IS_REACTION] ? fn : function reaction () { return runAsReaction(reaction, fn, this, arguments) } // save the scheduler and debugger on the reaction reaction.scheduler = options.scheduler reaction.debugger = options.debugger // save the fact that this is a reaction reaction[IS_REACTION] = true // run the reaction once if it is not a lazy one if (!options.lazy) { reaction() } return reaction } export function runAsReaction (reaction, fn, context, args) { // do not build reactive relations, if the reaction is unobserved if (reaction.unobserved) { return Reflect.apply(fn, context, args) } // only run the reaction if it is not already in the reaction stack // TODO: improve this to allow explicitly recursive reactions if (reactionStack.indexOf(reaction) === -1) { // release the (obj -> key -> reactions) connections // and reset the cleaner connections releaseReaction(reaction) try { // set the reaction as the currently running one // this is required so that we can create (observable.prop -> reaction) pairs in the get trap reactionStack.push(reaction) return Reflect.apply(fn, context, args) } finally { // always remove the currently running flag from the reaction when it stops execution reactionStack.pop() } } }
在runAsReaction中,會將傳入的reaction(也就是上面的const reaction = function() { runAsReaction(reaction) })執行自己的包裹函數壓入棧中,並且執行fn,這裡的fn即我們想自動響應的函數,執行這個函數自然會觸發get,此時的reactionStack中則會存在這個reaction。這裡註意fn如果裡面有異步代碼的情況,try finally的執行順序是這樣的:
// 執行try的內容, // 如果有return執行return內容,但不會返回,執行finally後返回,這裡面不會阻塞。 function test() { try { console.log(1); const s = () => { console.log(2); return 4; }; return s(); } finally { console.log(3) } } // 1 2 3 4 console.log(test())
所以如果異步代碼阻塞並且先於Getter執行,那麼就不會收集到這個依賴。
模仿
目標實現observable和observe以及衍生出來的Vue中的computed。
借用Vue3的思路,get時的操作稱為track,set時的操作稱為trigger,回調稱為effect。
先來個導圖:
function createObserve(obj) { let handler = { get: function (target, key, receiver) { let result = Reflect.get(target, key, receiver) track(target, key, receiver) return result }, set: function (target, key, value, receiver) { let result = Reflect.set(target, key, value, receiver) trigger(target, key, value, receiver) return result } } let proxyObj = new Proxy(obj, handler) return proxyObj } function observable(obj) { return createObserve(obj) }
這裡我們隻作瞭一層Proxy封裝,像Vue中應該會做一個遞歸的封裝。
區別是隻做一層封裝的話隻能檢測到外層的=操作,內層的如Array.push,或者嵌套的替換等都是無法經過set和get的。
實現track
在track中我們會將當前觸發的effect也就是observe的內容或者其他內容壓入關系鏈中,以便trigger時可以調用到這個effect。
const targetMap = new WeakMap() let activeEffectStack = [] let activeEffect function track(target, key, receiver?) { let depMap = targetMap.get(target) if (!depMap) { targetMap.set(target, (depMap = new Map())) } let dep = depMap.get(key) if (!dep) { depMap.set(key, ( dep = new Set() )) } if (!dep.has(activeEffect)) { dep.add(activeEffect) } }
targetMap是一個weakMap,使用weakMap的好處是當我們observable的對象不存在其他引用的時候會正確的被垃圾回收掉,這一條鏈是我們額外建立的內容,原對象不存在的情況下不應該在繼續存在。
這裡面最終會形成一個:
targetMap = { <Proxy 或者 Object>observeable: <Map>{ <observeable中的某一個key>key: ( observe, observe, observe... ) } }
activeEffectStack和activeEffect是兩個用於數據交換的全局變量,我們在get中會把當前的activeEffect添加到get的key的生成的Set中保存起來,讓set操作可以拿到這個activeEffect然後再次調用,實現響應式。
實現trigger
function trigger(target, key, value, receiver?) { let depMap = targetMap.get(target) if (!depMap) { return } let dep = depMap.get(key) if (!dep) { return } dep.forEach((item) => item && item()) }
trigger這裡按照思路實現一個最小的內容,隻是將get中添加的effect逐個調用。
實現observe
根據導圖,在observe中我們需要將傳入的function壓入activeEffectStack並調用一次function觸發get。
function observe(fn:Function) { const wrapFn = () => { const reaction = () => { try { activeEffect = fn activeEffectStack.push(fn) return fn() } finally { activeEffectStack.pop() activeEffect = activeEffectStack[activeEffectStack.length-1] } } return reaction() } wrapFn() return wrapFn }
function有可能出錯,finally中的代碼保證activeEffectStack中對應的那個會被正確刪除。
測試
let p = observable({num: 0}) let j = observe(() => {console.log("i am observe:", p.num);) let e = observe(() => {console.log("i am observe2:", p.num)}) // i am observe: 1 // i am observe2: 1 p.num++
實現computed
在Vue中一個很有用的東西是計算屬性(computed),它是依賴於其他屬性而生成的新值,會在它依賴的其他值更改時自動更改。
我們在實現瞭ovserve之後computed就實現瞭一大半。
class computedImpl { private _value private _setter private effect constructor(options) { this._value = undefined this._setter = undefined const { get, set } = options this._setter = set this.effect = observe(() => { this._value = get() }) } get value() { return this._value } set value (val) { this._setter && this._setter(val) } } function computed(fnOrOptions) { let options = { get: null, set: null } if (fnOrOptions instanceof Function) { options.get = fnOrOptions } else { const { get, set } = fnOrOptions options.get= get options.set = set } return new computedImpl(options) }
computed有兩種方式,一種是computed(function)這樣會當做get,另外還可以設置setter,setter更多的像是一個回調可以和依賴的其他屬性完全沒有關系。
let p = observable({num: 0}) let j = observe(() => {console.log("i am observe:", p.num); return `i am observe: ${p.num}`}) let e = observe(() => {console.log("i am observe2:", p.num)}) let w = computed(() => { return '我是computed 1:' + p.num }) let v = computed({ get: () => { return 'test computed getter' + p.num }, set: (val) => { p.num = `test computed setter${val}` } }) p.num++ // i am observe: 0 // i am observe2: 0 // i am observe: 1 // i am observe2: 1 // 我是computed 1:1 console.log(w.value) v.value = 3000 console.log(w.value) // i am observe: test computed setter3000 // i am observe2: test computed setter3000 // 我是computed 1:test computed setter3000 w.value = 1000 // 並沒有為w設置setter所以並沒有生效 // 我是computed 1:test computed setter3000 console.log(w.value)
到此這篇關於手摸手教你實現Vue3 Reactivity的文章就介紹到這瞭,更多相關Vue3 Reactivity內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!