Vue 原始碼解析(例項化前) – 響應式資料的實現原理

熱情的劉大爺發表於2019-01-14

前言

上一篇文章,大概的講解了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 到時候會把連結丟擲來,以供大家參考學習。

最後還是老話,點贊,點關注,有問題了,評論區開噴就好

來源:https://juejin.im/post/5c386acb518825261f735384

相關文章