通俗易懂的Vue響應式原理以及依賴收集

最近在看一些底層方面的知識。因此想作個系列嘗試去聊聊這些比較複雜又很重要的知識點。學習就比如是座大山,只有本身去爬山,才能看到不同的風景,體會更加深入。今天咱們就來聊聊Vue中比較重要的響應式原理以及依賴收集。html

響應式原理

Object.defineProperty() 和 Proxy 對象,均可以用來對數據的劫持操做。何爲數據劫持呢?就是在咱們訪問或者修改某個對象的某個屬性的時候,經過一段代碼進行攔截,而後進行額外的操做,返回結果。vue中雙向數據綁定就是一個典型的應用。vue

Vue2.x 是使用 Object.defindProperty(),來實現對對象的監聽。react

Vue3.x 版本以後就改用Proxy實現。數組

在MDN中是這樣定義:bash

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。閉包

Object.defineProperty(obj, prop, descriptor)mvvm

  • obj:要定義屬性的對象
  • prop:要定義或修改的屬性名稱
  • descriptor:要定義或修改的屬性描述符(configurable: 可改變的;writable:可寫的;enumerable:可枚舉的;get\set:設置或獲取對象的某個屬性的值)
const data = {}
const name = 'zhangsan'
Object.defineProperty(data, 'name', {
    writable: true,
    configurable: true,
    get: function () {
        console.log('get')
        return name
    },
    set: function (newVal) {
        console.log('set')
        name = newVal
    }
})
複製代碼

當把一個普通的 JavaScript 對象傳入 Vue 實例做爲 data 選項,Vue 將遍歷此對象全部的 property,並使用 Object.defineProperty 把這些 property 所有轉爲 getter/setter。簡單理解就是在data和用戶之間作了一層代理中間層,在vue initData的時候,將_data上面的數據代理到vm上,經過observer類將全部的data變成可觀察的,及對data定義的每個屬性進行getter\setter操做,這就是Vue實現響應式的基礎。ide

vue響應式
Vue數據響應式變化主要涉及 Observer, Watcher , Dep 這三個主要的類。所以要弄清Vue響應式變化須要明白這個三個類之間是如何運做聯繫的;以及它們的原理,負責的邏輯操做。

響應式原理(Observer)

Observer類是將每一個目標對象(即data)的鍵值轉換成getter/setter形式,用於進行依賴收集以及調度更新。那麼在vue這個類是如何實現的:函數

  • 一、observer實例綁定在data的ob屬性上面,防止重複綁定;
  • 二、若data爲數組,先實現對應的變異方法(Vue重寫了數組的7種原生方法)再將數組的每一個成員進行observe,使之成響應式數據;
  • 三、不然執行walk()方法,遍歷data全部的數據,進行getter/setter綁定。這裏的核心方法就是 defineReative(obj, keys[i], obj[keys[i]])
// 監聽對象屬性Observer類
class Observer {
  constructor(value) {
    this.value = value
    if (!value || (typeof value !== 'object')) {
      return
    } else {
      this.walk(value)
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}
複製代碼
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      return val
    },
    set: function reactiveSetter(newVal) {
      // 注意:value一直在閉包中,此處設置完以後,再get時也是會獲得最新的值
      if (newVal === val) return
      updateView()
    }
  })
}
function updateView() {
  console.log('視圖更新了')
}

const data = {
  name: 'zhangsan',
  age: 20
}
new Observer(data)

data.name = 'lisi'  // 打印‘視圖更新了’
複製代碼

這就是簡單的一個Observer類,這也是vue響應式的基本原理。但咱們都知道 object.defineproperty的存在一些缺點:性能

一、對於複雜的對象須要深度監聽,遞歸到底,一次性計算量大

二、沒法監聽新增屬性/刪除屬性(Vue.set Vue.delete)

三、沒法監聽數組,需特殊處理,也就是上面說的變異方法

這也就是vue3改進的一方面,後文咱們也會着重講解vue3 proxy如何作響應式的。

擴展1、vue如何深度監聽

上圖中咱們看到data中的一級目錄name、age在值改變的時候,會出發視圖更新,但在咱們實際開發過程當中,data可能會是比較複雜的對象,嵌套了好幾層:

const data = {
  name: 'zhangsan',
  age: 20,
  info: {
      address: '北京'
  }
}
data.info.address = '上海' // 並無執行。
複製代碼

形成這種緣由是,代碼中defineReactive接收到的val是一個對象,爲了不這種複雜的對象vue採用遞歸的思想在defineReactive函數中在執行一次observer函數就行,遞歸將對象在遍歷一次獲取key/value值,new Observer(val)。一樣在設置值的時候可能會把name也設置成一個對象,所以在data值更新的時候也須要進行判斷深度監聽

function defineReactive(obj, key, val) {
  new Observer(val) // 深度監聽
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      return val
    },
    set: function reactiveSetter(newVal) {
      // 注意:value一直在閉包中,此處設置完以後,再get時也是會獲得最新的值
      if (newVal === val) return
      new Observer(val) // 深度監聽
      updateView()
    }
  })
}
複製代碼

擴展2、vue數組的監聽

object.defineproperty對數組是不起做用的,那麼在vue中又是如何去監聽數組的變化,其實Vue 將被偵聽的數組的變動方法進行了包裹。接下來將用簡單代碼演示:

// 防止全局污染,從新定義數組原型
const oldArrayProperty = Array.prototype
// 建立新對象,原型指向oldArrayProperty
const arrProto = Object.create(oldArrayProperty);

['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
  arrProto[methodName] = function () { // 在定義數組的方法
    updateView()
    oldArrayProperty[methodName].call(this, ...arguments) // 實際執行數組的方法
  }
})

// 在Observer函數中對數組進行處理
if (Array.isArray(value)) {
    value.__proto__ = arrProto
  }
複製代碼

從代碼中看到,在Observer函數有一層對數組進行攔截,將數組的__proto__指向了一個arrProto,arrProto是一個對象,這個對象指向數組的原型,所以arrProto擁有了數組原型上的方法,而後在這對象上從新自定義了數組的7中方法將其包裹,但又不會影響數組原型的方法,這就是變異,再將數組的每一個成員進行observe,使之成響應式數據。

依賴收集(Watcher、Dep)

咱們如今有這麼一個Vue對象

new Vue({
    template: 
        `<div>
            <span>text1:</span> {{text1}}
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2'
    }
})
複製代碼

咱們能夠從以上代碼看出,data中text2並無被模板實際用到,爲了提升代碼執行效率,咱們沒有必要對其進行響應式處理,所以,依賴收集簡單理解就是收集只在實際頁面中用到的data數據,那麼Vue是如何進行依賴收集的,這也就是下面要講的Watcher、Dep類了。

被Observer的data在觸發 getter 時,Dep 就會收集依賴,而後打上標記,這裏就是標記爲Dep.target

Watcher是一個觀察者對象。依賴收集之後的watcher對象被保存在Dep的subs中,數據變更的時候Dep會通知watcher實例,而後由watcher實例回調cb進行視圖更新。

Watcher能夠接受多個訂閱者的訂閱,當有data變更時,就會經過 Dep 給 Watcher 發通知進行更新。

咱們能夠用一些簡單的代碼去實現這個過程。

class Observer {
  constructor(value) {
    this.value = value
    if (!value || (typeof value !== 'object')) {
      return
    } else {
      this.walk(value)
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}
// 訂閱者Dep,存放觀察者對象
class Dep {
  constructor() {
    this.subs = []
  }
  /*添加一個觀察者對象*/
  addSub (sub) {
    this.subs.push(sub)
  }
  /*依賴收集,當存在Dep.target的時候添加觀察者對象*/
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  // 通知全部watcher對象更新視圖
  notify () {
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}
class Watcher {
  constructor() {
    /* 在new一個Watcher對象時將該對象賦值給Dep.target,在get中會用到 */
    Dep.target = this;
  }
  update () {
    console.log('視圖更新啦')
  }
  /*添加一個依賴關係到Deps集合中*/
  addDep (dep) {
    dep.addSub(this)
  }
}
function defineReactive (obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      dep.depend() /*進行依賴收集*/
      return val
    },
    set: function reactiveSetter (newVal) {
      if (newVal === val) return
      dep.notify()
    }
  })
}
class Vue {
  constructor (options) {
    this._data = options.data
    new Observer(this._data) // 全部data變成可觀察的
    new Watcher() // 建立一個觀察者實例
    console.log('render~', this._data.test)
  }
}
let o = new Vue({
  data: {
    test: 'hello vue.'
  }
})
o._data.test = 'hello mvvm!'

Dep.target = null
複製代碼

總結

  • 一、在Vue中模版編譯過程當中的指令或者數據綁定都會實例化一個Watcher實例,實例化過程當中會觸發get()將自身指向Dep.target;
  • 二、data在Observer時執行getter會觸發dep.depend()進行依賴收集,
  • 三、當data中被 Observer的某個對象值變化後,觸發subs中觀察它的watcher執行 update() 方法,最後其實是調用watcher的回調函數cb,進而更新視圖。

Vue3-Proxy實現響應式

Proxy能夠理解成在目標對象前架設一個攔截層,外界對該對象的出發必須先經過這層攔截層,所以提供了一種機制能夠對外界的訪問進行過濾和改寫。

function reactive(value = {}) {
  if (!value || (typeof value !== 'object')) {
    return
  }
  // 代理配置
  const proxyConf = {
    get(target, key,receiver) {
      // 只處理非原型的屬性
      let ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('get', key)
      }
      const result = Reflect.get(target, key, receiver)
       // 深度監聽 
       // 性能如何提高? 何時用何時遞歸
      return reactive(result)
    },
    set(target, key, val, receiver) {
      // 重複的數據不處理
      const oldVal = target[key]
      if (val === oldVal) return true
      
      const ownKey = Reflect.ownKeys(target) 
      if (ownKeys.include(key)) {
          console.log('已有的key', key)
      } else {
          console.log('新增的key', key)
      }
      
      const result = Reflect.set(target, key, val, receiver)
      console.log('set', key, val)
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log('delete property', key)
      return result
    }
  }
  // 生成代理對象
  const observed = new Proxy(value, proxyConf)
  return observed
}
const data = {
  name: 'zhangsan',
  age: 20,
  info: {
    address: '北京'
  },
  num: [1, 2, 3]
}
const proxyData = reactive(data)
proxyData.name ='lisi'  // set name lisi
複製代碼

proxy深度監聽的性能提高,在proxy中對於複雜的對象,只會geter()的時候對當前層的監聽,好比說在info中

info: {
    address: '北京',
    a: {
        b: {
            c: {
                d: 2
            }
        }
    }
}
複製代碼

修改proxyData.info.a並不會把後面b、c、d遞歸出來,避免了object.defineProperty一次性所有遞歸計算完成。因爲proxy原生對數組就能監聽,因此也是對object.defineProperty缺點的一個改進。而且從代碼中能夠看出,在增長/刪除時proxy也同樣能夠監聽到,這就是proxy的優點。

擴展1、Reflect

reflect對象的方法和proxy對象的方法一一對應,只要是proxy對象的方法,就能在reflect對象找到對應的方法。這就使得proxy對象能夠方便的調用對應的reflect方法來完成默認的行爲,做爲修改行爲的基礎。

Reflect有實際上是對Object對象的規範化吧,將Object對象的一些明顯屬於語言內部的方法(好比Object.defineProperty)放到Reflect對象上。

Reflect.get(target, name, receiver): 查到並返回target對象上的name屬性,沒有該屬性會返回undefined

Reflect.set(target, name, value, receiver): 設置target對象的name屬性等於value

Reflect.has(object, name): 判斷對象上是否有name屬性

Reflect.ownKeys(target): 返回對象的全部屬性

擴展2、使用proxy實現觀察者模式

// 觀察者模式指的是函數自動觀察數據對象的模式,一旦數據有變化,數據就會自動執行
const queuedObservers = new Set()
const observe = fn => queuedObservers.add(fn)
const observable = obj => new Proxy(obj, {set})

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver)
  queuedObservers.forEach(observe => observe())
  return result
}

const person = observable({ // 觀察對象
  name: '張三',
  age: 20
})
function print() {  // 觀察者
  console.log(`${person.name}, ${person.age}`)
}
observe(print)
person.name = '李四'

複製代碼
相關文章
相關標籤/搜索