Vue數據綁定原理之數據劫持

首先咱們此次的源碼分析不只僅是經過源碼分析其實現原理,咱們偶爾還會經過Vue項目編寫的測試用例瞭解更多細節。html

原理結構

data.png

根據官方的指導圖來看,數據(data)在變動的時候會觸發setter而引發通知事件(notify),告知Watcher數據已經變了,而後Watcher再出發從新渲染事件(re-render),會調用組件的渲染函數去從新渲染DOMvue

每一個組件實例都對應一個 watcher 實例,它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染。react

其實看完官方介紹的我仍是一臉懵逼,畢竟我更但願知道它的實現細節,因此咱們一步一步的來看,首先是圖中的Data(紫色)部分。數組

數據劫持

Vue使用的是MVVM模式,Model層的改變會更新View-Model層,那麼它是如何檢測到數據層的改變的呢?瀏覽器

官方指導文檔-深刻響應式原理中咱們其實已經知道Vue是使用Object.defineProperty()實現數據劫持的,而且該屬性沒法經過其餘兼容方法完美的實現,正是由於如此,Vue纔不支持IE8如下的瀏覽器。閉包

好了咱們重頭開始,查看源碼咱們能夠看見順着Vue對象的實例化過程,其中有個步驟叫作initState(vm),這個方法中作的一部分事情就是觀測組件中聲明的data,它調用了initData(vm)async

// instance/state.js
function initData (vm: Component) {
  1. 代理data,props,methods到實例上,以便直接用this就能夠調用

  2. observe(data, true /* asRootData */)
}
複製代碼

到這裏,終於進入正題。ide

observe()

initData方法中調用了observe方法,並將data做爲參數傳了進去,根據函數名和參數咱們其實能夠猜到,這個方法就是用來觀測數據變化的。那首先咱們從單元測試來看一看observe有啥須要注意的:函數

// test/unit/modules/observer/observer.spec.js
// it("create on object")
const obj = {
  a: {},
  b: {}
}
// 也能夠是如下方法建立的
// const obj = Object.create(null)
// obj.a = {}
// obj.b = {}
const ob1 = observe(obj)
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
// should've walked children
expect(obj.a.__ob__ instanceof Observer).toBe(true)
expect(obj.b.__ob__ instanceof Observer).toBe(true)
// should return existing ob on already observed objects
const ob2 = observe(obj)
expect(ob2).toBe(ob1)
複製代碼
// test/unit/modules/observer/observer.spec.js
// it("create on array")

// on array
const arr = [{}, {}]
const ob1 = observe(arr)
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(arr)
expect(arr.__ob__).toBe(ob1)
// should've walked children
expect(arr[0].__ob__ instanceof Observer).toBe(true)
expect(arr[1].__ob__ instanceof Observer).toBe(true)
複製代碼

咱們能夠看到,observe方法爲obj和其子對象都綁定了一個Observer實例,若是是數組的話,則會遍歷數組給數組中的每個對象也綁定一個Observer實例。實際上就是循環加上遞歸,給每個數組或對象(plainObject)都綁定一個Observer實例,而且重複調用observe方法只會獲得同一實例,也就是單例模式。源碼分析

Observer類

上面咱們能夠看到observe方法是響應化data的一個入口,而它實際上又是經過實例化Observer類實現的,那麼Observer實例化的過程當中究竟作了哪些事呢。源碼中,該類的代碼有一段註釋:

Observer class that is attached to each observed object. Once attached, the observer converts the target object's property keys into getter/setters that collect dependencies and dispatch updates.

Observer類會被關聯在每一個被觀測的對象上。一旦關聯上,這個觀測器就會把目標對象上的每一個屬性都轉換爲getter/setter,以便用來收集依賴和分發更新事件。

再來看看源碼:

export class Observer {
  value: any;
  dep: Dep;

  constructor (value: any) {
    0. 把傳入的value綁定給this.value

    1. 新建Dep實例綁定給this.dep
 
    2.this綁定在傳入的value的原型屬性"__ob__"3. 若是value是數組,遍歷數組對每一個元素調用 observe(數組第i個元素)

    4. 不是數組,則對對象的每一個可枚舉的屬性調用 defineReactive
  }
}
複製代碼

我總結出了這個方法主要作了這三件事:

1. 將對象標記爲依賴 2. 循環觀測數組元素 3. 響應化對象的每一個可枚舉屬性

接下來咱們重點看看響應化數據這個功能是如何實現的。

defineReactive

話很少說,直接先上源碼歸納:

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
  0. 設置閉包實例 const dep = new Dep()

  1. 若是 property.configurable === false 直接 return

  2. 設置閉包變量 val 的值

  3. let childOb = !shallow && observe(val) 觀測該屬性

  4. Object.defineProperty(obj, key, {...}) !!!
}
複製代碼

從源碼歸納中我們能夠看到defineReactive其實主要作了這三件事:

1. 將屬性標記爲依賴
2. 遞歸觀測屬性
3. 數據劫持

而數據劫持這裏,使用到的就是咱們前面提到的Object.defineProperty!咱們來細品:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = 調用自帶getter或者獲取閉包變量val
    if (Dep.target) {
      // 依賴收集
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    調用自帶setter.call(obj, newVal)或者設置閉包變量val = newVal
    
    // 從新觀測新值
    childOb = !shallow && observe(newVal)
    // 依賴變動通知
    dep.notify()
  }
})
複製代碼

咱們把注意力放到重點上,一些小細節代碼就沒放上來。

首先,這裏設置了屬性的setget(若是不瞭解的同窗還須要先學習defineProperty)。在set中,會更新閉包變量val的值(若是屬性有自帶setter則會調用setter),而且它會調用依賴的通知方法,這個方法會告訴依賴的全部觀測者並調用每一個觀測者的update方法(咱們稍後再細講),這也就是官網提到的:

當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染

而在get中,附加功能就只有依賴收集,那爲何把依賴收集放到get中呢。我們反向思考一下,若是要收集依賴,那麼就要調用屬性的get也就是獲取屬性值,哪裏會獲取到屬性值呢,固然是模板裏,也就是模板渲染的時候,要把佔位符替(好比{{ msg }})換爲實際值,這個時候就會進行依賴收集。而模板沒有用到的屬性,則不會進行依賴收集。官網也有提到:

它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。

DIY

OK,看懂了嗎,接下來讓咱們本身來簡單的復現一下以上功能。

首先是observe方法,注意事項是返回Observer實例,而且是單例

function observe(value) {
  let ob

  if (value.hasOwnProperty("__ob__")) {
    ob = value.__ob__
  } else if (Array.isArray(value) || ArrisPlainObject(value)) {
    ob = new Observer(value)
  }

  return ob
}

function isPlainObject(obj) {
  return Object.prototype.toString.call(obj) === "[object Object]"
}
複製代碼

其次是Observer對象,它會標記依賴,綁定觀測實例到數據上,會處理數組,響應化全部屬性

class Observer {
  constructor(value) {
    this.value = value
    // this.dep = new Dep()
    // 掛載實例到value上
    const proto = Object.getPrototypeOf(value)
    proto.__ob__ = this

    if (Array.isArray(value)) {
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  observeArray(value) {
    // 觀測數組的每一個元素
    value.forEach((item) => {
      observe(item)
    })
  }
  walk(value) {
    // 響應化對象全部可枚舉屬性
    Object.keys(value).forEach((key) => {
      defineReactive(value, key)
    })
  }
}
複製代碼

最後是defineReactive,它會遞歸觀測屬性,標記依賴,響應化傳入的屬性

function defineReactive(obj, key, val) {
  // 建立閉包依賴標記
  // const dep = new Dep()

  // 閉包存儲實際值
  val = obj[key]
  // 遞歸觀測屬性
  observe(val)

  Object.defineProperty(obj, key, {
    set(newVal) {
      val = newVal
      observe(newVal)
      // dep.notify()
    },
    get() {
      // 收集依賴
      return val
    },
  })
}
複製代碼

考慮到咱們尚未了解Dep,因此相關代碼先忽略。而且咱們實現的是一個最簡版本,沒有考慮到過多的邊緣狀況。

接下來咱們試驗一下:

var data = {
  a: 0,
  b: {
    c: {
      d: 1,
    },
  },
}
observe(data)
複製代碼

咱們再控制檯中打印出data,發現咱們已經爲data,a,b,c綁定好了__ob__。只不過如今它還不能收集依賴以及更新依賴。

依賴標記

我稱其爲依賴標記,由於它會和被觀測的數據進行綁定,也就是說咱們把響應式數據看作是一個依賴,而這個依賴標記會去處理和依賴有關的事情,好比記錄觀測者,分發更新事件等。

Dep類

這個類其實十分簡單,功能也很明確。咱們先來看看源碼歸納:

class 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)
  }
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
複製代碼

咱們先來看看這明明白白的三個功能:

1. 添加訂閱者addSub
2. 刪除訂閱者removeSub
3. 通知訂閱者更新notify

而且咱們能夠看出依賴裏面存儲的訂閱者是一個Watcher數組。也就是說實際和Watcher交互的是Dep。他還有一個靜態屬性target該屬性指向的也是一個Watcher實例。

讓咱們咱們再回過頭來看看Observer中的相關操做。

export function defineReactive () {
  let childOb = !shallow && observe(val);

  Object.defineProperty(obj, key, {
    get: function reactiveGetter() {
      const value = 獲取value

      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      // ...
      childOb = !shallow && observe(newVal);

      dep.notify();
    },
  });
}
複製代碼

先講解最簡單的set,在更新響應式值的時候只調用了該值所屬的dep.notify(),它會通知全部訂閱者說我這邊數據變了,麻煩你更新同步一下。

而在獲取(「接觸」)該值的時候,調用了值得get,開始了依賴收集。首先若是有收集者,也就是Dep.target,那麼該值做爲一個依賴被收入,若是該值是一個數組或者對象,那麼該值被觀測後的Observer也做爲一個依賴被收入,而且若是是數組的話,會循環收入每一個元素也做爲依賴。總結一下:

若是當前有收集者 Dep.target -- 依賴+1
若是當前值是對象或數組 -- 依賴+1
若是當前值是數組 -- 依賴+n
複製代碼

Observer特地將和Watcher相關的代碼抽分出來爲Dep,目的也是讓整個數據響應過程更加鬆散,可能某天觀測數據變動方法再也不是Observer的時候,還能繼續進行依賴收集和更新通知。

另外Dep還提供了兩個靜態方法,用來修改Dep.target

const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
複製代碼

從這裏咱們能夠看出,Dep.target是全局惟一的,一個時間點只會有一個Dep.target。 那具體哪裏會用到它呢?注意到Dep的定義中,target的類型是Watcher,因此咱們須要在瞭解Watcher以後才能知道它會在何時被設置上。

DIY

因爲Dep的功能主要和Watcher相關,而且其功能很簡單,因此在咱們掌握Wathcer以後再來實現它。

數據觀測

接下來咱們來到了最關鍵的一步,它能將我們劫持的數據真正的用於視圖更新,並在視圖更新時同步數據。

Watcher類

以前提了不少Watcher,咱們從上文知道,它會在某個時間點成爲收集者Dep.target去收集依賴addDep,它會在數據變動時響應通知update。咱們先來看看它的構造函數:

class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // 初始化屬性 deps,newDeps,depIds,newDepIds...
    
    // 設置getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }

    // 獲取監聽的值
    this.value = this.get()
  }
}
複製代碼

這裏有個參數expOrFn有點隱晦,它能夠是一個函數或者一個表達式,做爲表達式,它一般是a.bmsg這樣的值。你可能有點熟悉了,當咱們在Vue組件中自定義watch的時候,用的也是相似的表達式。

watch: {
  msg (val, newVal) {}
}
複製代碼

沒錯,源碼註釋裏有提到,$watch() 和 指令都是用的Watcher。 而expOrFn是用來轉換爲獲取組件中的值的getter。好比expOrFn === 'msg',實際上被轉換爲了如下內容:

// 簡單表示
this.getter = function (obj) {
  return obj.msg
}
複製代碼

不過這裏的this.value = this.get()用的卻不是直接的getter,這是爲何呢? 咱們再來看看get()方法:

class Watcher {
  // ...
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
  
    // 遞歸收集可收集依賴
    if (this.deep) {
      traverse(value)
    }

    popTarget()
    this.cleanupDeps()
    return value
  }
}
複製代碼

這裏用到了咱們前面提到的Dep的兩個靜態方法pushTargetpopTarget。咱們知道這兩個方法是用來設置和取消Dep.target的,而咱們在Observer中瞭解到,在獲取屬性值的get方法中,會根據Dep.target來蒐集依賴。

而在這裏的watcher.get方法中,咱們能夠看到,首先添加了當前Watcher做爲Dep.target,而後獲取屬性的值觸發屬性的get方法,調用dep.depend()Wathcer收集當前依賴。咱們把Observer中屬性的get方法中收集依賴摺合一下:

Object.defineProperty(obj, key, {
  get: function reactiveGetter () {
    const dep = new Dep()
    // ...
    if (Dep.target) {
      // wathcer.addDep(dep)
      Dep.target.addDep(dep)
    }
    // ...
  }
}
複製代碼

而這裏的addDep就是Watcher的收集依賴的方法:

class Watcher {
  constructor () {
    // ...
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
  }
  // ...
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
}
複製代碼

而這個方法主要的做用就是去重而後存儲目標依賴depwatcher.newDeps裏。而後將watcher存儲到dep.subs裏,創建一個雙向引用。

而後接下來就是遞歸收集子對象依賴(若是有),而後清除Dep.target引用,最後調用this.cleanupDeps,而這個方法作的事也很簡單:

  1. 舊依賴列表有而新依賴列表沒有的這些依賴,因爲新依賴中已經沒有了dep -> watcher的引用,因此對應的也要清除dep <- wathcer引用,這裏調用了dep.removeSub(this),就是告訴你和我撇清關係,個人內心已經沒有你了。

  2. newDepsnewDepIds賦值給depsdepIds,而後清空newDepsnewDepIds,從新開始生活。

到這裏,Watcher的初始化就已經完成了。

小小的總結

固然咱們的分析還沒結束,只不過咱們須要短暫的總結一下,消化以前的概念,才能更深入的理解接下來的步驟。

數據響應.jpg

上圖是咱們目前所瞭解到的一個關係圖,黃色的流程表示依賴收集的過程,綠色的表示數據變動的過程。

綁定數據到DOM

目前爲止,咱們只是瞭解瞭如何劫持數據,而且在數據變動時更新它的觀測者。好比:

假設咱們已經收集完依賴了,也就是每一個響應化屬性都有一個訂閱列表subs,這裏面裝着和該屬性相關的觀測者。當屬性出現變動時,因爲數據被劫持(set),這些觀測者都會獲得通知,調用各自的update,去執行本身的回調函數。

可是,何時纔會收集依賴呢?咱們寫的{{msg}}模板表達式怎麼和data.msg關聯起來的呢?Watcher是何時實例化的?

咱們下一篇繼續分析。

原文連接:個人博客

相關文章
相關標籤/搜索