Vue源碼按部就班-數據響應式原理

  Vue做爲一種MVVM框架,可以實現數據的雙向綁定,讓Vue技術棧的前端從業者擺脫了繁瑣的DOM操做,這徹底得益於Vue框架開發者對原生Object對象的深度應用。Vue實現數據響應系統的核心技術就是數據劫持和訂閱-發佈,基本思想就是經過對數據操做進行截獲,在數據進行getter操做的時候更新依賴,即依賴收集過程(更新訂閱對象集);在數據進行setter時通知全部依賴的組件一併進行更新操做(通知訂閱對象)。Vue數據響應系統原理示意圖以下:javascript

Vue數據響應系統

接下來對Vue數據響應系統的這兩個核心技術進行探究:html

1. 數據劫持

  Vue數據劫持主要利用Object.defineProperty來實現對對象屬性的攔截操做,攔截工做主要就Object.defineProperty中的getter和setter作文章,看一栗子:前端

var person = {
    name: '小明',
}
var name = person.name;
Object.defineProperty(person, 'name', {
    get: function() {
       console.log('name getter方法被調用');
       return name; 
    },
    set: function(val) {
      console.log('name setter方法被調用');
      name = '李' + val;
    }
})

console.log(person.name);
person.name = '小亮';
console.log(person.name);

// 結果
name getter方法被調用    
小明    
name setter方法被調用    
name getter方法被調用

  能夠看到,設置了屬性的存取器後,在設置和獲取person.name的時候,自定義的getter和setter將會被調用,這就給在這兩個方法調用時進行其它內部的自定義操做提供了可能。瞭解了Object.defineProperty的基本使用後,咱們開始從Vue源碼角度開始逐步瞭解其具體的應用,在Vue的_init初始化過程當中(傳送門)有一個initState操做,該操做主要依次對options中定義的props、methods、data、computed、watch屬性進行初始化(主要分析對data的初始化initData),該過程就主要是實現了將options.data變得可觀察:
src/core/instance/state.jsjava

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
   ...
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
     ...
     else if (!isReserved(key)) {
      // 設置data的數據代理
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 將data數據屬性變爲可觀察
  observe(data, true /* asRootData */)
}

  繼續看對observe的定義:react

export function
observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 若是已經處於觀察中,則返回觀察對象,不然將value變爲可觀察
  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
}
src/core/observer/index.js
// Observer 監聽器類
class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep() // 訂閱器
    this.vmCount = 0
    def(value, '__ob__', this) // 爲value添加__ob__屬性
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 處理數組,數組中的每一個對象都會進行可觀察化操做
      this.observeArray(value)
    } else {
      // 處理對象,主要遍歷對象中全部屬性,並將全部屬性變得可觀察
      this.walk(value)
    }
  }
}


/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 定義一個訂閱器
  const dep = new Dep()
   ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 判斷是否有自定義的getter, 沒有則直接取值
      const value = getter ? getter.call(obj) : val
      // 判斷是經過watcher進行依賴蒐集調用仍是直接數據調用方式(好比在template目標中直接使用該值,此時
      // Dep.target爲undefined,該值爲具備全局性,指向當前的執行的watcher)
      if (Dep.target) {
        dep.depend() // 將屬性自身放入依賴列表中
        // 若是子屬性爲對象,則對子屬性進行依賴收集
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) { // 處理數組的依賴收集
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 判斷是否有自定義的getter, 沒有則直接設置值,不然經過getter取值
      const value = getter ? getter.call(obj) : val
      ...
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 從新計算該對象的數據是否須要進行觀察
      childOb = !shallow && observe(newVal)
      // 設置數據後通知全部的watcher訂閱者
      dep.notify()
    }
  })
}

  經過上面步驟將options中data的的屬性變得可觀察,defineReactive方法中的閉包方法set比較好理解,就如上面所說,設置新值newVal,並判斷該值是不是爲非基本數據類型,如若不是,可能就須要重新將newVal變得可觀察;而後通知訂閱器中全部的訂閱者,進行視圖更新等操做;get中的Dep.target不太好理解,我也是研究了一兩天才明白,這裏先不說,反正就是知足Dep.target不爲undefined,則進行依賴收集,不然就是普通的數據獲取操做,返回數據便可。git

2. 訂閱-發佈

  假使你們都曉得訂閱-發佈是個什麼狀況(不太清除可自行百度,不在此佔據篇幅了),那麼,咱們要知道,誰是訂閱者,訂閱的目標是什麼?有了這個疑問,咱們接下來看看相關的數據結構定義github

2.1 依賴收集(更新訂閱者)

// src/core/observer/dep.js
export default class Dep {
  static target: ?Watcher;  //target全局
  id: number;
  subs: Array<Watcher>;

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  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)
    }
    // 循環對訂閱者進行更新操做(調用watcher的update方法)
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  // 將當前的watcher推入堆棧中,關於爲何要推入堆棧,主要是要處理模板或render函數中嵌套了多層組件,須要遞歸處理
  targetStack.push(target)
  // 設置當前watcher到全局的Dep.target,經過在此處設置,key使得在進行get的時候對當前的訂閱者進行依賴收集
  Dep.target = target
}

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


// src/core/observer/watcher.js
 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)) {
        // 將訂閱的watcher添加到當前的發佈者watcher中
        dep.addSub(this)
      }
    }
  }

  以前在getter的依賴收集過程當中,當Dep.target成立時,會執行dep.depend()方法,能夠看到,Dep中定義的depend()方法會調用Dep.targetaddDep()方法,這個過程傳遞的參數就是在defineReactive中定義的dep對象,那麼Dep.target究竟是什麼東西呢,這個能夠看Watcher對應get方法中的定義:express

// Watcher -> get()
 get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 調用getter方法
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      // 深層次遍歷
      if (this.deep) {
        traverse(value)
      }
      // 彈出
      popTarget()
      // 清除依賴,主要是針對有些發生了改變的依賴進行更新,好比新添加了依賴或者去除了原有依賴
      this.cleanupDeps()
    }
    return value
  }

// Dep-> pushTarget()
export function pushTarget (target: ?Watcher) {
  // 將當前的watcher推入堆棧中,關於爲何要推入堆棧,主要是要處理模板或render函數中嵌套了多層組件,須要遞歸處理
  targetStack.push(target)
  // 設置當前watcher到全局的Dep.target,經過在此處設置,key使得在進行get的時候對當前的訂閱者進行依賴收集
  Dep.target = target
}

  由於js是單線程執行,同一時刻只能執行一個Watcher,執行當前Watcher實例時候,Dep.target指向當前Watcher訂閱者,當在執行下一個Watcher訂閱者的get方法時候,即指向下一個訂閱者,即Dep.target永遠指向的是當前的Watcher訂閱者,而後將當前訂閱者添加到dep對應的subs訂閱列表中,同時訂閱者內部也須要記錄有哪些訂閱目標(dep),便於進行依賴收集過程的更新操做。用一張圖來表述:數組

2.2 更新通知

  在數據劫持的閉包方法setter代碼中,經過調用dep.notify()進行數據更新通知,這個階段的主要工做就是將當前訂閱目標dep更新消息通知到訂閱列表中的訂閱者(Watcher),而後訂閱者利用註冊的回調方法進行視圖渲染等操做。數據結構

// src/core/observer/dep.js
notify () {
    ...
    // 循環對訂閱者進行更新操做(調用Watcher的update方法)
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
 }

// src/core/observer/watcher.js
update () {
    /* istanbul ignore else */
    if (this.lazy) { // 是否爲懶加載(這個什麼時候執行?)
      this.dirty = true
    } else if (this.sync) { // 是否爲同步方式更新
      this.run()
    } else { // 加入到訂閱者更新隊列(最終也要執行run方法)
      queueWatcher(this)
    }
  }

 $emsp;在run()中主要調用Watcher -> get(),在這說明一下get()this.getter.call(vm, vm)的getter來源主要有兩種-函數或表達式,函數好比render function、computed中的getter(), 表達式相似於"person.name"這種類型,通過parsePath轉換成爲getter()方法。傳送門中對Watcher的種類進行了總結。

總結

  以上就是我對Vue數據響應式系統的一些學習,因爲工做緣由,先後花了一週左右的時間才寫完,初步涉獵,估計仍是有很多理解上的不到位,但願有幸能被你們看到並指出,這一過程當中參考了很多的前人好文,太多就羅列一二,但願對你們也有幫助:

另外,歡迎去本人git 相互學習和star,不勝感激。

相關文章
相關標籤/搜索