Vue原理解析(七):全面深刻理解響應式原理(下)-數組進階篇

上一篇:Vue原理解析(六):全面深刻理解響應式原理(上)-對象基礎篇html

再初步瞭解了響應式的原理後,接下來咱們深刻響應式,解析數組響應式的原理。vue

數組更新

首先來看下改變數組的兩種方式:面試

export default {
  data() {
    list: [1, 2, 3]
  },
  methods: {
    changeArr1() {  // 從新賦值
      this.list = [4, 5, 6]
    },
    changeArr2() {  // 方法改變
      this.list.push(7)
    }
  }
}
複製代碼

對於這兩種改變數據的方式,vue內部的實現並不相同。算法

方式一:從新賦值

  • 實現原理和對象是同樣的,再vm._render()時有用到list,就將依賴收集起來,從新賦值後走對象派發更新的那一套。

方式二:方法改變

  • 走對象的那一套就不行了,由於並非從新賦值,雖然改變了數組自身但並不會觸發set,原有的響應式系統根本感知不到,因此咱們接下來就分析,vue是如何解決使用數組方法改變自身觸發視圖的。

Dep收集依賴的位置

上一篇它的聲音並不大,如今咱們來從新認識它。Dep類的主要做用就是管理依賴,在響應式系統中會有兩個地方要實例化它,固然它們都會進行依賴的收集,首先是以前具體包裝的時候:數組

function defineReactive(obj, key, val) {
  const dep = new Dep()  // 自動依賴管理器
  ...
  Object.defineProperty(obj, key, {
    get() {...},
    set() {...}
  })
}
複製代碼

這裏它會對每一個讀取到的key都進行依賴收集,不管是對象/數組/原始類型,若是是經過從新賦值觸發set就會使用這裏收集到的依賴進行更新,筆者這裏就把它命名爲自動依賴管理器,方便和以後的區分。瀏覽器

還有一個地方也會對它進行實例化就是Observer類中:bash

class Observer {
  constructor(value) {
    this.dep = new Dep() //  手動依賴管理器
    ...
  }
}
複製代碼

這個依賴管理器並不能經過set觸發,並且是隻會收集對象/數組的依賴。也就是說對象的依賴會被收集兩次,一次在自動依賴管理器內,一次在這裏,爲何要收集兩次,本章以後說明。而最重要的是數組使用方法改變自身去觸發更新的依賴就是再這收集的,這個前提仍是頗有必要交代下的。app

數組的響應式原理

數組響應式數據的建立

數組示例:
export default {
  data() {
    return {
      list: [{
        name: 'cc',
        sex: 'man'
      }, {
        name: 'ww',
        sex: 'woman'
      }]
    }
  }
}
複製代碼

流程開始仍是執行observe方法,接下來咱們更加詳細分析響應式系統:post

function observe(value) {
  if (!isObject(value) { //不是數組或對象,再見
    return
  }
  
  let ob
  if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {  // 避免重複包裝
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}
複製代碼

只要是響應式的數據都會有一個__ob__的屬性,它是在Observer類中掛載的,若是已經有__ob__屬性就直接賦值給ob,不會再次去建立Observer實例,避免重複包裝。首次確定沒__ob__屬性了,因此再從新看下Observer類的定義:ui

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()  // 手動依賴管理器
    
    def(value, '__ob__', this)  // 掛載__ob__屬性,三個參數
    ...
  }
}
複製代碼

首先定義一個手動依賴管理器,而後掛載一個不可枚舉的__ob__屬性到傳入的value下,表示它的一個響應式的數據,並且__ob__的值就是當前Observer類的實例,它擁有實例上的全部屬性和方法,這很重要,咱們接下來看下def是如何完成屬性掛載的:

function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
複製代碼

其實就是一個簡單的封裝,若是第四個參數不傳,enumerable項就是不可枚舉的了。接着看Observer類的定義:

class Observer {
  constructor(value) {
	...
    if (Array.isArray(value)) {  // 數組
      ...
    } else {  // 對象
      this.walk(value)  // {list: [{...}, {...}]}
    }
  }
  
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
複製代碼

首次傳入仍是對象的格式,因此會執行walk遍歷的將對象每一個屬性包裝爲響應式的,再來看下defineReactive方法:

function defineReactive(obj, key, val) { 

  const dep = new Dep()  // 自動依賴管理器
  
  val = obj[key]  // val爲數組 [{...}, {...}]
  
  let childOb = observe(val)  // 傳入到observe裏,返回Observer類實例
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {  // 依賴收集
      if (Dep.target) {
        dep.depend()  // 自動依賴管理器收集依賴
        if (childOb) {  // 只有對象或數組纔有返回值
          childOb.dep.depend()  // 手動依賴管理器收集依賴
          if (Array.isArray(val)) { 若是是數組
            dependArray(val) // 將數組每一項包裝爲響應式
          }
        }
      }
      return value
    },
    set(newVal) {
      ...
    }
  }
}
複製代碼

首先遞歸執行observe(val)會有一個返回值了,若是是對象或數組的話,childOb就是Observer類的實例,以數組格式在observe內作了什麼,咱們以後分析。接下來在get內的childOb.dep.depend()執行的就是Observer類裏定義的dep進行依賴收集,收集的render-watcher跟自動依賴管理器是同樣的。接下來若是是數組就執行dependArray方法:

function dependArray (value) {
  for (let e, i = 0, i < value.length; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()  // 是響應式數據
    if (Array.isArray(e)) {  // 若是是嵌套數組
      dependArray(e)  // 遞歸調用本身
    }
  }
}
複製代碼

這個方法的做用就是遞歸的爲每一項收集依賴,這裏每一項都必需要有__ob__屬性,而後執行Observer類裏的dep手動依賴收集器進行依賴收集。咱們如今知道數組的依賴是放在Observer類裏的dep屬性內,如今來看下怎麼去更新這個收集到的依賴。

數組方法更新依賴

在以前defineReactive方法裏有這麼一句,let childOb = observe(val),經過求值,val如今就是具體的數組,以數組的形式傳入到observe方法內,咱們來看下在Observer類中作什麼:

class Observer {
  constructor(value) {
    if (Array.isArray(value)) {  // 數組
      
      const augment = hasProto ? protoAugment : copyAugment  // 第一句
      
      augment(value, arrayMethods, arrayKeys)  // 第二句
      
      this.observeArray(value)  // 第三句
      
    }
  }
}
複製代碼

主要就是執行了三句邏輯,因此咱們首先來看下分別作了什麼。

數組方法改變自身觸發視圖原理:首先覆蓋數組的__proto__隱式原型,借用數組原生的方法,定義vue內部自定義的數組異變方法攔截原生方法,再調用異變方法改變自身以後手動觸發依賴。

有了這隻指向月亮的手,咱們如今就一塊兒去往心中的月亮。首先分析第一句:

const augment = hasProto ? protoAugment : copyAugment

--------------------------------------------------------

const hasProto = '__proto__' in {}

function protoAugment (target, src) {  // src爲攔截器
  target.__proto__ = src
}

function copyAugment (target, src, keys) {  // src爲攔截器
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
複製代碼

__proto__這個屬性並非全部瀏覽器都有的,筆者以前也一直覺得這是一個通用屬性,原來IE11纔開始有這個屬性,經過'__protp__' in {}也能夠快速判斷當前瀏覽瀏覽器是否IE10以上?確實用過,好用!

是否有__proto__屬性處理方法也不相同,若是有的的話,直接在protoAugment方法內使用攔截器覆蓋;若是沒有__proto__屬性,那就在當前調用數組下掛載攔截器裏的異變數組方法。

實現原理都是根據原型鏈的特性,再數組使用原生方法以前加一個攔截器,攔截器內定義的都是能夠改變數組自身的異變方法,若是攔截器內沒有就向一層去找。

接下來分析第二句,也是整個數組方法實現的核心:

augment(value, arrayMethods, arrayKeys)

----------------------------------------------------------------------------

const arrayProto = Array.prototype  // 數組原型,有全部數組原生方法
const arrayMethods = Object.create(arrayProto)  // 建立空對象攔截器

const methodsToPatch = [  // 七個數組使用會改變自身的方法
  'push','pop','shift','unshift','splice','sort','reverse'
]

methodsToPatch.forEach(function (method) {  // 往攔截器下掛載異變方法

  const original = arrayProto[method]  // 過濾出七個數組原生原始方法
  
  def(arrayMethods, method, function mutator (...args) {  // 不定參數
  
    const result = original.apply(this, args)  // 借用原生方法,this就是調用的數組
    
    const ob = this.__ob__  // 以前Observer類下掛載的__ob__
    
    let inserted  // 臨時保存數組新增的值
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) {
      ob.observeArray(inserted)  // 執行Observer類中的observeArray方法
    }
    ob.dep.notify()  // 觸發手動依賴收集器內的依賴
    
    return result  // 返回數組執行結果
  })
})

const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 
// 獲取攔截器內掛載好的七個方法key的數組集合,用於沒有__proto__的狀況

複製代碼

首先獲取數組的全部原生方法,從中過濾出七個調用能夠改變自身的方法,而後建立攔截器在它下面掛載七個通過異變的方法,這個異變方法的使用效果和原生方法是一致的,由於就是使用apply借用的,將執行後的結果保存給result,好比:

const arr = [1, 2, 3]
const result = arr.push(4)
複製代碼

這個時候arr就變成了[1,2,3,4]result保存的就是新數組的長度,既然模仿就模仿的像一點。

接下來的賦值const ob = this.__ob__,以前定義的__ob__不只僅是標記位,保存的也是Observer類的實例。

有三個操做數組的方法是會添加新值的,使用inserted變量保存新添的值。若是是使用splice方法,就將前面兩個表示位置的參數截取掉。而後使用observeArray方法將新添加的參數包裝爲響應式的。

最後通知手動依賴管理器內收集到的依賴派發更新,返回數組執行後的結果。

最後執行第三句:

this.observeArray(value)

observeArray(items) {
  for (let i = 0, i < items.length; i++) {
    observe(items[i])
  }
}
複製代碼

將數組內的是數組或對象的每一項都包裝成響應式的。因此當數組再使用方法時,首先會去arrayMethods攔截器內查找是不是異變方法,不是的話纔去調用數組原生方法:

export default {
  data() {
    return {
      list: [1, 2, 3]
    }
  },
  methods: {
    changeArr1() {
      this.list.push(4)  // 調用攔截器裏的異變方法
    },
    changeArr2() {
      this.list = this.list.concat(5) 
      // 調用原生方法,由於攔截器裏沒有,必須從新賦值由於不會改變自身
    }
  }
}
複製代碼

至此數組響應式系統相關的也講解完畢,整個響應式系統也分析完了。

數組響應式總結:數組的依賴收集仍是在get方法裏,不過依賴的存放位置會有不一樣,不是在defineReactive方法的dep裏,而是在Observer類中的dep裏,依賴的更新是在攔截器裏的數組異變方法最後手動更新的。

一樣數組響應式也是否是完美的,它也有缺點:

export default {
  data() {
    return {
      list: [1, 2, 3]
    }
  },
  methods: {
    changeListItem() {  // 改變數組某一項
      this.list[1] = 5
    },
    changeListLength() {  // 改變數組長度
      this.list.length = 0
    }
  }
}
複製代碼

以上兩種方式都改變了數組,但響應式是沒法監聽到的,由於不會觸發set也沒用使用數組方法去改變。不過你們還記得咱們以前介紹的手動依賴管理器麼?咱們能夠手動去通知它更新依賴而後觸發視圖變動~

export default {
  data() {
    return {
      list: [1, 2, 3],
      info: { name: 'cc' }
    }
  },
  methods: {
    changeListItem() {  // 改變數組某一項
      this.list[1] = 5
      this.list.__ob__.dep.notify()  // 手動通知
    },
    changeListLength() {  // 改變數組長度
      this.list.length = 0
      this.list.__ob__.dep.notify()  // 手動通知
    },
    changeInfo() {
      this.info.sex = 'man'
      this.info.__ob__.dep.notify()  // 對象也能夠
    }
  }
}
複製代碼

常規的對象增長屬性是不會被感知到的,也可使用手動通知的形式觸發依賴,知道這個原理仍是很cool的~

官方填坑

上面的奇技淫巧並不被推薦使用,咱們仍是介紹下官方推薦的彌補響應式不足的兩個API$set$delete,其實它們只是處理一些狀況,都不知足的最後仍是調了一下手動依賴管理器來實現,只是進行了簡單的二次封裝。

this.$set || Vue.set

function set(target, key, val) {
  if(Array.isArray(target)) {  // 數組
    target.length = Math.max(target.length, key)  // 最大值爲長度
    target.splice(key, 1, val)  // 移除一位,異變方法派發更新
    return val
  }
  
  if(key in target && !(key in Object.prototype)) {  // key屬於target
    target[key] = val  // 賦值操做觸發set
    return val
  }
  
  if(!target.__ob__) {  // 普通對象賦值操做
    target[key] = val
    return val
  }
  
  defineReactive(target.__ob__.value, key, val)  // 將新值包裝爲響應式
  
  target.__ob__.dep.notify()  // 手動觸發通知
  
  return val
}
複製代碼

首先判斷target是不是數組,是數組的話第二個參數就是長度了,設置數組的長度,而後使用splice這個異變方法插入val。 而後是判斷key是否屬於target,屬於的話就是賦值操做了,這個會觸發set去派發更新。接下來若是target並非響應式數據,那就是普通對象,那就設置一個對應key吧。最後以上狀況都不知足,說明是在響應式數據上新增了一個屬性,把新增的屬性轉爲響應式數據,而後通知手動依賴管理器派發更新。

this.$delete || Vue.delete

function del (target, key) {
  if (Array.isArray(target)) {  // 數組
    target.splice(key, 1)  // 移除指定下表
    return
  }
  
  if (!hasOwn(target, key)) {  // key不屬於target,再見
    return
  }
  
  delete target[key]  // 刪除對象指定key
  
  if (!target.__ob__) {  // 普通對象,再見
    return
  }
  target.__ob__.dep.notify()  // 手動派發更新
}
複製代碼

this.$delete就更加簡單了,首先若是是數組就使用異變方法splice移除指定下標值。若是target是對象但key不屬於它,再見。而後刪除制定key的值,若是target不是響應式對象,刪除的就是普通對象一個值,刪了就刪了。不然通知手動依賴管理器派發更新視圖。

最後按照慣例咱們仍是以一道vue可能會被問到的面試題做爲本章的結束~

面試官微笑而又不失禮貌的問道:

  • 請簡單描述下vue響應式系統?

懟回去:

  • 簡單來講就是使用Object.defineProperty這個API爲數據設置getset。當讀取到某個屬性時,觸發get將讀取它的組件對應的render watcher收集起來;當重置賦值時,觸發set通知組件從新渲染頁面。若是數據的類型是數組的話,還作了單獨的處理,對能夠改變數組自身的方法進行重寫,由於這些方法不是經過從新賦值改變的數組,不會觸發set,因此要單獨處理。響應系統也有自身的不足,因此官方給出了$set$delete來彌補。

下一篇:Vue原理解析(八):一塊兒搞明白使人頭疼的diff算法

順手點個贊或關注唄,找起來也方便~

參考:

Vue.js源碼全方位深刻解析

Vue.js深刻淺出

Vue.js組件精講

剖析 Vue.js 內部運行機制

相關文章
相關標籤/搜索