全面了解Vue3的 ref 和相關函數和計算屬性


基礎類型的響應性 —— ref

在vue3里面,我們可以通過 reactive 來實現引用類型的響應性,那么基礎類型的響應性如何來實現呢?

可能你會想到這樣來實現:

const count = reactive({value: 0})
count.value += 1

這么做確實可以實現,而且也很像 ref 的使用方式,都是要 .value 嘛。那么 ref內部 是不是這么實現的呢?

我們先定義兩個 ref 的實例並且打印看看。

    const refCount = ref(0) // 基礎類型
    console.log('refCount ', refCount )

    const refObject = ref({ value: 0 }) // 引用類型
    console.log('refObject ', refObject )

看一下結果:

基礎類型的 ref

引用類型的 ref

我們都知道 reactive 是通過 ES6 的 Proxy 來實現的,基礎類型的 ref 顯然和 Proxy 沒啥關系,而引用類型的 ref 是先把原型變成 reactive, 然后再掛到 value 上面。
這樣看來,和我們的猜測不太一樣呢,那么 ref 到底是如何實現的呢?我們可以看一下 ref 的源碼。

ref 的源碼

代碼來自於 vue.global.js ,調整了一下先后順序。

  function ref(value) {
      return createRef(value);
  }
  function createRef(rawValue, shallow = false) {
      if (isRef(rawValue)) {
          return rawValue;
      }
      return new RefImpl(rawValue, shallow);
  }
  class RefImpl {
      constructor(_rawValue, _shallow = false) {
          this._rawValue = _rawValue;
          this._shallow = _shallow;
          this.__v_isRef = true;
          this._value = _shallow ? _rawValue : convert(_rawValue); // 深層 ref or 淺層ref
      }
      get value() {
          track(toRaw(this), "get" /* GET */, 'value');
          return this._value;
      }
      set value(newVal) {
          if (hasChanged(toRaw(newVal), this._rawValue)) {
              this._rawValue = newVal;
              this._value = this._shallow ? newVal : convert(newVal);
              trigger(toRaw(this), "set" /* SET */, 'value', newVal);
          }
      }
  }
  const convert = (val) => isObject(val) ? reactive(val) : val;

  • ref
    這是我們使用的函數,里面使用 createRef 來創建一個實例。

  • createRef
    做一些基礎判斷,然后進入主題,正式創建ref。這里還可以創建 shallowRef。

  • RefImpl
    這個才是主體,顯然這是 ES6 的 class,constructor 是初始化函數,依據參數創建一個實例,並且設置實例的屬性。這個和上面 ref 的打印結果也是可以對應上的。
    整個class的代碼也是非常簡單,設置幾個“內部”屬性,記錄需要的數據,然后設置“外部”屬性 value,通過setter、getter 實現對 value 的操作攔截,set 里面主要是 trigger 這個函數,由它調用模板的自動刷新的功能。

  • convert
    很顯然,判斷一下參數是不是 object,如果是的話,變成 reactive 的形式。
    這個就可以解釋,引用類型的 ref 是如何實現響應性的,明顯是先變成 reactive,然后在掛到 value 上面(掛之前判斷一下是不是淺層的)。

ref 和 reactive 的關系

通過打印結果的對比以及分析源碼可以發現:

  • 基礎類型的 ref 和 reactive 沒有任何關系。
  • 引用類型的 ref ,先把 object 變成 reactive ,即利用 reactive 來實現引用類型的響應性。

關系就是這樣的,千萬不要再混淆了。

shallowRef

淺層響應式,只監聽 .value 的變化,真簡單類型的響應式。

function shallowRef(value) {
      return createRef(value, true); // true 淺層
  }

通過源碼我們可以發現,在把引用類型掛到 value 之前,先判斷一下是不是淺層的,如果是淺層的,並不會變成 reactive,而是直接把原來的對象掛在 value 上面,shallowRef 和 ref 的區別就在於這一點。

我們寫幾個實例看看效果:

  setup () {
     // 淺層的測試 
    // 基礎類型
    const srefCount = shallowRef(0)
    console.log('refCount ', srefCount )

    // 引用類型
    const srefObject = shallowRef({ value: 0 })
    console.log('refObject ', srefObject )

    // 嵌套對象
    const srefObjectMore = shallowRef({ info: {a: 'jyk'} })
    console.log('shallowRef ', srefObjectMore )

    // reactive 的 shallowRef
    const ret = reactive({name: 'jyk'})
    const shallowRefRet = shallowRef(ret)
    console.log('shallowRefRet ', shallowRefRet )

    // ==================== 事件 ==================
    // 修改基礎類型
    const setNumber = () => {
      srefCount.value = new Date().valueOf()
      console.log('srefCount ', srefCount )
    }

    // 修改引用類型的屬性
    const setObjectProp = () => {
      srefObject.value.value = new Date().valueOf()
      console.log('srefObject ', srefObject )
    }
 
    // 修改引用類型的value
    const setObject = () => {
      srefObject.value = { value: new Date().valueOf() }
      console.log('srefObject ', srefObject )
    }

    // 修改嵌套引用類型的屬性
    const setObjectMoreProp = () => {
      srefObjectMore.value.info.a = new Date().valueOf()
      console.log('srefObjectMore ', srefObjectMore )
    }
    
    // 修改嵌套引用類型的value
    const setObjectMore = () => {
      srefObjectMore.value = { qiantao: 1234567 }
      console.log('srefObjectMore ', srefObjectMore )
    }

    // 修改reactive 的淺層ref
    const setObjectreactive = () => {
      shallowRefRet.value.name = '淺層的reactive'
      console.log('shallowRefRet ', shallowRefRet )
    }
  }

看看結果:

淺層的ref

測試了一下響應性:

  • 基礎類型 srefCount 有響應性;
  • 引用類型 srefObject 的屬性沒有響應性,但是直接修改 .value 是有響應性的。
  • 嵌套的引用類型 srefObjectMore ,屬性和嵌套屬性都是沒有響應性的,但是直接修改 .value 是有響應性的。
  • reactive 套上 shallowRef ,然后修改 shallowRef.value.屬性 = xxx ,也是可以響應的,所以淺層的ref 也不絕對,還要看內部結構。

triggerRef

手動執行與 shallowRef 關聯的任何效果。

官網的中文版里面寫的很繞,其實就是 讓 shallowRef 原本不具有響應性的部分,具有響應性。
shallowRef 是淺層的,深層部分是沒有響應性的,那么如果非得讓這部分也具有響應性呢?
這時候可以用 triggerRef 來實現。
好吧,目前還沒有想到有啥具體的應用場景,因為一般都直接簡單粗暴的用 ref 或者 reactive 了,全都自帶響應性。

測試了各種情況,發現 triggerRef 並不支持 shallowReactive,還以為能支持呢。(或許是我寫的測試代碼有問題吧,官網也沒提 shallowReactive)

基於上面的例子,在適當的位置加上 triggerRef(xxx)就可以了。

  setup () {
    // 引用類型
    const srefObject = shallowRef({ value: 0 })
    // 嵌套對象
    const srefObjectMore = shallowRef({ value: {a: 'jyk'} })
    // reactive 的 shallowRef
    const ret = reactive({name: 'reactive'})
    const shallowRefRet = shallowRef(ret)
 
    // 淺層的reactive
    const myShallowReactive = shallowReactive({info:{name:'myShallowReactive'}})
    const setsRet = () => {
      myShallowReactive.info.name = new Date().valueOf()
      triggerRef(myShallowReactive)  // 修改后使用,不支持
   }

    // ==================== 事件 ==================

    // 修改引用類型的屬性
    const setObjectProp = () => {
      srefObject.value.value = new Date().valueOf()
      triggerRef(srefObject) // 修改后使用
    }
 
    // 修改引用類型的value
    const setObject = () => {
      srefObject.value = { value: new Date().valueOf() }
      triggerRef(srefObject)
   }

    // 修改嵌套引用類型的屬性
    const setObjectMoreProp = () => {
      srefObjectMore.value.value.a = new Date().valueOf()
      triggerRef(srefObjectMore)
  }
    
    // 修改嵌套引用類型的value
    const setObjectMore = () => {
      srefObjectMore.value.value = { value: new Date().valueOf() }
      triggerRef(srefObjectMore)
    }

    // 修改reactive 的淺層ref
    const setObjectreactive = () => {
      shallowRefRet.value.name = '淺層的reactive' + new Date().valueOf()
      triggerRef(shallowRefRet)
    }

    return {
      srefObject, // 引用類型
      srefObjectMore, // 嵌套引用類型
      shallowRefRet, // reactive 的淺層ref
      myShallowReactive, // 淺層的reactive
      setsRet, // 修改淺層的reactive
      setObjectProp, // 修改引用類型的屬性
      setObject, // 修改引用類型的value
      setObjectMoreProp, // 修改嵌套引用類型的屬性
      setObjectMore, // 修改嵌套引用類型的value
      setObjectreactive // 試一試reactive的淺層ref
    }
  }

深層部分,不使用 triggerRef 就不會刷新模板,使用了 triggerRef 就可以刷新模板。
話說,為啥會有這個函數?

isRef

通過 __v_isRef 屬性 判斷是否是 ref 的實例。這個沒啥好說的。
vue.global.js 源碼:

function isRef(r) {
     return Boolean(r && r.__v_isRef === true);
 }

unref

  • 使用.value的語法糖
    unref 是一個語法糖,判斷是不是 ref 的,如果是則取.value,不是的話取原型。
    vue.global.js 源碼:
  function unref(ref) {
      return isRef(ref) ? ref.value : ref;
  }
  • unref 的用途
    普通對象直接.屬性即可使用,但是 ref 卻需要.value才可以,混合使用的時候容易暈頭,尤其在函數內部接收參數的時候,無法確定傳入的是 reactive 還是 ref,如果每次都用 isReactive 或者 isRef 來判斷類型,然后決定是否用.value,那就太麻煩了。於是有了這個語法糖。

toRef 和 toRefs

toRef 可以用來為源響應式對象上的 property 性創建一個 ref。然后可以將 ref 傳遞出去,從而保持對其源 property 的響應式連接。
toRefs 將響應式對象轉換為普通對象,其中結果對象的每個 property 都是指向原始對象相應 property 的ref

話說,官網的解釋為啥總是這么令人費解呢?
我們還是先看看例子

 setup () {
    /**
     * 定義 reactive
     * 直接解構屬性,看響應性
     * 使用toRef解構,看響應性
     * 使用toRefs解構,看響應性
     * 按鈕只修改reactive
     */
    const person = reactive({
      name: 'jyk',
      age: 18
    })
    console.log('person ', person )

    // 直接獲取屬性
    const name = person.name
    console.log('name ', name )
    
    const refName = toRef(person, 'name')
    console.log('refName ', refName )

    const personToRefs = toRefs(person)
    console.log('personToRefs ', personToRefs )

    const update = () => {
      // 修改原型
      person.name = new Date()
    }

    return {
      person, // reactive
      name, // 獲取屬性
      refName, // 使用toRef
      personToRefs,
      update // 修改屬性
    }
  }

當我們修改person的屬性值的時候,toRef 和 toRefs 的實例也會自動變化。而直接獲取的name屬性並不會變化。

toRef 就是想實現直接使用對象的屬性名,並且仍然享有響應性的目的。
toRef 就是對reactive 進行解構,然后仍然享有響應性的目的。

其實,說是變成了ref,但是我們看看打印結果就會發現,其實並不完全是ref。

toRef

toRefs

看類名和屬性,toRef 和 ref 也是有區別的。

toRef 為啥可以響應

toRef 也是一個語法糖。

如果使用常規的方式對 reactive 進行解構的話就會發現,雖然解構成功了,但是也失去響應性(僅限於基礎類型的屬性,嵌套對象除外)。

那么如何實現解構后還具有響應性呢?這時候就需要使用 toRef 了。

看上面那個例子,使用 refName 的時候,相當於使用 person['name'],這樣就具有響應性了。

你可能會問,就這?對就是這么簡單,不信的話,我們來看看源碼:

  function toRef(object, key) {
      return isRef(object[key])
          ? object[key]
          : new ObjectRefImpl(object, key);
  }

  class ObjectRefImpl {
      constructor(_object, _key) {
          this._object = _object;
          this._key = _key;
          this.__v_isRef = true;
      }
      get value() {
          return this._object[this._key];  // 相當於 person['name']
      }
      set value(newVal) {
          this._object[this._key] = newVal;
      }
  }

看 get 部分,是不是 相當於 person['name'] ?

另外,雖然 toRef 看起來好像是變成了 ref,但是其實只是變成了 ref (RefImpl)的雙胞胎兄弟(ObjectRefImpl),並沒有變成 ref(RefImpl)。
ref 是 RefImpl, 而 toRef 是 ObjectRefImpl,這是兩個不同的class 。
toRef 可以看做是 ref 同系列的 class,后面還有一個同系列的。

toRefs

了解 toRef 之后,toRefs 就好理解了,從表面上看,可以把 reactive 的所有屬性都解構出來,從內部代碼來看,就是把多個 toRef 放在了數組(或者對象)里面。

function toRefs(object) {
      if ( !isProxy(object)) {
          console.warn(`toRefs() expects a reactive object but received a plain one.`);
      }
      const ret = isArray(object) ? new Array(object.length) : {};
      for (const key in object) {
          ret[key] = toRef(object, key);
      }
      return ret;
  }

customRef

自定義一個ref,並對其依賴項跟蹤和更新觸發進行顯式控制。它需要一個工廠函數,該函數接收 track 和 trigger 函數作為參數,並應返回一個帶有 get 和 set 的對象。

如果上面那段沒看懂的話,可以跳過。

簡單的說,就是在 ref 原有的 set、get 的基礎上,加入我們自己寫的代碼,以達到一定的目的。

話說,官網寫的例子還真是……
反正一開始我是沒看懂,后來又反復看,又把代碼敲出來運行,又查了一下“debounce”的意思。
最后終於明白了,這是一個防抖(延遲響應)的代碼。

一般用戶在文本框里輸入內容,立即就會響應,但是如果在查詢功能里面的話就會有點小郁悶。
用戶輸入個“1”,立即就去后端查詢“1”,
然后用戶又輸入個“2”,立即又去后端查詢“12”,
然后用戶又輸入個“3”,立即又去后端查詢“123”。
……
這個響應是很快,但是有點“折騰”的嫌疑,那么能不能等用戶把“123”都輸入好了,再去后端查詢呢?

官網的例子就是實現這樣的功能的,我們把例子完善一下,就很明顯了。

const useDebouncedRef = (value, delay = 200) => {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track() // vue內部的跟蹤函數
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger() // vue內部的自動更新的函數。
        }, delay) // 延遲時間
      }
    }
  })
}

  setup () {
    const text = useDebouncedRef('hello', 1000) // 定義一個 v-model
    console.log('customRef', text)

    const update = () => {
      // 修改后延遲刷新
      text.value = '1111' + new Date().valueOf()
    }

    return {
      text,
      update
    }
  }

  customRef 對象:{{text}} <br><br>
  <input v-model="text" type="text">
  • get
    沒有改變,直接用原方法。

  • set
    使用 setTimeout 實現延遲響應的功能,把 Vue 內部的 trigger() 放在 setTimeout 里面就好。

這樣就可以了,延遲多長的時間可以自定義,這里是一秒。一秒內用戶輸入的內容,會一次性更新,而不是每輸入一個字符就更新一次。

  • v-model="text"
    可以作為 v-model 來使用。

customRef 的 源碼

我們再來看看 customRef 內部源碼的實現方式。

  function customRef(factory) {
      return new CustomRefImpl(factory);
  }
  class CustomRefImpl {
      constructor(factory) {
          this.__v_isRef = true;
          const { get, set } = factory(() => track(this, "get" /* GET */, 'value'), () => trigger(this, "set" /* SET */, 'value'));
          this._get = get;
          this._set = set;
      }
      get value() {
          return this._get();
      }
      set value(newVal) {
          this._set(newVal);
      }
  }

很簡單的是不是,就是先這樣,然后在那樣,最后就搞定了。
好吧,就是把 factory參數解構出來,分成set和get,做成內部函數,然后在內部的set和get里面調用。

自定義 ref 的實例 —— 寫一個自己的計算屬性。

一提到計算屬性,我們會想到 Vue 提供的 computed,那么如果讓我們使用自定義ref 來實現計算屬性的功能的話,要如何實現呢?(注意:只是練習用)

我們可以這樣來實現:

const myComputed = (_get, _set) => {
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        if (typeof _get === 'function') {
          return _get()
        } else {
          console.warn(`沒有設置 get 方法`)
        }
      },
      set(newValue) {
        if (typeof _set === 'function') {
          _set(newValue)
          trigger()
        } else {
          console.warn(`沒有設置 set 方法`)
        }
      }
    }
  })
}

setup () {
    const refCount = ref(0)

    const myCom = myComputed(() => refCount.value + 1)
    // const myCom = myComputed(() => refCount.value, (val) => { refCount.value = val})

    const update = () => {
      // 修改原型
      refCount.value = 3
    }


    const setRef = () => {
      // 直接賦值
      refCount.value += 1
    }

    return {
      refCount, // 基礎類型
      myCom, // 引用類型
      update, // 修改屬性
      setRef // 直接設置
    }
  }

  <div>
      展示 自定義 的 customRef 實現計算屬性 <br>
      ref 對象:{{refCount}} <br><br>
      自定義的計算屬性 對象:{{myCom}} <br><br>
      <input v-model="myCom" type="text">
      <el-button @click="update" type="primary">修改屬性</el-button><br><br>
    </div>
  • myComputed
    首先定義一個函數,接收兩個參數,一個get,一個set。

  • customRef
    返回一個 customRef 的實例,內部設置get 和 set。

  • 調用方法
    調用的時候,可以只傳入get函數,也可以傳入get、set兩個函數。
    修改 refCount.value 的時候,v-model 的 myCom 也會發生變化。

  • 實用性
    那么這種方式有啥使用效果呢?
    在做組件的時候,組件的屬性props是不能直接用在內部組件的 v-model 里面的,因為props只讀,那么怎么辦呢?

可以在組件內部設置一個ref,然后對props做監聽,或者用computed來做。
除了上面的幾種方法外,也可以用這里的方法來實現,把 refCount 變成 props 的屬性就可以了,然后set里面使用 smit 提交。

computed

寫完了自己的計算屬性后,我們還是來看看 Vue 提供的計算屬性。
代碼來自於 vue.global.js ,調整了一下先后順序。

  function computed(getterOrOptions) {
      let getter;
      let setter;
      if (isFunction(getterOrOptions)) {
          getter = getterOrOptions;
          setter =  () => {
                  console.warn('Write operation failed: computed value is readonly');
              }
              ;
      }
      else {
          getter = getterOrOptions.get;
          setter = getterOrOptions.set;
      }
      return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set);
  }
  class ComputedRefImpl {
      constructor(getter, _setter, isReadonly) {
          this._setter = _setter;
          this._dirty = true;
          this.__v_isRef = true;
          this.effect = effect(getter, {
              lazy: true,
              scheduler: () => {
                  if (!this._dirty) {
                      this._dirty = true;
                      trigger(toRaw(this), "set" /* SET */, 'value');
                  }
              }
          });
          this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
      }
      get value() {
          if (this._dirty) {
              this._value = this.effect();
              this._dirty = false;
          }
          track(toRaw(this), "get" /* GET */, 'value');
          return this._value;
      }
      set value(newValue) {
          this._setter(newValue);
      }
  }
  • computed
    暴露給我們用的方法,來定義一個計算屬性。只有一個參數,可以是一個函數(function),也可以是一個對象。內部會做一個判斷,然后做拆分。

  • ComputedRefImpl
    是不是有點眼熟?這個是 ref 同款系列,都是 RefImpl 風格的,而且內部代碼結構也很相似。
    這個是computed 的主體類,也是先定義內部屬性,然后設置value的get和set。在get和set里面,調用外部設置的函數。

源碼:

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

在線演示:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM