[譯] 以 Vue 為例,解釋 JavaScript 的反應性
作者|Gregg Pollack
譯者|張衛濱
很多前端 JavaScript 框架(如 Angular、React 和 Vue)都有自己的反應性(Reactivity)引擎。理解反應式是什麼以及如何執行能夠提升你的開發水平,同時能夠更高效地使用 JavaScript。在本文中,我們構建了與 Vue 原始碼相同的反應性功能。
反應性系統
當你第一次見到 Vue 的反應性系統時,你可能會感覺有些神奇。以下面這個簡單的 Vue 應用為例:
不知道基於什麼原因,Vue 能夠知道 price
的值是否發生了變化,並且在變化的時候能夠完成如下三件事情:
- 更新 Web 頁面
price
的值; - 重新計算乘法表示式
price * quantity
,並更新頁面; - 再次呼叫
totalPriceWithTax
函式並更新頁面。
但是,稍等,我似乎聽到你想問,Vue 如何知道在 price
發生變化的時候都要更新哪些值,它又是如何跟蹤所有的內容的呢?
這並不是 JavaScript 程式設計通常的執行方式。
如果這對你來說不那麼直觀,那麼我們需要明白程式通常並不是按照這種方式來執行的。例如,如果我執行下面的樣例程式碼:
你猜將會列印出什麼內容呢?因為我們沒有使用 Vue,它將會列印出 10
:
在 Vue 中,我們想要在 price
或 quantity
更新的時候, total
也進行更新。我們希望的輸出是:
但令人遺憾的是,JavaScript 是過程性的,並不是反應式的,所以在現實程式碼並這並不可行。為了讓 total
具有反應性,我們必須讓 JavaScript 語言按照不同的方式來執行。
問題
我們需要記住如何計算 total
,這樣才能在 price
或 quantity
發生變化的時候重新執行。
解決方案
首先,我們需要有某種方法告訴我們的應用,“我將要執行的程式碼是什麼, 將它儲存起來 ,在稍後某個時間點我可能需要你執行它”。然後,我們執行程式碼,在 price
或 quantity
變數發生變化的時候,再次執行儲存的程式碼:
我們想到的辦法可能就是將函式的內容記錄下來,這樣就能再次執行了:
需要注意,我們在 target
變數中儲存了一個匿名函式,然後呼叫了 record
函式。如果採用 ES6 的箭頭語法的話,我還可以寫成如下的形式:
record
的定義非常簡單:
我們將 target
儲存了起來(我們的示例中也就是 { total = price * quantity }
),這樣的話,我們就能在隨後執行它,可能會藉助一個 replay
函式執行我們記錄下來的所有內容。
這樣會遍歷我們在 storage
陣列中儲存的所有匿名函式,並執行它們。
那麼在我們的程式碼中,只需:
非常簡單,對吧?如果你想要通讀程式碼並再次嘗試的話,下面給出了完整的程式碼。
問題
我們可以按需繼續記錄 target,但是更好的方式是有一種健壯的方案,能夠擴充套件我們的應用。我們可以使用一個類,讓這個類維護一個 target 的列表,當需要它們重新執行的時候,這個類會得到通知。
解決方案:依賴類
要解決這個問題,我們將這些行為封裝到單獨的類中,使用一個依賴類( Dependency Class )來實現標準的觀察者模式程式設計。
如果我們建立 JavaScript 類來管理依賴的話(類似於 Vue 的處理方式),它看起來可能會如下所示:
需要注意,我們這裡不再使用 storage
,而是使用 subscribers
來儲存匿名函式,也不再使用 record
函式了,而是呼叫 depend
,同時使用 notify
代替了 replay
。要讓它執行起來,只需:
它依然可以執行,而且我們的程式碼看上去具備了一定的可重用性。唯一感覺尤其詭異的地方就是設定和執行 target
。
問題
未來,每個類都會有一個 Dep 類,如果能將建立匿名函式觀察更新的行為封裝起來就更好了。接下來, watcher
函式將會出場來負責這種行為。
所以,我們將不會再呼叫:
(這就是上面示例的程式碼)
相反,我們只需這樣呼叫:
解決方案:Watcher 函式
在 Watcher 函式中,我們可以做幾件很簡單的事情:
可以看到, watcher
函式接受一個 myFunc
變數,將其作為我們的全域性 target
屬性,呼叫 dep.depend()
,將會以訂閱者的形式新增我們的 target,呼叫 target
函式並重置 target
。
現在,我們可以執行下面的程式碼:
你可能會想,我們為什麼要將 target
實現為全域性變數,而不是將其傳遞給所需的函式。這裡有一定的原因,在本文結束的時候,相信你就明白了。
問題
我們現在有了一個 Dep
類,但是我們真正想要實現的是每個變數都有自己的 Dep。在進行下一步講解之前,我們先將它們放到屬性中。
我們先假設每個屬性( price
和 quantity
)都有其自己的內部 Dep 類。
現在,當我們執行:
因為訪問到了 data.price
的值,所以我希望 price
屬性的 Dep 類要將我們的匿名函式(儲存在 target
中)放到它的訂閱陣列中(通過呼叫 dep.depend()
)。因為 data.quantity
也被訪問到了,所以我希望 quantity
屬性的 Dep 類要將該匿名函式(儲存在 target
中)放到它的訂閱陣列中:
如果我還有其他的匿名函式只訪問 data.price
的話,我希望要將這個函式放到 price
屬性的 Dep 類中。
那麼,我該在何時為 price
的訂閱者呼叫 dep.notify()
呢?答案是為 price
賦值的時候。在本文結束的時候,我希望能夠在命令列中實現如下的效果:
我們希望能有某種方式嵌入到資料屬性中( price
或 quantity
),這樣的話,當屬性被訪問的時候,能夠將 target
儲存到訂閱者陣列中,當屬性變更時,能夠執行儲存在訂閱者陣列中的函式。
解決方案:Object.defineProperty()
我們需要學習 ES 5 JavaScript 所提供 Object.defineProperty() 函式。它允許我們為屬性定義 getter 和 setter 函式。在展示如何與 Dep 類協作之前,我們看一下它的基礎用法。
可以看到,這裡只是列印了兩條日誌。但是,它並沒有實際 get
和 set
值,這是因為我們將功能覆蓋掉了。現在,我們將功能新增回來。 get()
預期要返回一個值,而 set()
依然要更新值,所以我們新增一個 internalValue
變數來儲存當前的 price
值。
我們的 get
和 set
都能正常執行了,你覺得控制檯的列印資訊會是什麼呢?
所以,當取值和設定值的時候,我們有了一種得到提醒的方法。藉助一些遞迴,我們就可以將其用到資料陣列的所有條目中了。
值得一提的是, Object.keys(data)
能夠返回物件中 key 所組成的陣列。
現在,所有的屬性都有 getter 和 setter 了,我們來看一下控制檯:
將這兩個理念組合在一起
當這樣的程式碼執行並嘗試 get price
屬性的值時,我們希望 price
能夠記住這個匿名函式( target
)。通過這種方式,如果 price
發生了變化,或者被 set
了一個新的值,這個函式就能重新執行,因為它能夠知道這行程式碼依賴該屬性。所以,你可以按照如下的方式來思考。
Get=>記住該匿名函式,當值發生變化的時候我們會重新執行。
Set=>執行儲存的匿名函式,我們的值就會發生變化。
或者,在 Dep Class 的場景下:
Price 訪問 (get)=>呼叫 dep.depend()
儲存當前的 target
;
Price set=>呼叫 price 的 dep.notify()
,重新執行所有的 target
。
接下來,我們將這兩個理念組合起來,並看一下最終的程式碼。
在我們執行的時候,看一下控制檯的輸出:
完全符合我們的預期!現在 price
和 quantity
都是反應式的了。當 price
或 quantity
的值更新時,我們的程式碼完全重新執行了。
Vue 文件中的圖示對你來說應該就非常清晰了。
看到漂亮的 Data 圓圈中的 getters 和 setters 了嗎?它看起來似曾相識!每個元件例項都有一個 watcher
(藍色圓圈),它會從 getter 中收集依賴(紅線)。當 setter 隨後被呼叫時,它會 通知 watcher,從而會導致元件的重新渲染。如下的圖片新增了一些我自己的註釋。
現在,是不是感覺一目瞭然了呢?
當然,Vue 底層的處理要更復雜,但是你現在已經掌握了它的基礎。
我們學到了什麼呢?
- 如何建立 Dep 來收集依賴(depend)並重新執行所有的依賴(notify);
- 如何建立 watcher 來管理我們正在執行的程式碼,這些程式碼可能需要作為依賴新增進來(target);
- 如何使用 Object.defineProperty() 來建立 getter 和 setter。
相關文章
- 以 Toast 為例講解 Vue 元件的概念ASTVue元件
- JavaScript函式的反應性JavaScript函式
- [譯] 以申請大學流程來解釋 JavaScript 的 filter 方法JavaScriptFilter
- [譯]JavaScript響應式的最佳解釋JavaScript
- 以vue-cli為例,瞭解webpack的hash、chunkhash、contenthashVueWeb
- 【譯】MongoDb vs Mysql—以NodeJs為例MongoDBMySqlNodeJS
- C++建造者模式(以英雄屬性為例)C++模式
- 反應性和非反應性程式碼的分離 - DZone
- 【詳解】以 ASP.NET Core 為例的CI/CDASP.NET
- 前端優化反應:虛擬dom解釋前端優化
- 以 Golang 為例詳解 AST 抽象語法樹GolangAST抽象語法樹
- RabbitMQ的使用--以topic路由為例MQ路由
- 一個富文字編輯器quill 以Vue專案為例UIVue
- 「ArrayBuffer」應用-以自動調整照片方向為例
- 解釋Vue深入響應式原理Vue
- JavaScript 例項屬性JavaScript
- rita:利用 NATS 實現以事件為中心和反應模式的工具包事件模式
- JAVA中動態性例項解釋 (轉)Java
- ffmpeg filter命令解讀--以多路視訊拼接為例Filter
- 以美顏sdk為例,詳解sdk接入流程
- vue例項的屬性和方法Vue
- 建立索引的原則-以innodb為例索引
- [JavaScript] {}解釋為語句塊,物件的toStringJavaScript物件
- 以畫素風遊戲為例,分析戲劇性畫面的營造遊戲
- statsmodels中的summary解讀(以linear regression模型為例)模型
- 安卓反編譯詳解安卓編譯
- 以《GTA》為例:為什麼遊戲需要平衡畫面真實度和互動性的比例?遊戲
- 以opencv為例說明cmake中的findpackage()OpenCVPackage
- vue例項中watch屬性的使用Vue
- 瞭解JavaScript中的Memoization以提高效能,再看React的應用JavaScriptReact
- lisp 裡的 ,@ 反引號 的解釋Lisp
- javascript的cssText屬性程式碼例項JavaScriptCSS
- 以QT為例談環境搭建QT
- 模擬登陸——以github為例Github
- 解決程式(因為數字的問題)沒反應的方法
- 有符號浮點運算的基本步驟:以雙線性插值為例符號
- 為什麼量子計算如此難以解釋? - quantamagazine
- 【Vue全解0】Vue例項Vue