vue系列—響應式原理實現及Observer源碼解析(一)
- 2019 年 10 月 28 日
- 筆記
閱讀目錄
- 一. 什麼是響應式?
-
二:如何偵測數據的變化?
- 三. Observer源碼解析
一. 什麼是響應式?
我們可以這樣理解,當一個數據狀態發生改變的時候,那麼與這個數據狀態相關的事務也會發生改變。用我們的前端專業術語來講,當我們JS中的對象數據發生改變的時候,與JS中對象數據相關聯的DOM視圖也會隨着改變。
我們可以先來簡單的理解下Vue中如下的一個demo
<!DOCTYPE html> <html> <head> <title>vue響應性的測試</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <div>{{ count }}</div> <button @click="changeValue">點擊我自增</button> </div> <script type="text/javascript"> var app = new Vue({ el: '#app', data() { return { count: 1 } }, methods: { changeValue() { this.count++; } } }) </script> </body> </html>
如上demo,當我們點擊按鈕的時候,我們的count值會自增1,即data對象中的count屬性值發生改變,它會重新對html頁面進行渲染,因此相關聯數據對象屬性值的視圖也會發生改變。
那麼Vue中它是如何做到的呢?
想要完成此過程,我們需要做如下事情:
1)偵測對象數據的變化。
2)收集視圖依賴了哪些數據。
3)數據變化時,自動通知和數據相關聯的視圖頁面,並對視圖進行更新。
2. 如何偵測數據的變化?
數據對象偵測也可以叫數據劫持,vue.js 是採用數據劫持及發佈者-訂閱者模式,通過Object.defineProperty()來劫持各個屬性的setter,getter。在數據變動時發佈消息給訂閱者,觸發相應的監聽回調。當然我們也可以使用ES6中的Proxy來對各個屬性進行代理。
2.1 Object.defineProperty() 偵測對象屬性值變化
var obj = {}; var value = '初始化值'; Object.defineProperty(obj, 'name', { get() { console.log('監聽getter數據值的變化'); return value; }, set(newVlue) { console.log('監聽setter數據值的變化'); value = newVlue; } }); console.log(obj.name); obj.name = 'kongzhi'; console.log(obj.name);
如上代碼打印效果如下所示:
如上我們可以看到,當我們執行 console.log(obj.name); 獲取 obj對象中屬性name的值的時候,Object.defineProperty方法會監聽obj對象屬性值的變化,自動調用get方法,因此首先會打印 “監聽getter數據值的變化” 信息出來,接着打印 “初始化值”,當我們給 obj.name 設置值的時候,就會自動調用set方法,因此會打印 “監聽setter數據值的變化” 信息出來;然後我們打印 console.log(obj.name); 又會自動調用get方法,因此會打印 “監聽getter數據值的變化”, 最後更新數據,打印出 “kongzhi” 信息。
如上我們已經了解了 Object.defineProperty()方法的基本使用了,因此我們現在可以封裝一個數據監聽器函數,比如叫它為 Observer. 它的作用是能夠對數據對象的所有屬性進行監聽。如下代碼實現:
function Observer(data) { this.data = data; this.init(); } Observer.prototype.init = function() { var data = this.data; // 遍歷data對象 Object.keys(data).forEach((key) => { this.defineReactive(data, key, data[key]); }); }; Observer.prototype.defineReactive = function(data, key, value) { // 遞歸遍歷子對象 var childObj = observer(value); // 對對象的屬性進行監聽 Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: true, // 可刪除或可修改目標屬性 get: function() { return value; }, set: function(newValue) { if (newValue === value) { return; } value = newValue; // 如果新值是對象的話,遞歸該對象 進行監聽 childObj = observer(newValue); } }); }; function observer (value) { if (!value || typeof value !== 'object') { return; } return new Observer(value); } // 調用方式如下: var data = { "name": "kongzhi", "user": { "name": "tugenhua" } }; observer(data); data.name = 'kongzhi2'; console.log(data.name); // 打印:kongzhi2 data.user.name = 'tugenhua22'; console.log(data.user.name); // 打印:tugenhua22
如上代碼我們可以監聽每個對象屬性數據的變化了,那麼監聽到該屬性值變化後我們需要把該消息通知到訂閱者,因此我們需要實現一個消息訂閱器,該訂閱器的作用是收集所有的訂閱者。當有對象屬性值發生改變的時候,我們會把該消息通知給所有訂閱者。
假如我們把該訂閱器函數為Dep; 那麼基本代碼如下:
function Dep() { this.subs = []; } Dep.prototype.addSub = function(sub) { this.subs.push(sub); } Dep.prototype.removeSub = function(sub) { if (this.subs.length) { var index = this.subs.indexOf(sub); if (index !== -1) { this.subs.splice(index, 1); } } } Dep.prototype.depend = function() { Dep.target.addDep(this); } Dep.prototype.notify = function() { // 遍歷,通知所有的訂閱者 this.subs.forEach((sub) => { sub.update(); }) } Dep.target = null;
如上代碼,我們就可以使用addSub方法來添加一個訂閱者,或者使用removeSub來刪除一個訂閱者, 我們也可以調用 notify 方法來通知所有的訂閱者。 如上 Object.prototype.defineReactive 代碼中我們能監聽對象屬性值發生改變,如果值發生改變我們需要來通知所有的訂閱者,因此上面的代碼我們需要改變一些代碼,如下所示:
Object.prototype.defineReactive = function(data, key, value) { ..... // 調用管理所有訂閱者的類 var dep = new Dep(); // 對對象的屬性進行監聽 Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: true, // 可刪除或可修改目標屬性 get: function() { // 新增的 if (Dep.target) { dep.depend(); } return value; }, set: function(newValue) { if (newValue === value) { return; } value = newValue; // 如果新值是對象的話,遞歸該對象 進行監聽 childObj = observer(newValue); // 有值發生改變的話,我們需要通知所有的訂閱者 dep.notify(); } }); }
如上面的demo,我們已經改變了數據後,我們會使用getter/setter監聽到數據的變化,數據變化後,我們會調用Dep類中 notify方法,該方法的作用是遍歷通知所有的訂閱者,通知完訂閱者後,我們需要做什麼呢?就是自動幫我們更新頁面,因此每個訂閱者都會調用Watcher類中的update方法,來更新數據。
因此我們需要實現一個Watcher類,Watcher的作用是派發數據更新,不過真正修改DOM,還是需要使用VNode. VNode我們後面會講解到。
Watcher是什麼?它和Dep是什麼關係?
Dep用於依賴收集和派發更新,它收集所有的訂閱者,當有數據變動的時候,它會把消息通知到所有的訂閱者,同時它也調用Watcher實列中的update方法,用於派發更新。
Watcher 用於初始化數據的watcher的實列。它原型上有一個update方法,用於派發更新。比如調用回調函數來更新頁面等操作。
Watcher 簡單實現的代碼如下:
function Watcher (obj, expOrFn, cb) { this.obj = obj; this.expOrFn = expOrFn; this.cb = cb; // 如果expOrFn是事件函數的話 if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = this.parseGetter(expOrFn); }; // 觸發getter,從而讓Dep添加自己作為訂閱者 this.value = this.get(); } Watcher.prototype.addDep = function(dep) { dep.addSub(this); }; Watcher.prototype.update = function() { var value = this.get(); var oldValue = this.value; if (oldValue === value) { return; } this.value = value; this.cb.call(this.obj, value, oldValue); } Watcher.prototype.get = function() { Dep.target = this; var value = this.getter.call(this.obj, this.obj); return value; }; /* 如下函數的作用:像vue中的 vm.$watch('xxx.yyy', function() {}); 這樣的數據能監聽到 比如如下這樣的data數據: var data = { "name": "kongzhi", "age": 31, "user": { "name": "tugenhua" } }; 我們依次會把data對象中的 'name', 'age', 'user' 屬性傳遞調用該函數。 如果是 'name', 'age', 'user' 這樣的,那麼 exp 就等於這些值。因此: this.getter = this.parseGetter(expOrFn); 因此最後 this.getter 就返回了一個函數。 當我們在 Watcher 類中執行 this.value = this.get(); 代碼的時候 就會調用 getter方法, 因此會自動執行 parseGetter 函數中返回的函數,參數為 data對象,該函數使用了一個閉包,閉包中保存的 參數 exps 就是我們的 'name', 'age', 'user' 及 'user.name' 其中一個,然後依次執行。最後返回的值: obj = data['name'] 或 data['age'] 等等這些,因此會返回值value了。 */ Watcher.prototype.parseGetter = function(exp) { var reg = /[^w.$]/; if (reg.test(exp)) { return; } var exps = exp.split('.'); return function(obj) { for (var i = 0, len = exps.length; i < len; i++) { if (!obj) { return; } obj = obj[exps[i]]; } return obj; } }
如上Watcher類,傳入三個參數,obj 是一個對象屬性,expOrFn 有可能是一個函數或者是其他類型,比如字符串等,cb是我們的回調函數,然後原型上分別有 addDep,update,get方法函數。
現在我們需要如下調用即可:
var data = { "name": "kongzhi", "age": 31, "user": { "name": "tugenhua" } }; // 初始化, 對data數據進行監聽 new Observer(data); // 變量data對象的所有屬性,分別調用 Object.keys(data).forEach((key) => { if (data.hasOwnProperty(key)) { new Watcher(data, key, (newValue, oldValue) => { console.log('回調函數調用了'); console.log('新值返回:' + newValue); console.log('舊值返回:' + oldValue); }); } });
我們可以在控制台修改下data中的值看下是否要調用回調函數,效果如下所示:
2.2 如何偵測數組的索引值的變化
<!DOCTYPE html> <html> <head> <title>vue響應性的測試</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <div v-if="arrs.length > 0" v-for="(item, index) in arrs"> {{item}} </div> </div> <script type="text/javascript"> var app = new Vue({ el: '#app', data() { return { arrs: ['1', '2', '3'] } }, methods: {} }); app.arrs[1] = 'ccc'; // 改變不了的。不是響應性的 </script> </body> </html>
Vue官網文檔建議我們使用 Vue.set(arrs, index, newValue) 方法來達到觸發視圖更新的效果,比如可以改成如下代碼即可生效:
// app.arrs[1] = 'ccc'; Vue.set(app.arrs, 1, 'ccc'); // 會生效的
那麼vue為何不能監聽數組索引的變化?
Vue官方說明的是:由於Javascript的限制。Vue不能檢測以下變動的數組:
當你利用索引直接設置一個項時,比如:vm.items[indexOfItem] = newValue;
當你修改數組的長度時:比如 vm.items.length = newLength;
但是我們自己使用 Object.defineProperty 是可以監聽到數組索引的變化的,如下代碼:
var arrs = [ { "name": "kongzhi111", "age": 30 }, { "name": "kongzhi222", "age": 31 } ]; function defineReactive(obj, key, value) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { console.log('調用了getter函數獲取值了'); return value; }, set: function(newValue) { if (value === newValue) { return; } value = newValue; console.log('數據發生改變了'); } }) } // 代碼初始化調用 defineReactive(arrs[0], 'name', 'kongzhi111'); /* 會先調用 getter方法,會打印 "調用了getter函數獲取值了"信息出來。 然後打印:kongzhi111 值了。 */ console.log(arrs[0].name); // 改變數組中第一項name數據 arrs[0].name = "tugenhua"; /* * 會先調用setter方法,打印:"數據發生改變了" 信息出來。 * 然後打印結果為:{name: 'tugenhua', age: 30} */ console.log(arrs[0]);
如下圖所示:
但是Vue源碼中並沒有對數組進行監聽,據說尤大是說為了性能考慮。所以沒有對數組使用 Object.defineProperty 做監聽,我們可以來看下源碼就知道了,源碼js地址為:src/core/observer/index.js 代碼如下所示:
export class Observer { ..... constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } }
如上代碼可以看到,如果 Array.isArray(value) 是數組的話,就調用 observeArray函數,否則的話調用walk函數,walk函數代碼如下所示:
walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } export function defineReactive () { .... Object.defineProperty(obj, key, { get: function reactiveGetter () {}, set: function reactiveSetter (newVal) {} } }
因此如果是數組的話,就沒有使用 Object.defineProperty 對數據進行監聽,因此數組的改變不會有響應性的。
但是數組的一些push等這樣的方法會進行重寫的,這個晚點再說。因此改變數組的索引也不會被監聽到的。那麼既然尤大說為了性能考慮,那麼我們就可以來測試下,假如是數組的話,我們也使用 Object.defineProperty 來監聽下,看下會怎樣影響性能的呢?因此我們需要把源碼改成如下測試下:
src/core/observer/index.js 對應的代碼改成如下:
export class Observer { .... constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { /* if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) */ this.walkTest(value); } else { this.walk(value) } } walkTest(values: Array) { for (let i = 0, l = values.length; i < l; i++) { defineReactive(values, values[i]); } } }
如上代碼,如果是數組的話,我們依然監聽,我們先把源碼注釋掉,然後添加 walkTest 函數及調用該函數。
然後我們需要在defineReactive函數中的get/set中打印一些信息出來,代碼改成如下所示:
export function defineReactive () { ..... Object.defineProperty(obj, key, { get: function reactiveGetter () { // 如下打印是新增的 typeof key === "number" && console.log('getter'); const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 如下打印是新增的 typeof key === "number" && console.log('setter'); const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } } }
然後我們需要寫一個測試代碼,我們就在源碼中的 example/commit/index.html 代碼中測試下即可,改成如下代碼:
<!DOCTYPE html> <html> <head> <title>Vue.js github commits example</title> <script src="../../dist/vue.js"></script> </head> <body> <div id="demo"> <span v-for="(item, index) in arrs" @click="clickFunc(item, index)"> {{item}} </span> </div> <script type="text/javascript"> new Vue({ el: '#demo', data: { arrs: [1, 2] }, methods: { clickFunc(item, index) { console.log(item, index); this.arrs[index] = item + 1; } } }) </script> </body> </html>
如上代碼,我們改完,等頁面打包完成後,我們刷新下頁面可以打印信息如下所示:
如上我們可以看到,數組裏面只有2個元素,長度為2, 但是從上面結果可以看到,數組被遍歷了2次,頁面渲染一次。
為什麼會遍歷2次呢?那是因為 在getter函數內部如果是數組的話會調用dependArray(value)這個函數,在該函數內部又會遞歸循環判斷是不是數組等操作。
現在當我們點擊2的時候,那麼數字就變為3. 效果如下所示:
如上可以看到,會先調用 clickFunc 函數,打印console.log(item, index)信息出來,然後再調用 this.arrs[index] = item + 1; 設置值,因此會調用 setter函數,然後數據更新了,重新渲染頁面,又會調用getter函數,數組又遍歷了2次。
如果我們的數組有10000個元素的長度的話,那麼至少要執行2次,也就是遍歷2次10000的,對性能有點影響。這也有可能是尤大考慮的一個因素,因此它把數組的監聽去掉了,並且對數組的一些常用的方法進行了重寫。因此數組中 push, shift 等這樣的會生效,對數組中索引值改變或改變數組的長度不會生效。但是Vue官方中可以使用 Vue.set() 這樣的方法代替。
2.3 如何監聽數組內容的增加或減少?
Object.defineProperty 雖然能監聽到數組索引值的變化,但是卻監聽不到數組的增加或刪除的。
我們繼續看如下demo.
var obj = {}; var bvalue = 1; Object.defineProperty(obj, "b", { set: function(value) { bvalue = value; console.log('監聽了setter方法'); }, get: function() { console.log('監聽了getter方法'); return bvalue; } }); obj.b = 1; // 打印:監聽了setter方法 console.log('-------------'); obj.b = []; // 打印:監聽了setter方法 console.log('-------------'); obj.b = [1, 2]; // 打印:監聽了setter方法 console.log('-------------'); obj.b[0] = 11; // 打印:監聽了getter方法 console.log('-------------'); obj.b.push(12); // 打印:監聽了getter方法 console.log('-------------'); obj.b.length = 5; // 打印:監聽了getter方法 console.log('-------------'); obj.b[0] = 12;
如上測試代碼,我們可以看到,給對象obj中的屬性b設置值,即 obj.b = 1; 可以監聽到 set 方法。給對象中的b賦值一個新數組對象後,也可以監聽到 set方法,如:obj.b = []; 或 obj.b = [1, 2]; 但是我們給數組中的某一項設置值,或使用push等方法,或改變數組的長度,都不會調用 set方法。
也就是說 Object.defineProperty()方法對數組中的push、shift、unshift、等這樣的方法是無法監聽到的,因此我們需要自己去重寫這些方法來實現使用 Object.defineProperty() 監聽到數組的變化。
下面先看一個簡單的demo,如下所示:
// 獲得原型上的方法 var arrayProto = Array.prototype; // 創建一個新對象,該對象有數組中所有的方法 var arrayMethods = Object.create(arrayProto); // 對新對象做一些攔截操作 Object.defineProperty(arrayMethods, 'push', { value(...args) { console.log('參數為:' + args); // 調用真正的 Array.prototype.push 方法 arrayProto.push.apply(this, args); }, enumerable: false, writable: true, configurable: true }); // 方法調用如下: var arrs = [1]; /* 重置數組的原型為 arrayMethods 如果不重置,那麼該arrs數組中的push方法不會被Object.defineProperty監聽到 */ arrs.__proto__ = arrayMethods; /* * 會執行 Object.defineProperty 中的push方法, * 因此會打印 參數為:2, 3 */ arrs.push(2, 3); console.log(arrs); // 輸出 [1, 2, 3];
如上代碼,首先我們獲取原型上的方法,使用代碼:var arrayProto = Array.prototype; 然後我們使用Object.create()方法創建一個相同的對象arrayMethods(為了避免污染全局),因此該對象會有 Array.prototype 中的所有屬性和方法。然後對該arrayMethods中的push方法進行監聽。監聽成功後,調用數組真正的push方法,把值push進去。
注意:我們在調用的時候 一定要 arrs.__proto__ = arrayMethods; 要把數組 arrs 的 __proto__ 指向了 arrayMethods 才會被監聽到的。
理解__proto__ 是什麼呢?
var Kongzhi = function () {}; var k = new Kongzhi(); /* 打印: Kongzhi { __proto__: { constructor: fn() __proto__: { // ... } } } */ console.log(k); console.log(k.__proto__ === Kongzhi.prototype); // ture
如上代碼,我們首先定義了一個Kongzhi的構造函數,然後實列化該構造函數,最後賦值給k, 那麼new 時候,我們看new做了哪些事情?
其實我們可以把new的過程拆成如下:
var k = {}; // 初始化一個對象 k.__proto__ = Kongzhi.prototype; Kongzhi.call(k);
因此我們可以把如上的代碼改成如下也是可以的:
var Kongzhi = function () {}; var k = {}; k.__proto__ = Kongzhi.prototype; Kongzhi.call(k); console.log(k); console.log(k.__proto__ === Kongzhi.prototype); // ture
和上面的效果一樣的。
現在我們來理解下 __proto__ 到底是什麼?其實在我們定義一個對象的時候,它內部會默認初始化一個屬性為 __proto__; 比如如代碼可以驗證: var obj = {}; console.log(obj);我們在控制台上看下結果就可以看到,當我們訪問對象中的某個屬性的時候,如果這個對象內部不存在這個屬性的話,那麼它就會去 __proto__ 里去找這個屬性,這個__proto__又會有自己的 __proto__。因此會這樣一直找下去,這就是我們以前常說的原型鏈的概念。
我們可以再來看如下代碼:
var Kongzhi = function() {}; Kongzhi.prototype.age = function() { console.log(31) }; var k = new Kongzhi(); k.age(); // 會打印出 31
如上代碼,首先 var k = new Kongzhi(); 因此我們可以知道 k.__proto__ = Kongzhi.prototype;所以當我們調用 k.age()方法的時候,首先 k 中沒有age()這個方法,
因此會去它的 __proto__ 中去找,也就是 Kongzhi.prototype中去找,Kongzhi.prototype.age = function() {}; 正好有這個方法,因此就會執行。
對__proto__ 理解概念後,我們再來看上面中這句代碼:arrs.__proto__ =arrayMethods;也就是可以繼續轉化變成如下代碼:
arrs.__proto__ = Object.create(Array.prototype); 同樣的道理,我們使用Object.defineProperty去監聽 arrayMethods這個新數組原型的話,如代碼:Object.defineProperty(arrayMethods, ‘push’, {});因此使用arrs.push(2, 3) 的時候也會被 Object.defineProperty 監聽到的。因為 arrs.__proto__ === arrayMethods 的。
如上只是一個簡單的實現,為了把數組中的所有方法都加上,因此代碼改造成如下所示:
function renderFunc() { console.log('html頁面被渲染了'); } // 定義數組的常見有的方法 var methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']; // 先獲取原型上的方法 var arrayProto = Array.prototype; // 創建一個新對象原型,並且重寫methods中的方法 var arrayMethods = Object.create(arrayProto); methods.forEach((method) => { Object.defineProperty(arrayMethods, method, { enumerable: false, writable: true, configurable: true, value(...args) { console.log('數組被調用了'); // 調用數組中的方法 var original = arrayProto[method]; original.apply(this, args); renderFunc(); } }) }); /* * */ function observer(obj) { if (Array.isArray(obj)) { obj.__proto__ = arrayMethods; } else if (typeof obj === 'object'){ for (const key in obj) { defineReactive(obj, key, obj[key]); } } } function defineReactive(obj, key, value) { // 遞歸循環 observer(value); Object.defineProperty(obj, key, { get: function() { console.log('監聽getter函數'); return value; }, set: function(newValue) { // 遞歸循環 observer(value); if (newValue === value) { return; } value = newValue; renderFunc(); console.log('監聽setter函數'); } }); } // 初始化 var obj = [1, 2]; observer(obj); /* * 調用push方法,會被監聽到,因此會打印:數組被調用了 * 然後調用 renderFunc 方法,打印:html頁面被渲染了 */ obj.push(3); console.log(obj); // 打印:[1, 2, 3] console.log('-----------'); var obj2 = {'name': 'kongzhi111'}; observer(obj2); // 會調用getter函數,打印:監聽getter函數, 同時打印值: kongzhi111 console.log(obj2.name); console.log('-----------'); /* 如下會先調用:renderFunc() 函數,因此打印:html頁面被渲染了 同時會打印出:監聽setter函數 */ obj2.name = 'kongzhi2222';
如上代碼演示可以看到,我們對數組中的 ‘pop’, ‘shift’, ‘unshift’, ‘sort’, ‘reverse’, ‘splice’, ‘push’ 等方法做了重寫操作,會監聽到數組中這些方法。observer方法中會判斷是否是數組,如果是數組的話,obj.__proto__ = arrayMethods; 讓該對象的 __proto__ 指向了原型。因此調用數組上的方法就會被監聽到。當然__proto__這邊有瀏覽器兼容問題的,這邊先沒有處理,待會在Vue源碼中我們可以看到尤大是使用什麼方式來處理__proto__的兼容性的。同時也對對象進行了監聽了。如上代碼可以看得到。
2.4 使用Proxy來實現數據監聽
Proxy是Es6的一個新特性,Proxy會在目標對象之前架設一層 “攔截”, 當外界對該對象訪問的時候,都必須經過這層攔截,Proxy就相當於這種機制,類似於代理的含義,它可以對外界訪問對象之前進行過濾和改寫該對象。
目前Vue使用的都是Object.defineProperty()方法針對對象通過 遞歸 + 遍歷的方式來實現對數據的監控的。
我們也知道,通過該方法,不能觸發數組中的方法,比如push,shift等這些,我們需要在vue中重寫該方法,因此Object.defineProperty()方法存在如下缺點:
1. 監聽數組的方法不能觸發Object.defineProperty方法中set操作(如果我們需要監聽的話,我們需要重寫數組的方法)。
2. 必須遍歷每個對象的每個屬性,如果對象嵌套比較深的話,我們需要遞歸調用。
因此為了解決Object.defineProperty() 如上的缺點,我們監聽對象數據的變化時,我們可以使用Proxy來解決,但是Proxy有兼容性問題。我們這邊先來了解下Proxy的基本使用方法吧!
Proxy基本語法如下:
const obj = new Proxy(target, handler);
參數說明如下:
target: 被代理的對象。
handler: 是一個對象,聲明了代理target的一些操作。
obj: 是被代理完成之後返回的對象。
下面我們來看一個如下簡單的demo如下:
const target = { 'name': "kongzhi" }; const handler = { get: function(target, key) { console.log('調用了getter函數'); return target[key]; }, set: function(target, key, value) { console.log('調用了setter函數'); target[key] = value; } }; console.log('------') const testObj = new Proxy(target, handler); console.log(testObj.name); testObj.name = '1122'; console.log(testObj.name);
如上代碼,我們調用 console.log(testObj.name); 這句代碼的時候,會首先調用get()函數,因此會打印:’調用了get函數’; 然後輸出 ‘kongzhi’ 信息出來,當執行 testObj.name = ‘1122’; 這句代碼的時候,會調用set()函數,因此會打印: “調用了setter函數” 信息出來,接着打印 console.log(testObj.name); 又會調用get()函數, 因此會打印 “調用了getter函數” 信息出來,接着執行:console.log(testObj.name); 打印信息 ‘1122’ 出來。
如上:target是被代理的對象,handler是代理target的,handler上有set和get方法,當我們每次打印target中的name屬性值的時候會自動執行handler中get函數方法,當我們每次設置 target.name屬性值的時候,會自動調用handler中的set方法,因此target對象對應的屬性值會發生改變。同時改變後的testObj對象也會發生改變。
我們下面再來看一個使用 Proxy 代理對象的demo,如下代碼:
function render() { console.log('html頁面被渲染了'); } const obj = { name: 'kongzhi', love: { book: ['nodejs', 'javascript', 'css', 'html'], xxx: '111' }, arrs: [1, 2, 3] }; const handler = { get: function(target, key) { if (target[key] && typeof target[key] === 'object') { return new Proxy(target[key], handler); } return Reflect.get(target, key); }, set: function(target, key, value) { render(); return Reflect.set(target, key, value); } }; let proxy = new Proxy(obj, handler); // 會調用set函數,然後執行 render 函數 最後打印 "html頁面被渲染了" proxy.name = 'tugenhua'; // 打印:tugenhua console.log(proxy.name); // 會調用set函數,然後執行 render 函數 最後打印 "html頁面被渲染了" proxy.love.xxx = '222'; // 打印:222 console.log(proxy.love.xxx); // 會調用set函數,然後執行 render 函數 最後打印 "html頁面被渲染了" proxy.arrs[0] = 4; // 打印:4 console.log(proxy.arrs[0]); // 打印: 3 但是不會調用 set 函數 console.log(proxy.arrs.length);
三. Observer源碼解析
<!DOCTYPE html> <html> <head> <title>Vue.js github commits example</title> <!-- 下面的是vue源碼 --> <script src="../../dist/vue.js"></script> </head> <body> <div id="demo"> <span v-for="(item, index) in arrs"> {{item}} </span> </div> <script type="text/javascript"> new Vue({ el: '#demo', data: { branches: ['master', 'dev'], currentBranch: 'master', commits: null, arrs: [1, 2] } }); </script> </body> </html>
如上demo代碼,我們在vue實例化頁面後,會首先調用 src/core/instance/index.js 的代碼,基本代碼如下:
import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue
如上Vue構造函數中首先會判斷是否是正式環境和是否實例化了Vue。然後會調用 this._init(options)方法。因此進入:src/core/instance/init.js代碼,主要代碼如下:
import { initState } from './state'; export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this; ..... 省略很多代碼 initState(vm); ..... 省略很多代碼 } }
因此就會進入 src/core/instance/state.js 主要代碼如下:
import { set, del, observe, defineReactive, toggleObserving } from '../observer/index' .... 省略很多代碼 export function initState (vm: Component) { ..... if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } ..... } .... 省略很多代碼 function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} .... 省略了很多代碼 // observe data observe(data, true /* asRootData */) }
如上代碼我們就可以看到,首先會調用 initState 這個函數,然後會進行 if 判斷 opts.data 是否有data這個屬性,該data就是我們的在 Vue實例化的時候傳進來的,之前實列化如下:
new Vue({ el: '#demo', data: { branches: ['master', 'dev'], currentBranch: 'master', commits: null, arrs: [1, 2] } });
如上的data,因此 opts.data 就為true,有這個屬性,因此會調用 initData(vm) 方法,在 initData(vm) 函數中,如上代碼我們也可以看到,最後會調用 observe(data, true /* asRootData */) 方法。該方法中的data參數值就是我們之前 new Vue({ data: {} }) 中的data值,我們通過打斷點的方式可以看到如下值:
因此會進入 src/core/observer/index.js 中的代碼 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) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
執行 observe 函數代碼,如上代碼所示,該代碼的作用是給data創建一個 Observer實列並返回,從最後一句代碼我們可以看得到,如上代碼 ob = new Observer(value); return ob;
如上代碼首先會if 判斷,該value是否有 ‘__ob__’ 這個屬性,我們value是沒有 __ob__ 這個屬性的,如果有 __ob__這個屬性的話,說明已經實列化過Observer,如果實列化過,就直接返回該實列,否則的話,就實例化 Observer, Vue的響應式數據都會有一個__ob__的屬性,裏面存放了該屬性的Observer實列,目的是防止重複綁定。我們現在先來看看 代碼:
if (hasOwn(value, ‘__ob__’)) {} 中的value屬性值如下所示:
如上我們可以看到,value是沒有 __ob__ 這個屬性的,因此會執行 ob = new Observer(value); 我們再來看看new Observer 實列化過程中發生了什麼。代碼如下:
export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
如上代碼我們可以看得到,首先會調用 this.dep = new Dep() 代碼,該代碼在 src/core/observer/dep.js中,基本代碼如下:
export default class Dep { ...... constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null; ......
Dep代碼的作用和我們之前講的一樣,就是消息訂閱器,該訂閱器的作用是收集所有的訂閱者。
代碼往下執行,我們就會執行 def(value, ‘__ob__’, this) 這句代碼,因此會調用 src/core/util/lang.js 代碼,
代碼如下:
// ...... 省略了很多的代碼 import { arrayMethods } from './array'; // ...... 省略了很多的代碼 /** @param obj; obj = { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master" }; @param key "__ob__"; @param val: Observer對象 val = { dep: { "id": 2, subs: [] }, vmCount: 0, value: { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master" } }; */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
如上代碼我們可以看得到,我們使用了 Object.defineProperty(obj, key, {}) 這樣的方法監聽對象obj中的 __ob__ 這個key。但是obj對象中又沒有該key,因此Object.defineProperty會在該對象上定義一個新屬性為 __ob__, 也就是說,如果我們的數據被 Object.defineProperty綁定過的話,那麼綁定完成後,就會有 __ob__這個屬性,因此我們之前通過了這個屬性來判斷是否已經被綁定過了。我們可以看下demo代碼來理解下 Object.defineProperty的含義:
代碼如下所示:
var obj = { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master" }; var key = "__ob__"; var val = { dep: { "id": 2, subs: [] }, vmCount: 0, value: { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master" } }; Object.defineProperty(obj, key, { value: val, writable: true, configurable: true }); console.log(obj);
打印obj的值如下所示:
如上我們看到,我們通過 Object.defineProperty()方法監聽對象後,如果該對象沒有該key的話,就會在該obj對象中添加該key屬性。
再接着 就會執行如下代碼:
if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) }
如上代碼,首先會判斷該 value 是否是一個數組,如果不是數組的話,就執行 this.walk(value)方法,如果是數組的話,就判斷 hasProto 是否為true(也就是判斷瀏覽器是否支持__proto__屬性),hasProto 源碼如下:
export const hasProto = '__proto__' in {};
如果__proto__指向了對象原型的話(換句話說,瀏覽器支持__proto__),就調用 protoAugment(value, arrayMethods) 函數,該函數的代碼如下:
function protoAugment (target, src: Object) { target.__proto__ = src }
其中 arrayMethods 基本代碼在 源碼中: src/core/observer/array.js 中,該代碼是對數組中的方法進行重寫操作,和我們之前講的是一樣的。基本代碼如下所示:
import { def } from '../util/index' const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) });
現在我們再來看之前的代碼 protoAugment 函數中,其實這句代碼和我們之前講的含義是一樣的,是讓 value對象參數指向了 arrayMethods 原型上的方法,然後我們使用 Obejct.defineProperty去監聽數組中的原型方法,當我們在data對象參數arrs中調用數組方法,比如push,unshift等方法就可以理解為映射到 arrayMethods 原型上,因此會被 Object.defineProperty方法監聽到。因此會執行對應的set/get方法。
如上 methodsToPatch.forEach(function (method) { } 代碼中,為什麼針對 方法為 ‘push, unshift, splice’ 等一些數組新增的元素也會調用 ob.observeArray(inserted) 進行響應性變化。inserted 參數為一個數組。也就是說我們不僅僅對data現有的元素進行響應性監聽,還會對數組中一些新增刪除的元素也會進行響應性監聽。…args運算符會轉化為數組。
比如如下簡單的測試代碼如下:
function a(...args) { console.log(args); // 會打印 [1] }; a(1); // 函數方法調用 // observeArray 函數代碼如下: observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } }
如上代碼可以看到,我們對使用 push, unshift, splice 新增/刪除 的元素也會遍歷進行監聽, 再回到代碼中,為了方便查看,繼續看下代碼,回到如下代碼中:
if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) }
如果我們的瀏覽器不支持 hasProto, 也就是說 有的瀏覽器不支持__proto__這個屬性的話,我們就會調用copyAugment(value, arrayMethods, arrayKeys); 方法去處理,我們再來看下該方法的源碼如下:
/* @param {target} target = { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master", __ob__: { dep: { id: 2, sub: [] }, vmCount: 0, commits: null, branches: ["master", "dev"], currentBranch: "master" } }; @param {src} arrayMethods 數組中的方法實列 @param {keys} ["push", "shift", "unshift", "pop", "splice", "reverse", "sort"] */ function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }
如上代碼可以看到,對於瀏覽器不支持 __proto__屬性的話,就會對數組的方法進行遍歷,然後繼續調用def函數進行監聽:
如下 def代碼,該源碼是在 src/core/util/lang.js 中:
export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
回到之前的代碼,如果是數組的話,就會調用 this.observeArray(value) 方法,observeArray方法如下所示:
observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } };
如果它不是數組的話,那麼有可能是一個對象,或其他類型的值,我們就會調用 else 裏面中 this.walk(value) 的代碼,walk函數代碼如下所示:
walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } }
如上代碼,進入walk函數,obj是一個對象的話,使用 Object.keys 獲取所有的keys, 然後對keys進行遍歷,依次調用defineReactive函數,該函數代碼如下:
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() // 獲取屬性自身的描述符 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters /* 檢查屬性之前是否設置了 getter / setter 如果設置了,則在之後的 get/set 方法中執行 設置了的 getter/setter */ const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } /* observer源碼如下: 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) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } let childOb = !shallow && observe(val); 代碼的含義是:遞歸循環該val, 判斷是否還有子對象,如果 還有子對象的話,就繼續實列化該value, */ let childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 如果屬性原本擁有getter方法的話則執行該方法 const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { // 如果有子對象的話,對子對象進行依賴收集 childOb.dep.depend(); // 如果value是數組的話,則遞歸調用 if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { /* 如果屬性原本擁有getter方法則執行。然後獲取該值與newValue對比,如果相等的 話,直接return,否則的值,執行賦值。 */ const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { // 如果屬性原本擁有setter方法的話則執行 setter.call(obj, newVal) } else { // 如果屬性原本沒有setter方法則直接賦新值 val = newVal } // 繼續判斷newVal是否還有子對象,如果有子對象的話,繼續遞歸循環遍歷 childOb = !shallow && observe(newVal); // 有值發生改變的話,我們需要通知所有的訂閱者 dep.notify() } }) }
如上 defineReactive 函數,和我們之前自己編寫的代碼類似。上面都有一些注釋,可以稍微的理解下。
如上代碼,如果數據有值發生改變的話,它就會調用 dep.notify()方法來通知所有的訂閱者,因此會調用 Dep中的notice方法,我們繼續跟蹤下看下該對應的代碼如下(源碼在:src/core/observer/dep.js):
import type Watcher from './watcher' export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; .... notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } .... }
在notice方法中,我們循環遍歷訂閱者,然後會調用watcher裏面的update的方法來進行派發更新操作。因此我們繼續可以把視線轉移到 src/core/observer/watcher.js 代碼內部看下相對應的代碼如下:
export default class Watcher { ... update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } ... }
如上update方法,首先會判斷 this.lazy 是否為true,該參數的含義可以理解為懶加載類型。
其次會判斷this.sync 是否為同步類型,如果是同步類型的話,就會直接調用 run()函數方法,因此就會直接立刻執行回調函數。我們下面可以稍微簡單的看下run()函數方法如下所示:
run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
如上代碼我們可以看到,const value = this.get(); 獲取到了最新值,然後立即調用 this.cb.call(this.vm, value, oldValue); 執行回調函數。
否則的話就調用 queueWatcher(this);函數,從字面意思我們可以理解為隊列Watcher, 也就是說,如果某一次數據發生改變的話,我們先把該更新的數據緩存起來,等到下一次DOM更新的時候會執行。我們可以理解為異步更新,異步更新往往是同一事件循環中多次修改同一個值,那麼Watcher就會被緩存多次。
理解同步更新和異步更新
同步更新:
上面代碼中執行 this.run()函數是同步更新,所謂的同步更新是指當觀察者的主體發生改變的時候會立刻執行回調函數,來觸發更新代碼。但是這種情況,在日常的開發中並不會有很多,在同一個事件循環中可能會改變很多次,如果我們每次都觸發更新的話,那麼對性能來講會非常損耗的,因此在日常開發中,我們使用的異步更新比較多。
異步更新:
Vue異步執行DOM更新,只要觀察到數據的變化,Vue將開啟一個隊列,如果同一個Watcher被觸發多次,它只會被推入到隊列中一次。那麼這種緩衝對於去除一些重複操作的數據是很有必要的,因為它不會重複DOM操作。
在下一次的事件循環nextTick中,Vue會刷新隊列並且執行,Vue在內部會嘗試對異步隊列使用原生的Promise.then和MessageChannel。如果不支持原生的話,就會使用setTimeout(fn, 0)代替操作。
我們現在再回到代碼中,我們需要運行 queueWatcher (this) 函數,該函數的源碼在 src/core/observer/scheduler.js 中,如下代碼所示:
let flushing = false; let has = {}; // 簡單用個對象保存一下wather是否已存在 export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
如上代碼,首先獲取 const id = watcher.id; 如果 if (has[id] == null) {} 為null的話,就執行代碼,如果執行後會把 has[id] 設置為true。防止重複執行。接着代碼又會判斷 if (!flushing) {};如果flushing為false的話,就執行代碼: queue.push(watcher); 可以理解為把 Watcher放入一個隊列中,那為什麼要判斷 flushing 呢?那是因為假如我們正在更新隊列中watcher的時候,這個時候我們的數據又被放入隊列中怎麼辦呢?因此我們加了flushing這個參數來表示隊列的更新狀態。
如上flushing代表的更新狀態的含義,那麼這個更新狀態又分為2種情況。
第一種情況是:flushing 為false,說明這個watcher還沒有處理,就找到這個watcher在隊列中的位置,並且把最新的放在後面,如代碼:queue.push(watcher);
第二種情況是:flushing 為true,說明這個watcher已經更新過了,那麼就把這個watcher再放到當前執行的下一位,當前watcher處理完成後,再會立即處理這個新的。如下代碼:
let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher);
最後代碼就會調用 nextTick 函數的代碼去異步執行回調。nextTick下文會逐漸講解到,我們這邊只要知道他是異步執行即可。因此watcher部分代碼先理解到此了。