Vue 源碼解析(實例化前) - 響應式數據的實現原理

前言

上一篇文章,大概的講解了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


defineProperty

在 MDN 上的解釋是:github

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
複製代碼

這裏,其實就是用來實現響應式數據的核心之一,主要作的事情就是數據的更新, Object.defineProperty() 最多接收三個參數:obj , prop , descriptor編程

objapi

要在其上定義屬性的對象。
複製代碼

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 到時候會把連接拋出來,以供你們參考學習。

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

相關文章
相關標籤/搜索