Vue源碼之:從源碼角度看數據綁定

參考文檔:vue

https://vue-js.com/learn-vue/react

https://github.com/answershuto/learnVuegit

本文主要講的是下圖重的右邊 Data 和 Watcher 之間的聯繫github

在vue當中,咱們都知道是經過雙向數據綁定的原理來進行響應式的渲染。 那,到底具體的原理是什麼呢web

固然,你們可能都會說 Object.defineProperty()方法去堅挺數據的變化。那具體是怎麼監聽的呢? 僅僅如此嘛?數組

Object數據類型

使Object數據變得‘可觀測’

這就要借用上文提到過的Obeject.definety() 首先,咱們定義一個數據對象car瀏覽器

let car = {
 'brand':'BMW',  'price':3000 } 複製代碼

咱們定義了這個car的品牌brand是BMW,價格price是3000。如今咱們能夠經過car.brandcar.price直接讀寫這個car對應的屬性值。可是,當這個car的屬性被讀取或修改時,咱們並不知情。那麼應該如何作纔可以讓car主動告訴咱們,它的屬性被修改了呢?緩存

接下來,咱們使用Object.defineProperty()改寫上面的例子:閉包

let car = {}
let val = 3000 Object.defineProperty(car, 'price', {  enumerable: true,  configurable: true,  get(){  console.log('price屬性被讀取了')  return val  },  set(newVal){  console.log('price屬性被修改了')  val = newVal  } }) 複製代碼

經過Object.defineProperty()方法給car定義了一個price屬性,並把這個屬性的讀和寫分別使用get()set()進行攔截,每當該屬性進行讀或寫操做的時候就會觸發get()set()。以下圖:app

能夠看到,car已經能夠主動告訴咱們它的屬性的讀寫狀況了,這也意味着,這個car的數據對象已是「可觀測」的了。

爲了把car的全部屬性都變得可觀測,咱們能夠編寫以下代碼:

export class Observer {
 value: any;  dep: 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  /*  將Observer實例綁定到data的__ob__屬性上面去,以前說過observe的時候會先檢測是否已經有__ob__對象存放Observer實例了,def方法定義能夠參考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16   */  def(value, '__ob__', this)  if (Array.isArray(value)) { //這部分能夠不看,數組的下面會講到  /*  若是是數組,將修改後能夠截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組數據變化響應的效果。  這裏若是當前瀏覽器支持__proto__屬性,,則直接覆蓋當前數組對象原型上的原生數組方法若是不支持該屬性,則直接覆蓋數組對象的原型。  */  const augment = hasProto  ? protoAugment /*直接覆蓋原型的方法來修改目標對象*/  : copyAugment /*定義(覆蓋)目標對象或數組的某一個方法*/  augment(value, arrayMethods, arrayKeys)   /*若是是數組則須要遍歷數組的每個成員進行observe*/  this.observeArray(value)  } else {  /*若是是對象則直接walk進行綁定*/  this.walk(value)  }  }   /**  * Walk through each property and convert them into  * getter/setters. This method should only be called when  * value type is Object.  */  /*  遍歷每個對象而且在它們上面綁定getter與setter。這個方法只有在value的類型是對象的時候才能被調用  */  walk (obj: Object) {  const keys = Object.keys(obj)  /*walk方法會遍歷對象的每個屬性進行defineReactive綁定*/  for (let i = 0; i < keys.length; i++) {  defineReactive(obj, keys[i], obj[keys[i]])  }  }   /**  * Observe a list of Array items.  */  /*對一個數組的每個成員進行observe*/  observeArray (items: Array<any>) {  for (let i = 0, l = items.length; i < l; i++) {  /*數組須要遍歷每個成員進行observe*/  observe(items[i])  }  } } 複製代碼
/**
 * Attempt to create an observer instance for a value,  * returns the new observer if successfully observed,  * or the existing observer if the value already has one.  */  /*  嘗試建立一個Observer實例(__ob__),若是成功建立Observer實例則返回新的Observer實例,若是已有Observer實例則返回現有的Observer實例。  */ export function observe (value: any, asRootData: ?boolean): Observer | void {  if (!isObject(value)) {  return  }  let ob: Observer | void  /*這裏用__ob__這個屬性來判斷是否已經有Observer實例,若是沒有Observer實例則會新建一個Observer實例並賦值給__ob__這個屬性,若是已有Observer實例則直接返回該Observer實例*/  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {  ob = value.__ob__  } else if (  /*  這裏的判斷是爲了確保value是單純的對象,而不是函數或者是Regexp等狀況。  並且該對象在shouldConvert的時候纔會進行Observer。這是一個標識位,避免重複對value進行Observer  */  observerState.shouldConvert &&  !isServerRendering() &&  (Array.isArray(value) || isPlainObject(value)) &&  Object.isExtensible(value) &&  !value._isVue  ) {  ob = new Observer(value)  }  if (asRootData && ob) {  /*若是是根數據則計數,後面Observer中的observe的asRootData非true*/  ob.vmCount++  }  return ob } 複製代碼
/**
 * Define a reactive property on an Object.  */  /*爲對象defineProperty上在變化時通知的屬性*/ export function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: Function ) {  /*在閉包中定義一個dep對象*/  const dep = new Dep()   const property = Object.getOwnPropertyDescriptor(obj, key)  if (property && property.configurable === false) {  return  }   /*若是以前該對象已經預設了getter以及setter函數則將其取出來,新定義的getter/setter中會將其執行,保證不會覆蓋以前已經定義的getter/setter。*/  // cater for pre-defined getter/setters  const getter = property && property.get  const setter = property && property.set   /*對象的子對象遞歸進行observe並返回子節點的Observer對象*/  let childOb = observe(val)  Object.defineProperty(obj, key, {  enumerable: true,  configurable: true,  get: function reactiveGetter () {  /*若是本來對象擁有getter方法則執行*/  const value = getter ? getter.call(obj) : val  if (Dep.target) {  /*進行依賴收集*/  dep.depend()  if (childOb) {  /*子對象進行依賴收集,其實就是將同一個watcher觀察者實例放進了兩個depend中,一個是正在自己閉包中的depend,另外一個是子元素的depend*/  childOb.dep.depend()  }  if (Array.isArray(value)) {  /*是數組則須要對每個成員都進行依賴收集,若是數組的成員仍是數組,則遞歸。*/  dependArray(value)  }  }  return value  },  set: function reactiveSetter (newVal) {  /*經過getter方法獲取當前值,與新值進行比較,一致則不須要執行下面的操做*/  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方法則執行setter*/  setter.call(obj, newVal)  } else {  val = newVal  }  /*新的值須要從新進行observe,保證數據響應式*/  childOb = observe(newVal)  /*dep對象通知全部的觀察者*/  dep.notify()  }  }) } 複製代碼

在上面的代碼中,咱們定義了observer類,它用來將一個正常的object轉換成可觀測的object

而且給value新增一個__ob__屬性,值爲該valueObserver實例。這個操做至關於爲value打上標記,表示它已經被轉化成響應式了,避免重複操做

而後判斷數據的類型,若是是array類型,則調用observeArray對數組的每個成員進行observe,只有object類型的數據纔會調用walk將每個屬性轉換成getter/setter的形式來偵測變化。最後,在defineReactive中當傳入的屬性值仍是一個object時使用new observer(val)來遞歸子屬性,這樣咱們就能夠把obj中的全部屬性(包括子屬性)都轉換成getter/seter的形式來偵測變化。 也就是說,只要咱們將一個object傳到observer中,那麼這個object就會變成可觀測的、響應式的object

那麼如今,咱們就能夠這樣定義car:

let car = new Observer({
 'brand':'BMW',  'price':3000 }) 複製代碼

依賴收集

什麼是依賴收集

上面。咱們已經讓object數據變的可觀測。變的可觀測之後,咱們就能知道數據何時發生了變化,那麼當數據發生變化時,咱們去通知視圖更新就行了。那麼問題又來了,視圖那麼大,咱們到底該通知誰去變化?總不能一個數據變化了,把整個視圖所有更新一遍吧,這樣顯然是不合理的。此時,你確定會想到,視圖裏誰用到了這個數據就更新誰唄。對!你想的沒錯,就是這樣。

咱們給每一個數據都建一個依賴數組(由於一個數據可能被多處使用),誰依賴了這個數據(即誰用到了這個數據)咱們就把誰放入這個依賴數組中,那麼當這個數據發生變化的時候,咱們就去它對應的依賴數組中,把每一個依賴都通知一遍,告訴他們:"大家依賴的數據變啦,大家該更新啦!"。這個過程就是依賴收集。

什麼時候收集依賴,什麼時候通知依賴更新?

所謂誰用到了這個數據,其實就是誰獲取了這個數據,而可觀測的數據被獲取時會觸發getter屬性,那麼咱們就能夠在getter中收集這個依賴。一樣,當這個數據變化時會觸發setter屬性,那麼咱們就能夠在setter中通知依賴更新。

在getter中收集依賴,在setter中通知依賴更新。

把依賴收集到哪裏

咱們給每一個數據都建一個依賴數組,誰依賴了這個數據咱們就把誰放入這個依賴數組中。單單用一個數組來存放依賴的話,功能好像有點欠缺而且代碼過於耦合。咱們應該將依賴數組的功能擴展一下,更好的作法是咱們應該爲每個數據都創建一個依賴管理器,把這個數據全部的依賴都管理起來。OK,到這裏,咱們的依賴管理器Dep類應運而生,代碼以下:

// 源碼位置:src/core/observer/dep.js
export default class Dep {  constructor () {  this.subs = []  }   addSub (sub) {  this.subs.push(sub)  }  // 刪除一個依賴  removeSub (sub) {  remove(this.subs, sub)  }  // 添加一個依賴  depend () {  if (window.target) {  this.addSub(window.target)  }  }  // 通知全部依賴更新  notify () {  const subs = this.subs.slice()  for (let i = 0, l = subs.length; i < l; i++) {  subs[i].update()  }  } }  /**  * Remove an item from an array  */ export function remove (arr, item) {  if (arr.length) {  const index = arr.indexOf(item)  if (index > -1) {  return arr.splice(index, 1)  }  } } 複製代碼

在上面的依賴管理器Dep類中,咱們先初始化了一個subs數組,用來存放依賴,而且定義了幾個實例方法用來對依賴進行添加,刪除,通知等操做。

有了依賴管理器後,咱們就能夠在getter中收集依賴,在setter中通知依賴更新了,代碼以下

/**
 * Define a reactive property on an Object.  */  /*爲對象defineProperty上在變化時通知的屬性*/ export function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: Function ) {  /*在閉包中定義一個dep對象*/  const dep = new Dep()   const property = Object.getOwnPropertyDescriptor(obj, key)  if (property && property.configurable === false) {  return  }   /*若是以前該對象已經預設了getter以及setter函數則將其取出來,新定義的getter/setter中會將其執行,保證不會覆蓋以前已經定義的getter/setter。*/  // cater for pre-defined getter/setters  const getter = property && property.get  const setter = property && property.set   /*對象的子對象遞歸進行observe並返回子節點的Observer對象*/  let childOb = observe(val)  Object.defineProperty(obj, key, {  enumerable: true,  configurable: true,  get: function reactiveGetter () {  /*若是本來對象擁有getter方法則執行*/  const value = getter ? getter.call(obj) : val  if (Dep.target) {  /*進行依賴收集*/  dep.depend()  if (childOb) {  /*子對象進行依賴收集,其實就是將同一個watcher觀察者實例放進了兩個depend中,一個是正在自己閉包中的depend,另外一個是子元素的depend*/  childOb.dep.depend()  }  if (Array.isArray(value)) {  /*是數組則須要對每個成員都進行依賴收集,若是數組的成員仍是數組,則遞歸。*/  dependArray(value)  }  }  return value  },  set: function reactiveSetter (newVal) {  /*經過getter方法獲取當前值,與新值進行比較,一致則不須要執行下面的操做*/  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方法則執行setter*/  setter.call(obj, newVal)  } else {  val = newVal  }  /*新的值須要從新進行observe,保證數據響應式*/  childOb = observe(newVal)  /*dep對象通知全部的觀察者*/  dep.notify()  }  }) } 複製代碼

依賴究竟是誰

其實在Vue中還實現了一個叫作Watcher的類,而Watcher類的實例就是咱們上面所說的那個"誰"。換句話說就是:誰用到了數據,誰就是依賴,咱們就爲誰建立一個Watcher實例。在以後數據變化時,咱們不直接去通知依賴更新,而是通知依賴對應的Watch實例,由Watcher實例去通知真正的視圖。

Watcher類的具體實現以下:

export default class Watcher {
 constructor (vm,expOrFn,cb) {  this.vm = vm;  this.cb = cb;  this.getter = parsePath(expOrFn)  this.value = this.get()  }  get () {  window.target = this;  const vm = this.vm  let value = this.getter.call(vm, vm)  window.target = undefined;  return value  }  update () {  const oldValue = this.value  this.value = this.get()  this.cb.call(this.vm, this.value, oldValue)  } }  /**  * Parse simple path.  * 把一個形如'data.a.b.c'的字符串路徑所表示的值,從真實的data對象中取出來  * 例如:  * data = {a:{b:{c:2}}}  * parsePath('a.b.c')(data) // 2  */ const bailRE = /[^\w.$]/ export function parsePath (path) {  if (bailRE.test(path)) {  return  }  const segments = path.split('.')  return function (obj) {  for (let i = 0; i < segments.length; i++) {  if (!obj) return  obj = obj[segments[i]]  }  return obj  } } 複製代碼

誰用到了數據,誰就是依賴,咱們就爲誰建立一個Watcher實例,在建立Watcher實例的過程當中會自動的把本身添加到這個數據對應的依賴管理器中,之後這個Watcher實例就表明這個依賴,當數據變化時,咱們就通知Watcher實例,由Watcher實例再去通知真正的依賴。

那麼,在建立Watcher實例的過程當中它是如何的把本身添加到這個數據對應的依賴管理器中呢?

下面咱們分析Watcher類的代碼實現邏輯:

當實例化Watcher類時,會先執行其構造函數; 在構造函數中調用了this.get()實例方法; 在get()方法中,首先通過window.target = this把實例自身賦給了全局的一個惟一對象window.target上,而後經過let value = this.getter.call(vm, vm)獲取一下被依賴的數據,獲取被依賴數據的目的是觸發該數據上面的getter,上文咱們說過,在getter裏會調用dep.depend()收集依賴,而在dep.depend()中取到掛載window.target上的值並將其存入依賴數組中,在get()方法最後將window.target釋放掉。

而當數據變化時,會觸發數據的setter,在setter中調用了dep.notify()方法,在dep.notify()方法中,遍歷全部依賴(即watcher實例),執行依賴的update()方法,也就是Watcher類中的update()實例方法,在update()方法中調用數據變化的更新回調函數,從而更新視圖。

簡單總結一下就是:Watcher先把本身設置到全局惟一的指定位置(window.target),而後讀取數據。由於讀取了數據,因此會觸發這個數據的getter。接着,在getter中就會從全局惟一的那個位置讀取當前正在讀取數據的Watcher,並把這個watcher收集到Dep中去。收集好以後,當數據發生變化時,會向Dep中的每一個Watcher發送通知。經過這樣的方式,Watcher能夠主動去訂閱任意一個數據的變化。爲了便於理解,咱們畫出了其關係流程圖,以下圖:

以上,就完全完成了對Object數據的偵測,依賴收集,依賴的更新等全部操做。

不足之處

雖然咱們經過Object.defineProperty方法實現了對object數據的可觀測,可是這個方法僅僅只能觀測到object數據的取值及設置值,當咱們向object數據裏添加一對新的key/value或刪除一對已有的key/value時,它是沒法觀測到的,致使當咱們對object數據添加或刪除值時,沒法通知依賴,沒法驅動視圖進行響應式更新。

固然,Vue也注意到了這一點,爲了解決這一問題,Vue增長了兩個全局API:Vue.set和Vue.delete,這兩個API的實現原理將會在後面學習全局API的時候說到。

總結

首先,咱們經過Object.defineProperty方法實現了對object數據的可觀測,而且封裝了Observer類,讓咱們可以方便的把object數據中的全部屬性(包括子屬性)都轉換成getter/seter的形式來偵測變化。

接着,咱們學習了什麼是依賴收集?而且知道了在getter中收集依賴,在setter中通知依賴更新,以及封裝了依賴管理器Dep,用於存儲收集到的依賴。

最後,咱們爲每個依賴都建立了一個Watcher實例,當數據發生變化時,通知Watcher實例,由Watcher實例去作真實的更新操做。

其整個流程大體以下:

Data經過observer轉換成了getter/setter的形式來追蹤變化。 當外界經過Watcher讀取數據時,會觸發getter從而將Watcher添加到依賴中。 當數據發生了變化時,會觸發setter,從而向Dep中的依賴(即Watcher)發送通知。 Watcher接收到通知後,會向外界發送通知,變化通知到外界後可能會觸發視圖更新,也有可能觸發用戶的某個回調函數等。

Array 數據類型

如何偵測

爲何Object數據和Array型數據會有兩種不一樣的變化偵測方式?

這是由於對於Object數據咱們使用的是JS提供的對象原型上的方法Object.defineProperty,而這個方法是對象原型上的,因此Array沒法使用這個方法,因此咱們須要對Array型數據設計一套另外的變化偵測機制。

萬變不離其宗,雖然對Array型數據設計了新的變化偵測機制,可是其根本思路仍是不變的。那就是:仍是在獲取數據時收集依賴,數據變化時通知依賴更新。

在哪裏收集依賴

回想一下日常在開發的時候,在組件的data中是否是都這麼寫的:

data(){
 return {  arr:[1,2,3]  } } 複製代碼

arr這個數據始終都存在於一個object數據對象中,並且咱們也說了,誰用到了數據誰就是依賴,那麼要用到arr這個數據,是否是得先從object數據對象中獲取一下arr數據,而從object數據對象中獲取arr數據天然就會觸發arrgetter,因此咱們就能夠在getter中收集依賴。

總結一句話就是:Array型數據仍是在getter中收集依賴。

使Array型數據可觀測

Array型數據發生變化,那必然是操做了Array,而JS中提供的操做數組的方法就那麼幾種,咱們能夠把這些方法都重寫一遍,在不改變原有功能的前提下,咱們爲其新增一些其餘功能,例以下面這個例子:

let arr = [1,2,3]
arr.push(4) Array.prototype.newPush = function(val){  console.log('arr被修改了')  this.push(val) } arr.newPush(4) 複製代碼

數組方法攔截器

在Vue中建立了一個數組方法攔截器,它攔截在數組實例與Array.prototype之間,在攔截器內重寫了操做數組的一些方法,當數組實例使用操做數組方法時,其實使用的是攔截器中重寫的方法,而再也不使用Array.prototype上的原生方法。以下圖所示:

/*取得原生數組的原型*/
const arrayProto = Array.prototype /*建立一個新的數組對象,修改該對象上的數組的七個方法,防止污染原生數組方法*/ export const arrayMethods = Object.create(arrayProto) /**  * Intercept mutating methods and emit events  */  /*這裏重寫了數組的這些方法,在保證不污染原生數組原型的狀況下重寫數組的這些方法,截獲數組的成員發生的變化,執行原生數組操做的同時dep通知關聯的全部觀察者進行響應式處理*/ ;[  '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)   /*數組新插入的元素須要從新進行observe才能響應式*/  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  /*dep通知全部註冊的觀察者進行響應式處理*/  ob.dep.notify()  return result  }) }) 複製代碼

使用攔截器

在上一小節的圖中,咱們把攔截器作好還不夠,還要把它掛載到數組實例與Array.prototype之間,這樣攔截器纔可以生效。

其實掛載不難,咱們只需把數據的__proto__屬性設置爲攔截器arrayMethods便可,源碼實現以下

if (Array.isArray(value)) {
 /*  若是是數組,將修改後能夠截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組數據變化響應的效果。  這裏若是當前瀏覽器支持__proto__屬性,,則直接覆蓋當前數組對象原型上的原生數組方法若是不支持該屬性,則直接覆蓋數組對象的原型。  */  const augment = hasProto  ? protoAugment /*直接覆蓋原型的方法來修改目標對象*/  : copyAugment /*定義(覆蓋)目標對象或數組的某一個方法*/  augment(value, arrayMethods, arrayKeys)   /*若是是數組則須要遍歷數組的每個成員進行observe*/  this.observeArray(value)  } 複製代碼
/**
 * Augment an target Object or Array by intercepting  * the prototype chain using __proto__  */  /*直接覆蓋原型的方法來修改目標對象或數組*/ function protoAugment (target, src: Object) {  /* eslint-disable no-proto */  target.__proto__ = src  /* eslint-enable no-proto */ }  /**  * Augment an target Object or Array by defining  * hidden properties.  */ /* istanbul ignore next */ /*定義(覆蓋)目標對象或數組的某一個方法*/ 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])  } } 複製代碼

上面代碼中首先判斷了瀏覽器是否支持__proto__,若是支持,則調用protoAugment函數把value.__proto__ = arrayMethods;若是不支持,則調用copyAugment函數把攔截器中重寫的7個方法循環加入到value上。

數組新增元素偵測

咱們知道,能夠向數組內新增元素的方法有3個,分別是:pushunshiftsplice。咱們只需對這3中方法分別處理,拿到新增的元素,再將其轉化便可

若是是pushunshift方法,那麼傳入參數就是新增的元素;若是是splice方法,那麼傳入參數列表中下標爲2的就是新增的元素,拿到新增的元素後,就能夠調用observe函數將新增的元素轉化成響應式的了。

數組如何收集依賴

數組的依賴也在getter中收集,那麼在getter中到底該如何收集呢?這裏有一個須要注意的點,那就是依賴管理器定義在Observer類中,而咱們須要在getter中收集依賴,也就是說咱們必須在getter中可以訪問到Observer類中的依賴管理器,才能把依賴存進去。源碼是這麼作的:

function defineReactive (obj,key,val) {
 let childOb = observe(val)  Object.defineProperty(obj, key, {  enumerable: true,  configurable: true,  get(){  if (childOb) {  childOb.dep.depend()  }  return val;  },  set(newVal){  if(val === newVal){  return  }  val = newVal;  dep.notify() // 在setter中通知依賴更新  }  }) }  /**  * Attempt to create an observer instance for a value,  * returns the new observer if successfully observed,  * or the existing observer if the value already has one.  * 嘗試爲value建立一個0bserver實例,若是建立成功,直接返回新建立的Observer實例。  * 若是 Value 已經存在一個Observer實例,則直接返回它  */ export function observe (value, asRootData){  if (!isObject(value) || value instanceof VNode) {  return  }  let ob  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {  ob = value.__ob__  } else {  ob = new Observer(value)  }  return ob } 複製代碼

在上面代碼中,咱們首先經過observe函數爲被獲取的數據arr嘗試建立一個Observer實例,在observe函數內部,先判斷當前傳入的數據上是否有__ob__屬性,由於在上篇文章中說了,若是數據有__ob__屬性,表示它已經被轉化成響應式的了,若是沒有則表示該數據還不是響應式的,那麼就調用new Observer(value)將其轉化成響應式的,並把數據對應的Observer實例返回。

而在defineReactive函數中,首先獲取數據對應的Observer實例childOb,而後在getter中調用Observer實例上依賴管理器,從而將依賴收集起來。

深度偵測

let arr = [
 {  name:'NLRX' age:'18'  } ] 複製代碼

數組中包含了一個對象,若是該對象的某個屬性發生了變化也應該被偵測到,這就是深度偵測。

if (Array.isArray(value)) {  
 /*  若是是數組,將修改後能夠截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組數據變化響應的效果。  這裏若是當前瀏覽器支持__proto__屬性,,則直接覆蓋當前數組對象原型上的原生數組方法若是不支持該屬性,則直接覆蓋數組對象的原型。  */  const augment = hasProto  ? protoAugment /*直接覆蓋原型的方法來修改目標對象*/  : copyAugment /*定義(覆蓋)目標對象或數組的某一個方法*/  augment(value, arrayMethods, arrayKeys)   /*若是是數組則須要遍歷數組的每個成員進行observe*/  this.observeArray(value)  } else {  /*若是是對象則直接walk進行綁定*/  this.walk(value)  } 複製代碼

如何通知依賴

咱們應該在攔截器裏通知依賴,要想通知依賴,首先要能訪問到依賴。要訪問到依賴也不難,由於咱們只要能訪問到被轉化成響應式的數據value便可,由於vaule上的__ob__就是其對應的Observer類實例,有了Observer類實例咱們就能訪問到它上面的依賴管理器,而後只需調用依賴管理器的dep.notify()方法,讓它去通知依賴更新便可。源碼以下:

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)   /*數組新插入的元素須要從新進行observe才能響應式*/  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  /*dep通知全部註冊的觀察者進行響應式處理*/  ob.dep.notify()  return result  }) 複製代碼

不足之處

前文中咱們說過,對於數組變化偵測是經過攔截器實現的,也就是說只要是經過數組原型上的方法對數組進行操做就均可以偵測到,可是別忘了,咱們在平常開發中,還能夠經過數組的下標來操做數據,以下:

let arr = [1,2,3]
arr[0] = 5; // 經過數組下標修改數組中的數據 arr.length = 0 // 經過修改數組長度清空數組 複製代碼

而使用上述例子中的操做方式來修改數組是沒法偵測到的。 一樣,Vue也注意到了這個問題, 爲了解決這一問題,Vue增長了兩個全局API:Vue.setVue.delete,這兩個API的實現原理將會在後面學習全局API的時候說到。

總結

首先咱們分析了對於Array型數據也在getter中進行依賴收集;其次咱們發現,當數組數據被訪問時咱們垂手可得能夠知道,可是被修改時咱們卻很難知道,爲了解決這一問題,咱們建立了數組方法攔截器,從而成功的將數組數據變的可觀測。接着咱們對數組的依賴收集及數據變化如何通知依賴進行了深刻分析;最後咱們發現Vue不但對數組自身進行了變化偵測,還對數組中的每個元素以及新增的元素都進行了變化偵測,咱們也分析了其實現原理。

本文使用 mdnice 排版

相關文章
相關標籤/搜索