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