源碼閱讀:Vue的響應式原理(一)

前言

  1. observer部分完整的源碼注釋放在github上了,有興趣的可以去看看,如果發現有誤情不吝賜教!observer
  2. 這篇文很長長長長長長長長,而且比較費腦,我也整理了很久,如果對這篇文感興趣,請自帶??和無限的耐心~
  3. 這篇文只是Vue的響應式原理的一部分,后面還有很多很多很多的知識本文沒有涉及到。

Vue的雙向數據綁定和Angular很不一樣。
Angular采用的是“臟檢查”的方法,當我們觸發了某些事件(定時,異步請求,事件觸發等),執行完事件之后,Angular會對所有“注冊”過的值進行一遍“全面檢查”,也就是遍歷所有的值,判斷是否和之前的一致。這種方法效率不高,因為我們修改一個小地方都會帶來兩次以上的全面檢查,如果我們綁定的view比較多,就可能會存在比較明顯的性能問題了。
而Vue的處理方式則不同,它結合觀察者模式發布-訂閱模式,當我們改變了一個數值,它會主動通知與它相關的訂閱者,告訴他們可以進行相關的操作了,這種方法和“臟檢查”相比,更加優雅,效率會更高。

1. 響應式的基石:Object.defineProperty(obj, prop, descriptor)

MDN : Object.defineProperty(obj, prop, descriptor)

我們都知道對象有兩種屬性,一種是數據屬性,一種是訪問器屬性。
數據屬性有4個特性:configurable, enumerable, value, writable
訪問器屬性也有4個特性:configurable, enumerable, get, set

平時我們通過普通賦值的方法(比如:obj.a - 'a')添加的屬性都是數據屬性,且默認configurable,和enumerable都為true
而用Object.defineProperty()可以添加數據屬性或者訪問器屬性,默認configurableenumerable都為false

關于getter/setter
訪問器屬性是沒有value的,但是他們可以用來劫持對另一個數據的訪問,舉個例子:

var log = console.log.bind(console);
var obj = {
    _year: 2017
};
Object.defineProperty(obj, 'year', {
    get: function getter () {
        return this._year;
    },
    set: function setter (value) {
        this._year = value;
    }
});
log(obj.year); // 2017
obj.year = 2018;
log(obj._year); // 2018

這個例子中我們訪問obj.year,會返回obj._year的值,我們修改obj.year,會修改obj._year的值。

根據這個特性,我們可以實現視圖-數據雙向綁定:

<body>
    <p id="test-p">lalal</p>
</body>
<script>
    var log = console.log.bind(console);
    var obj = {}
    Object.defineProperty(obj, 'test-p', {
        get: function getter () {
            return document.getElementById('test-p').innerHTML;
        },
        set: function setter (value) {
            document.getElementById('test-p').innerHTML = value;
        }
    });
    log(obj['test-p']);
    setTimeout(function changeData () {
        obj['test-p'] = 'hahah';
    }, 3000);
</script>

是不是特別好玩?

2. 觀察者模式

維基百科:觀察者模式

在此種模式中,一個目標對象管理所有相依于它的觀察者對象,并且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。
一圖勝千言,我畫了張簡單的流程圖,應該很容易看懂:

觀察者模式

詳細一點講,流程大概是這樣的:

目標對象有這么幾個方法:

  1. setState:設置對象的狀態,該函數調用了NotifyObserver方法
  2. getState:取得對象當前的狀態
  3. addObserver:添加觀察者
  4. removeObserver:刪除觀察者
  5. NotifyObserver:通知觀察者:我的狀態改變了,該方法會調用各個觀察者的Notify方法

觀察者對象有個方法:
Notify:該方法會會調用目標對象的getState方法,然后對目標對象的新值作出一些反應,比如說,打印出來之類的。

如果我們寫一個最簡單的觀察者模式,那可能是這樣的:

var log = console.log.bind(console);

function Oberser (target, cb) {
  (function(){ // 添加到目標對象
    target.addOberser && target.addOberser(this);
    console.log('oberser added')
  }).call(this);
  
  this.notify = cb;
}

var target = {
  _value: 2017,
  obersers: [],
  addOberser: function (oberser) { // 添加觀察者
    this.obersers.push(oberser);
  },
  removeOberser: function (oberser) { // 刪除觀察者
    // ...
  },
  notifyOberser: function () { // 通知觀察者
    this.obersers.map(oberser => oberser.notify && oberser.notify());
  }
}

Object.defineProperties(target, {
  value: {
    get: function () {
      return this._value;
    },
    set: function (newValue) {
      this._value = newValue;
      this.notifyOberser(); // 調用notifyOberser
    }
  }
});

var oberser1 = new Oberser(target, function () {
    log(`I'm observer1, the value of my target is ${target.value}`);
});
var oberser2 = new Oberser(target, function () {
    log(`I'm observer2, the value of my target is ${target.value}`);
});

target.value = 2018;
/*
oberser added
oberser added
I'm observer1, the value of my target is 2018
I'm observer2, the value of my target is 2018
*/

當然了如果我們要觀察同一個對象中的多個屬性,就不能用這種方法了,因為我們總不能一個屬性更新,所有觀察者都全部調用一遍吧?最好是每一個屬性都能有自己的觀察者。

3. 正題

怎么給每個對象都維護一個觀察者的列表呢?Vue是這樣做的:
Vue在觀察者模式中結合發布-訂閱模式,其中涉及到了三個重要的對象:Observer, Dep, Watcher
Observer負責觀察目標數據的變化,如果數據變化了,那么通知Dep。
Dep負責維護一個訂閱者列表(收集依賴),當接收到Observer的通知時,他就通知所有訂閱者:目標數據更新了。
Watcher維護一個回調函數,當接收到Dep的通知時,執行回調函數。

Vue響應式原理

可以這么理解:Observer是教師,Dep是教學在線,Watcher是學生。教師不必維護自己的學生列表,教務處幫他維護。學生不必維護自己的課表,因為教務處也會幫他維護。每次教師布置了新作業等(比喻不是很恰當),他只需要跟教學在線說一聲就可以了,教學在線就發郵件告訴每一個上了這門課的學生:有新作業了。學生就可以分別對這個新作業作出不同的反應。

原理已經了解得差不多了,接下來看一下源碼吧。
先看一下 Observer類,Vue會給每一個響應式的數據添加一個observer,這個observer就負責觀察這個數據有沒有發生變化。
中文注釋是我加的,英文注釋是作者加的,不要漏了英文注釋,很重要!

export class Observer {
  value: any; // 被觀察的對象,比如vue的根屬性data,在vue實例初始化的時候,vue會為data屬性添加一個observer對象,介時observer對象的value屬性指向data,而data的__ob__屬性指向observer對象
  dep: Dep; // 每一個observer對象都有一個dep,負責收集依賴和通知
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this) // value的__ob__屬性指向這個observer對象本身,比如L38注釋中說到的data屬性
    if (Array.isArray(value)) { // 如果value為數組,那么增強這個數組
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value) // 往下遞歸數組,如果數組中有元素為對象或者數組,也會給其添加observer
    } else { // 如果value為對象,那么往下遞歸對象,如果對象中有屬性為對象或者數組,也會給其添加observer
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) { // 遍歷對象,把對象中的屬性都轉化為getter/setter對
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) // 很重要!
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) { // 遍歷數組,如果數組中有元素為對象或者數組,也會給其添加observer
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observer類的constructor函數中,可以看到對于數組和對象,Vue的處理是不一樣的,為什么呢?
數組是沒有Object.defineProperty(obj, prop, descriptor)這個方法的,這就意味著我們沒有辦法監聽數組中屬性的添加,刪除。
如果你在Vue中處理過數組,你應該知道,在Vue中,數組只有7個常用方法可以觸發視圖的更新:push(), pop(), shift(), unshift(), splice(), sort(), reverse()。這是因為Vue對這些方法進行了增強,原理很簡單,類似于這樣(當然實際上要嚴謹一些,這里只是幫助理解):

var log = console.log.bind(console);
var arrayMethods = Object.create(Array.prototype); // 繼承自Array.prototype,保留了數組原本的特性
arrayMethods.unshift = function (value) { // 重寫方法
    Array.prototype.unshift.call(this, value); // 調用原來的方法
     notify(); // 并進行通知
}
function notify () {
    console.log('unshift');
}
var arr = [1, 2, 3];
arr.__proto__ = arrayMethods;
arr.unshift(0);
log(arr)
/*
[ 1, 2, 3 ]
unshift
[ 0, 1, 2, 3 ]
*/

上面的代碼截斷了數組的原型鏈,我們新創建了一個對象arrayMethods,這個對象繼承自Array.prototype,然后改寫里面的unshift()方法。這樣既保證我們保留了數組的length等屬性,有可能使用自己定義的unshift()方法,我們在unshift()方法中調用了notify()函數。

Vue是怎么給每一個對象都加上一個Observer對象的?上面代碼中,在處理數組的函數observeArray里,可以看到Vue遍歷了一遍數組,并對每一個元素調用了observe()函數。
而在處理對象的函數walk中,對每一個屬性都調用了defineReactive函數(這個函數非常重要,后面再說),這個defineReactive函數內部也對屬性都調用了一遍observe()函數。
也就是說,Vue是通過observe()函數來給對象添加observer的。看一下observe()函數:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) { // 只有對象或數組才會進入這個函數
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // value已經有了自己的observer
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value) // value為對象且沒有自己的observer,那么為他新建一個observer,注意這里說明了Vue對每一層的屬性或元素遞歸添加了observer
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

注意到里面有一行代碼:ob = new Observer(value),也就是說,Vue遞歸遍歷了每一層的屬性或元素,如果這個元素/屬性的類型為對象/數組,那么它也會有一個自己的observer。

好,現在我們已經明白了Vue怎么遍歷數組來把數組轉化為響應式的了,那接下來再看看Vue如何處理對象屬性:
高能預警!

export function defineReactive ( // 每個屬性都轉化為getter/setter,并且每個類型為對象(包括數組)的屬性都會擁有自己的observer
  obj: Object, 
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean // shallow為true的話,屬性不會有自己的observer,也就是該屬性將不具備響應性
) {
  const dep = new Dep() // 注意這個函數將會出現兩個dep,這里第一個dep,將會被閉包進getter/setter函數中

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key] // 注意,這個val也會被閉包進getter/setter方法中,我之前還疑惑把屬性都轉化為getter/setter值是怎么存儲數據的,就是把這個val閉包進去的
  }

  let childOb = !shallow && observe(val) // 每一個observer會有一個dep屬性,所以這里有了第二個dep,這個dep會在該屬性的屬性被增刪的時候通知訂閱者
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val // 執行屬性原本自己有的getter
      if (Dep.target) { // 如果存在 Dep.target 這個全局變量不為空,表示是在新建 Watcher 的時候調用的
        dep.depend() // 這里是第一個dep,當Dep.target依賴于這個屬性的時候,他會調用該屬性的getter,這是dep.depend()就會把Dep.target添加進自己的訂閱列表,這樣在屬性的setter被調用的時候,這個dep就可以通知Dep.target了
        if (childOb) {
          childOb.dep.depend() // 第二個dep也會收集依賴,那么該屬性的屬性被添加或者刪除的時候,這個dep就可以通知這個屬性的訂閱者了
          if (Array.isArray(value)) { // 如果value為一個數組,那么是無法通過getters來竊聽對數組元素的訪問的,所以要向下遍歷數組,給里面的元素都收集依賴
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) { // 沒有變化/newVal為NaN/value為NaN
        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()
    }
  })
}

這個函數邏輯比較復雜,讓我們好好來捋一下思路:
1. 閉包的妙用
如果你好好觀察一下屬性的getter/setter方法,你會發現他們閉包了這幾個變量:
getter, setter, val, dep,childOb

其中,getter, setter兩個變量可能是我們自己定義的getter/setter方法,因為我們有時候也會有需要訪問器屬性的時候。

val是我們原本使用自己的getter/setter想要訪問的值,比如這篇文章第一個代碼塊的_year屬性。
我之前還在疑惑,因為訪問器屬性是沒有自己的值的,Vue把對象的屬性轉化為訪問器屬性之后,要怎么維護之前的值,原來是閉包進來了!
看一下源碼,當我們調用屬性的setter方法的時候,最后修改的是這個val的值,而我們調用getter方法的時候,返回的也是這個val的值。

dep, childOb在第3小節一起講。

2.Dep
是時候介紹一下Dep類了,不然后面的講不下去。
Dep的結構很簡單,大概長這樣:

export default class Dep { // dep是dependence的縮寫,他負責收集依賴,以及通知訂閱者。每一個Observer對象有其自己的的dep
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>; // 訂閱者列表,

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) { // 添加訂閱者
    this.subs.push(sub)
  }
 
  removeSub (sub: Watcher) { // 刪除訂閱者
    remove(this.subs, sub)
  }

  depend () { // 添加依賴,也就是把當前Dep.target添加到這個dep實例的subs列表中
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () { // 通知watcher,執行所有watcher的.update()方法,更新watcher的數據
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

可以看到,一個Dep類會有一個自己的id,維護自己的一個訂閱者列表,并切可以添加,刪除,通知訂閱者。
Dep類中有一個靜態屬性Dep.target,學過C++的同學應該知道,靜態屬性也就是類屬性,是所有實例共享的。這個target是干什么用的呢?

Vue在處理一個watcher的時候,就會把Dep.target的值設為當前的watcher,舉個例子,這是我們的Vue實例(該例子從官網中復制過來的):

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實例
      return this.message.split('').reverse().join('')
    }
  }
})

現在假設處理到了reversedMessage,先把Dep.target指向它。很明顯reversedMessage依賴了message,我們是不是要在message對應的dep的訂閱者列表中加上reversedMessage?問題來了,我們要怎么添加這個依賴?

首先因為reversedMessage會訪問到message,也就是會調用message的getter方法,那我們可以在getter方法中進行依賴收集,但是getter方法是沒辦法傳參的,所以它也沒辦法知道誰訂閱了它。

這時候Dep.target就起作用了,我們前面已經說過,Vue處理到了哪個watcher,就會把Dep.target指向它,那么此時的Dep.target肯定就是reversedMessage,我們只需要在getter函數中把Dep.target添加進訂閱者列表就可以了!
那么這時,當我們改變message的值時,會調用其setter函數,setter函數中dep就會調用dep.notify()方法,通知reversedMessage:我更新了!

真的太妙了!

3. 兩個dep
源碼中我在注釋中也提醒過了,這個函數中出現了兩個dep,一個是在函數開頭就新建的dep,另一個是屬性自己的observer中的dep。
dep的作用是什么?收集依賴并在適當的時候通知訂閱者:目標數據更新了。

在源碼中,屬性的getter方法中,給dep, childOb都添加了依賴,為什么在setter方法中,只通知了dep?或者說,childOb的意義在哪里呢?

先看一個例子:

var obj = {
  _a: { aa: 1}
};
Object.defineProperty(obj, 'a', {
  configurable: true,
  enumerable: true,
  get: function () {
    log('get a:' + this._a);
    return this._a;
  },
  set: function (newV) {
    log('set a:' + newV);
    this._a = newV;
  }
});
obj.a; // get a:[object Object]
  obj.a.bb = 2; // get a:[object Object]
delete obj.a;
// 刪除該屬性的時候,沒有調用getter/setter函數!

可以很明顯看出getter/setter的缺陷:只能監聽到屬性的更改,不能監聽到屬性的刪除與添加。

我們都知道Vue提供了內置的Vue.set(), Vue.delete()方法來讓我們響應式的添加和刪除數組的元素或對象的屬性。
官方文檔
官方文檔是這么說的:

Vue.set()這個方法主要用于避開 Vue 不能檢測屬性被添加的限制。
Vue.delete()這個方法主要用于避開 Vue 不能檢測到屬性被刪除的限制。

我們前面已經證明了,setter是不會在屬性被刪除或者添加的時候調用的,那么Vue是怎么在刪除和添加的時候通知watcher的?其實跟數組方法的增強事同一個套路,把Vue.delete()的源碼簡化簡化再簡化之后:

function del () {
  delete obj.a;
  childOb.notify(); // 通知obj的watcher:我有一個屬性刪除了。
}

所以為什么在getter方法中要添加childOb的依賴,就是為了在刪除或者添加屬性的時候進行通知。

4. 如何向下收集依賴
是這樣的,假設數據是這樣的let data = {a: {b: {c: {d: {e: 1}}}}},有一個模板引用了{{a.b.c}},那么我們修改a.b.c.d.e,這時watcher會被通知到嗎?

答案是不會。為什么?一步一步來看。

  1. 首先我們知道每一層的屬性,也就是a, b, c, d, e,都有自己的observer,而且watcher訂閱observer是通過getter方法來實現的,沒有getter方法就沒法訂閱。

  2. 它調用了c的getter方法,因此c更改了(整個對象被替換),會有通知,c刪除了,也會有通知。

  3. 我們修改了a.b.c.d.e

  4. 模板引用的是{{a.b.c}},那么它沒有調用到e的getter方法。

  5. 因此我們修改了e,watcher就沒辦法知道了。

Vue是怎么解決這個問題的呢?

它是這樣做的:當模板引用了{{a.b.c}}時,此時Dep.target是這個模板,然后,Vue從c開始往下遍歷,對每個屬性都"touch"一下,也就是強行調用一下getter方法,這樣,模板就加入了所有屬性的訂閱者列表中。

有興趣的同學可以自己去看一下Vue源碼中的traverse.js

5. dependArray函數
終于要進入尾聲了???,看源碼真的很費心神,但是收獲真的超級大呀!
defineReactive函數中的getter方法中,對數組有一個額外的處理過程:如果value為數組,那么對其執行dependArray函數。

想了好久才想明白為什么要進一步的處理。

回到最開始,我們給一個對象添加一個observer,那么他會遍歷所有的屬性,把屬性都轉化為getter / setter。

但是給數組添加一個observer,他只是添加了8個具有響應性的方法。(當然他也會給子對象添加observer)

這時我們push,pop數組,是響應式的,數組的dep知道他要通知訂閱者們。

但是如果我們改變的是數組的元素,比如,對于一個數組var arr = [1, 2, 3, {a: 4}],現在我們這樣操作arr[0] = 0,數組是不會有響應的。

這也是為什么vue給數組加了兩個方法Vue.set, Vue.delete來添加和刪除元素的原因。

再回到這個問題上,我們已經有了Vue.set, Vue.delete兩個方法,那我們操作基本類型的元素基本沒啥問題了,但是如果是像arr[3].a = 5這種呢?Vue的解決方法就是遞歸遍歷數組,遇到類型為object的元素,就把當前的Dep.target添加到它的訂閱者列表中,這時它的變化就可以被監聽了。

這一切的根本原因,就是數組沒法通過getter/setter對象來監聽元素的變化。

最后附上dependArray的源碼。

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend() // //如果數組元素也是對象,那么他們observe過程也生成了ob實例,那么就讓ob的dep也收集依賴
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容