相信你們在面試的時候沒被面試官少問vue的響應式原理,你們可能都會說經過發佈訂閱模式+數據劫持(Object.defineProperty)把對象裏的屬性轉化爲get和set,當屬性被修改或訪問就通知變化
,然而,大多數人可能只是知道這一層面,並無徹底理解。本文將從一個簡單的例子出發,一步步深刻響應式原理。vue
舉一個簡單的例子,咱們先定義一個對象:react
const hero = {
hp: 1000,
ad: 100
}
複製代碼
這裏定義了一個英雄,hp爲1000,ad爲100。面試
如今咱們能夠經過hero.hp
和hero.ad
來讀寫對應的屬性值,可是這個英雄的屬性被讀寫時,咱們並不知道。數組
這時候經過Object.defineProperty
就能夠在對應的get
和set
來實現了。瀏覽器
let hero = {}
let val = 1000
Object.defineProperty(hero, 'hp', {
get() {
console.log('hp屬性被讀取了!')
return val
},
set(newVal) {
console.log('hp屬性被修改了!')
val = newVal
}
})
複製代碼
經過Object.defineProperty
方法,給hero
定義了一個hp
屬性,這個屬性在被讀寫的時候都會觸發一段console.log
。如今來嘗試一下:markdown
hero.hp
// -> 1000
// -> hp屬性被讀取了!
hero.hp = 4000
// -> hp屬性被修改了!
複製代碼
能夠看到,英雄已經能夠主動告訴咱們其屬性的讀寫狀況了,這也意味着,這個英雄的數據對象已是「可觀測」的了。爲了把英雄的全部屬性都變得可觀測,咱們能夠想一個辦法:模塊化
/** * 使一個對象轉化成可觀測對象 * @param { Object } obj 對象 * @param { String } key 對象的key * @param { Any } val 對象的某個key的值 */
function reactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`個人${key}屬性被讀取了!`)
return val
},
set(newVal) {
console.log(`個人${key}屬性被修改了!`)
val = newVal
}
})
}
/** * 把一個對象的每一項都轉化成可觀測對象 * @param { Object } obj 對象 */
function observable(obj) {
const keys = Object.keys(obj)
keys.forEach((key) => { reactive(obj, key, obj[key]) })
return obj
}
複製代碼
如今可使用上面的方法來定義一個響應式的英雄對象。函數
const hero = observable({
hp: 1000,
ad: 100
})
複製代碼
你們能夠在控制檯自行嘗試讀寫英雄的屬性,看看它是否是已經變得可觀測的。學習
如今,對象已經可觀測,任何讀寫操做他都會主動告訴咱們,若是咱們但願在修改完對象的屬性值以後,他能主動告訴他的其餘信息該怎麼作?假設有一個watcher
方法優化
watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '後排' : '坦克'
})
複製代碼
咱們定義了一個watcher
做爲監聽器,它監聽了hero
的type
屬性。這個type
屬性的值取決於hero.hp
,換句話來講,當hero.hp
發生變化時,hero.type
也應該發生變化,前者是後者的依賴。咱們能夠把這個hero.type
稱爲計算屬性。
watcher
的三個參數分別是被監聽的對象、被監聽的屬性以及回調函數。回調函數返回一個該被監聽屬性的值。順着這個思路,咱們嘗試着編寫一段代碼:
/** * 當計算屬性的值被更新時調用 * @param { Any } val 計算屬性的值 */
function computed(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()
computed(val)
return val
},
set() {
console.error('計算屬性沒法被賦值!')
}
})
}
複製代碼
如今咱們能夠把英雄放在監聽器裏面,嘗試跑一下上面的代碼:
watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '後排' : '坦克'
})
hero.type
hero.hp = 4000
hero.type
// -> 個人hp屬性被讀取了!
// -> 個人類型是:後排
// -> 個人hp屬性被修改了!
// -> 個人hp屬性被讀取了!
// -> 個人類型是:坦克
複製代碼
這樣看起來確實不錯,可是咱們如今是經過hero.type
來獲取這個英雄的類型,並非他主動告訴咱們的,若是但願他的hp修改後能夠當即告訴咱們該怎麼作? ----依賴收集
當一個可觀測的對象被讀取後,會觸發對應的get
和set
,若是在這裏面執行監聽器的computed
方法,可讓對象發出通知嗎?
因爲computed
方法須要接受回調函數,而可觀測對象內並沒有這個函數,因此須要創建一個「中介」把可觀測對象和監聽器鏈接起來。
中介用來收集監聽器的回調函數的值一級computed()
方法
這個中介就叫「依賴收集器」:
const Dep = {
target: null
}
複製代碼
target
用來存放監聽器裏的computed
方法。
回到監聽器,看看在什麼地方把computed
賦值給Dep.target
/** * 觀測者 * @param { Object } obj 被觀測對象 * @param { String } key 被觀測對象的key * @param { Function } cb 回調函數,返回「計算屬性」的值 */
function watcher(obj, key, cb) {
// 定義一個被動觸發函數,當這個「被觀測對象」的依賴更新時調用
const onDepUpdated = () => {
const val = cb()
computed(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()
方法,這個方法很簡單,就是把監聽器回調函數的值以及computed()
給打包到一塊,而後賦值給Dep.target
。這一步很是關鍵,經過這樣的操做,依賴收集器就得到了監聽器的回調值以及computed()
方法。做爲全局變量,Dep.target
理所固然的可以被可觀測對象的getter/setter
所使用。
從新看一下咱們的watcher實例:
watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '後排' : '坦克'
})
複製代碼
在它的回調函數中,調用了英雄的hp
屬性,也就是觸發了對應的get
函數。理清楚這一點很重要,由於接下來咱們須要回到定義可觀測對象的reactive()
方法當中,對它進行改寫:
/** * 使一個對象轉化成可觀測對象 * @param { Object } obj 對象 * @param { String } key 對象的key * @param { Any } val 對象的某個key的值 */
function reactive(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
,當get
被觸發的時候,就會往裏面添加一個Dep.target
。回到關鍵知識點Dep.target
等於監聽器的computed()
方法,這個時候可觀測對象已經和監聽器捆綁到一塊。任什麼時候候當可觀測對象的set
被觸發時,就會調用數組中所保存的Dep.target
方法,也就是自動觸發監聽器內部的computed()
方法。
至於爲何這裏的deps
是一個數組而不是一個變量,是由於可能同一個屬性會被多個計算屬性所依賴,也就是存在多個Dep.target
。定義deps
爲數組,若當前屬性的set
被觸發,就能夠批量調用多個計算屬性的computed()
方法了。
完成了這些步驟,基本上咱們整個響應式系統就已經搭建完成,下面貼上完整的代碼:
/** * 定義一個「依賴收集器」 */
const Dep = {
target: null
}
/** * 使一個對象轉化成可觀測對象 * @param { Object } obj 對象 * @param { String } key 對象的key * @param { Any } val 對象的某個key的值 */
function reactive(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)
keys.forEach((key) => { reactive(obj, key, obj[key]) })
return obj
}
/** * 當計算屬性的值被更新時調用 * @param { Any } val 計算屬性的值 */
function computed(val) {
console.log(`個人類型是:${val}`);
}
/** * 觀測者 * @param { Object } obj 被觀測對象 * @param { String } key 被觀測對象的key * @param { Function } cb 回調函數,返回「計算屬性」的值 */
function watcher(obj, key, cb) {
// 定義一個被動觸發函數,當這個「被觀測對象」的依賴更新時調用
const onDepUpdated = () => {
const val = cb()
computed(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({
hp: 1000,
ad: 100
})
watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '後排' : '坦克'
})
console.log(`英雄初始類型:${hero.type}`)
hero.hp = 4000
// -> 個人hp屬性被讀取了!
// -> 英雄初始類型:後排
// -> 個人hp屬性被修改了!
// -> 個人hp屬性被讀取了!
// -> 個人類型是:坦克
複製代碼
上述代碼在瀏覽器控制檯可直接執行
在上面的例子中,依賴收集器只是一個簡單的對象,其實在reactive()
內部的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.reactive(obj, key, obj[key])
})
return obj
}
reactive(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, computed) {
this.obj = obj
this.key = key
this.cb = cb
this.computed = computed
return this.defineComputed()
}
defineComputed() {
const self = this
const onDepUpdated = () => {
const val = self.cb()
this.computed(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({
hp: 1000,
ad: 100
})
new Watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '後排' : '坦克'
}, (val) => {
console.log(`個人類型是:${hero.type}`)
})
console.log(`英雄初始類型:${hero.type}`)
hero.hp = 4000
// -> 英雄初始類型:後排
// -> 個人類型是:坦克
// -> 4000
複製代碼
上述代碼在瀏覽器控制檯可直接執行
上述代碼,是否是和vue
裏的源碼很類似?其實思路是同樣的,本文把核心部分挑出供你們食用。若是你們在學習vue
源碼時,不知如何下手,但願這篇文章能給你提供幫助。做者也是參考了許多他人的思想和不斷的嘗試才掌握。
本文是做者蠻早之前的筆記從新整理了一篇供你們食用,若有意見或其餘問題歡迎你們指出,若是對你有幫助請記得點贊關注收藏三連擊。