全面瞭解Vue3的 reactive 和相關函數

Vue3的 reactive 怎麼用,原理是什麼,官網上和reactive相關的那些函數又都是作什麼用處的?這裏會一一解答。vue

ES6的Proxy

Proxy 是 ES6 提供的一個能夠攔截對象基礎操做的代理。由於 reactive 採用 Proxy 代理的方式,實現引用類型的響應性,因此咱們先看看 Proxy 的基礎使用方法,以便於我理解 reactive 的結構。react

咱們先來定義一個函數,瞭解一下 Proxy 的基本使用方式:git

// 定義一個函數,傳入對象原型,而後建立一個Proxy的代理
const myProxy = (_target) => {
  // 定義一個 Proxy 的實例
  const proxy = new Proxy(_target, {
    // 攔截 get 操做
    get: function (target, key, receiver) {
      console.log(`getting ${key}!`, target[key])
      // 用 Reflect 調用原型方法
      return Reflect.get(target, key, receiver)
    },
    // 攔截 set 操做
    set: function (target, key, value, receiver) {
      console.log(`setting ${key}:${value}!`)
      // 用 Reflect 調用原型方法
      return Reflect.set(target, key, value, receiver)
    }
  })
  // 返回實例
  return proxy
}

// 使用方法,是否是和reactive有點像?
const testProxy = myProxy({
  name: 'jyk',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
})
console.log('本身定義的Proxy實例:')
console.log(testProxy)
// 測試攔截狀況
testProxy.name = '新的名字' // set操做 
console.log(testProxy.name) // get 操做

Proxy 有兩個參數 target 和 handle。
* target:要代理的對象,也能夠是數組,可是不能是基礎類型。
* handler:設置要攔截的操做,這裏攔截了 set 和 get 操做,固然還能夠攔截其餘操做。es6

咱們先來看一下運行結果:
本身寫的 Proxy 實例的運行結果api

  • Handler 能夠看到咱們寫的攔截函數 get 和 set;
  • Target 能夠看到對象原型。

注意:這裏只是實現了 get 和 set 的攔截,並無實現數據的雙向綁定,模板也不會自動更新內容,Vue內部作了不少操做才實現了模板的自動更新功能。數組

用 Proxy 給 reactive 套個娃,會怎麼樣?

有個奇怪的地方,既然 Proxy 能夠實現對 set 等操做的攔截,那麼 reactive 爲啥不返回一個能夠監聽的鉤子呢?爲啥要用 watch 來實現監聽的工做?函數

爲啥會這麼想?由於看到了 Vuex4.0 的設計,明明已經把 state 總體自動變成了 reactive 的形式,那麼爲啥還非得在 mutations 裏寫函數,實現 set 操做呢?好麻煩的樣子。性能

外部直接對 reactive 進行操做,而後 Vuex 內部監聽一下,這樣你們不就都省事了嗎?要實現插件功能,仍是跟蹤功能,不都是能夠自動實現了嘛。測試

因此我以爲仍是能夠套個娃的。插件

實現模板的自動刷新

原本覺得上面那個 myProxy 函數,傳入一個 reactive 以後,就能夠自動實現更新模板的功能了,結果模板沒理我。

這不對呀,我只是監聽了一下,不是又交給 reactive 了嗎?爲啥模板不理我?

通過各類折騰,終於找到了緣由,因而函數改爲了這樣:

/**
   * 用 Proxy定義一個 reactive 的套娃,實現能夠監放任意屬性變化的目的。(不包含嵌套對象的屬性)
   * @param {*} _target  要攔截的目標
   * @param {*} callback 屬性變化後的回調函數
   */
  const myReactive = (_target, callback) => {
    let _change = (key, value) => {console.log('內部函數')}
    const proxy = new Proxy(_target, {
      get: function (target, key, receiver) {
        if (typeof key !== 'symbol') {
          console.log(`getting ${key}!`, target[key])
        } else {
          console.log('getting symbol:', key, target[key])
        }
        // 調用原型方法
        return Reflect.get(target, key, receiver)
      },
      set: function (target, key, value, receiver) {
        console.log(`setting ${key}:${value}!`)
        // 源頭監聽
        if (typeof callback === 'function') {
          callback(key, value)
        }
        // 任意位置監聽
        if (typeof _target.__watch === 'function') {
          _change(key, value)
        }
        // 調用原型方法
        return Reflect.set(target, key, value, target)  // 這裏有變化,最後一個參數改爲 target
      }
    })
    // 實現任意位置的監聽,
    proxy.__watch = (callback) => {
      if (typeof callback === 'function') {
        _change = callback
      }
    }
    // 返回實例
    return proxy
  }

代碼稍微多了一些,咱們一塊一塊看。

  • get
    這裏要作一下 symbol 的判斷,不然會報錯。好吧,其實咱們彷佛不須要 console.log。

  • set
    這裏改了一下最後一個參數,這樣模板就能夠本身更新了。

  • 設置 callback 函數,實現源頭監聽
    設置一個回調函數,才能在攔截到set操做的時候,通知外部的調用者。只是這樣只適合於定義實例的地方。那麼接收參數的地方怎麼辦呢?

調用方法以下:

// 定義一個攔截reactive的Proxy
    // 而且實現源頭的監聽
    const myProxyReactive = myReactive(retObject,
      ((key, value) =>{
        console.log(`ret外部得到通知:${key}:${value}`)
      })
    )

這樣咱們就能夠在回調函數裏面獲得修改的屬性名稱,以及屬性值。

這樣咱們作狀態管理的時候,是否是就不用特地去寫 mutations 裏面的函數了呢?

  • 內部設置一個鉤子函數
    設置一個 _change() 鉤子函數,這樣接收參數的地方,能夠經過這個鉤子來獲得變化的通知。

調用方法以下:

// 任意位置的監聽
    myProxyReactive.__watch((key, value) => {
      console.log(`任意位置的監聽:${key}:${value}`)
    })

只是好像哪裏不對的樣子。
首先這個鉤子沒找到合適的地方放,目前放在了原型對象上面,就是說破壞了原型對象的結構,這個彷佛會有些影響。

而後,接收參數的地方,不是能夠直接獲得修改的狀況嗎?是否還須要作這樣的監聽?

最後,好像沒有 watch 的 deep 監聽來的方便,那麼問題又來了,爲啥 Vuex 不用 watch 呢?或者悄悄的用了?

深層響應式代理:reactive

說了半天,終於進入正題了。
reactive 會返回對象的響應式代理,這種響應式轉換是深層的,能夠影響全部的嵌套對象。

注意:返回的是 object 的代理,他們的地址是相同的,並無對object進行clone(克隆),因此修改代理的屬性值,也會影響原object的屬性值;同時,修改原object的屬性值,也會影響reactive返回的代理的屬性值,只是代理沒法攔截直接對原object的操做,因此模板不會有變化。

這個問題並不明顯,由於咱們通常不會先定義一個object,而後再套上reactive,而是直接定義一個 reactive,這樣也就「不存在」原 object 了,可是咱們要了解一下原理。

咱們先定義一個 reactive 實例,而後運行看結果。

// js對象
const person = {
  name: 'jyk',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
}
// person 的 reactive 代理 (驗證地址是否相同)
const personReactive = reactive(person)
// js 對象 的 reactive 代理 (通常用法)
const objectReactive = reactive({
  name: 'jykReactive',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
})

// 查看 reactive 實例結構
console.log('reactive', objectReactive )

// 獲取嵌套對象屬性
const contacts = objectReactive .contacts
// 由於深層響應,因此依然有響應性
console.log('contacts屬性:', contacts)
 
// 獲取簡單類型的屬性
let name = objectReactive.name 
// name屬性是簡單類型的,因此失去響應性
console.log('name屬性:', name) 

運行結果:
reactive的打印結果

  • Handler:能夠看到 Vue 除重寫 set 和 get 外,還重寫了deleteProperty、has和ownKeys。

  • Target: 指向一個Object,這是創建reactive實例時的對象。

屬性的結構:
reactive的屬性打印結果

而後再看一下兩個屬性的打印結果,由於 contacts 屬性是嵌套的對象,因此單獨拿出來也是具備響應性的。

而 name 屬性因爲是 string 類型,因此單獨拿出來並不會自動得到響應性,若是單獨拿出來還想保持響應性的話,可使用toRef。

注意:若是在模板裏面使用{{personReactive.name}}的話,那麼也是有響應性的,由於這種用法是得到對象的屬性值,能夠被Proxy代理攔截,因此並不須要使用toRef。
若是想在模板裏面直接使用{{name}}而且要具備響應性,這時才須要使用toRef。

淺層響應式代理:shallowReactive

有的時候,咱們並不須要嵌套屬性也具備響應性,這時可使用shallowReactive 來得到淺層的響應式代理,這種方式只攔截本身的屬性的操做,不涉及嵌套的對象屬性的操做。

const personShallowReactive = shallowReactive({
  name: 'jykShallowReactive',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
})

// 查看 shallowReactive 實例結構
console.log('shallowReactive', objectShallowReactive)

// 獲取嵌套對象屬性
const contacts = objectShallowReactive.contacts
// 由於淺層代理,因此沒有響應性
console.log('contacts屬性:', contacts)

// 獲取簡單類型的屬性
let name = objectShallowReactive.name 
// 由於淺層代理且簡單類型,因此失去響應性
console.log('name屬性:', name) 

shallowReactive的打印結果

shallowReactive 也是用 Proxy 實現響應性的,而單獨使用contacts屬性並無響應性,由於 shallowReactive 是淺層代理,因此不會讓嵌套對象得到響應性。

注意:objectShallowReactive.contacts.QQ = 123 ,這樣修改屬性也是沒有響應性的。

單獨使用的屬性的形式:

shallowReactive的屬性

嵌套對象和name屬性,都沒有變成響應式。

作一個不容許響應的標記:markRaw

有的時候咱們不但願js對象變成響應式的,這時咱們能夠用markRaw 作一個標記,這樣即便使用 reactive 也不會變成響應式。

若是肯定某些數據是不會變化的,那麼也就不用變成響應式,這樣能夠節省一些沒必要要的性能開銷。

// 標記js對象
const object = markRaw({
  name: 'jyk',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
})

// 試圖對標記的對象作相應性代理
const retObject2 = reactive(object)
// 使用對象的屬性作相應性代理
const retObject1 = reactive({
  name: object.name
})
console.log('做爲初始值:', retObject1) // 沒法變成響應性代理
console.log('沒法變成響應式:', retObject2) // 能夠變成響應性代理

運行結果:

markRaw的打印結果

作標記後的js對象做爲參數,不會變成響應式,可是使用屬性值做爲參數,仍是能夠變成響應式。

那麼哪些地方能夠用到呢?咱們能夠在給組件設置(引用類型的)屬性的時候使用,默認狀況下組件的屬性都是自帶響應性的,可是若是父組件裏設置給子組件的屬性值永遠不會發生變化,那麼還變成響應式的話,就有點浪費性能的嫌疑了。

若是想節約一下的話,能夠在父組件設置屬性的時候加上markRaw標記。

深層只讀響應式代理:readonly

有的時候雖然咱們想獲得一個響應式的代理,可是隻想被讀取,而不但願被修改(好比組件的props,組件內部不但願被修改),那麼這時候咱們能夠用readonly。

readonly能夠返回object、reactive或者ref的深層只讀代理,咱們來分別測試一下:

// object的只讀響應代理
const objectReadonly = readonly(person)
// reactive 的只讀響應代理
const reactiveReadonly = readonly(objectReactive)
// 查看 readonly 實例結構
console.log('object 的readonly', objectReadonly)
console.log('reactive 的readonly', reactiveReadonly)

// 獲取嵌套對象屬性
const contacts = reactiveReadonly.contacts
console.log('contacts屬性:', contacts) // 由於深層響應,因此依然有響應性

// 獲取簡單類型的屬性
let name = reactiveReadonly.name 
console.log('name屬性:', name) // 屬性是簡單類型的,因此失去響應性

運行結果:

object的readonly

  • Handler,明顯攔截的函數變少了,set的參數也變少了,點進去看源碼,也僅僅只有一行返回警告的代碼,這樣實現攔截設置屬性的操做。
  • Target,指向object。

運行結果:

reactive的readonly

  • Handler,這部分是同樣的。
  • Target,指向的不是object,而是一個Proxy代理,也就是reactive。

淺層只讀響應代理:shallowReadonly

和readonly相對應,shallowReadonly是淺層的只讀響應代理,和readonly的使用方式同樣,只是不會限制嵌套對象只讀。

// object 的淺層只讀代理
const objectShallowReadonly = shallowReadonly(person)
// reactive 的淺層只讀代理
const reactiveShallowReadonly = shallowReadonly(objectReactive)

shallowReadonly的結構和 readonly 的一致,就不貼截圖了。

獲取原型:toRaw

toRaw 能夠獲取 Vue 創建的代理的原型對象,可是不能獲取咱們本身定義的Proxy的實例的原型。

toRaw大可能是在Vue內部使用,目前只發如今向indexedDB裏面寫入數據的時候,須要先用 toRaw 取原型,不然會報錯。

// 獲取reactive、shallowReactive、readonly、shallowReadonly的原型
console.log('深層響應的原型', toRaw(objectReactive))
console.log('淺層響應的原型', toRaw(objectShallowReactive))
console.log('深層只讀的原型', toRaw(objectReadonly))
console.log('淺層只讀的原型', toRaw(objectShallowReadonly))

運行結果都是普通的object,就不貼截圖了。

類型判斷

Vue提供了三個用於判斷類型的函數:

* isProxy:判斷對象是不是Vue創建的Proxy代理,包含reactive、readonly、shallowReactive和shallowReadonly建立的代理,可是不會判斷本身寫的Proxy代理。

  • isReactive:判斷是不是reactive建立的代理。若是readonly的原型是reactive,那麼也會返回true。

* isReadonly:判斷是不是readonly、shallowReadonly建立的代理。這個最簡單,只看代理不看target。

咱們用這三個函數判斷一下咱們上面定義的這些Proxy代理,看看結果如何。

咱們寫點代碼對比一下:

const myProxyObject = myProxy({title:'222', __v_isReactive: false})
    console.log('myProxyObject', myProxyObject)
    const myProxyReactive = myProxy(objectReactive)
    console.log('myProxyReactive', myProxyReactive)

    // 試一試 __v_isReadonly
    console.log('objectReactive', objectReactive)
    console.log('__v_isReadonly'
      , objectReactive.__v_isReadonly
      , objectReactive.__v_isReactive
      )

    return {
      obj: { // js對象
        check1: isProxy(person),
        check2: isReactive(person),
        check3: isReadonly(person)
      },
      myproxy: { // 本身定義的Proxy object
        check1: isProxy(myProxyObject),
        check2: isReactive(myProxyObject),
        check3: isReadonly(myProxyObject)
      },
      myproxyReactive: { // 本身定義的Proxy reactive
        check1: isProxy(myProxyReactive),
        check2: isReactive(myProxyReactive),
        check3: isReadonly(myProxyReactive)
      },
      // 深層響應  reactive(object)
      reto: { // reactive(object)
        check1: isProxy(objectReactive),
        check2: isReactive(objectReactive),
        check3: isReadonly(objectReactive)
      },
      // 淺層響應 參數:object
      shallowRetObj: {
        check1: isProxy(objectShallowReactive),
        check2: isReactive(objectShallowReactive),
        check3: isReadonly(objectShallowReactive)
      },
      // 淺層響應 參數:reactive
      shallowRetRet: {
        check1: isProxy(objectShallowReactive),
        check2: isReactive(objectShallowReactive),
        check3: isReadonly(objectShallowReactive)
      },

      // 深層只讀,參數 object =======================
      readObj: { // readonly object
        check1: isProxy(objectReadonly),
        check2: isReactive(objectReadonly),
        check3: isReadonly(objectReadonly)
      },
      // 深層只讀,參數 reactive
      readRet: { // readonly reactive
        check1: isProxy(reactiveReadonly),
        check2: isReactive(reactiveReadonly),
        check3: isReadonly(reactiveReadonly)
      },
      // 淺層只讀 參數:object
      shallowReadObj: {
        check1: isProxy(objectShallowReadonly),
        check2: isReactive(objectShallowReadonly),
        check3: isReadonly(objectShallowReadonly)
      },
      // 淺層只讀 參數:reactive
      shallowReadRet: {
        check1: isProxy(reactiveShallowReadonly),
        check2: isReactive(reactiveShallowReadonly),
        check3: isReadonly(reactiveShallowReadonly)
      },
      person
    }

對比結果:

驗證類型的對比測試

總結一下:

  • isReadonly 最簡單,只有readonly、shallowReadonly創建的代理纔會返回 true,其餘的都是 false。

  • isProxy也比較簡單,Vue創建的代理纔會返回true,若是是本身定義的Proxy,要看原型是誰,若是原型是 reactive(包括其餘三個)的話,也會返回true。

  • isReactive就有點複雜,reactive 創建的代理會返回 true,其餘的代理(包含本身寫的)還要看一下原型,若是是 reactive 的話,也會返回true。

判斷依據

那麼這三個函數是依據什麼判斷的呢?本身作的 Proxy 無心中監控到了「__v_isReactive」,難道是隱藏屬性?測試了一下,果真是這樣。

myProxy({title:'測試隱藏屬性', __v_isReactive: true}),這樣定義一個實例,也會返回true。

reactive直接賦值的方法

使用的時候咱們會發現一個問題,若是直接給 reactive 的實例賦值的話,就會「失去」響應性,這個並非由於 reactive 失效了,而是由於 setup 只會運行一次,return也只有一次給模板提供數據(地址)的機會,模板只能獲得一開始提供的 reactive 的地址,若是後續直接對 reactive 的實例賦值操做,會覆蓋原有的地址,產生一個新的Proxy代理地址,然而模板並不會獲得這個新地址,還在使用「舊」地址,由於沒法獲知新地址的存在,因此模板不會有變化。

那麼就不能直接賦值了嗎?其實仍是有方法的,只須要保證地址不會發生變化便可。

對象的總體賦值的方法。

有請 ES6 的 Object.assign 登場,這個方法是用來合併兩個或者多個對象的屬性的,若是屬性名稱相同後面的屬性會覆蓋前面的屬性。因此你們在使用的時候要謹慎使用,確保兩個對象的屬性就兼容的,不會衝突。

代碼以下:

Object.assign(objectReactive, {name: '合併', age: 20, newProp: '新屬性'})

數組的總體賦值的方法。

數組就方便多了,能夠先清空再 push 的方式,代碼以下:

// retArray.length = 0 // 這裏清空的話,容易照成閃爍,因此不要急
setTimeout(() => {
  const newArray = [
    { name: '11', age: 18 },
    { name: '22', age: 18 }
  ]
  // 等到這裏再清空,就不閃爍了。
  retArray.length = 0
  retArray.push(...newArray)
}, 1000)

var 和 let、const

ES6 新增了 let 和 const,那麼咱們應該如何選擇呢?
簡單的說,var沒必要繼續使用了。

let 和 const 的最大區別就是,前者是定義「變量」的,後者是定義「常量」的。

可能你會以爲奇怪,上面的代碼都是用const定義的,可是後續代碼都是各類改呀,怎麼就常量了?其實const判斷的是,地址是否改變,只要地址不變就能夠。

對於基礎類型,值變了地址就變了;而對於引用類型來講,改屬性值的話,對象地址是不會發生變化的。

而 const 的這個特色整合能夠用於保護 reactive 的實例。由Vue的機制決定,reactive的實例的地址是不能夠改變的,變了的話模板就不會自動更新,const能夠確保地址不變,變了會報錯(開發階段須要eslint支持)。

因而const和reactive(包括 ref 等)就成了絕配。

源碼:

https://gitee.com/naturefw/nf-vue-cdn/tree/master/cdn/project-compositionapi

在線演示:

https://naturefw.gitee.io/nf-vue-cdn/cdn/project-compositionapi/

相關文章
相關標籤/搜索