原文地址:The Best Explanation of JavaScript Reactivity
許多前端JavaScript框架(例如Angular,React和Vue)都有自己的Reactivity引擎。通過了解響應式及其工作原理,您可以提高開發技能並更有效地使用JavaScript框架。在視訊和下面的文章中,我們構建了您在Vue原始碼中看到的相同型別的Reactivity。
?響應式系統
當你第一次看到它時,Vue的響應式系統看起來很神奇。拿這個簡單的Vue應用程式來說:
不知何故Vue
只是知道如果price
改變,它應該做三件事:
- 更新網頁上
price
的值。 - 重新計算乘法表示式
price * quantity
,並更新頁面。 - 再次呼叫函式
totalPriceWithTax
並更新頁面。 但是等等,你是否會好奇,Vue如何知道price
更改時所要更新的內容,以及它如何跟蹤所有內容? 這並不是JavaScript通常的工作方式
我們解決一個大問題是通常不會這樣程式設計。例如,如果我執行如下程式碼:
你覺得它列印什麼?由於我們沒有使用Vue,它將列印出來10
。
在Vue中,我們希望total
隨著price
或quantity
更新而更新。我們想要:
不幸的是,JavaScript是程式性的,而不是響應式的,所以這在現實中不起作用。為了實現total
響應,我們必須使用JavaScript來使事情表現得與眾不同。
⚠️ 問題
我們需要儲存如何計算得到total
,這樣可以在price
或quantity
更新時重新執行它,
✅ 解決方案
首先,需要一些方法告訴我們的應用程式,“我即將執行的程式碼,儲存它,我可能需要在其他時間執行它。”然後我們開始執行程式碼,如果price
或quantity
變數得到更新,再次執行儲存的程式碼。
target
變數中儲存了一個匿名函式,然後呼叫一個record
函式。使用ES6箭頭語法我也可以這樣寫:
這個record
函式定義很簡單:
我們正在儲存target
(在我們的例子中{ total = price * quantity }
),所以我們可以稍後執行它,可以通過一個replay
函式來執行儲存的所有內容。
這將遍歷執行storage
陣列中儲存的所有匿名函式。
然後在程式碼中,我們可以:
很簡單吧?如果您需要閱讀並嘗試再次理解它,這裡有完整的程式碼,僅供參考,如果您想知道原因,我會以特定的方式對此進行編碼。
⚠️ 問題
我們可以根據需要繼續記錄target
,但是有一個更強大的解決方案可以擴充套件我們的應用程式。一個負責維護target
列表的類,當需要它們重新執行時,這些target
列表會得到通知。
✅ 解決方案:依賴類
我們解決這個問題的一種方法是將這種行為封裝到它自己的類中,這是一個實現標準觀察者模式的依賴類。
因此,如果我們建立一個JavaScript類來管理我們的依賴項(它更接近Vue的處理方式),它可能看起來像這樣:
請注意,我們現在儲存匿名函式是subscribers
而不是storage
。我們現在呼叫的函式是depend
而不是record
,我們現在使用notify
而不是replay
。為了讓這個執行:
它仍然有效,現在的程式碼感覺更可重用。唯一仍然感覺有點奇怪的是設定和執行target
。
⚠️ 問題
我們將為每個變數設定一個Dep
類,並且很好地封裝了建立需要監視更新的匿名函式的行為。也許一個watcher
函式可能是為了處理這種行為。
所以不要這樣呼叫:
(這只是上面的程式碼) 我們可以改為:✅ 解決方案:觀察者函式
在我們的Watcher
函式中,我們可以做一些簡單的事情:
watcher
函式接受一個myFunc
引數,將其設定為全域性target
屬性,呼叫dep.depend()
將target
新增到訂閱者subscriber
,執行target
函式,然後重置target
函式。
現在,當我們執行以下內容時:
您可能想知道為什麼我們將target設為全域性變數,而不是將其傳遞到我們需要的函式中。這將在我們的文章結尾處解釋。⚠️ 問題
我們有一個單獨的Dep class
,但我們真正想要的是每個變數都有自己的Dep
。在繼續之前,將資料設為物件屬性。
price
和quantity
)都有自己的內部Dep
類。
現在我們執行時:
由於訪問了data.price
的值,我希望price
屬性的Dep
類將儲存在target
的匿名函式推送到其subscriber
陣列(通過呼叫dep.depend()
)。由於data.quantity
被訪問,我還希望quantity
屬性Dep
類將儲存在target
的匿名函式推送到其subscriber
陣列中。
如果我有另一個匿名函式,只是data.price
被訪問,我希望它只是推送到price
屬性Dep
類。
price
的 subscribers
什麼時候呼叫dep.notify()
?我希望在price
設定時呼叫它們。在文章的最後,我希望能夠進入控制檯並執行:
我們需要一些方式來掛鉤資料屬性(如price
或quantity
),所以當它被訪問時,我們可以儲存target
到我們的 subscribers
陣列中,當它被更改時,執行儲存在 subscribers
陣列中的函式。
✅ 解決方案:Object.defineProperty()
我們需要了解Object.defineProperty()函式,它是簡單的ES5 JavaScript。它允許我們為屬性定義getter
和setter
函式。在我向您展示如何在Dep
類中使用它之前,將向您展示最基本的用法。
如您所見,它只記錄兩行。但是,它實際上沒有get
或set
任何值,因為我們過度使用了該功能。我們現在加回來吧。get()
期望返回一個值,set()
仍然需要更新一個值,所以讓我們新增一個internalValue
變數來儲存我們當前的price
值。
既然我們的get
和set
工作正常,您認為將列印到控制檯的是什麼?
get
並set
值時,我們可以獲得通知。通過一些遞迴,我們可以為資料陣列中的所有項執行它,對吧?
Object.keys(data)
返回物件鍵的陣列。
現在一切都有getter
和setter
,我們在控制檯上看到了這一點。
? 將兩種想法放在一起
當像這樣的一段程式碼執行並getprice
值時,我們想要price
記住這個匿名函式(target)
。這樣,如果price
被更改,或者set為新值,它將觸發此函式以重新執行,因為它知道此行依賴於它。
Get =>記住當前匿名函式,當我們的值發生變化時,會再次執行它。
Set =>執行儲存的匿名函式,我們的值隨之改變。
或者就我們的Dep Class
而言
Price accessed (get) =>呼叫dep.depend()
以儲存當前target
Price set =>呼叫dep.notify()
給price
,重新執行全部targets
讓我們結合這兩個想法,並完成我們的最終程式碼。
現在看看我們執行時會發生什麼。
正是我們所希望的!price
和quantity
確實都響應了!每當值price
或quantity
更新時,全部的程式碼都會重新執行。
Vue文件中的這個插圖現在應該開始有意義了。
你看到那個美麗的紫色資料圈getters and setters
了嗎?看起來應該很熟悉!每個元件例項都有一個watcher
例項(藍色),它從getter
(紅線)收集依賴項。稍後呼叫setter
時,它會通知watcher
導致元件重新渲染。註釋之後的圖如下。
是的,這現在不是更有意義嗎?
顯然,Vue如何做到這一點更復雜,但你現在知道了基礎知識。
⏪ 那麼我們學到了什麼?
- 如何建立一個
Dep
類來收集依賴項(depend
)並重新執行所有依賴項(notify
)。 - 如何建立一個
Watcher
程式來管理正在執行的程式碼,這些程式碼可能需要作為依賴項(target
)被新增。 - 如何使用
Object.defineProperty()
建立getter
和setter
。