上一篇文章,大概的講解了Vue實例化前的一些配置,若是沒有看到上一篇,通道在這裏:Vue 源碼解析 - 實例化 Vue 前(一)javascript
在上一篇的結尾,我說這一篇後着重講一下 defineReactive 這個方法,這個方法,其實就是你們能夠在外面看見一些文章對 vue 實現響應式數據原理的過程。前端
在這裏,根據源碼,我決定在給你們講一遍,看看和你們平時本身看的,有沒有區別,若是有遺漏的點,歡迎評論vue
先來一段 defineReactive 的源碼:java
//在Object上定義反應屬性。
function defineReactive ( obj, key, val, customSetter, shallow ) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
var getter = property && property.get;
if (!getter && arguments.length === 2) {
val = obj[key];
}
var setter = property && property.set;
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 (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
複製代碼
在講解這段源碼以前,我想先在開始講一下 Object 的兩個方法 Object.defineProperty() 和 Object.getOwnPropertyDescriptor() react
雖然不少前端的大佬知道它的做用,可是我相信仍是有一些朋友是不認識的,我但願我寫的文章,不僅是傳達vue內部實現的一些精神,更能幫助一些小白去了解一些原生的api。git
在 MDN 上的解釋是:github
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
複製代碼
這裏,其實就是用來實現響應式數據的核心之一,主要作的事情就是數據的更新, Object.defineProperty() 最多接收三個參數:obj , prop , descriptor:編程
obj:api
要在其上定義屬性的對象。
複製代碼
prop:數組
要定義或修改的屬性的名稱。
複製代碼
descriptor:
將被定義或修改的屬性描述符。
複製代碼
返回值:
被傳遞給函數的對象。
複製代碼
在這裏要注意一點:在ES6中,因爲 Symbol類型的特殊性,用Symbol類型的值來作對象的key與常規的定義或修改不一樣,而Object.defineProperty 是定義key爲Symbol的屬性的方法之一。
對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個具備值的屬性,該值多是可寫的,也可能不是可寫的。存取描述符是由getter-setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是二者。
數據描述符和存取描述符均具備如下可選鍵值:
configurable:
當且僅當該屬性的 configurable 爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。
默認值: false
複製代碼
enumerable:
當且僅當該屬性的 enumerable 爲 true 時,該屬性纔可以出如今對象的枚舉屬性中。
默認爲 false。
複製代碼
數據描述符同時具備如下可選鍵值:
value:
該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。
默認爲 undefined。
複製代碼
writable:
當且僅當該屬性的 writable 爲 true 時,value 才能被賦值運算符改變。
默認爲 false。
複製代碼
存取描述符同時具備如下可選鍵值:
get:
一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,可是會傳入this對象(因爲繼承關係,這裏的this並不必定是定義該屬性的對象)。
默認爲 undefined。
複製代碼
set:
一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法。該方法將接受惟一參數,即該屬性新的參數值。
默認爲 undefined。
複製代碼
obj:
須要查找的目標對象
複製代碼
prop:
目標對象內屬性名稱(String類型)
複製代碼
descriptor:
將被定義或修改的屬性描述符。
複製代碼
返回值:
返回值其實就是 Object.defineProperty() 中的那六個在 descriptor
對象中可設置的屬性,這裏就不廢話浪費篇幅了,你們看一眼上面就好
複製代碼
defineReactive 的參數我就不一一列舉的來說了,大概從參數名也能夠知道大概的意思,具體講函數內容的時候,在細講。
var dep = new Dep();
複製代碼
在一進入到 defineReactive 這個函數時,就實例化了一個Dep的構造函數,並把它指向了一個名爲dep的變量,下面,咱們來看看Dep這個構造函數都作了什麼:
var uid = 0;
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 () {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
Dep.target = null;
複製代碼
在實例化 Dep 以前,給 Dep 添加了一個 target 的屬性,默認值爲 null;
Dep在實例化的時候,聲明瞭一個 id 的屬性,每一次實例化Dep的id都是惟一的;
而後聲明瞭一個 subs 的空數組, subs 要作的事情,就是收集全部的依賴;
addSub:
從字面意思,你們也能夠看的出來,它就是作了一個添加依賴的動做;
removeSub:
其實就是移除了某一個依賴,只不過實現沒有在當前的方法裏寫,而是調用的一個 remove 的方法:
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
複製代碼
這個方法,就是從數組中,移除了某一項;
depend:
添加一個依賴數組項;
notify:
通知每個數組項,更新每個方法;
這裏 subs 調用了 slice 方法,官方註釋是 「 stabilize the subscriber list first 」 字面意思是 「首先穩定訂戶列表」,這裏我不是很清楚,若是知道的大佬,還請指點一下
複製代碼
Dep.target 在 Vue 實例化以前一直都是 null ,只有在 Vue 實例化後,實例化了一個 Watcher 的構造函數,在調用 Watcher 的 get 方法的時候,纔會改變 Dep.target 不爲 null ,因爲 Watcher 涉及的內容也不少,因此我準備單拿出一章內容,在 Vue 實例化以後去講解,如今,咱們就暫時看成 Dep.target 不爲空。
如今,Dep 構造函數講解的就差很少了,咱們繼續接着往下看:
var property = Object.getOwnPropertyDescriptor(obj, key);
複製代碼
方法返回指定對象上一個自有屬性對應的屬性描述符並賦值給property;
if (property && property.configurable === false) {
return
}
複製代碼
咱們要實現響應式數據的時候,要看當前的 object 上面是否有當前要實現響應式數據的這個屬性,若是沒有,而且 configurable 爲 false,那麼就直接退出該方法。
在上面咱們介紹過 configurable 這個屬性,若是它是 flase ,說明它是不容許被更改的,那麼就確定不支持響應式數據了,那確定是要退出該方法的。
var getter = property && property.get;
if (!getter && arguments.length === 2) {
val = obj[key];
}
複製代碼
獲取當前該屬性的 get 方法,若是沒有該方法,而且只有兩個參數(obj 和 key),那麼 val 就是直接從這個當前的 obj 裏面獲取。
var setter = property && property.set;
複製代碼
獲取當前屬性的 set 方法。
var childOb = !shallow && observe(val);
複製代碼
判斷是否要淺拷貝,若是傳的是 false ,那麼就是要進行深拷貝,這個時候,就須要把當前的值傳遞給 observe 的方法:
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
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
}
複製代碼
在 defineReactive 中,調用 observe 方法,只傳了一個參數,因此這裏是只有 value 一個值的,第二個值其實就是一個 boolean 值,用來判斷是不是根數據;
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
複製代碼
首先,要檢查當前的值是否是對象,或者說當前的值的原型是否在 VNode 上,那就直接 return 出當前方法, VNode 是一個構造函數,內容比較多,因此這一章暫時不講,接下來單獨寫一篇去講 VNode。
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
複製代碼
這裏用來判斷對象是否具備該屬性,而且對象上的該屬性原型是否指向的是 Observer ;
若是是,說明這個值是以前存在的,那麼變量 ob 就等於當前觀察的實例;
若是不是,則是作以下判斷:
var shouldObserve = true;
function toggleObserving (value) {
shouldObserve = value;
}
複製代碼
shouldObserve 用來判斷是否應該觀察,默認是觀察;
var _isServer;
var isServerRendering = function () {
if (_isServer === undefined) {
/* istanbul ignore if */
if (!inBrowser && !inWeex && typeof global !== 'undefined') {
// detect presence of vue-server-renderer and avoid
// Webpack shimming the process
_isServer = global['process'] && global['process'].env.VUE_ENV === 'server';
} else {
_isServer = false;
}
}
return _isServer
};
複製代碼
是否支持服務端渲染;
Array.isArray(value)
複製代碼
當前的值是不是數組;
isPlainObject(value)
複製代碼
用來判斷是不是Object;具體代碼上一篇文章當中有描述,入口在這裏:Vue 源碼解析 - 實例化 Vue 前(一)
Object.isExtensible(value)
複製代碼
判斷一個對象是不是可擴展的
value._isVue
複製代碼
判斷是否能夠被觀察到,初始化是在 initMixin 方法裏初始化的,這裏暫時先不作太多的介紹。
這麼多判斷的整體意思,就是用來判斷,當前的值,是不是被觀察的,若是沒有,那麼就建立一個新的出來,並賦值給變量 ob;
asRootData 若是是 true,而且 ob 也存在的話,那麼就給 vmCount 加 1;
最後返回一個 ob。
接下來,開始響應式數據的核心代碼部分了:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
},
set: function reactiveSetter (newVal) {
}
});
複製代碼
首先,要確保要監聽的該屬性,是可枚舉、可修改的的;
var value = getter ? getter.call(obj) : val;
複製代碼
先前,在前面把當前屬性的 get 方法,傳給 getter 變量,若是 getter 變量存在,那麼就把當前的 getter 的 this 指向當前的 obj 並傳給 value 變量;若是不存在,那麼就把當前方法接收到的 val 參數傳給 value 變量;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
複製代碼
每次在 get 的時候,判斷 Dep.target 是否爲空,若是不爲空,那麼就去添加一個依賴,調用實例對象 dep 的 depend 方法,這裏在 Watcher 的構造函數裏,還作了一些特殊處理,等到講解 Watcher 的時候,我會把這裏在帶過去一塊兒講一下。
反正你們記着,在 get 的時候添加了一個依賴就好。
若是是存在子級的話,而且給子級添加一個依賴:
function dependArray (value) {
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
複製代碼
若是當前的值是數組,那麼咱們就要給這個數組添加一個監聽,由於自己 Array 是不支持 defineProperty 方法的;
因此在這裏,做者給全部的數組項,添加了一個依賴,這樣每個數組選項,都有了本身的監聽,當它被改變的時候,會根據監聽的依賴,去作對應的更新。
var value = getter ? getter.call(obj) : val;
複製代碼
這裏,和 get 時候同樣,獲取當前的一個值,若是不存在,就返回函數接收到的值;
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
複製代碼
若是當前值和新的值同樣,那就說明沒有什麼變化,這樣就不須要改,直接 return 出去;
若是是在開發環境下,而且存在 customSetter 方法,那麼就調用它;
若是當前的屬性存在 set 方法,那麼就把 set 方法指向 obj,並把 newVal 傳過去;
若是不存在,那麼就直接把值給覆蓋掉;
若是不是淺拷貝的話,那麼就把當前的新值傳給 observe 方法,去檢查是否已經被觀察,而且把新的值覆蓋到 childOb 上;
最後調用 dep 的 notify 方法去通知全部的依賴進行值的更新。
到這裏,基本上 vue 實現的響應式數據的原理,拋析的就差很少了,可是總體涉及的東西比較多,可能看起來會比較費勁一些,這裏我歸納一下:
對應 vue 的響應式數據,到這裏就總結完了,將來在實例化 vue 對象的地方,會涉及到不少有關響應式數據的地方,因此建議你們好好看一下這裏。
對於源碼,咱們瞭解了做者的思想就好,咱們不必定要徹底按照做者的寫法來寫,咱們要學習的,是他的編程思想,而不是他的寫法,其實好多地方我以爲寫的不是很合適,可是我不是很明白爲何要這麼作,也許是我水平還比較低,沒有涉及到,接下來我會對這些疑問點,進行總結,去研究爲何要這麼作,若是不合適,我會在 github 中添加 issues 到時候會把連接拋出來,以供你們參考學習。
最後仍是老話,點贊,點關注,有問題了,評論區開噴就好