當應用在運行時,內部狀態是會不斷變化的。而對於 web 應用而言這會直接致使頁面不停的從新渲染。那麼如何經過狀態變化肯定具體要從新渲染哪一個部分呢?在 MVVM 框架出現以前,大多數時候都須要手動去建立並維護數據與顯示層的聯繫,隨着應用的複雜度提升,內部狀態和 UI 的聯繫變得錯綜複雜,難以維護。前端 MVVM 的框架正是經過編寫一個通用的 ViewModel 層,負責讓 Model 層的變化自動同步到 View 層,還負責讓 View 層的修改同步回 Model。今天咱們一塊兒來剖析一下,當應用的內部狀態改變時,Vue.js 是怎麼作到偵測到變化的。前端
Vue.js 的變化偵測與 React 不一樣,對 React而言,當狀態發生變化時,它並不知道具體哪一個狀態變化了,只知道狀態有可能變了,而後發送信號給框架,框架內部收到信號後,會進行暴力比對找出來那些DOM節點須要從新渲染。對 Vue.js 而言,當狀態發生改變時,它馬上就知道了,並且必定程度上知道具體哪些狀態改變了,且若是一個狀態上綁定了多個依賴,當狀態改變時,會向全部綁定的依賴發送通知。可是,這種粒度更細也是要付出必定代價的,每一個狀態綁定的依賴越多,依賴跟蹤在內存上的消耗就更大,這種狀況在 Vue.js 2.0 引入虛擬 DOM 以後改善了不少。web
在 JS 中,咱們偵聽對象變化的手段無非兩種:Object.defineProperty 和 ES6 的 Proxy。因爲 ES6 的支持狀況不理想,Vue.js 2.0 中採用的是第一種方法,但在新版本中應該會放棄 Object.defineProperty 選擇 Proxy。由於 Object.defineProperty 是存在明顯缺陷的,後文會提到。首先咱們能夠採用下面的函數來封裝 Object.defineProperty :bash
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
return val
},
set: function(newVal) {
if (val === newVal) return
val = newVal
}
})
}
複製代碼
此時,思考一下,要觀察數據的真正目的是什麼?框架
目的就是當數據變化時,能夠通知那些曾經使用過該數據的地方。因此咱們須要先收集依賴,這樣當數據變化時在去通知這些依賴。顯而易見,能夠在 getter 中收集依賴,在 setter 中觸發依賴。函數
首先咱們能夠封裝一個通用的依賴類,在 Vue.js 中是 Dep 類:學習
class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (somethingToWatch) {
this.addSub(somethingToWatch)
}
}
notify () {
const subs = this.subs.slice()
for (let sub of subs) {
sub.update()
}
}
}
function remove (arr, items) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
複製代碼
接着改造一下 defineReactive:ui
function defineReactive(data, key, val) {
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
dep.depend()
return val
},
set: function(newVal) {
if (val === newVal) return
val = newVal
dep.notify()
}
})
}
複製代碼
在上面的 Dep 類中出現了 somethingToWatch,顯然它正是咱們在數據變化以後須要通知的對象。在 Vue.js 中,咱們通知用到數據的地方有不少,好比模板中,或是自定義的一個 watch 。因此此時須要一個抽象的類來覆蓋這些狀況,Vue.js 中這個類爲 Watcher:this
class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get () {
somethingToWatch = this
let value = this.getter.call(this.vm, this.vm)
somethingToWatch = undefined
return value
}
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
複製代碼
在這段代碼中,當 Watcher 初始化時,會調用 get 方法,而在 get 方法中,咱們將 somethingToWatch 指向了當前的 Watcher 實例,當咱們在獲取 value 值的時候又會觸發數據的 getter,從而自動將 Watcher 實例添加到 Dep 中。當數據變化時,Dep 會觸發依賴列表中全部依賴的 update 方法,也就是 Watcher 中的 update 方法,Watcher 中的 update 方法。spa
能夠看一個 vm.$watch('a.b.c', (oldVal, newVal) => {}) 的例子,當 a.b.c 變化時,要調用後面的回調函數。首先,要解析 a.b.c,在 Vue.js 中用 parsePath 來完成:.net
const bailRE = /^\w+.$/
function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let segment of segments) {
if (!obj) return
obj = obj[segment]
}
}
return obj
}
複製代碼
至此,咱們就拿到了 a.b.c 這個屬性,且在 Watcher 的 get 方法中訪問了它,觸發了它的 getter,從而將當前 Watcher 實例添加到 a.b.c 的依賴列表裏。且當 a.b.c 發生變化時,回調函數將會在 Watcher 中的 update 方法裏被調用。
能夠看到使用 Object.defineProperty 能夠偵測到對象的某個屬性值變化,可是咱們須要偵聽全部屬性值(包括子屬性)的變化。如今開始封裝 Observer 類來實現這一目的:
/**
* Observer 類會被附加到每個被偵測的 object上。
* 一旦加上,會將 object 全部的屬性都轉化爲 getter/setter 的形式
* 來收集屬性依賴,而且在屬性變化時通知這些依賴
*/
class Observer {
constructor(value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
/**
* walk 將每個屬性都轉爲 getter/setter
*/
walk (obj) {
const keys = Object.keys(obj)
for (let key of keys) {
defineReactive(obj, key, obj[key])
}
}
}
function defineReactive(data, key, val) {
// 新增,用於遞歸子屬性
if (typeof val == 'object') {
new Observer(val)
}
...
}
複製代碼
經過定義了 Observer 類,咱們將一個正常的 object 轉換爲了被偵測的 object。而後判斷數據類型,只有 Object 類型的數據纔會調用 walk 方法將每個屬性都變爲 getter/setter 模式。而改造後的 defineReactive 加上了一段新代碼用於判斷當子屬性爲 Object 時,對子屬性調用 new Observer(val),從而造成遞歸。這樣咱們就把全部的屬性都變爲 getter/setter 的形式了。
思考一個場景,當咱們在一個 Vue 實例中,定義 data: { a: {} },又定義了一個方法 action () { this.a.name = 'jay' },若是調用了 action 方法,能不能偵聽到對象 a 的改變呢? 答案是否認的,因爲在初始化過程當中, a 並無 name 這個屬性,也就是說在 walk 方法中,咱們沒有將 name 屬性變爲 getter/setter 模式,因此沒法偵測到這個變化,也不會向依賴發送通知。
再好比,咱們在 action 中刪除某個已經存在的屬性值,Object.defineProperty 只能判斷一個數據是否被修改,故一樣也是沒法偵測到變化的。要解決這兩個問題,咱們能夠調用 vm.delete 這兩個API。
Data 經過 Observe 轉換爲 getter/setter 形式來追蹤變化。當外界經過 Watcher 讀取數據是,會觸發 getter 從而將 Watcher 添加到依賴中。當數據發生變化時,會觸發 setter,從而向 Dep 中的依賴發送通知。Watcher 收到通知後,會向外界發送通知,外界收到通知後,可能會觸發視圖更新,也可能觸發用戶的回調函數。
本系列文章均是深刻淺出 Vue.js的學習筆記,有興趣的小夥伴能夠去看書哈。