💡本篇博文可能學到的知識點html
現代前端開發必不可少會用到的 Vue、React 等框架,這些框架的共同之處在於都提供了響應式(Reactive)和組件化(Composable)的視圖組件,組件化開發從新定義了前端開發技術棧。結合前端構建工具以及基於框架出現的各類通過精心設計的UI組件庫,讓前端也進入到了一個工程化的時代。構建頁面變得從未有過的簡潔高效。前端
若是你是一名經驗豐富(nian ling da)的程序員,或多或少都會接觸到沒有這些框架以前的狀態,那時候咱們還手持 jQuery 利器,操縱着一手好 dom,當你初次接觸到響應式框架的時候或許會被它的好用所驚豔到。咱們只須要改變數據,dom就更新了。本篇博文主要是來討論被驚豔到的響應式框架是如何實現的。咱們首先來看看 Vue 是如何實現響應式系統的?vue
假設咱們購物車中有一個商品,單價 price
,數量 quantity
,總價爲 total
。react
有這樣一個簡單的功能,點擊按鈕讓 price
發生變化,那麼 total
爲計算屬性,也隨之發生變化。程序員
<!-- App.vue --> <div id="app"> <div>Price: ¥{{ price }}</div> <div>Quantity: {{ quantity }}</div> <div>Total: ¥{{ total }}</div> <div> <button @click="price = 10"> change price </button> </div> </div>
// main.js new Vue({ el: '#app', data: { price: 6.00, quantity: 2 }, computed: { total () { return this.price * this.quantity } } })
連接描述設計模式
考慮一下 Vue是如何實現這樣的功能的,其實咱們是更改了數據,而依賴於它的計算屬性也發生了變化。數組
在官方文檔,深刻響應式原理裏有講到app
當你把一個普通的 JavaScript 對象傳入 Vue 實例作爲 data
選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty
把這些屬性所有轉爲 getter/setter。在內部它們讓 Vue 可以追蹤依賴,在屬性被訪問和修改時通知變動。框架
每一個組件實例都對應一個 watcher 實例,它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染。dom
受現代 JavaScript 的限制
對於已建立的實例,Vue 沒法檢測到對象屬性的添加和刪除。Vue 容許使用 Vue.set(object, propertyName, value)
的方法進行動態添加響應式屬性,或者使用別名寫成 this.$set(object, propertyName, value)
對於數組,Vue 沒法檢測如下數組的變更:
vm.items[indexOfItem] = newValue
vm.items.length = newLength
第一條的解決方式一樣是使用 this.$set(vm.items, indexOfItem, newValue)
第二條的解決方式是用 vm.items.splice(indexOfItem, 1, newValue)
對於以上的弊端,其實官方已經給出瞭解決方法,就是使用 Proxy
代替 Object.defineProperty
API,這個改動會伴隨着萬衆期待的 Vue 3.0 的發佈而應用。你們都知道 Vue 2.x 也就是如今的版本,響應式是經過 Object.defineProperty
API來實現的,那麼既然知道了解決方法,咱們不妨提早學習一下 Proxy
是如何作到響應式的。
Proxy 對象用於定義基本操做的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)。
let p = new Proxy(target, hanlder)
參數:
target
用 Proxy 包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。
handler
一個對象,其屬性是當執行一個操做時定義代理的行爲的函數。
MDN 上的基礎示例:
let handler = { get: function(target, name){ return name in target ? target[name] : 37; } }; let p = new Proxy({}, handler); p.a = 1; p.b = undefined; console.log(p.a, p.b); // 1, undefined console.log('c' in p, p.c); // false, 37
上述例子使用了 get
,當對象中不存在屬性名時,缺省返回數爲37。咱們知道了對於操做對象,能夠在使用 handler
處理一些事務。關於 handler
則有十三種。
從新回到咱們的小例子,咱們有一些變量,單價 price
,數量 quantity
,總價爲 total
。咱們來看下 JavaScript 是如何工做的
let price = 6 let quantity = 2 let total = 0 total = price * quantity console.log(total) // 12 price = 10 console.log(total) // 12
這好像不是咱們的指望,咱們指望的是,改變了 price,total的值也會更新。如何作到這一點,並不難。
let price = 6 let quantity = 2 let total = 0 total = price * quantity console.log(total) // 12 price = 10 console.log(total) // 12 const updateTotal = () => { total = price * quantity } updateTotal() console.log(total) // 20
咱們定義了一個方法 updateTotal
,讓這個方法執行了咱們須要更新 total
的業務邏輯,再從新執行這個方法那麼 total
的值就改變了。
咱們能夠考慮下咱們想到達到什麼樣的目的,其實很簡單,就是當改變 price 或者 quantity 的時候 total 的值會跟着改變。學過設計模式的話,咱們很容易想到這個場景符合觀察者模式。
觀察者模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知。
如今咱們使用觀察者模式定義了觀察變量 price 與 quantity,若是它們的值發生變化,那麼依賴於它的計算屬性 total 將會獲得一個 notify,這個 notify 便是咱們的目標,去執行 updateTotal。
把觀察者模式抽象爲一個依賴類
// 表明依賴類 class Dep { constructor () { this.subscribers = [] // 把全部目標收集到訂閱裏 } addSub (sub) { // 當有可觀察目標時,添加到訂閱裏 if (sub && !this.subscribers.includes(sub)) { // 只添加未添加過的訂閱 this.subscribers.push(sub) } } notify () { // 當被觀察的屬性發生變更時通知全部依賴於它的對象 this.subscribers.forEach(fn => fn()) // 從新執行全部訂閱過的目標方法 } }
那麼如何使變量 price 和 quantify 變得可觀察,在 Vue 2.x 中使用的是 Object.defineProperty
,本文會使用 Proxy
來實現。
// 使變量變爲一個可觀察的對象的屬性 const dataObj = { price: 6, quantity: 2 } let total = 0 let target = null class Dep { // 表明依賴類 ... } const dep = new Dep() data = new Proxy(dataObj, { get (obj, key) { dep.addSub(target) // 將目標添加到訂閱中 return obj[key] }, set (obj, key, newVal) { obj[key] = newVal // 將新的值賦值給舊的值,引發值的變化 dep.notify() // 被觀察的屬性值發生變化,即通知全部依賴於它的對象 } }) total = data.price * data.quantity console.log(total) // 12 data.price = 10 console.log(total) // 12 target = () => { total = data.price * data.quantity } target() target = null console.log(total) // 20
上面代碼稍微有些凌亂,咱們重構一下,觀察者模式結合 Proxy 最終目的就是輸出被觀察的對象。咱們能夠抽象爲一個 observer
// 將依賴類與 Proxy 封裝爲 observer,輸入一個普通對象,輸出爲被觀察的對象 const observer = dataObj => { const dep = new Dep() return new Proxy(dataObj, { get (obj, key) { dep.addSub(target) // 將目標添加到訂閱中 return obj[key] }, set (obj, key, newVal) { obj[key] = newVal // 將新的值賦值給舊的值,引發值的變化 dep.notify() // 被觀察的屬性值發生變化,即通知全部依賴於它的對象 } }) } const data = observer(dataObj)
咱們注意到,其實每次咱們還要從新執行咱們的目標 target ,讓 total 值發生變化。這塊兒邏輯咱們能夠抽象爲一個 watcher
,讓它幫咱們作一些重複作的業務邏輯。
const watcher = fn => { target = fn target() target = null } watcher(() => { total = data.price * data.quantity })
咱們最終代碼優化爲:
// 使變量變爲一個可觀察的對象的屬性 const dataObj = { price: 6, quantity: 2 } let total = 0 let target = null // 表明依賴類 class Dep { ... } // 使用 Proxy 實現了觀察者模式 const observer = dataObj => { ... } const data = observer(dataObj) // 高階函數,重複執行訂閱方法 const watcher = fn => { ... } watcher(() => { total = data.price * data.quantity }) console.log(total) // 12 data.price = 30 console.log(total) // 60
咱們最終實現了開始的想法,total 會根據 price 值的改變而改變。實現了簡單的響應式系統。
爲了縮小篇幅,上面的方法同時也有講過,即摺疊(簡化)起來,咱們再回到 Vue 是如何追蹤數據的依賴的那張圖。再看看咱們是如何實現的。
經過小例子咱們學習到的內容
本文講到了 Vue2.x 中響應式系統的一些弊端,在即將到來的 Vue 3.0 Updates 中都將獲得解決,在去年這時候尤大在 Plans for the Next Iteration of Vue.js 這篇博文中也有提到過。讓咱們期待 Vue 3.0 的到來吧。
原文: http://xlbd.me/build-a-reacti...