摘要:三依賴收集我們知道,當一個可觀測對象的屬性被讀寫時,會觸發它的方法。依賴收集器的就是用來存放監聽器里面的方法的。
每當問到VueJS響應式原理,大家可能都會脫口而出“Vue通過Object.defineProperty方法把data對象的全部屬性轉化成getter/setter,當屬性被訪問或修改時通知變化”。然而,其內部深層的響應式原理可能很多人都沒有完全理解,網絡上關于其響應式原理的文章質量也是參差不齊,大多是貼個代碼加段注釋了事。本文將會從一個非常簡單的例子出發,一步一步分析響應式原理的具體實現思路。
一、使數據對象變得“可觀測”首先,我們定義一個數據對象,就以王者榮耀里面的其中一個英雄為例子:
const hero = { health: 3000, IQ: 150 }
我們定義了這個英雄的生命值為3000,IQ為150。但是現在還不知道他是誰,不過這不重要,只需要知道這個英雄將會貫穿我們整篇文章,而我們的目的就是通過這個英雄的屬性,知道這個英雄是誰。
現在我們可以通過hero.health和hero.IQ直接讀寫這個英雄對應的屬性值。但是,當這個英雄的屬性被讀取或修改時,我們并不知情。那么應該如何做才能夠讓英雄主動告訴我們,他的屬性被修改了呢?這時候就需要借助Object.defineProperty的力量了。
關于Object.defineProperty的介紹,MDN上是這么說的:
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 并返回這個對象。
在本文中,我們只使用這個方法使對象變得“可觀測”,更多關于這個方法的具體內容,請參考https://developer.mozilla.org...,就不再贅述了。
那么如何讓這個英雄主動通知我們其屬性的讀寫情況呢?首先改寫一下上面的例子:
let hero = {} let val = 3000 Object.defineProperty(hero, "health", { get () { console.log("我的health屬性被讀取了!") return val }, set (newVal) { console.log("我的health屬性被修改了!") val = newVal } })
我們通過Object.defineProperty方法,給hero定義了一個health屬性,這個屬性在被讀寫的時候都會觸發一段console.log。現在來嘗試一下:
console.log(hero.health) // -> 3000 // -> 我的health屬性被讀取了! hero.health = 5000 // -> 我的health屬性被修改了
可以看到,英雄已經可以主動告訴我們其屬性的讀寫情況了,這也意味著,這個英雄的數據對象已經是“可觀測”的了。為了把英雄的所有屬性都變得可觀測,我們可以想一個辦法:
/** * 使一個對象轉化成可觀測對象 * @param { Object } obj 對象 * @param { String } key 對象的key * @param { Any } val 對象的某個key的值 */ function defineReactive (obj, key, val) { Object.defineProperty(obj, key, { get () { // 觸發getter console.log(`我的${key}屬性被讀取了!`) return val }, set (newVal) { // 觸發setter console.log(`我的${key}屬性被修改了!`) val = newVal } }) } /** * 把一個對象的每一項都轉化成可觀測對象 * @param { Object } obj 對象 */ function observable (obj) { const keys = Object.keys(obj) keys.forEach((key) => { defineReactive(obj, key, obj[key]) }) return obj }
現在我們可以把英雄這么定義:
const hero = observable({ health: 3000, IQ: 150 })
讀者們可以在控制臺自行嘗試讀寫英雄的屬性,看看它是不是已經變得可觀測的。
二、計算屬性現在,英雄已經變得可觀測,任何的讀寫操作他都會主動告訴我們,但也僅此而已,我們仍然不知道他是誰。如果我們希望在修改英雄的生命值和IQ之后,他能夠主動告訴他的其他信息,這應該怎樣才能辦到呢?假設可以這樣:
watcher(hero, "type", () => { return hero.health > 4000 ? "坦克" : "脆皮" })
我們定義了一個watcher作為“監聽器”,它監聽了hero的type屬性。這個type屬性的值取決于hero.health,換句話來說,當hero.health發生變化時,hero.type也應該發生變化,前者是后者的依賴。我們可以把這個hero.type稱為“計算屬性”。
那么,我們應該怎樣才能正確構造這個監聽器呢?可以看到,在設想當中,監聽器接收三個參數,分別是被監聽的對象、被監聽的屬性以及回調函數,回調函數返回一個該被監聽屬性的值。順著這個思路,我們嘗試著編寫一段代碼:
/** * 當計算屬性的值被更新時調用 * @param { Any } val 計算屬性的值 */ function onComputedUpdate (val) { console.log(`我的類型是:${val}`); } /** * 觀測者 * @param { Object } obj 被觀測對象 * @param { String } key 被觀測對象的key * @param { Function } cb 回調函數,返回“計算屬性”的值 */ function watcher (obj, key, cb) { Object.defineProperty(obj, key, { get () { const val = cb() onComputedUpdate(val) return val }, set () { console.error("計算屬性無法被賦值!") } }) }
現在我們可以把英雄放在監聽器里面,嘗試跑一下上面的代碼:
watcher(hero, "type", () => { return hero.health > 4000 ? "坦克" : "脆皮" }) hero.type hero.health = 5000 hero.type // -> 我的health屬性被讀取了! // -> 我的類型是:脆皮 // -> 我的health屬性被修改了! // -> 我的health屬性被讀取了! // -> 我的類型是:坦克
現在看起來沒毛病,一切都運行良好,是不是就這樣結束了呢?別忘了,我們現在是通過手動讀取hero.type來獲取這個英雄的類型,并不是他主動告訴我們的。如果我們希望讓英雄能夠在health屬性被修改后,第一時間主動發起通知,又該怎么做呢?這就涉及到本文的核心知識點——依賴收集。
三、依賴收集我們知道,當一個可觀測對象的屬性被讀寫時,會觸發它的getter/setter方法。換個思路,如果我們可以在可觀測對象的getter/setter里面,去執行監聽器里面的onComputedUpdate()方法,是不是就能夠實現讓對象主動發出通知的功能呢?
由于監聽器內的onComputedUpdate()方法需要接收回調函數的值作為參數,而可觀測對象內并沒有這個回調函數,所以我們需要借助一個第三方來幫助我們把監聽器和可觀測對象連接起來。
這個第三方就做一件事情——收集監聽器內的回調函數的值以及onComputedUpdate()方法。
現在我們把這個第三方命名為“依賴收集器”,一起來看看應該怎么寫:
const Dep = { target: null }
就是這么簡單。依賴收集器的target就是用來存放監聽器里面的onComputedUpdate()方法的。
定義完依賴收集器,我們回到監聽器里,看看應該在什么地方把onComputedUpdate()方法賦值給Dep.target:
function watcher (obj, key, cb) { // 定義一個被動觸發函數,當這個“被觀測對象”的依賴更新時調用 const onDepUpdated = () => { const val = cb() onComputedUpdate(val) } Object.defineProperty(obj, key, { get () { Dep.target = onDepUpdated // 執行cb()的過程中會用到Dep.target, // 當cb()執行完了就重置Dep.target為null const val = cb() Dep.target = null return val }, set () { console.error("計算屬性無法被賦值!") } }) }
我們在監聽器內部定義了一個新的onDepUpdated()方法,這個方法很簡單,就是把監聽器回調函數的值以及onComputedUpdate()給打包到一塊,然后賦值給Dep.target。這一步非常關鍵,通過這樣的操作,依賴收集器就獲得了監聽器的回調值以及onComputedUpdate()方法。作為全局變量,Dep.target理所當然的能夠被可觀測對象的getter/setter所使用。
重新看一下我們的watcher實例:
watcher(hero, "type", () => { return hero.health > 4000 ? "坦克" : "脆皮" })
在它的回調函數中,調用了英雄的health屬性,也就是觸發了對應的getter函數。理清楚這一點很重要,因為接下來我們需要回到定義可觀測對象的defineReactive()方法當中,對它進行改寫:
function defineReactive (obj, key, val) { const deps = [] Object.defineProperty(obj, key, { get () { if (Dep.target && deps.indexOf(Dep.target) === -1) { deps.push(Dep.target) } return val }, set (newVal) { val = newVal deps.forEach((dep) => { dep() }) } }) }
可以看到,在這個方法里面我們定義了一個空數組deps,當getter被觸發的時候,就會往里面添加一個Dep.target。回到關鍵知識點Dep.target等于監聽器的onComputedUpdate()方法,這個時候可觀測對象已經和監聽器捆綁到一塊。任何時候當可觀測對象的setter被觸發時,就會調用數組中所保存的Dep.target方法,也就是自動觸發監聽器內部的onComputedUpdate()方法。
至于為什么這里的deps是一個數組而不是一個變量,是因為可能同一個屬性會被多個計算屬性所依賴,也就是存在多個Dep.target。定義deps為數組,若當前屬性的setter被觸發,就可以批量調用多個計算屬性的onComputedUpdate()方法了。
完成了這些步驟,基本上我們整個響應式系統就已經搭建完成,下面貼上完整的代碼:
/** * 定義一個“依賴收集器” */ const Dep = { target: null } /** * 使一個對象轉化成可觀測對象 * @param { Object } obj 對象 * @param { String } key 對象的key * @param { Any } val 對象的某個key的值 */ function defineReactive (obj, key, val) { const deps = [] Object.defineProperty(obj, key, { get () { console.log(`我的${key}屬性被讀取了!`) if (Dep.target && deps.indexOf(Dep.target) === -1) { deps.push(Dep.target) } return val }, set (newVal) { console.log(`我的${key}屬性被修改了!`) val = newVal deps.forEach((dep) => { dep() }) } }) } /** * 把一個對象的每一項都轉化成可觀測對象 * @param { Object } obj 對象 */ function observable (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } return obj } /** * 當計算屬性的值被更新時調用 * @param { Any } val 計算屬性的值 */ function onComputedUpdate (val) { console.log(`我的類型是:${val}`) } /** * 觀測者 * @param { Object } obj 被觀測對象 * @param { String } key 被觀測對象的key * @param { Function } cb 回調函數,返回“計算屬性”的值 */ function watcher (obj, key, cb) { // 定義一個被動觸發函數,當這個“被觀測對象”的依賴更新時調用 const onDepUpdated = () => { const val = cb() onComputedUpdate(val) } Object.defineProperty(obj, key, { get () { Dep.target = onDepUpdated // 執行cb()的過程中會用到Dep.target, // 當cb()執行完了就重置Dep.target為null const val = cb() Dep.target = null return val }, set () { console.error("計算屬性無法被賦值!") } }) } const hero = observable({ health: 3000, IQ: 150 }) watcher(hero, "type", () => { return hero.health > 4000 ? "坦克" : "脆皮" }) console.log(`英雄初始類型:${hero.type}`) hero.health = 5000 // -> 我的health屬性被讀取了! // -> 英雄初始類型:脆皮 // -> 我的health屬性被修改了! // -> 我的health屬性被讀取了! // -> 我的類型是:坦克
上述代碼可以直接在code pen或者瀏覽器控制臺上執行。
四、代碼優化在上面的例子中,依賴收集器只是一個簡單的對象,其實在defineReactive()內部的deps數組等和依賴收集有關的功能,都應該集成在Dep實例當中,所以我們可以把依賴收集器改寫一下:
class Dep { constructor () { this.deps = [] } depend () { if (Dep.target && this.deps.indexOf(Dep.target) === -1) { this.deps.push(Dep.target) } } notify () { this.deps.forEach((dep) => { dep() }) } } Dep.target = null
同樣的道理,我們對observable和watcher都進行一定的封裝與優化,使這個響應式系統變得模塊化:
class Observable { constructor (obj) { return this.walk(obj) } walk (obj) { const keys = Object.keys(obj) keys.forEach((key) => { this.defineReactive(obj, key, obj[key]) }) return obj } defineReactive (obj, key, val) { const dep = new Dep() Object.defineProperty(obj, key, { get () { dep.depend() return val }, set (newVal) { val = newVal dep.notify() } }) } }
class Watcher { constructor (obj, key, cb, onComputedUpdate) { this.obj = obj this.key = key this.cb = cb this.onComputedUpdate = onComputedUpdate return this.defineComputed() } defineComputed () { const self = this const onDepUpdated = () => { const val = self.cb() this.onComputedUpdate(val) } Object.defineProperty(self.obj, self.key, { get () { Dep.target = onDepUpdated const val = self.cb() Dep.target = null return val }, set () { console.error("計算屬性無法被賦值!") } }) } }
然后我們來跑一下:
const hero = new Observable({ health: 3000, IQ: 150 }) new Watcher(hero, "type", () => { return hero.health > 4000 ? "坦克" : "脆皮" }, (val) => { console.log(`我的類型是:${val}`) }) console.log(`英雄初始類型:${hero.type}`) hero.health = 5000 // -> 英雄初始類型:脆皮 // -> 我的類型是:坦克
代碼已經放在code pen,瀏覽器控制臺也是可以運行的~
五、尾聲看到上述的代碼,是不是發現和VueJS源碼里面的很像?其實VueJS的思路和原理也是類似的,只不過它做了更多的事情,但核心還是在這里邊。
在學習VueJS源碼的時候,曾經被響應式原理弄得頭昏腦漲,并非一下子就看懂了。后在不斷的思考與嘗試下,同時參考了許多其他人的思路,才總算把這一塊的知識點完全掌握。希望這篇文章對大家有幫助,如果發現有任何錯漏的地方,也歡迎向我指出,謝謝大家~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/92373.html
摘要:所以我今后打算把每一個內容分成白話版和源碼版。有什么錯誤的地方,感謝大家能夠指出響應式系統我們都知道,只要在實例中聲明過的數據,那么這個數據就是響應式的。什么是響應式,也即是說,數據發生改變的時候,視圖會重新渲染,匹配更新為最新的值。 寫文章不容易,點個贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 V...
摘要:總結最后我們依照下圖參考深入淺出,再來回顧下整個過程在后,會調用函數進行初始化,也就是過程,在這個過程通過轉換成了的形式,來對數據追蹤變化,當被設置的對象被讀取的時候會執行函數,而在當被賦值的時候會執行函數。 前言 Vue 最獨特的特性之一,是其非侵入性的響應式系統。數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。這使得狀態管理非常簡單直接,不過理解...
摘要:接下來,我們就一起深入了解的數據響應式原理,搞清楚響應式的實現機制。回調函數只是打印出新的得到的新的值,由執行后生成。及異步更新相信讀過前文,你應該對響應式原理有基本的認識。 前言 Vue.js 的核心包括一套響應式系統。 響應式,是指當數據改變后,Vue 會通知到使用該數據的代碼。例如,視圖渲染中使用了數據,數據改變后,視圖也會自動更新。 舉個簡單的例子,對于模板: {{ name ...
摘要:而是在初始化時,在讀取了監聽的數據的值之后,便立即調用一遍你設置的監聽回調,然后傳入剛讀取的值設置了時,如何工作我們都知道有一個選項,是用來深度監聽的。 寫文章不容易,點個贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下...
摘要:響應式數據響應式數據不是憑空出現的。對于對象而言,如果是之前不存在的屬性,首先可以將進行響應化處理比如調用,然后將對具體屬性定義監聽比如調用函數,最后再去做賦值,可能具體的處理過程千差萬別,但是內部實現的原理應該就是如此僅僅是猜測。 前言 首先歡迎大家關注我的Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。 國內前端算...
閱讀 1634·2021-10-25 09:46
閱讀 3229·2021-10-08 10:04
閱讀 2376·2021-09-06 15:00
閱讀 2777·2021-08-19 10:57
閱讀 2084·2019-08-30 11:03
閱讀 980·2019-08-30 11:00
閱讀 2384·2019-08-26 17:10
閱讀 3554·2019-08-26 13:36