vue面試題(vue2響應式原始碼剖析)

平平丶淡淡發表於2024-03-18

一、前言

這篇文章結合Vue2.7.16的原始碼和一個Vue2的專案,來詳細講解Vue2實現響應式資料的核心程式碼

1.1 準備

  1. 安裝@vue/cli

    npm install -g @vue/cli
    
  2. 建立vue專案

    vue create vue2-test
    
  3. 修改Vue例項的配置物件

    image-20240318200957743

二、響應式處理的入口

  1. 透過 new Vue() 呼叫 Vue 建構函式,然後會執行裡面的this._init(options)方法

    image-20240318201336829
  2. _init(options)方法定義在Vue.prototype上,定義是透過執行initMixin(Vue)實現的

    image-20240318201612226

  3. _init(options)方法中,做了一些初始化的操作。其中initState(vm)用來初始化狀態。

    image-20240318201828392

  4. initState(vm),按照props、methods、data、computed、watch的順序,對一些屬性做了初始化操作。其中initData(vm)就是對配置物件中的data做響應式處理的。

    image-20240318202029673

  5. initData()方法中,做了以下處理:

    • 獲取到配置物件上面的data配置項,如果是用方法定義的就透過呼叫方法獲取,如果是用物件配置的,就直接訪問。然後將data繫結到vm._data上。
    • 檢查data中的屬性名是否與methods、props中定義重複
    • 透過proxy(),將vm._data上的資料直接放在了vm身上。內部就是使用Object.defineProperty()實現透過vm.屬性去訪問vm._data.屬性。這樣訪問要方便一點。
    • 呼叫observe(data)來對資料進行觀測,這也是響應式處理的入口

    image-20240318202356812

    image-20240318203317526

三、入口函式observe

  1. observe接收一個value(第一次傳入的就是data),然後判斷其身上是否有_ob_屬性,該屬性用來標識是否已經被觀測。如果已經被觀測過了,就直接返回value身上的ob物件。

  2. 判斷資料型別等條件,條件滿足,則建立一個Observer例項並返回。

  3. 在上面initData()方法中,會拿到返回的ob物件,並將ob物件身上的vmCount++。它的作用就是用來區分我們操作的物件是根\(data還是其子屬性。 在Vue中,應該避免直接在一個Vue例項或其根`\)data`物件上新增或刪除響應式屬性。

    image-20240318203541883

    image-20240318204737737

    image-20240318204810530

四、 Observer類

  1. Observer類有兩個屬性:depvmCountvmCount的作用已經說過了。dep是一個Dep例項,用來收集依賴和派發更新。

  2. Observer的構造器中對depvmCount進行了初始化,然後在def(value, '__ob__', this)方法中,透過Object.defineProperty為當前的value(第一次呼叫時是data)新增了一個_ob_屬性,屬性值為當前的Observer例項

  3. 根據當前的value是不是陣列,來進行不同的操作。如果不是陣列,則遍歷當前value的所有屬性,執行defineReactive()方法。是陣列的情況,在後面單獨說明。

    image-20240318205354618

五、defineReactive

  1. defineReactive()方法就是用來對當前的屬性做響應式處理的,主要做了以下操作:

    • 建立一個Dep例項物件,用來收集當前屬性的依賴和派發更新。
    • 透過Object.getOwnPropertyDescriptor(obj, key)來獲取當前屬性的所有自有描述資訊,比如:是否可寫、可列舉、可配置、get()、set()
    • 如果當前屬性不可配置,就直接返回不做處理。
    • 獲取當前屬性的getter和setter,後面會用到。獲取當前屬性的屬性值,後面會用到。
    • 根據是否深度觀測,來決定是否呼叫observe(value)方法。呼叫該方法,就可以實現遞迴地對所有屬性進行響應式處理,同時,該方法還會為當前屬性身上新增一個_ob_屬性,指向一個Observer例項,然後將該例項物件返回,即childOb
    • 使用Object.defineProperty為當前屬性新增getter、setter、enumerable: true、configurable: true等屬性,即在此做資料劫持。

    image-20240318210214193

  2. 在get方法中,首先會獲取當前屬性的屬性值(有getter就透過getter,沒有就使用前面獲取到的val)。

    然後透過dep.depend()Dep.target(當前訪問該屬性的watcher)新增到當前屬性的依賴列表中,同時也新增到childOb的依賴列表中。這裡之所以新增到childOb的列表中,是為了在其它地方也能知道有哪些watcher依賴該屬性。因為可以透過屬性._ob_.dep來進行派發更新。這也是$set$delete實現資料響應式的前提。

    最後將資料返回。

    image-20240318211537805

  3. 在set方法中,首先獲取到當前屬性的屬性值(有getter就透過getter,沒有就使用前面獲取到的val)。

    然後對比當前set方法接收到的值,如果沒有變化,就不做處理。如果發生了變化,就修改當前的值(有setter就透過setter修改,沒有就手動修改)。

    考慮到set方法接收的新值可能也是一個物件,所以需要對這個新的值再次呼叫observe(newValue)進行觀測。

    最後呼叫dep.notify()派發更新(通知依賴列表中的所有watcher執行update方法)。

    image-20240318212315488

六、收集依賴和派發更新

  1. 在get方法中,會收集依賴當前屬性的所有watcher

    image-20240318212558614

  2. Dep類的結構如下

    根據Dep類的定義,可以知道每一個Dep例項都有一個id(唯一標識)和一個subs(用來儲存watcher)。

    此外還有兩個最重要的方法,depend()(收集依賴)和notify()(派發更新)。

    同時Dep還有一個靜態屬性target,指向的就是當前的watcher。

    image-20240318212825867

  3. depend()

    需要注意的是depend()方法並沒有直接將Dep.target新增到subs中,它反而是呼叫了Dep.targetaddDep(this)方法,將當前的Dep例項傳給了watcher

    image-20240318212948340

    watcher的addDep()方法,將傳過來的dep資訊儲存下來,並透過dep.addSub(this),將當前watcher新增到dep的subs中。

    這裡保證了watcher不重複收集dep,dep不重複收集watcher。而且,當watcher被銷燬的時候,就可以根據收集的dep資訊,通知相應的dep將自己從subs中移除,以免後面進行派發更新的時候通知給一個已經不存在的watcher。

    image-20240318213154409

  4. notify()

    這裡根據watcher的id進行了排序,因為watcher有三種:渲染watcher、計算屬性watcher、使用者watcher。這三者要保證計算屬性watcher、使用者watcher、渲染watcher的順序執行。

    遍歷呼叫watcher的update方法實現更新操作。

    image-20240318213335207

七、陣列的響應式處理

  1. 對於陣列的響應式處理,這裡首先做了一個判斷,在if (!mock)裡面,對陣列的七個方法進行了重寫,準確來說,是為響應式的陣列修改了原型物件。

    重寫的七個方法,在原有方法的功能之上,實現了派發更新,就是透過陣列身上的_ob_.dep實現的。

    image-20240318215518076

    這裡的hasProto是如何定義的?就是判斷物件是否支援原型,那一般情況都是支援的。所以就先不管下面的分支處理了。

    image-20240318221925392

    再來看一下arrayMethods是什麼

    • 首先透過Object.create(arrayProto),將新建立的arrayMethods物件的_proto_屬性指向arrayProto,也就是arrayMethods._proto_ = = Array.prototype
    • 對會改變陣列的七個方法,進行了重寫。具體實現是
      • 獲取陣列原始的方法original
      • 透過def()方法,呼叫Object.definePropertyarrayMethods身上新增新的方法。新方法在原始方法original的基礎上進行增強
      • 對於push、unshift、splice等會插入新值的方法,需要獲取到新的值,然後對這個值做觀測。

    image-20240318220012088

    最後,再將陣列的原型屬性指向新的物件arrayMethods,即

    array._proto_ == arrayMethods

    arrayMethods._proto_ = = Array.prototype

    image-20240318224636418

  2. 此外,還在if (!shallow)這個判斷中,呼叫了observeArray(value)方法。該方法遍歷陣列元素,對每一個元素執行observe(),就是為了將陣列中那些物件元素身上的屬性變成響應式的。而對於基本型別的元素不做任何處理。因為陣列可能會有很多的元素,為每一個元素新增getter和setter是很耗費效能的。

    image-20240318220937633

相關文章