寫了一半關機了,又得重新寫,好氣。
上一節講到initData函數,其中包含格式化、代理、監聽。
// Line-3011 function initData(vm) { var data = vm.$options.data; //data = vm._data = ... 格式化data // ...proxy(vm, "_data", keys[i]); 代理 // 監聽 observe(data, true /* asRootData */ ); }
這一節重點開始跑observe函數,該函數接受2個參數,一個是數據,一個布爾值,代表是否是頂層根數據。
// Line-899
function observe(value, asRootData) { if (!isObject(value)) { return } var ob; // 判斷是否有__ob__屬性 即是否已被監聽
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } // 若無進行條件判斷
else if ( observerState.shouldConvert && // 是否應該被監聽 默認為true
!isServerRendering() && // 是否是服務器渲染
(Array.isArray(value) || isPlainObject(value)) && // 數據必須為數組或對象
Object.isExtensible(value) && // 是否可擴展 => 能添加新屬性
!value._isVue // vue實例才有的屬性
) { // 生成一個觀察者
ob = new Observer(value); } // 根數據記數屬性++
if (asRootData && ob) { ob.vmCount++; } return ob }
observe函數除去大量的判斷,關鍵部分就是new了一個觀察者來進行數據監聽,所以直接跳進該構造函數:
// Line-833
var Observer = function Observer(value) { // data
this.value = value; // 依賴收集
this.dep = new Dep(); this.vmCount = 0; // 通過Object.defineProperty定義__ob__屬性 this指向Observer實例
def(value, '__ob__', this); // 根據類型調用不同的遍歷方法
if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } };
這個構造函數給實例綁了3個屬性,分別為data對象的value、記數用的vmCount、依賴dep,接着根據數據類型調用不同的遍歷方法進行依賴收集。
Dep對象比較簡單,包含2個屬性和4個對應的原型方法,如下:
// Line-720 // 超級簡單 var Dep = function Dep() { this.id = uid++; this.subs = []; }; // 原型方法 Dep.prototype.addSub = function addSub(sub) { this.subs.push(sub); }; Dep.prototype.removeSub = function removeSub(sub) { remove(this.subs, sub); }; Dep.prototype.depend = function depend() { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify() { // stabilize the subscriber list first var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };
可見,new出來的dep實例只有2個屬性,一個是每次+1的計數uid,還有一個是依賴收集數組。
原型上的4個方法分別對應增、刪、添加依賴、廣播,由於暫時用不到update函數,所以先放着。
接着,用自定義的def方法把__ob__屬性綁到了生成的observe實例上,該屬性引用了自身。
最后,根據value是類型是數組還是對象,調用不同的方法進行處理。案例中傳進來的value是一個object,所以會跳到walk方法中。
這里不妨看看如果是數組會怎樣。
// Line-838 if (Array.isArray(value)) { // 判斷是否支持__proto__屬性 var augment = hasProto ? protoAugment : copyAugment; // 原型擴展 augment(value, arrayMethods, arrayKeys); // 數組監聽方法 this.observeArray(value); }
其中,根據環境是否支持__proto__分別調用protoAugment或copyAugment,這兩個方法比較簡單,上代碼就能明白。
// Line-876 function protoAugment(target, src) { // 直接指定原型 target.__proto__ = src; } // Line-887 function copyAugment(target, src, keys) { // 遍歷keys // 調用def(tar,key,value) => (tar[key] = (value => src[key])) for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; def(target, key, src[key]); } }
選擇了對應的方法就開始調用,傳進的參數除了value還是兩個奇怪的值:arrayMethods、arrayKeys。
// Line-767 // 創建一個對象 原型為數組對象的原型 var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); console.log(Array.isArray(arrayMethods)); //false console.log('push' in arrayMethods); //true // Line-814 var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
簡單來講,arrayMethods就是一個對象,擁有數組的方法但不是數組。
Object.getOwnPropertyNames方法以數組形式返回對象所有可枚舉與不可枚舉方法,所以arrayKeys直接在控制台打印可以看到:
最后,不管選擇哪個方法,都會將“改造過”的數組方法添加到value對象上,由於代碼跑不到,等下次給出具體值吧。這里接着會調用observeArray方法,將數組value穿進去。
// Line-865 Observer.prototype.observeArray = function observeArray(items) { // 遍歷分別調用observe方法 for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };
繞了一圈,最后還是遍歷value,挨個調用observe方法,並指向了walk方法。
// Line-855 Observer.prototype.walk = function walk(obj) { // 獲取對象的鍵 var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { // 核心方法 defineReactive$$1(obj, keys[i], obj[keys[i]]); } };
這個方法比較簡單,獲取傳進來的對象鍵,遍歷后調用defineReactive$$1方法。看名字也就差不多明白了,這是響應式的核心函數,雙綁爸爸。
下節再來說這個,完結完結!
補充tips:
之前有一段代碼,我說將改造過的數組方法添加到數組value上,這個改造是什么意思呢?其實關於arrayMethods代碼沒有全部貼出來,這里做簡單的解釋。
// Line-767 var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); // 以下數組方法均會造成破壞性操作 [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function(method) { // 緩存原生方法 var original = arrayProto[method]; // 修改arrayMethods上的數組方法 def(arrayMethods, method, function mutator() { // 將argument轉換為真數組 var arguments$1 = arguments; var i = arguments.length; var args = new Array(i); while (i--) { args[i] = arguments$1[i]; } // 首先執行原生方法 var result = original.apply(this, args); var ob = this.__ob__; var inserted; // 值添加方法 // push和unshift會添加一個值 即args // splice(a,b,c,..)方法只有c后面是添加的值 所以用slice排除前兩個參數 switch (method) { case 'push': inserted = args; break case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } // 對添加的值調用數組監聽方法 if (inserted) { ob.observeArray(inserted); } // 廣播變化 提示DOM更新及其他操作 ob.dep.notify(); return result }); });
完整的arrayMethods如上所述,解釋大部分都寫出來了,這也是vue通過對數組方法的劫持來達到變化監聽的原理,對象的劫持下節再來分析。
慣例,來一張圖: