1. 程式人生 > >深入淺出基於“依賴收集”的響應式原理

深入淺出基於“依賴收集”的響應式原理

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

每當問到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 = 3000Object.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.typehero.health = 5000hero.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屬性被讀取了!// -> 我的型別是:坦克
四、程式碼優化

在上面的例子中,依賴收集器只是一個簡單的物件,其實在defineReactive()內部的deps陣列等和依賴收集有關的功能,都應該整合在Dep例項當中,所以我們可以把依賴收集器改寫一下:

            
           

相關推薦

深入淺出基於依賴收集”的響應原理

每當問到VueJS響應式原理,大家可能都會脫口而出“Vue通過Object.defineProp

深入淺出 - vue之深入響應原理

高質量文章 談,前端框架的『御劍之道』 2018你應該知道的Web效能資訊採集指南 嗨,送你一張Web效能優化地圖 為什麼Vue使用非同步更新佇列? 聊聊我對現代前端框架的認知 深入淺出 - vue變化偵測原理 Vue 專案架構設計與工程化實踐 深

深入淺出Vue基於依賴收集”的響應原理

原文地址: https://zhuanlan.zhihu.com/p/29318017 每當問到VueJS響應式原理,大家可能都會脫口而出“Vue通過Object.defineProperty方法把data物件的全部屬性轉化成getter/setter,當屬性被訪問或修改時

Vue響應原理

允許 clas 沒有 改變 $set 開發 轉化 閱讀 提前 前面的話   Vue最顯著的特性之一便是不太引人註意的響應式系統(reactivity system)。模型層(model)只是普通JS對象,修改它則更新視圖(view)。這會讓狀態管理變得非常簡單且直觀,不過

bootstrap入門-3.響應原理

pos copy arc uip 邊距 ... 串行 psu 獲得 Bootstrap網格系統(Grid System)   響應式網格系統隨著屏幕或視口(viewport)尺寸的增加,系統會自動分為最多12列。 1 1 1 1 1 1 1

深入響應原理--Vue

執行 ide 格式 dev fun 添加屬性 類型 即使 develop Vue 最顯著的特性之一便是不太引人註意的響應式系統(reactivity system)。模型層(model)只是普通 JavaScript 對象,修改它則更新視圖(view)。這會讓狀態管理變

關於vue的響應原理

define fin data vue 轉化 數據 收集 get() 不支持 Vue 是基於Object.defineProperty()來實現數據響應的,而Object.defineProperty()是ES5無法 shim(修復) 的特性 這也就是Vue不支持 IE8以

vue.js響應原理解析與實現

github 遞歸 val 實現 mode 最新 中比 ava 新頁面 從很久之前就已經接觸過了angularjs了,當時就已經了解到,angularjs是通過臟檢查來實現數據監測以及頁面更新渲染。之後,再接觸了vue.js,當時也一度很好奇vue.js是如何監測數據更新並

vue 響應原理

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</tit

Vue 資料響應原理

Vue 資料響應式原理 Vue.js 的核心包括一套“響應式系統”。“響應式”,是指當資料改變後,Vue 會通知到使用該資料的程式碼。例如,檢視渲染中使用了資料,資料改變後,檢視也會自動更新。 舉個簡單的例子,對於模板: {{ name }} 建立一個 Vue 元件: var vm = new

vue.js響應原理解析與實現—實現v-model與{{}}指令

只需要 spl foreach 形式 pen for 元素節點 目標 @param 離線瀏覽器軟件 服務器遠程連接 1、可多站同時下載、多站同時扒 2、可單頁扒 3、可自定義, 重寫JS\圖片\CSS路徑 4、執行全站下載後,會下載到本程序根目錄下的html文件夾下。

vue響應原理(雙向繫結)-1

首先將該任務分為幾個子任務: 輸入框以及文字節點與data中的資料繫結 輸入框內容變化時,data中的資料同步變化。即view=>model的變化 data中的資料變化時,文字節點的內容同步變化。即model=>view的變化。 任務一:資料

vue 資料劫持 響應原理 Observer Dep Watcher

1、vue響應式原理流程圖概覽   2、具體流程 (1)vue示例初始化(原始碼位於instance/index.js) import { initMixin } from './init' import { stateMixin } from './state' import

Vue.js 深入響應原理

深入響應式原理 現在是時候深入一下了!Vue 最獨特的特性之一,是其非侵入性的響應式系統。資料模型僅僅是普通的 JavaScript 物件。而當你修改它們時,檢視會進行更新。這使得狀態管理非常簡單直接,不過理解其工作原理同樣重要,這樣你可以迴避一些常見的問題。在這個章節,我們將進入一些 Vue

深入解析vue.js響應原理與實現

vue.js響應式原理解析與實現。angularjs是通過髒檢查來實現資料監測以及頁面更新渲染。之後,再接觸了vue.js,當時也一度很好奇vue.js是如何監測資料更新並且重新渲染頁面。vue.js響應式原理解析與實現 Object.defineProperty es5新增了

Vue 原始碼(一):響應原理

相關文章網上已經很多了,趁 3.0 沒出跟風打個卡 前言 本文只做簡單介紹,結合程式碼食用更佳:github/vue-learn-source-code 效果預覽:github pages Object.defineProperty defineProperty 讓我們可以劫持某個屬性的 g

淺析Vue響應原理(三)

Vue響應式原理之defineReactive defineReactive 不論如何,最終響應式資料都要通過defineReactive來實現,實際要藉助ES5新增的Object.defineProperty。 defineReactive接受五個引數。obj是要新增響應式資料的物件;key是屬性名,

Vue 進階系列之響應原理及實現

什麼是響應式Reactivity Reactivity表示一個狀態改變之後,如何動態改變整個系統,在實際專案應用場景中即資料如何動態改變Dom。 需求 現在有一個需求,有a和b兩個變數,要求b一直是a的10倍,怎麼做? 簡單嘗試1: let a = 3; let b

基於rem的響應佈局

rem的佈局新見解 其實,在寫這篇部落格之前,我建議大家可以參看這篇文章從網易與淘寶的font-size思考前端設計稿與工作流,我在仔細消化這篇文章後做了一個自己的看法和理解,歡迎交流。 畫素單位的選擇 em的沒落 em的計算與繼承:em是基於父

vue響應原理學習(三)— Watcher的實現

普及知識點 為什麼我們改變了資料,Vue能夠自動幫我們重新整理DOM。就是因為有 Watcher。當然,Watcher 只是派發資料更新,真正的修改DOM,還需要借用VNode,我們這裡先不討論VNode。 computed 計算屬性,內部實現也是基於 Watcher watc