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