同窗們,你是否想學習Vue
的數據響應式原理而無從下手呢?是否有過被複雜的源碼教程勸退的經歷呢?若是你和我同樣,作過一個項目以後想深刻原理的話,恭喜你,你來對地方了。這個系列文章將從純粹的Vue
響應式原理出發,沒有其餘因素的干擾,帶領你們實現一個本身的響應式系統。vue
友情提示:由於咱們的代碼會通過多個版本的修改,因此我但願你們在看文章的時候可以把涉及到的代碼手敲一遍,這樣可以幫助理解。react
項目地址:giteegit
系列地址:github
2:數組的處理shell
3:渲染watcherexpress
4:最終章數組
數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。markdown
使用Vue
時,咱們只須要修改數據(state
),視圖就可以得到相應的更新,這就是響應式系統。要實現一個本身的響應式系統,咱們首先要明白要作什麼事情:數據結構
DOM
)依賴了哪些數據(state
)DOM
接下來,咱們將一步步地實現一個本身的玩具響應式系統閉包
幾乎全部的文章和教程,在講解Vue
響應式系統時都會先講:Vue
使用Object.defineProperty
來進行數據劫持。那麼,咱們也從數據劫持講起,你們可能會對劫持
這個概念有些迷茫,沒有關係,看完下面的內容,你必定會明白。
Object.defineProperty
的用法在此很少作介紹,不明白的同窗可在MDN上查閱。下面,咱們爲obj
定義一個a
屬性
const obj = {}
let val = 1
Object.defineProperty(obj, a, {
get() { // 下文中該方法統稱爲getter
console.log('get property a')
return val
},
set(newVal) { // 下文中該方法統稱爲setter
if (val === newVal) return
console.log(`set property a -> ${newVal}`)
val = newVal
}
})
複製代碼
這樣,當咱們訪問obj.a
時,打印get property a
並返回1,obj.a = 2
設置新的值時,打印set property a -> 2
。這至關於咱們自定義了obj.a
取值和賦值的行爲,使用自定義的getter
和setter
來重寫了原有的行爲,這也就是數據劫持
的含義。
可是上面的代碼有一個問題:咱們須要一個全局的變量來保存這個屬性的值,所以,咱們能夠用下面的寫法
// value使用了參數默認值
function defineReactive(data, key, value = data[key]) {
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
}
})
}
defineReactive(obj, a, 1)
複製代碼
若是obj
有多個屬性呢?咱們能夠新建一個類Observer
來遍歷該對象
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
const obj = { a: 1, b: 2 }
new Observer(obj)
複製代碼
若是obj
內有嵌套的屬性呢?咱們可使用遞歸來完成嵌套屬性的數據劫持
// 入口函數
function observe(data) {
if (typeof data !== 'object') return
// 調用Observer
new Observer(data)
}
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
// 遍歷該對象,並進行數據劫持
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
function defineReactive(data, key, value = data[key]) {
// 若是value是對象,遞歸調用observe來監測該對象
// 若是value不是對象,observe函數會直接返回
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue) // 設置的新值也要被監聽
}
})
}
const obj = {
a: 1,
b: {
c: 2
}
}
observe(obj)
複製代碼
對於這一部分,你們可能有點暈,接下來梳理一下:
執行observe(obj)
├── new Observer(obj),並執行this.walk()遍歷obj的屬性,執行defineReactive()
├── defineReactive(obj, a)
├── 執行observe(obj.a) 發現obj.a不是對象,直接返回
├── 執行defineReactive(obj, a) 的剩餘代碼
├── defineReactive(obj, b)
├── 執行observe(obj.b) 發現obj.b是對象
├── 執行 new Observer(obj.b),遍歷obj.b的屬性,執行defineReactive()
├── 執行defineReactive(obj.b, c)
├── 執行observe(obj.b.c) 發現obj.b.c不是對象,直接返回
├── 執行defineReactive(obj.b, c)的剩餘代碼
├── 執行defineReactive(obj, b)的剩餘代碼
代碼執行結束
複製代碼
能夠看出,上面三個函數的調用關係以下:
三個函數相互調用從而造成了遞歸,與普通的遞歸有所不一樣。 有些同窗可能會想,只要在setter
中調用一下渲染函數來從新渲染頁面,不就能完成在數據變化時更新頁面了嗎?確實能夠,可是這樣作的代價就是:任何一個數據的變化,都會致使這個頁面的從新渲染,代價未免太大了吧。咱們想作的效果是:數據變化時,只更新與這個數據有關的DOM
結構,那就涉及到下文的內容了:依賴
在正式講解依賴收集以前,咱們先看看什麼是依賴。舉一個生活中的例子:淘寶購物。如今淘寶某店鋪上有一塊顯卡(空氣)處於預售階段,若是咱們想買的話,咱們能夠點擊預售提醒
,當顯卡開始賣的時候,淘寶爲咱們推送一條消息,咱們看到消息後,能夠開始購買。
將這個例子抽象一下就是發佈-訂閱模式:買家點擊預售提醒,就至關於在淘寶上登記了本身的信息(訂閱),淘寶則會將買家的信息保存在一個數據結構中(好比數組)。顯卡正式開放購買時,淘寶會通知全部的買家:顯卡開賣了(發佈),買家會根據這個消息進行一些動做(好比買回來挖礦)。
在Vue
響應式系統中,顯卡對應數據,那麼例子中的買家對應什麼呢?就是一個抽象的類: Watcher
。你們沒必要糾結這個名字的含義,只須要知道它作什麼事情:每一個Watcher
實例訂閱一個或者多個數據,這些數據也被稱爲wacther
的依賴(商品就是買家的依賴);當依賴發生變化,Watcher
實例會接收到數據發生變化這條消息,以後會執行一個回調函數來實現某些功能,好比更新頁面(買家進行一些動做)。
所以Watcher
類能夠以下實現
class Watcher {
constructor(data, expression, cb) {
// data: 數據對象,如obj
// expression:表達式,如b.c,根據data和expression就能夠獲取watcher依賴的數據
// cb:依賴變化時觸發的回調
this.data = data
this.expression = expression
this.cb = cb
// 初始化watcher實例時訂閱數據
this.value = this.get()
}
get() {
const value = parsePath(this.data, this.expression)
return value
}
// 當收到數據變化的消息時執行該方法,從而調用cb
update() {
this.value = parsePath(this.data, this.expression) // 對存儲的數據進行更新
cb()
}
}
function parsePath(obj, expression) {
const segments = expression.split('.')
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
複製代碼
若是你對
Watcher
這個類何時實例化有疑問的話,不要緊,下面立刻就會講到
其實前文例子中還有一個點咱們還沒有提到:顯卡例子中說到,淘寶會將買家信息保存在一個數組中,那麼咱們的響應式系統中也應該有一個數組來保存買家信息,也就是watcher
。
總結一下咱們須要實現的功能:
watcher
watcher
實例須要訂閱(依賴)數據,也就是獲取依賴或者收集依賴watcher
的依賴發生變化時觸發watcher
的回調函數,也就是派發更新。每一個數據都應該維護一個屬於本身的數組,該數組來存放依賴本身的watcher
,咱們能夠在defineReactive
中定義一個數組dep
,這樣經過閉包,每一個屬性就能擁有一個屬於本身的dep
function defineReactive(data, key, value = data[key]) {
const dep = [] // 增長
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.notify()
}
})
}
複製代碼
到這裏,咱們實現了第一個功能,接下來實現收集依賴的過程。
如今咱們把目光集中到頁面的初次渲染過程當中(暫時忽略渲染函數和虛擬DOM
等部分):渲染引擎會解析模板,好比引擎遇到了一個插值表達式,若是咱們此時實例化一個watcher
,會發生什麼事情呢?從Watcher
的代碼中能夠看到,實例化時會執行get
方法,get
方法的做用就是獲取
本身依賴的數據,而咱們重寫了數據的訪問行爲,爲每一個數據定義了getter
,所以getter
函數就會執行,若是咱們在getter
中把當前的watcher
添加到dep
數組中(淘寶低登記買家信息),不就可以完成依賴收集了嗎!!
注意:執行到
getter
時,new Watcher()
的get
方法尚未執行完畢。
new Watcher()
時執行constructor
,調用了實例的get
方法,實例的get
方法會讀取數據的值,從而觸發了數據的getter
,getter
執行完畢後,實例的get
方法執行完畢,並返回值,constructor
執行完畢,實例化完畢。
有些同窗可能會有疑惑:明明是
watcher
收集依賴,應該是watcher
收集數據,怎麼成了數據的dep
收集watcher
了呢?有此疑問的同窗能夠再看一下前面淘寶的例子(是淘寶記錄了用戶信息),或者深刻了解一下發布-訂閱模式。
經過上面的分析,咱們只須要對getter
進行一些修改:
get: function reactiveGetter() {
dep.push(watcher) // 新增
return value
}
複製代碼
問題又來了,watcher
這個變量從哪裏來呢?咱們是在模板編譯函數中的實例化watcher
的,getter
中取不到這個實例啊。解決方法也很簡單,將watcher
實例放到全局不就好了嗎,好比放到window.target
上。所以,Watcher
的get
方法作以下修改
get() {
window.target = this // 新增
const value = parsePath(this.data, this.expression)
return value
}
複製代碼
這樣,將get
方法中的dep.push(watcher)
修改成dep.push(window.target)
便可。
注意,不能這樣寫
window.target = new Watcher()
。由於執行到getter
的時候,實例化watcher
尚未完成,因此window.target
仍是undefined
依賴收集過程:渲染頁面時碰到插值表達式,
v-bind
等須要數據等地方,會實例化一個watcher
,實例化watcher
就會對依賴的數據求值,從而觸發getter
,數據的getter
函數就會添加依賴本身的watcher
,從而完成依賴收集。咱們能夠理解爲watcher
在收集依賴,而代碼的實現方式是在數據中存儲依賴本身的watcher
細心的讀者可能會發現,利用這種方法,每遇到一個插值表達式就會新建一個
watcher
,這樣每一個節點就會對應一個watcher
。實際上這是vue1.x
的作法,以節點爲單位進行更新,粒度較細。而vue2.x
的作法是每一個組件對應一個watcher
,實例化watcher
時傳入的也再也不是一個expression
,而是渲染函數,渲染函數由組件的模板轉化而來,這樣一個組件的watcher
就能收集到本身的全部依賴,以組件爲單位進行更新,是一種中等粒度的方式。要實現vue2.x
的響應式系統涉及到不少其餘的東西,好比組件化,虛擬DOM
等,而這個系列文章只專一於數據響應式的原理,所以不能實現vue2.x
,可是二者關於響應式的方面,原理相同。
實現依賴收集後,咱們最後要實現的功能是派發更新,也就是依賴變化時觸發watcher
的回調。從依賴收集部分咱們知道,獲取哪一個數據,也就是說觸發哪一個數據的getter
,就說明watcher
依賴哪一個數據,那數據變化的時候如何通知watcher
呢?相信不少同窗都已經猜到了:在setter
中派發更新。
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.forEach(d => d.update()) // 新增 update方法見Watcher類
}
複製代碼
咱們能夠將dep
數組抽象爲一個類:
class Dep {
constructor() {
this.subs = []
}
depend() {
this.addSub(Dep.target)
}
notify() {
const subs = [...this.subs]
subs.forEach((s) => s.update())
}
addSub(sub) {
this.subs.push(sub)
}
}
複製代碼
defineReactive
函數只需作相應的修改
function defineReactive(data, key, value = data[key]) {
const dep = new Dep() // 修改
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
dep.depend() // 修改
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.notify() // 修改
}
})
}
複製代碼
在watcher
的get
方法中
get() {
window.target = this // 設置了window.target
const value = parsePath(this.data, this.expression)
return value
}
複製代碼
你們可能注意到了,咱們沒有重置window.target
。有些同窗可能認爲這沒什麼問題,可是考慮以下場景:有一個對象obj: { a: 1, b: 2 }
咱們先實例化了一個watcher1
,watcher1
依賴obj.a
,那麼window.target
就是watcher1
。以後咱們訪問了obj.b
,會發生什麼呢?訪問obj.b
會觸發obj.b
的getter
,getter
會調用dep.depend()
,那麼obj.b
的dep
就會收集window.target
, 也就是watcher1
,這就致使watcher1
依賴了obj.b
,但事實並不是如此。爲解決這個問題,咱們作以下修改:
// Watcher的get方法
get() {
window.target = this
const value = parsePath(this.data, this.expression)
window.target = null // 新增,求值完畢後重置window.target
return value
}
// Dep的depend方法
depend() {
if (Dep.target) { // 新增
this.addSub(Dep.target)
}
}
複製代碼
經過上面的分析可以看出,window.target
的含義就是當前執行上下文中的watcher
實例。因爲js
單線程的特性,同一時刻只有一個watcher
的代碼在執行,所以window.target
就是當前正在處於實例化過程當中的watcher
咱們以前實現的update
方法以下:
update() {
this.value = parsePath(this.data, this.expression)
this.cb()
}
複製代碼
你們回顧一下vm.$watch
方法,咱們能夠在定義的回調中訪問this
,而且該回調能夠接收到監聽數據的新值和舊值,所以作以下修改
update() {
const oldValue = this.value
this.value = parsePath(this.data, this.expression)
this.cb.call(this.data, this.value, oldValue)
}
複製代碼
在Vue源碼--56行中,咱們會看到這樣一個變量:targetStack
,看起來好像和咱們的window.target
有點關係,沒錯,確實有關係。設想一個這樣的場景:咱們有兩個嵌套的父子組件,渲染父組件時會新建一個父組件的watcher
,渲染過程當中發現還有子組件,就會開始渲染子組件,也會新建一個子組件的watcher
。在咱們的實現中,新建父組件watcher
時,window.target
會指向父組件watcher
,以後新建子組件watcher
,window.target
將被子組件watcher
覆蓋,子組件渲染完畢,回到父組件watcher
時,window.target
變成了null
,這就會出現問題,所以,咱們用一個棧結構來保存watcher
。
const targetStack = []
function pushTarget(_target) {
targetStack.push(window.target)
window.target = _target
}
function popTarget() {
window.target = targetStack.pop()
}
複製代碼
Watcher
的get
方法作以下修改
get() {
pushTarget(this) // 修改
const value = parsePath(this.data, this.expression)
popTarget() // 修改
return value
}
複製代碼
此外,Vue
中使用Dep.target
而不是window.target
來保存當前的watcher
,這一點影響不大,只要能保證有一個全局惟一的變量來保存當前的watcher
便可
現將代碼總結以下:
// 調用該方法來檢測數據
function observe(data) {
if (typeof data !== 'object') return
new Observer(data)
}
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
// 數據攔截
function defineReactive(data, key, value = data[key]) {
const dep = new Dep()
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
dep.depend()
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.notify()
}
})
}
// 依賴
class Dep {
constructor() {
this.subs = []
}
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
notify() {
const subs = [...this.subs]
subs.forEach((s) => s.update())
}
addSub(sub) {
this.subs.push(sub)
}
}
Dep.target = null
const TargetStack = []
function pushTarget(_target) {
TargetStack.push(Dep.target)
Dep.target = _target
}
function popTarget() {
Dep.target = TargetStack.pop()
}
// watcher
class Watcher {
constructor(data, expression, cb) {
this.data = data
this.expression = expression
this.cb = cb
this.value = this.get()
}
get() {
pushTarget(this)
const value = parsePath(this.data, this.expression)
popTarget()
return value
}
update() {
const oldValue = this.value
this.value = parsePath(this.data, this.expression)
this.cb.call(this.data, this.value, oldValue)
}
}
// 工具函數
function parsePath(obj, expression) {
const segments = expression.split('.')
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
// for test
let obj = {
a: 1,
b: {
m: {
n: 4
}
}
}
observe(obj)
let w1 = new Watcher(obj, 'a', (val, oldVal) => {
console.log(`obj.a 從 ${oldVal}(oldVal) 變成了 ${val}(newVal)`)
})
複製代碼
Vue
可以實現如此強大的功能,離不開閉包的功勞:在defineReactive
中就造成了閉包,這樣每一個對象的每一個屬性就能保存本身的值value
和依賴對象dep
。
答案是否認的。在Dep
的depend
方法中,咱們看到,只有Dep.target
爲真時纔會添加依賴。好比在派發更新時會觸發watcher
的update
方法,該方法也會觸發parsePath
來取值,可是此時的Dep.target
爲null
,不會添加依賴。仔細觀察能夠發現,只有watcher
的get
方法中會調用pushTarget(this)
來對Dep.target
賦值,其餘時候Dep.target
都是null
,而get
方法只會在實例化watcher
的時候調用,所以,在咱們的實現中,一個watcher
的依賴在其實例化時就已經肯定了,以後任何讀取值的操做均不會增長依賴。
咱們結合上面的代碼來思考下面這個問題:
let w2 = new Watcher(obj, 'b.m.n', (val, oldVal) => {
console.log(`obj.b.m.n 從 ${oldVal}(oldVal) 變成了 ${val}(newVal)`)
})
複製代碼
咱們知道,w2
會依賴obj.b.m.n
, 可是w2
會依賴obj.b, obj.b.m
嗎?或者說,obj.b,和obj.b.m
,它們閉包中保存的dep
中會有w2
嗎?答案是會。咱們先不從代碼角度分析,設想一下,若是咱們讓obj.b = null
,那麼很顯然w2
的回調函數應該被觸發,這就說明w2
會依賴中間層級的對象屬性。
接下來咱們從代碼層面分析一下:new Watcher()
時,會調用watcher的get
方法,將Dep.target
設置爲w2
,get
方法會調用parsePath
來取值,咱們來看一下取值的具體過程:
function parsePath(obj, expression) {
const segments = expression.split('.') // 先將表達式分割,segments:['b', 'm', 'n']
// 循環取值
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
複製代碼
以上代碼流程以下:
obj
爲對象obj
,讀取obj.b
的值,觸發getter
,觸發dep.depend()
(該dep
是obj.b
的閉包中的dep
),Dep.target
存在,添加依賴obj
爲obj.b
,讀取obj.b.m
的值,觸發getter
,觸發dep.depend()
(該dep
是obj.b.m
的閉包中的dep
),Dep.target
存在,添加依賴obj
爲對象obj.b.m
,讀取obj.b.m.n
的值,觸發getter
,觸發dep.depend()
(該dep
是obj.b.m.n
的閉包中的dep
),Dep.target
存在,添加依賴從上面的代碼能夠看出,w2
會依賴與目標屬性相關的每一項,這也是符合邏輯的。
總結一下:
observe(obj)
,將obj
設置爲響應式對象,observe函數,Observe, defineReactive函數
三者互相調用,從而遞歸地將obj
設置爲響應式對象watcher
,這個過程會讀取依賴數據的值,從而完成在getter中獲取依賴
setter
,從而派發更新,執行回調,完成在setter中派發更新
從嚴格意義來講,咱們如今完成的響應式系統還不能用於渲染頁面,由於真正用於渲染頁面的watcher
是不須要設置回調函數的,咱們稱之爲渲染watcher
。此外,渲染watcher
能夠接收一個渲染函數而不是表達式做爲參數,當依賴變化時自動從新渲染,而這樣又會帶來重複依賴的問題。此外,另外一個重要的內容咱們尚未涉及到,就是數組的處理。
如今看不懂前面提到的問題,沒有關係,這個系列以後的文章會一步步來解決這些問題,但願你們可以繼續關注。