一、前言
這篇文章結合Vue2.7.16
的原始碼和一個Vue2
的專案,來詳細講解Vue2
實現響應式資料的核心程式碼
1.1 準備
-
安裝
@vue/cli
npm install -g @vue/cli
-
建立vue專案
vue create vue2-test
-
修改Vue例項的配置物件
二、響應式處理的入口
-
透過
new Vue()
呼叫Vue
建構函式,然後會執行裡面的this._init(options)
方法 -
_init(options)
方法定義在Vue.prototype
上,定義是透過執行initMixin(Vue)
實現的 -
在
_init(options)
方法中,做了一些初始化的操作。其中initState(vm)
用來初始化狀態。 -
initState(vm)
,按照props、methods、data、computed、watch的順序,對一些屬性做了初始化操作。其中initData(vm)
就是對配置物件中的data做響應式處理的。 -
在
initData()
方法中,做了以下處理:- 獲取到配置物件上面的data配置項,如果是用方法定義的就透過呼叫方法獲取,如果是用物件配置的,就直接訪問。然後將data繫結到
vm._data
上。 - 檢查data中的屬性名是否與methods、props中定義重複
- 透過
proxy()
,將vm._data
上的資料直接放在了vm
身上。內部就是使用Object.defineProperty()
實現透過vm.屬性
去訪問vm._data.屬性
。這樣訪問要方便一點。 - 呼叫
observe(data)
來對資料進行觀測,這也是響應式處理的入口
- 獲取到配置物件上面的data配置項,如果是用方法定義的就透過呼叫方法獲取,如果是用物件配置的,就直接訪問。然後將data繫結到
三、入口函式observe
-
observe接收一個value(第一次傳入的就是data),然後判斷其身上是否有
_ob_
屬性,該屬性用來標識是否已經被觀測。如果已經被觀測過了,就直接返回value身上的ob物件。 -
判斷資料型別等條件,條件滿足,則建立一個
Observer
例項並返回。 -
在上面
initData()
方法中,會拿到返回的ob物件,並將ob物件身上的vmCount++
。它的作用就是用來區分我們操作的物件是根\(data還是其子屬性。 在Vue中,應該避免直接在一個Vue例項或其根`\)data`物件上新增或刪除響應式屬性。
四、 Observer類
-
Observer
類有兩個屬性:dep
和vmCount
。vmCount
的作用已經說過了。dep
是一個Dep
例項,用來收集依賴和派發更新。 -
在
Observer
的構造器中對dep
和vmCount
進行了初始化,然後在def(value, '__ob__', this)
方法中,透過Object.defineProperty
為當前的value(第一次呼叫時是data)新增了一個_ob_
屬性,屬性值為當前的Observer
例項 -
根據當前的value是不是陣列,來進行不同的操作。如果不是陣列,則遍歷當前value的所有屬性,執行
defineReactive()
方法。是陣列的情況,在後面單獨說明。
五、defineReactive
-
defineReactive()
方法就是用來對當前的屬性做響應式處理的,主要做了以下操作:- 建立一個Dep例項物件,用來收集當前屬性的依賴和派發更新。
- 透過
Object.getOwnPropertyDescriptor(obj, key)
來獲取當前屬性的所有自有描述資訊,比如:是否可寫、可列舉、可配置、get()、set()等
。 - 如果當前屬性不可配置,就直接返回不做處理。
- 獲取當前屬性的getter和setter,後面會用到。獲取當前屬性的屬性值,後面會用到。
- 根據是否深度觀測,來決定是否呼叫
observe(value)
方法。呼叫該方法,就可以實現遞迴地對所有屬性進行響應式處理,同時,該方法還會為當前屬性身上新增一個_ob_
屬性,指向一個Observer
例項,然後將該例項物件返回,即childOb
。 - 使用
Object.defineProperty
為當前屬性新增getter、setter、enumerable: true、configurable: true等屬性,即在此做資料劫持。
-
在get方法中,首先會獲取當前屬性的屬性值(有getter就透過getter,沒有就使用前面獲取到的
val
)。然後透過
dep.depend()
將Dep.target
(當前訪問該屬性的watcher)新增到當前屬性的依賴列表中,同時也新增到childOb
的依賴列表中。這裡之所以新增到childOb
的列表中,是為了在其它地方也能知道有哪些watcher依賴該屬性。因為可以透過屬性._ob_.dep
來進行派發更新。這也是$set
、$delete
實現資料響應式的前提。最後將資料返回。
-
在set方法中,首先獲取到當前屬性的屬性值(有getter就透過getter,沒有就使用前面獲取到的
val
)。然後對比當前set方法接收到的值,如果沒有變化,就不做處理。如果發生了變化,就修改當前的值(有setter就透過setter修改,沒有就手動修改)。
考慮到set方法接收的新值可能也是一個物件,所以需要對這個新的值再次呼叫
observe(newValue)
進行觀測。最後呼叫
dep.notify()
派發更新(通知依賴列表中的所有watcher執行update方法)。
六、收集依賴和派發更新
-
在get方法中,會收集依賴當前屬性的所有watcher
-
Dep類的結構如下
根據Dep類的定義,可以知道每一個Dep例項都有一個id(唯一標識)和一個subs(用來儲存watcher)。
此外還有兩個最重要的方法,depend()(收集依賴)和notify()(派發更新)。
同時Dep還有一個靜態屬性target,指向的就是當前的watcher。
-
depend()
需要注意的是depend()方法並沒有直接將
Dep.target
新增到subs
中,它反而是呼叫了Dep.target
的addDep(this)
方法,將當前的Dep例項傳給了watcherwatcher的
addDep()
方法,將傳過來的dep資訊儲存下來,並透過dep.addSub(this)
,將當前watcher新增到dep的subs中。這裡保證了watcher不重複收集dep,dep不重複收集watcher。而且,當watcher被銷燬的時候,就可以根據收集的dep資訊,通知相應的dep將自己從subs中移除,以免後面進行派發更新的時候通知給一個已經不存在的watcher。
-
notify()
這裡根據watcher的id進行了排序,因為watcher有三種:渲染watcher、計算屬性watcher、使用者watcher。這三者要保證計算屬性watcher、使用者watcher、渲染watcher的順序執行。
遍歷呼叫watcher的update方法實現更新操作。
七、陣列的響應式處理
-
對於陣列的響應式處理,這裡首先做了一個判斷,在
if (!mock)
裡面,對陣列的七個方法進行了重寫,準確來說,是為響應式的陣列修改了原型物件。重寫的七個方法,在原有方法的功能之上,實現了派發更新,就是透過陣列身上的
_ob_.dep
實現的。這裡的
hasProto
是如何定義的?就是判斷物件是否支援原型,那一般情況都是支援的。所以就先不管下面的分支處理了。再來看一下arrayMethods是什麼
- 首先透過
Object.create(arrayProto)
,將新建立的arrayMethods物件的_proto_
屬性指向arrayProto
,也就是arrayMethods._proto_ = = Array.prototype
- 對會改變陣列的七個方法,進行了重寫。具體實現是
- 獲取陣列原始的方法
original
- 透過
def()
方法,呼叫Object.defineProperty
給arrayMethods
身上新增新的方法。新方法在原始方法original
的基礎上進行增強 - 對於push、unshift、splice等會插入新值的方法,需要獲取到新的值,然後對這個值做觀測。
- 獲取陣列原始的方法
最後,再將陣列的原型屬性指向新的物件
arrayMethods
,即array._proto_ == arrayMethods
arrayMethods._proto_ = = Array.prototype
- 首先透過
-
此外,還在
if (!shallow)
這個判斷中,呼叫了observeArray(value)
方法。該方法遍歷陣列元素,對每一個元素執行observe(),就是為了將陣列中那些物件元素身上的屬性變成響應式的。而對於基本型別的元素不做任何處理。因為陣列可能會有很多的元素,為每一個元素新增getter和setter是很耗費效能的。