前言
- observer部分完整的源碼注釋放在github上了,有興趣的可以去看看,如果發現有誤情不吝賜教!observer
- 這篇文很長長長長長長長長,而且比較費腦,我也整理了很久,如果對這篇文感興趣,請自帶??和無限的耐心~
- 這篇文只是Vue的響應式原理的一部分,后面還有很多很多很多的知識本文沒有涉及到。
Vue的雙向數據綁定和Angular很不一樣。
Angular采用的是“臟檢查”的方法,當我們觸發了某些事件(定時,異步請求,事件觸發等),執行完事件之后,Angular會對所有“注冊”過的值進行一遍“全面檢查”,也就是遍歷所有的值,判斷是否和之前的一致。這種方法效率不高,因為我們修改一個小地方都會帶來兩次以上的全面檢查,如果我們綁定的view比較多,就可能會存在比較明顯的性能問題了。
而Vue的處理方式則不同,它結合觀察者模式和發布-訂閱模式,當我們改變了一個數值,它會主動通知與它相關的訂閱者,告訴他們可以進行相關的操作了,這種方法和“臟檢查”相比,更加優雅,效率會更高。
1. 響應式的基石:Object.defineProperty(obj, prop, descriptor)
MDN : Object.defineProperty(obj, prop, descriptor)
我們都知道對象有兩種屬性,一種是數據屬性,一種是訪問器屬性。
數據屬性有4個特性:configurable, enumerable, value, writable
訪問器屬性也有4個特性:configurable, enumerable, get, set
平時我們通過普通賦值的方法(比如:obj.a - 'a'
)添加的屬性都是數據屬性,且默認configurable
,和enumerable
都為true
。
而用Object.defineProperty()
可以添加數據屬性或者訪問器屬性,默認configurable
,enumerable
都為false
。
關于getter/setter
訪問器屬性是沒有value
的,但是他們可以用來劫持對另一個數據的訪問,舉個例子:
var log = console.log.bind(console);
var obj = {
_year: 2017
};
Object.defineProperty(obj, 'year', {
get: function getter () {
return this._year;
},
set: function setter (value) {
this._year = value;
}
});
log(obj.year); // 2017
obj.year = 2018;
log(obj._year); // 2018
這個例子中我們訪問obj.year
,會返回obj._year
的值,我們修改obj.year
,會修改obj._year
的值。
根據這個特性,我們可以實現視圖-數據雙向綁定:
<body>
<p id="test-p">lalal</p>
</body>
<script>
var log = console.log.bind(console);
var obj = {}
Object.defineProperty(obj, 'test-p', {
get: function getter () {
return document.getElementById('test-p').innerHTML;
},
set: function setter (value) {
document.getElementById('test-p').innerHTML = value;
}
});
log(obj['test-p']);
setTimeout(function changeData () {
obj['test-p'] = 'hahah';
}, 3000);
</script>
是不是特別好玩?
2. 觀察者模式
在此種模式中,一個目標對象管理所有相依于它的觀察者對象,并且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。
一圖勝千言,我畫了張簡單的流程圖,應該很容易看懂:
詳細一點講,流程大概是這樣的:
目標對象有這么幾個方法:
- setState:設置對象的狀態,該函數調用了NotifyObserver方法
- getState:取得對象當前的狀態
- addObserver:添加觀察者
- removeObserver:刪除觀察者
- NotifyObserver:通知觀察者:我的狀態改變了,該方法會調用各個觀察者的Notify方法
觀察者對象有個方法:
Notify:該方法會會調用目標對象的getState方法,然后對目標對象的新值作出一些反應,比如說,打印出來之類的。
如果我們寫一個最簡單的觀察者模式,那可能是這樣的:
var log = console.log.bind(console);
function Oberser (target, cb) {
(function(){ // 添加到目標對象
target.addOberser && target.addOberser(this);
console.log('oberser added')
}).call(this);
this.notify = cb;
}
var target = {
_value: 2017,
obersers: [],
addOberser: function (oberser) { // 添加觀察者
this.obersers.push(oberser);
},
removeOberser: function (oberser) { // 刪除觀察者
// ...
},
notifyOberser: function () { // 通知觀察者
this.obersers.map(oberser => oberser.notify && oberser.notify());
}
}
Object.defineProperties(target, {
value: {
get: function () {
return this._value;
},
set: function (newValue) {
this._value = newValue;
this.notifyOberser(); // 調用notifyOberser
}
}
});
var oberser1 = new Oberser(target, function () {
log(`I'm observer1, the value of my target is ${target.value}`);
});
var oberser2 = new Oberser(target, function () {
log(`I'm observer2, the value of my target is ${target.value}`);
});
target.value = 2018;
/*
oberser added
oberser added
I'm observer1, the value of my target is 2018
I'm observer2, the value of my target is 2018
*/
當然了如果我們要觀察同一個對象中的多個屬性,就不能用這種方法了,因為我們總不能一個屬性更新,所有觀察者都全部調用一遍吧?最好是每一個屬性都能有自己的觀察者。
3. 正題
怎么給每個對象都維護一個觀察者的列表呢?Vue是這樣做的:
Vue在觀察者模式中結合發布-訂閱模式,其中涉及到了三個重要的對象:Observer
, Dep
, Watcher
。
Observer
:負責觀察目標數據的變化,如果數據變化了,那么通知Dep。
Dep
:負責維護一個訂閱者列表(收集依賴),當接收到Observer
的通知時,他就通知所有訂閱者:目標數據更新了。
Watcher
:維護一個回調函數,當接收到Dep
的通知時,執行回調函數。
可以這么理解:Observer
是教師,Dep
是教學在線,Watcher
是學生。教師不必維護自己的學生列表,教務處幫他維護。學生不必維護自己的課表,因為教務處也會幫他維護。每次教師布置了新作業等(比喻不是很恰當),他只需要跟教學在線說一聲就可以了,教學在線就發郵件告訴每一個上了這門課的學生:有新作業了。學生就可以分別對這個新作業作出不同的反應。
原理已經了解得差不多了,接下來看一下源碼吧。
先看一下 Observer
類,Vue會給每一個響應式的數據添加一個observer,這個observer就負責觀察這個數據有沒有發生變化。
中文注釋是我加的,英文注釋是作者加的,不要漏了英文注釋,很重要!
export class Observer {
value: any; // 被觀察的對象,比如vue的根屬性data,在vue實例初始化的時候,vue會為data屬性添加一個observer對象,介時observer對象的value屬性指向data,而data的__ob__屬性指向observer對象
dep: Dep; // 每一個observer對象都有一個dep,負責收集依賴和通知
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this) // value的__ob__屬性指向這個observer對象本身,比如L38注釋中說到的data屬性
if (Array.isArray(value)) { // 如果value為數組,那么增強這個數組
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value) // 往下遞歸數組,如果數組中有元素為對象或者數組,也會給其添加observer
} else { // 如果value為對象,那么往下遞歸對象,如果對象中有屬性為對象或者數組,也會給其添加observer
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) { // 遍歷對象,把對象中的屬性都轉化為getter/setter對
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 很重要!
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) { // 遍歷數組,如果數組中有元素為對象或者數組,也會給其添加observer
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
在Observer
類的constructor
函數中,可以看到對于數組和對象,Vue的處理是不一樣的,為什么呢?
數組是沒有Object.defineProperty(obj, prop, descriptor)
這個方法的,這就意味著我們沒有辦法監聽數組中屬性的添加,刪除。
如果你在Vue中處理過數組,你應該知道,在Vue中,數組只有7個常用方法可以觸發視圖的更新:push(), pop(), shift(), unshift(), splice(), sort(), reverse()
。這是因為Vue對這些方法進行了增強,原理很簡單,類似于這樣(當然實際上要嚴謹一些,這里只是幫助理解):
var log = console.log.bind(console);
var arrayMethods = Object.create(Array.prototype); // 繼承自Array.prototype,保留了數組原本的特性
arrayMethods.unshift = function (value) { // 重寫方法
Array.prototype.unshift.call(this, value); // 調用原來的方法
notify(); // 并進行通知
}
function notify () {
console.log('unshift');
}
var arr = [1, 2, 3];
arr.__proto__ = arrayMethods;
arr.unshift(0);
log(arr)
/*
[ 1, 2, 3 ]
unshift
[ 0, 1, 2, 3 ]
*/
上面的代碼截斷了數組的原型鏈,我們新創建了一個對象arrayMethods
,這個對象繼承自Array.prototype
,然后改寫里面的unshift()
方法。這樣既保證我們保留了數組的length
等屬性,有可能使用自己定義的unshift()
方法,我們在unshift()
方法中調用了notify()
函數。
Vue是怎么給每一個對象都加上一個Observer對象的?上面代碼中,在處理數組的函數observeArray
里,可以看到Vue遍歷了一遍數組,并對每一個元素調用了observe()
函數。
而在處理對象的函數walk
中,對每一個屬性都調用了defineReactive
函數(這個函數非常重要,后面再說),這個defineReactive
函數內部也對屬性都調用了一遍observe()
函數。
也就是說,Vue是通過observe()
函數來給對象添加observer的。看一下observe()
函數:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 只有對象或數組才會進入這個函數
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // value已經有了自己的observer
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) // value為對象且沒有自己的observer,那么為他新建一個observer,注意這里說明了Vue對每一層的屬性或元素遞歸添加了observer
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
注意到里面有一行代碼:ob = new Observer(value)
,也就是說,Vue遞歸遍歷了每一層的屬性或元素,如果這個元素/屬性的類型為對象/數組,那么它也會有一個自己的observer。
好,現在我們已經明白了Vue怎么遍歷數組來把數組轉化為響應式的了,那接下來再看看Vue如何處理對象屬性:
高能預警!
export function defineReactive ( // 每個屬性都轉化為getter/setter,并且每個類型為對象(包括數組)的屬性都會擁有自己的observer
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean // shallow為true的話,屬性不會有自己的observer,也就是該屬性將不具備響應性
) {
const dep = new Dep() // 注意這個函數將會出現兩個dep,這里第一個dep,將會被閉包進getter/setter函數中
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key] // 注意,這個val也會被閉包進getter/setter方法中,我之前還疑惑把屬性都轉化為getter/setter值是怎么存儲數據的,就是把這個val閉包進去的
}
let childOb = !shallow && observe(val) // 每一個observer會有一個dep屬性,所以這里有了第二個dep,這個dep會在該屬性的屬性被增刪的時候通知訂閱者
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // 執行屬性原本自己有的getter
if (Dep.target) { // 如果存在 Dep.target 這個全局變量不為空,表示是在新建 Watcher 的時候調用的
dep.depend() // 這里是第一個dep,當Dep.target依賴于這個屬性的時候,他會調用該屬性的getter,這是dep.depend()就會把Dep.target添加進自己的訂閱列表,這樣在屬性的setter被調用的時候,這個dep就可以通知Dep.target了
if (childOb) {
childOb.dep.depend() // 第二個dep也會收集依賴,那么該屬性的屬性被添加或者刪除的時候,這個dep就可以通知這個屬性的訂閱者了
if (Array.isArray(value)) { // 如果value為一個數組,那么是無法通過getters來竊聽對數組元素的訪問的,所以要向下遍歷數組,給里面的元素都收集依賴
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) { // 沒有變化/newVal為NaN/value為NaN
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
這個函數邏輯比較復雜,讓我們好好來捋一下思路:
1. 閉包的妙用
如果你好好觀察一下屬性的getter/setter方法,你會發現他們閉包了這幾個變量:
getter, setter, val, dep,childOb
其中,getter, setter
兩個變量可能是我們自己定義的getter/setter方法,因為我們有時候也會有需要訪問器屬性的時候。
val
是我們原本使用自己的getter/setter想要訪問的值,比如這篇文章第一個代碼塊的_year
屬性。
我之前還在疑惑,因為訪問器屬性是沒有自己的值的,Vue把對象的屬性轉化為訪問器屬性之后,要怎么維護之前的值,原來是閉包進來了!
看一下源碼,當我們調用屬性的setter方法的時候,最后修改的是這個val
的值,而我們調用getter方法的時候,返回的也是這個val
的值。
dep, childOb
在第3小節一起講。
2.Dep
類
是時候介紹一下Dep
類了,不然后面的講不下去。
Dep
的結構很簡單,大概長這樣:
export default class Dep { // dep是dependence的縮寫,他負責收集依賴,以及通知訂閱者。每一個Observer對象有其自己的的dep
static target: ?Watcher;
id: number;
subs: Array<Watcher>; // 訂閱者列表,
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) { // 添加訂閱者
this.subs.push(sub)
}
removeSub (sub: Watcher) { // 刪除訂閱者
remove(this.subs, sub)
}
depend () { // 添加依賴,也就是把當前Dep.target添加到這個dep實例的subs列表中
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () { // 通知watcher,執行所有watcher的.update()方法,更新watcher的數據
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
可以看到,一個Dep
類會有一個自己的id,維護自己的一個訂閱者列表,并切可以添加,刪除,通知訂閱者。
Dep
類中有一個靜態屬性Dep.target
,學過C++的同學應該知道,靜態屬性也就是類屬性,是所有實例共享的。這個target是干什么用的呢?
Vue在處理一個watcher的時候,就會把Dep.target
的值設為當前的watcher,舉個例子,這是我們的Vue實例(該例子從官網中復制過來的):
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 計算屬性的 getter
reversedMessage: function () {
// `this` 指向 vm 實例
return this.message.split('').reverse().join('')
}
}
})
現在假設處理到了reversedMessage
,先把Dep.target
指向它。很明顯reversedMessage
依賴了message
,我們是不是要在message
對應的dep的訂閱者列表中加上reversedMessage
?問題來了,我們要怎么添加這個依賴?
首先因為reversedMessage
會訪問到message
,也就是會調用message
的getter方法,那我們可以在getter方法中進行依賴收集,但是getter方法是沒辦法傳參的,所以它也沒辦法知道誰訂閱了它。
這時候Dep.target
就起作用了,我們前面已經說過,Vue處理到了哪個watcher,就會把Dep.target
指向它,那么此時的Dep.target
肯定就是reversedMessage
,我們只需要在getter函數中把Dep.target
添加進訂閱者列表就可以了!
那么這時,當我們改變message
的值時,會調用其setter函數,setter函數中dep就會調用dep.notify()方法,通知reversedMessage
:我更新了!
真的太妙了!
3. 兩個dep
源碼中我在注釋中也提醒過了,這個函數中出現了兩個dep,一個是在函數開頭就新建的dep,另一個是屬性自己的observer中的dep。
dep的作用是什么?收集依賴并在適當的時候通知訂閱者:目標數據更新了。
在源碼中,屬性的getter方法中,給dep, childOb
都添加了依賴,為什么在setter方法中,只通知了dep
?或者說,childOb
的意義在哪里呢?
先看一個例子:
var obj = {
_a: { aa: 1}
};
Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
get: function () {
log('get a:' + this._a);
return this._a;
},
set: function (newV) {
log('set a:' + newV);
this._a = newV;
}
});
obj.a; // get a:[object Object]
obj.a.bb = 2; // get a:[object Object]
delete obj.a;
// 刪除該屬性的時候,沒有調用getter/setter函數!
可以很明顯看出getter/setter的缺陷:只能監聽到屬性的更改,不能監聽到屬性的刪除與添加。
我們都知道Vue提供了內置的Vue.set()
, Vue.delete()
方法來讓我們響應式的添加和刪除數組的元素或對象的屬性。
官方文檔
官方文檔是這么說的:
Vue.set()這個方法主要用于避開 Vue 不能檢測屬性被添加的限制。
Vue.delete()這個方法主要用于避開 Vue 不能檢測到屬性被刪除的限制。
我們前面已經證明了,setter是不會在屬性被刪除或者添加的時候調用的,那么Vue是怎么在刪除和添加的時候通知watcher的?其實跟數組方法的增強事同一個套路,把Vue.delete()
的源碼簡化簡化再簡化之后:
function del () {
delete obj.a;
childOb.notify(); // 通知obj的watcher:我有一個屬性刪除了。
}
所以為什么在getter方法中要添加childOb
的依賴,就是為了在刪除或者添加屬性的時候進行通知。
4. 如何向下收集依賴
是這樣的,假設數據是這樣的let data = {a: {b: {c: {d: {e: 1}}}}}
,有一個模板引用了{{a.b.c}}
,那么我們修改a.b.c.d.e
,這時watcher會被通知到嗎?
答案是不會。為什么?一步一步來看。
首先我們知道每一層的屬性,也就是
a, b, c, d, e
,都有自己的observer,而且watcher訂閱observer是通過getter方法來實現的,沒有getter方法就沒法訂閱。它調用了
c
的getter方法,因此c
更改了(整個對象被替換),會有通知,c
刪除了,也會有通知。我們修改了
a.b.c.d.e
模板引用的是
{{a.b.c}}
,那么它沒有調用到e
的getter方法。因此我們修改了
e
,watcher就沒辦法知道了。
Vue是怎么解決這個問題的呢?
它是這樣做的:當模板引用了{{a.b.c}}
時,此時Dep.target
是這個模板,然后,Vue從c
開始往下遍歷,對每個屬性都"touch"一下,也就是強行調用一下getter方法,這樣,模板就加入了所有屬性的訂閱者列表中。
有興趣的同學可以自己去看一下Vue源碼中的traverse.js
5. dependArray函數
終于要進入尾聲了???,看源碼真的很費心神,但是收獲真的超級大呀!
在defineReactive
函數中的getter方法中,對數組有一個額外的處理過程:如果value
為數組,那么對其執行dependArray
函數。
想了好久才想明白為什么要進一步的處理。
回到最開始,我們給一個對象添加一個observer,那么他會遍歷所有的屬性,把屬性都轉化為getter / setter。
但是給數組添加一個observer,他只是添加了8個具有響應性的方法。(當然他也會給子對象添加observer)
這時我們push,pop數組,是響應式的,數組的dep知道他要通知訂閱者們。
但是如果我們改變的是數組的元素,比如,對于一個數組var arr = [1, 2, 3, {a: 4}],現在我們這樣操作arr[0] = 0
,數組是不會有響應的。
這也是為什么vue給數組加了兩個方法Vue.set
, Vue.delete
來添加和刪除元素的原因。
再回到這個問題上,我們已經有了Vue.set
, Vue.delete
兩個方法,那我們操作基本類型的元素基本沒啥問題了,但是如果是像arr[3].a = 5
這種呢?Vue的解決方法就是遞歸遍歷數組,遇到類型為object的元素,就把當前的Dep.target添加到它的訂閱者列表中,這時它的變化就可以被監聽了。
這一切的根本原因,就是數組沒法通過getter/setter對象來監聽元素的變化。
最后附上dependArray
的源碼。
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend() // //如果數組元素也是對象,那么他們observe過程也生成了ob實例,那么就讓ob的dep也收集依賴
if (Array.isArray(e)) {
dependArray(e)
}
}
}