前言
在上一章節我們已經粗略的分析了整個的Vue 的原始碼(還在草稿箱,需要梳理清楚才放出來),但是還有很多東西沒有深入的去進行分析,我會通過如下幾個重要點,進行進一步深入分析。
- 深入瞭解 Vue 響應式原理(資料攔截)
- 深入瞭解 Vue.js 是如何進行「依賴收集」,準確地追蹤所有修改
- 深入瞭解 Virtual DOM
- 深入瞭解 Vue.js 的批量非同步更新策略
- 深入瞭解 Vue.js 內部執行機制,理解呼叫各個 API 背後的原理
這一章節我們針對1. 深入瞭解 Vue 響應式原理(資料攔截) 來進行分析。
initState
我們在上一章節中已經分析了,在初始化Vue例項的時候,會執行_init
方法, 其中會執行initState
方法, 這個方法非常重要, 其對我們new Vue
例項化物件時,傳遞經來的引數props
, methods
,data
, computed
,watch
的處理。
其程式碼如下:
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
複製程式碼
這一章節,我們只分析對data
的處理, 也就是initData(vm)
方法, 其程式碼如下(刪除了異常處理的程式碼):
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
複製程式碼
從上面的程式碼分析,首先可以得出如下一個
總結:
- data裡面的key一定不能和methods, props裡面的key重名
proxy(vm, "_data", key);
只是將data
裡面的屬性重新掛載(代理)在vm
例項上,我們可以通過如下兩種方式訪問data
裡面的資料, 如vm.visibility
或者vm._data.visibility
效果是一樣的。observe(data, true /* asRootData */);
是最重要的一個方法,下面我們來分析這個方法
observe
observe
中文翻譯就是觀察
, 就是將原始的data
變成一個可觀察的物件
, 其程式碼如下(刪除了一些邏輯判斷):
function observe (value, asRootData) {
ob = new Observer(value);
}
複製程式碼
這個方法就是new
了一個Observer
物件, 其建構函式如下:
var Observer = function Observer (value) {
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
做特殊處理,我們現在傳遞的物件是一個Object
, 但是裡面todos
是一個陣列,我們後面會分析陣列處理的情況, 接下來呼叫this.walk
方法,就是遍歷物件中的每一個屬性:
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
複製程式碼
defineReactive$$1
方法通過Object.defineProperty
來重新封裝data
, 給每一個屬性新增一個getter
,setter
來做資料攔截
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var 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) {
var 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 (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();
}
});
}
複製程式碼
defineReactive$$1
方法就是利用Object.defineProperty
來設定data
裡面已經存在的屬性來設定getter
,setter
, 具體get
和set
在什麼時候發揮效用我們先不分析。
var childOb = !shallow && observe(val);
是一個遞迴調observe
來攔截所有的子屬性。
在data
中的屬性todos
是一個陣列, 我們又回到observe
方法, 其主要目的是通過ob = new Observer(value);
來生成一個Observer
物件:
var Observer = function Observer (value) {
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
有特殊的處理,下面我們我們來具體分析protoAugment
方法
protoAugment(陣列)
protoAugment(value, arrayMethods);
傳了兩個引數,第一個引數,就是我們的陣列,第二個引數arrayMethods
需要好好分析,是Vue
中對Array
的特殊處理的地方。
其原始碼檔案在vue\src\core\observer\array.js
下,
- 首先基於
Array.prototype
原型建立了一個新的物件arrayMethods
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
複製程式碼
- 重寫了
Array
如下7 個方法:
var 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
})
})
複製程式碼
總結:從上面可知, Vue
只會對上述七個方法進行監聽, 如果使用Array 的其他的方法是不會觸發Vue 的雙向繫結的。比如說用concat
,map
等方法都不會觸發雙向繫結。
this.$set
上面已經分析了Object
,Array
的資料監聽,但是上面的情況都是在初始化Vue
例項的時候,已經知道了data
中有哪些屬性了,然後對每個屬性進行資料攔截,現在有一種情況就是,如果我們有需要需要給data
動態的新增屬性,我們該怎麼做呢?
Vue
單獨開放出了一個介面$set
, 他掛載在vm
原型上,我們先說下其使用方式是:
this.$set(this.newTodo,"name", '30')
function set (target, key, val) {
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val
}
複製程式碼
通過上面的分析,使用$set
方法,需要注意如下幾點:
- target 不能是
undefined
,null
,string
,number
,symbol
,boolean
六種基礎資料型別 - target 不能直接掛載在
Vue
例項物件上, 而且不能直接掛載在rootdata
屬性上
$set
最終呼叫defineReactive$$1(ob.value, key, val);
方法去動態新增屬性, 並且給該屬性新增getter
,setter
動態新增的屬性,同樣也需要動態更新檢視,則是呼叫ob.dep.notify();
方法來動態更新檢視
總結
- 如果
data
屬性是一個Object
, 則將其將其進行轉換,主要是做如下兩件事情:
- 給物件新增一個
__ob__
的屬性, 其是一個Observer
物件
- 遍歷
data
的說有屬性('key'), 通過Object.defineProperty
設定其getter
和setter
來進行資料攔截
- 如果
data
(或者子屬性)是一個Array
, 則將其原型轉換成arrayMethods
(基於Array.prototype
原型建立的一個新的物件,但是重新定義了 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse')七個方法,來進行對Array
的資料攔截(這也就是Vue 對陣列操作,只有這七個方法能實現雙向繫結的原因)
在這篇文章我們已經分析了Vue 響應式原理 , 我們接下來會繼續分析深入瞭解 Vue.js 是如何進行「依賴收集」,準確地追蹤所需修改