您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何理解從觀察者模式到響應式的設計原理”,在日常操作中,相信很多人在如何理解從觀察者模式到響應式的設計原理問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何理解從觀察者模式到響應式的設計原理”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
一、觀察者模式
觀察者模式,它定義了一種 一對多 的關系,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知所有的觀察者對象,使得它們能夠自動更新自己。在觀察者模式中有兩個主要角色:Subject(主題)和 Observer(觀察者)。
由于觀察者模式支持簡單的廣播通信,當消息更新時,會自動通知所有的觀察者。下面我們來看一下如何使用 TypeScript 來實現觀察者模式:
1.1 定義 ConcreteObserver
interface Observer { notify: Function; } class ConcreteObserver implements Observer{ constructor(private name: string) {} notify() { console.log(`${this.name} has been notified.`); } }
1.2 定義 Subject 類
class Subject { private observers: Observer[] = []; public addObserver(observer: Observer): void { this.observers.push(observer); } public notifyObservers(): void { console.log("notify all the observers"); this.observers.forEach(observer => observer.notify()); } }
1.3 使用示例
// ① 創建主題對象 const subject: Subject = new Subject(); // ② 添加觀察者 const observerA = new ConcreteObserver("ObserverA"); const observerC = new ConcreteObserver("ObserverC"); subject.addObserver(observerA); subject.addObserver(observerC); // ③ 通知所有觀察者 subject.notifyObservers();
對于以上的示例來說,主要包含三個步驟:① 創建主題對象、② 添加觀察者、③ 通知觀察者。上述代碼成功運行后,控制臺會輸出以下結果:
notify all the observers ObserverA has been notified. ObserverC has been notified.
在前端大多數場景中,我們所觀察的目標是數據,當數據發生變化的時候,頁面能實現自動的更新,對應的效果如下圖所示:
要實現自動更新,我們需要滿足兩個條件:一個是能實現精準地更新,另一個是能檢測到數據的異動。要能實現精準地更新就需要收集對該數據異動感興趣的更新函數(觀察者),在完成收集之后,當檢測到數據異動,就可以通知對應的更新函數。
上面的描述看起來比較繞,其實要實現自動更新,我們就是要讓 ① 創建主題對象、② 添加觀察者、③ 通知觀察者 這三個步驟實現自動化,這就是實現響應式的核心思路。接下來,我們來舉一個具體的示例:
相信熟悉 Vue2 響應式原理的小伙伴,對上圖中的代碼都不會陌生,其中第二步驟也被稱為收集依賴。通過使用 Object.defineProperty API,我們可以攔截對數據的讀取和修改操作。
若在函數體中對某個數據進行讀取,則表示此函數對該數據的異動感興趣。當進行數據讀取時,就會觸發已定義的 getter 函數,這時就可以把數據的觀察者存儲起來。而當數據發生異動的時候,我們就可以通知觀察者列表中的所有觀察者,從而執行相應的更新操作。
Vue3 使用了 Proxy API 來實現響應式,Proxy API 相比 Object.defineProperty API 有哪些優點呢?這里阿寶哥不打算展開介紹了,后面打算寫一篇專門的文章來介紹 Proxy API。下面阿寶哥將開始介紹本文的主角 —— observer-util:
Transparent reactivity with 100% language coverage. Made with ?? and ES6 Proxies.
https://github.com/nx-js/observer-util
該庫內部也是利用了 ES6 的 Proxy API 來實現響應式,在介紹它的工作原理前,我們先來看一下如何使用它。
二、observer-util 簡介
observer-util 這個庫使用起來也很簡單,利用該庫提供的 observable 和 observe 函數,我們就可以方便地實現數據的響應式。下面我們先來舉個簡單的例子:
2.1 已知屬性
import { observable, observe } from '@nx-js/observer-util'; const counter = observable({ num: 0 }); const countLogger = observe(() => console.log(counter.num)); // 輸出 0 counter.num++; // 輸出 1
在以上代碼中,我們從 @nx-js/observer-util 模塊中分別導入 observable 和 observe 函數。其中 observable 函數用于創建可觀察的對象,而 observe 函數用于注冊觀察者函數。以上的代碼成功執行后,控制臺會依次輸出 0 和 1。除了已知屬性外,observer-util 也支持動態屬性。
2.2 動態屬性
import { observable, observe } from '@nx-js/observer-util'; const profile = observable(); observe(() => console.log(profile.name)); profile.name = 'abao'; // 輸出 'abao'
以上的代碼成功執行后,控制臺會依次輸出 undefined 和 abao。observer-util 除了支持普通對象之外,它還支持數組和 ES6 中的集合,比如 Map、Set 等。這里我們以常用的數組為例,來看一下如何讓數組對象變成響應式對象。
2.3 數組
import { observable, observe } from '@nx-js/observer-util'; const users = observable([]); observe(() => console.log(users.join(', '))); users.push('abao'); // 輸出 'abao' users.push('kakuqo'); // 輸出 'abao, kakuqo' users.pop(); // 輸出 'abao,'
這里阿寶哥只介紹了幾個簡單的示例,對 observer-util 其他使用示例感興趣的小伙伴,可以閱讀該項目的 README.md 文檔。接下來,阿寶哥將以最簡單的例子為例,來分析一下 observer-util 這個庫響應式的實現原理。
如果你想在本地運行以上示例的話,可以先修改 debug/index.js 目錄下的 index.js文件,然后在根目錄下執行 npm run debug 命令。
三、observer-util 原理解
析首先,我們再來回顧一下最早的那個例子:
import { observable, observe } from '@nx-js/observer-util'; const counter = observable({ num: 0 }); // A const countLogger = observe(() => console.log(counter.num)); // B counter.num++; // C
在第 A 行中,我們通過 observable 函數創建了可觀察的 counter 對象,該對象的內部結構如下:
通過觀察上圖可知,counter 變量所指向的是一個 Proxy 對象,該對象含有 3 個 Internal slots。那么 observable 函數是如何將我們的 { num: 0 } 對象轉換成 Proxy 對象呢?在項目的 src/observable.js 文件中,我們找到了該函數的定義:
// src/observable.js export function observable (obj = {}) { // 如果obj已經是一個observable對象或者不應該被包裝,則直接返回它 if (proxyToRaw.has(obj) || !builtIns.shouldInstrument(obj)) { return obj } // 如果obj已經有一個對應的observable對象,則將其返回。否則創建一個新的observable對象 return rawToProxy.get(obj) || createObservable(obj) }
在以上代碼中出現了 proxyToRaw 和 rawToProxy 兩個對象,它們被定義在 src/internals.js 文件中:
// src/internals.js export const proxyToRaw = new WeakMap() export const rawToProxy = new WeakMap()
這兩個對象分別存儲了 proxy => raw 和 raw => proxy 之間的映射關系,其中 raw 表示原始對象,proxy 表示包裝后的 Proxy 對象。很明顯首次執行時,proxyToRaw.has(obj) 和 rawToProxy.get(obj) 分別會返回 false 和 undefined,所以會執行 || 運算符右側的邏輯。
下面我們來分析一下 shouldInstrument 函數,該函數的定義如下:
// src/builtIns/index.js export function shouldInstrument ({ constructor }) { const isBuiltIn = typeof constructor === 'function' && constructor.name in globalObj && globalObj[constructor.name] === constructor return !isBuiltIn || handlers.has(constructor) }
在 shouldInstrument 函數內部,會使用參數 obj 的構造函數判斷其是否為內置對象,對于 { num: 0 } 對象來說,它的構造函數是 ƒ Object() { [native code] },因此 isBuiltIn 的值為 true,所以會繼續執行 || 運算符右側的邏輯。其中 handlers 對象是一個 Map 對象:
// src/builtIns/index.js const handlers = new Map([ [Map, collectionHandlers], [Set, collectionHandlers], [WeakMap, collectionHandlers], [WeakSet, collectionHandlers], [Object, false], [Array, false], [Int8Array, false], [Uint8Array, false], // 省略部分代碼 [Float64Array, false] ])
看完 handlers 的結構,很明顯 !builtIns.shouldInstrument(obj) 表達式的結果為 false。所以接下來,我們的焦點就是 createObservable 函數:
function createObservable (obj) { const handlers = builtIns.getHandlers(obj) || baseHandlers const observable = new Proxy(obj, handlers) // 保存raw => proxy,proxy => raw 之間的映射關系 rawToProxy.set(obj, observable) proxyToRaw.set(observable, obj) storeObservable(obj) return observable }
通過觀察以上代碼,我們就知道了為什么調用 observable({ num: 0 }) 函數之后,返回的是一個 Proxy 對象。對于 Proxy 的構造函數來說,它支持兩個參數:
const p = new Proxy(target, handler)
target:要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理);
handler:一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p 的行為。
示例中的 target 指向的就是 { num: 0 } 對象,而 handlers 的值會根據 obj 的類型而返回不同的 handlers:
// src/builtIns/index.js export function getHandlers (obj) { return handlers.get(obj.constructor) // [Object, false], }
而 baseHandlers 是一個包含了 get、has 和 set 等 “陷阱“ 的對象:
export default { get, has, ownKeys, set, deleteProperty }
在創建完 observable 對象之后,會保存 raw => proxy,proxy => raw 之間的映射關系,然后再調用 storeObservable 函數執行存儲操作,storeObservable 函數被定義在 src/store.js 文件中:
// src/store.js const connectionStore = new WeakMap() export function storeObservable (obj) { // 用于后續保存obj.key -> reaction之間映射關系 connectionStore.set(obj, new Map()) }
介紹了那么多,阿寶哥用一張圖來總結一下前面的內容:
至于 proxyToRaw 和 rawToProxy 對象有什么用呢?相信看完以下代碼,你就會知道答案。
// src/observable.js export function observable (obj = {}) { // 如果obj已經是一個observable對象或者不應該被包裝,則直接返回它 if (proxyToRaw.has(obj) || !builtIns.shouldInstrument(obj)) { return obj } // 如果obj已經有一個對應的observable對象,則將其返回。否則創建一個新的observable對象 return rawToProxy.get(obj) || createObservable(obj) }
下面我們來開始分析第 B 行:
const countLogger = observe(() => console.log(counter.num)); // B
observe 函數被定義在 src/observer.js 文件中,其具體定義如下:
// src/observer.js export function observe (fn, options = {}) { // const IS_REACTION = Symbol('is reaction') const reaction = fn[IS_REACTION] ? fn : function reaction () { return runAsReaction(reaction, fn, this, arguments) } // 省略部分代碼 reaction[IS_REACTION] = true // 如果非lazy,則直接運行 if (!options.lazy) { reaction() } return reaction }
在上面代碼中,會先判斷傳入的 fn 是不是 reaction 函數,如果是的話,直接使用它。如果不是的話,會把傳入的 fn 包裝成 reaction 函數,然后再調用該函數。在 reaction 函數內部,會調用另一個函數 —— runAsReaction,顧名思義該函數用于運行 reaction 函數。
runAsReaction 函數被定義在 src/reactionRunner.js 文件中:
// src/reactionRunner.js const reactionStack = [] export function runAsReaction (reaction, fn, context, args) { // 省略部分代碼 if (reactionStack.indexOf(reaction) === -1) { // 釋放(obj -> key -> reactions) 鏈接并復位清理器鏈接 releaseReaction(reaction) try { // 壓入到reactionStack堆棧中,以便于在get陷阱中能建立(observable.prop -> reaction)之間的聯系 reactionStack.push(reaction) return Reflect.apply(fn, context, args) } finally { // 從reactionStack堆棧中,移除已執行的reaction函數 reactionStack.pop() } } }
在 runAsReaction 函數體中,會把當前正在執行的 reaction 函數壓入 reactionStack棧中,然后使用 Reflect.apply API 調用傳入的 fn 函數。當 fn 函數執行時,就是執行 console.log(counter.num) 語句,在該語句內,會訪問 counter 對象的 num 屬性。counter 對象是一個 Proxy 對象,當訪問該對象的屬性時,會觸發 baseHandlers 中 get 陷阱:
// src/handlers.js function get (target, key, receiver) { const result = Reflect.get(target, key, receiver) // 注冊并保存(observable.prop -> runningReaction) registerRunningReactionForOperation({ target, key, receiver, type: 'get' }) const observableResult = rawToProxy.get(result) if (hasRunningReaction() && typeof result === 'object' && result !== null) { // 省略部分代碼 } return observableResult || result }
在以上的函數中,registerRunningReactionForOperation 函數用于保存 observable.prop -> runningReaction 之間的映射關系。其實就是為對象的指定屬性,添加對應的觀察者,這是很關鍵的一步。所以我們來重點分析 registerRunningReactionForOperation 函數:
// src/reactionRunner.js export function registerRunningReactionForOperation (operation) { // 從棧頂獲取當前正在執行的reaction const runningReaction = reactionStack[reactionStack.length - 1] if (runningReaction) { debugOperation(runningReaction, operation) registerReactionForOperation(runningReaction, operation) } }
在 registerRunningReactionForOperation 函數中,首先會從 reactionStack 堆棧中獲取正在運行的 reaction 函數,然后再次調用 registerReactionForOperation 函數為當前的操作注冊 reaction 函數,具體的處理邏輯如下所示:
// src/store.js export function registerReactionForOperation (reaction, { target, key, type }) { // 省略部分代碼 const reactionsForObj = connectionStore.get(target) // A let reactionsForKey = reactionsForObj.get(key) // B if (!reactionsForKey) { // C reactionsForKey = new Set() reactionsForObj.set(key, reactionsForKey) } if (!reactionsForKey.has(reaction)) { // D reactionsForKey.add(reaction) reaction.cleaners.push(reactionsForKey) } }
在調用 observable(obj) 函數創建可觀察對象時,會為以 obj 對象為 key,保存在 connectionStore (connectionStore.set(obj, new Map()) )對象中。
阿寶哥把 registerReactionForOperation 函數內部的處理邏輯分為 4 個部分:
(A):從 connectionStore (WeakMap)對象中獲取 target 對應的值,會返回一個 reactionsForObj(Map)對象;
(B):從 reactionsForKey (Map)對象中獲取 key(對象屬性)對應的值,如果不存在的話,會返回 undefined;
(C):如果 reactionsForKey 為 undefined,則會創建一個 Set 對象,并把該對象作為 value,保存在 reactionsForObj(Map)對象中;
(D):判斷 reactionsForKey(Set)集合中是否含有當前的 reaction 函數,如果不存在的話,把當前的 reaction 函數添加到 reactionsForKey(Set)集合中。
為了讓大家能夠更好地理解該部分的內容,阿寶哥繼續通過畫圖來總結上述的內容:
因為對象中的每個屬性都可以關聯多個 reaction 函數,為了避免出現重復,我們使用 Set 對象來存儲每個屬性所關聯的 reaction 函數。而一個對象又可以包含多個屬性,所以 observer-util 內部使用了 Map 對象來存儲每個屬性與 reaction 函數之間的關聯關系。
此外,為了支持能把多個對象變成 observable 對象并在原始對象被銷毀時能及時地回收內存, observer-util 定義了 WeakMap 類型的 connectionStore 對象來存儲對象的鏈接關系。對于當前的示例,connectionStore 對象的內部結構如下所示:
最后,我們來分析 counter.num++; 這行代碼。簡單起見,阿寶哥只分析核心的處理邏輯,對完整代碼感興趣的小伙伴,可以閱讀該項目的源碼。當執行 counter.num++; 這行代碼時,會觸發已設置的 set 陷阱:
// src/handlers.js function set (target, key, value, receiver) { // 省略部分代碼 const hadKey = hasOwnProperty.call(target, key) const oldValue = target[key] const result = Reflect.set(target, key, value, receiver) if (!hadKey) { queueReactionsForOperation({ target, key, value, receiver, type: 'add' }) } else if (value !== oldValue) { queueReactionsForOperation({ target, key, value, oldValue, receiver, type: 'set' }) } return result }
對于我們的示例,將會調用 queueReactionsForOperation 函數:
// src/reactionRunner.js export function queueReactionsForOperation (operation) { // iterate and queue every reaction, which is triggered by obj.key mutation getReactionsForOperation(operation).forEach(queueReaction, operation) }
在 queueReactionsForOperation 函數內部會繼續調用 getReactionsForOperation 函數獲取當前 key 對應的 reactions:
// src/store.js export function getReactionsForOperation ({ target, key, type }) { const reactionsForTarget = connectionStore.get(target) const reactionsForKey = new Set() if (type === 'clear') { reactionsForTarget.forEach((_, key) => { addReactionsForKey(reactionsForKey, reactionsForTarget, key) }) } else { addReactionsForKey(reactionsForKey, reactionsForTarget, key) } // 省略部分代碼 return reactionsForKey }
在成功獲取當前 key 對應的 reactions 對象之后,會遍歷該對象執行每個 reaction,具體的處理邏輯被定義在 queueReaction 函數中:
// src/reactionRunner.js function queueReaction (reaction) { debugOperation(reaction, this) // queue the reaction for later execution or run it immediately if (typeof reaction.scheduler === 'function') { reaction.scheduler(reaction) } else if (typeof reaction.scheduler === 'object') { reaction.scheduler.add(reaction) } else { reaction() } }
因為我們的示例并沒有配置 scheduler 參數,所以就會直接執行 else 分支的代碼,即執行 reaction() 該語句。
好的,observer-util 這個庫內部如何把普通對象轉換為可觀察對象的核心邏輯已經分析完了。對于普通對象來說,observer-util 內部通過 Proxy API 提供 get 和 set 陷阱,實現自動添加觀察者(添加 reaction 函數)和通知觀察者(執行 reaction 函數)的處理邏輯。
如果你看完本文所介紹的內容,應該就可以理解 Vue3 中 reactivity 模塊內 targetMap 的相關定義:
// vue-next/packages/reactivity/src/effect.ts type Dep = Set<ReactiveEffect> type KeyToDepMap = Map<any, Dep> const targetMap = new WeakMap<any, KeyToDepMap>()
除了普通對象和數組之外,observer-util 還支持 ES6 中的集合,比如 Map、Set 和 WeakMap 等。當處理這些對象時,在創建 Proxy 對象時,會使用 collectionHandlers對象,而不是 baseHandlers 對象。
到此,關于“如何理解從觀察者模式到響應式的設計原理”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。