手摸手教你實現(xiàn)Vue3 Reactivity
前言
Vue3的響應(yīng)式基于Proxy,對比Vue2中使用的Object.definedProperty的方式,使用Proxy在新增的對象以及數(shù)組的攔截上都有很好的支持。
Vue3的響應(yīng)式是一個獨立的系統(tǒng),可以抽離出來使用,那他到底是如何實現(xiàn)的呢?
都知道有Getter和Setter,那Getter和Setter中分別都進行了哪些主要操作才能實現(xiàn)響應(yīng)式呢?
哼哼,帶著這些問題一起來看看吧,文章會一步一步實現(xiàn)一個完整的響應(yīng)式系統(tǒng)(誤)~。
開始
observer-util這個庫使用了與Vue3同樣的思路編寫,Vue3中的實現(xiàn)更加復(fù)雜,從一個更加純粹的庫開始(我不會承認是因為Vue3中有些未看懂的,不會)。
根據(jù)官網(wǎng)的例子:
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和普通的響應(yīng)式。
observable之后的對象被添加了代理,之后observe中添加的響應(yīng)函數(shù)會在依賴的屬性改變時調(diào)用一次。
小小思考
這里粗略思考是一個訂閱發(fā)布的模型,被observable代理之后的對象建立一個發(fā)布者倉庫,observe這時候會訂閱counter.num,之后訂閱的內(nèi)容改變時便會一一回調(diào)。
偽代碼:
// 添加監(jiān)聽
xxx.addEventListener('counter.num', () => console.log(counter.num))
// 改變內(nèi)容
counter.num++
// 發(fā)送通知
xxx.emit('counter.num', counter.num)
而響應(yīng)式的核心也就是這個,添加監(jiān)聽與發(fā)送通知會經(jīng)由observable和observe自動完成。
代碼實現(xiàn)
經(jīng)由上面的思考,在Getter里我們需要將observe傳過來的回調(diào)添加到訂閱倉庫中。
具體的實現(xiàn)中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)
}
}
這個函數(shù)會獲取出一個reaction(也就是observe傳過來的回調(diào)),并且通過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,根據(jù)key,也就是實際業(yè)務(wù)中g(shù)et時候的key,將這個reaction添加進Set中,整個的結(jié)構(gòu)是這樣的:
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) })執(zhí)行自己的包裹函數(shù)壓入棧中,并且執(zhí)行fn,這里的fn即我們想自動響應(yīng)的函數(shù),執(zhí)行這個函數(shù)自然會觸發(fā)get,此時的reactionStack中則會存在這個reaction。這里注意fn如果里面有異步代碼的情況,try finally的執(zhí)行順序是這樣的:
// 執(zhí)行try的內(nèi)容,
// 如果有return執(zhí)行return內(nèi)容,但不會返回,執(zhí)行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執(zhí)行,那么就不會收集到這個依賴。
模仿
目標實現(xiàn)observable和observe以及衍生出來的Vue中的computed。
借用Vue3的思路,get時的操作稱為track,set時的操作稱為trigger,回調(diào)稱為effect。
先來個導(dǎo)圖:

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中應(yīng)該會做一個遞歸的封裝。
區(qū)別是只做一層封裝的話只能檢測到外層的=操作,內(nèi)層的如Array.push,或者嵌套的替換等都是無法經(jīng)過set和get的。
實現(xiàn)track
在track中我們會將當前觸發(fā)的effect也就是observe的內(nèi)容或者其他內(nèi)容壓入關(guān)系鏈中,以便trigger時可以調(diào)用到這個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的對象不存在其他引用的時候會正確的被垃圾回收掉,這一條鏈是我們額外建立的內(nèi)容,原對象不存在的情況下不應(yīng)該在繼續(xù)存在。
這里面最終會形成一個:
targetMap = {
<Proxy 或者 Object>observeable: <Map>{
<observeable中的某一個key>key: ( observe, observe, observe... )
}
}
activeEffectStack和activeEffect是兩個用于數(shù)據(jù)交換的全局變量,我們在get中會把當前的activeEffect添加到get的key的生成的Set中保存起來,讓set操作可以拿到這個activeEffect然后再次調(diào)用,實現(xiàn)響應(yīng)式。
實現(xiàn)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這里按照思路實現(xiàn)一個最小的內(nèi)容,只是將get中添加的effect逐個調(diào)用。
實現(xiàn)observe
根據(jù)導(dǎo)圖,在observe中我們需要將傳入的function壓入activeEffectStack并調(diào)用一次function觸發(fā)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中對應(yīng)的那個會被正確刪除。
測試
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++
實現(xiàn)computed
在Vue中一個很有用的東西是計算屬性(computed),它是依賴于其他屬性而生成的新值,會在它依賴的其他值更改時自動更改。
我們在實現(xiàn)了ovserve之后computed就實現(xiàn)了一大半。
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,另外還可以設(shè)置setter,setter更多的像是一個回調(diào)可以和依賴的其他屬性完全沒有關(guān)系。
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設(shè)置setter所以并沒有生效
// 我是computed 1:test computed setter3000
console.log(w.value)
到此這篇關(guān)于手摸手教你實現(xiàn)Vue3 Reactivity的文章就介紹到這了,更多相關(guān)Vue3 Reactivity內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue項目保持element組件同行,設(shè)置組件不自動換行問題
這篇文章主要介紹了Vue項目保持element組件同行,設(shè)置組件不自動換行問題。具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02
使用Vue-scroller頁面input框不能觸發(fā)滑動的問題及解決方法
這篇文章主要介紹了使用Vue-scroller頁面input框不能觸發(fā)滑動的問題,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
基于vue-router的matched實現(xiàn)面包屑功能
本文主要介紹了基于vue-router的matched實現(xiàn)面包屑功能,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09

