1. 程式人生 > >Vue 2.3.4原始碼分析之雙向繫結原理

Vue 2.3.4原始碼分析之雙向繫結原理

    要想實現雙向繫結需要做到兩點:1.如何監聽data物件是否改變;2.物件變化後如何去更新檢視

    一、如何監聽data物件是否改變

     在Vue監聽data物件是否改變主要通過defineReactive方法來做到的,就是利用Object.defineProperty的get和set方法。如下程式碼所示:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  const dep = new Dep()//每個監聽的物件都新建一個dep物件,dep相當於觀察者模式中的被觀察物件,watcher相當於觀察者物件


  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

  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const 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) {
      const 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 = observe(newVal)
      dep.notify()//通知所有訂閱了該dep的觀察者watcher物件去更新檢視,watcher會呼叫updateComponent方法去更新檢視
    }
  })
}

    但是Object.defineProperty也不是沒有缺點,就是它無法監聽陣列的push,shift等方法。那麼如何監聽陣列的這一些方法呢?

    我們知道陣列物件本身沒有push這一些方法,都是繼承至Array.prototype物件的,所以我們想到可以重寫這一些方法,但不能直接修改Array.prototype物件上的方法,否則將影響所有陣列的使用。那Vue是如何做到的呢?分為兩種情況:

    1.如果瀏覽器支援__proto__熟悉的話,vue採用的是代理模式實現的,實現方式如下:

    首先Vue建立了一個繼承至Array.prototype的arrayMethods代理物件,在該代理物件中重寫了相關的陣列原生方法,

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

    然後再將陣列物件繼承至arrayMethods物件

function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

    2.如果瀏覽器不支援__proto__屬性,就只需在陣列物件上新增了push等方法,這樣當陣列呼叫push等方法時將不再使用Array.prototype的方法,而是這一些新增的方法。

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

    二、物件變化後如何去進行檢視渲染

    首先物件變化,會觸發對於的defineReactive中的set方法,在set方法中會觸發dep.notify(),該方法會觸發所有訂閱了該dep的watcher去只執行檢視渲染,可以參考http://blog.csdn.net/zhoulu001/article/details/79372555

   總結如下,其實vue的雙向繫結主要基於觀察者模式實現的。Dep相當於被觀察物件,Watcher相關於觀察者物件,在頁面首次渲染後,defineReactive中的get方法就讓watcher進行了對dep的訂閱,一旦data資料變化,就呼叫dep.notify()去通知所有的訂閱了該dep的watcher,去執行渲染操作。