前言
上一篇文章,大概的講解了Vue例項化前的一些配置,如果沒有看到上一篇,通道在這裡:Vue 原始碼解析 - 例項化 Vue 前(一)
在上一篇的結尾,我說這一篇後著重講一下 defineReactive 這個方法,這個方法,其實就是大家可以在外面看見一些文章對 vue 實現響應式資料原理的過程。
在這裡,根據原始碼,我決定在給大家講一遍,看看和大家平時自己看的,有沒有區別,如果有遺漏的點,歡迎評論
正文
先來一段 defineReactive 的原始碼:
//在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()
雖然很多前端的大佬知道它的作用,但是我相信還是有一些朋友是不認識的,我希望我寫的文章,不只是傳達vue內部實現的一些精神,更能幫助一些小白去了解一些原生的api。
defineProperty
在 MDN 上的解釋是:
Object.defineProperty() 方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性, 並返回這個物件。
複製程式碼
這裡,其實就是用來實現響應式資料的核心之一,主要做的事情就是資料的更新, Object.defineProperty() 最多接收三個引數:obj , prop , descriptor:
obj:
要在其上定義屬性的物件。
複製程式碼
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。
複製程式碼
Object.getOwnPropertyDescriptor()
obj:
需要查詢的目標物件
複製程式碼
prop:
目標物件內屬性名稱(String型別)
複製程式碼
descriptor:
將被定義或修改的屬性描述符。
複製程式碼
返回值:
返回值其實就是 Object.defineProperty() 中的那六個在 descriptor
物件中可設定的屬性,這裡就不廢話浪費篇幅了,大家看一眼上面就好
複製程式碼
defineReactive 的引數我就不一一列舉的來講了,大概從引數名也可以知道大概的意思,具體講函式內容的時候,在細講。
Dep
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 的方法:
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) {
}
});
複製程式碼
首先,要確保要監聽的該屬性,是可列舉、可修改的的;
get
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 方法的;
所以在這裡,作者給所有的陣列項,新增了一個依賴,這樣每一個陣列選項,都有了自己的監聽,當它被改變的時候,會根據監聽的依賴,去做對應的更新。
set
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 實現的響應式資料的原理,拋析的就差不多了,但是整體涉及的東西比較多,可能看起來會比較費勁一些,這裡我概括一下:
- 每次在監聽某一個屬性時,要先例項化一個佇列 Dep,負責監聽依賴和通知依賴;
- 確認當前要監聽的屬性是否存在,並且是可修改的;
- 如果沒有接收到引數 val,並且引數只接收到2個,那麼就直接把 val 設定成當前的屬性的值,不存在就是 undefined;
- 判斷當前要監聽的值是需要深拷貝還是淺拷貝,如果是深拷貝,那麼就去檢查當前的值是否被監聽,沒有被監聽,那麼就去例項化一個監聽物件;
- 在呼叫 get 方法,獲取到當前屬性的值,不存在就接收呼叫該方法時接收到的值;
- 檢查當前的佇列,要對哪一個 obj 進行變更,如果存在檢查的目標的話,那就新增一個依賴;
- 如果存在觀察例項的話,在去檢查一下當前的值是否是陣列,如果是陣列的話,那麼就做一個陣列項的依賴檢查;
- 在更新值的時候,發現當前值和要改變的值是相同的,那麼就不進行任何操作;
- 如果是開發環境下,還會執行一個回撥,該回撥實在值改變前但是符合改變條件時執行的;
- 如果當前的屬性存在 setter 方法,那麼就把當前的值傳給 setter 方法,並讓當前的 setter 方法的 this 指向當前的 obj,如果不存在,直接用新值覆蓋舊值就好;
- 如果是深拷貝的話,就去檢查遍當前的值是否被觀察,如果沒有被觀察,就進行觀察;(上面大家可能有發現,它已經進行了一次觀察,為什麼還要執行呢?因為上面是在初始化的時候去觀察的,當該值改變以後,比如型別改變,是要進行重新觀察,確保如果改變為類似陣列的值的時候,還可以進行雙向繫結)
- 最後,通知所有新增對該屬性進行依賴的位置。
結束語
對應 vue 的響應式資料,到這裡就總結完了,未來在例項化 vue 物件的地方,會涉及到很多有關響應式資料的地方,所以建議大家好好看一下這裡。
對於原始碼,我們瞭解了作者的思想就好,我們不一定要完全按照作者的寫法來寫,我們要學習的,是他的程式設計思想,而不是他的寫法,其實好多地方我覺得寫的不是很合適,但是我不是很明白為什麼要這麼做,也許是我水平還比較低,沒有涉及到,接下來我會對這些疑問點,進行總結,去研究為什麼要這麼做,如果不合適,我會在 github 中新增 issues 到時候會把連結丟擲來,以供大家參考學習。
最後還是老話,點贊,點關注,有問題了,評論區開噴就好