vue3 之 響應(yīng)式 ref 、computed 、reactive的區(qū)別

前言

我們都知道vue3.0版本對所有的底層代碼做了一次更新,尤其是響應(yīng)式跟2.0的變化最大;

在2.0的時候使用的是Object.defineproperty()做的數(shù)據(jù)劫持, 不過Object.defineproperty是對所有的屬性做的數(shù)據(jù)劫持不是目標(biāo)對象,而且對數(shù)組是無法進(jìn)行劫持的,也就是數(shù)組的變化監(jiān)聽實際上是,在原有的數(shù)組方法上進(jìn)行的改造實現(xiàn)的;

但是3.0不一樣,是使用proxy代理模式進(jìn)行的數(shù)據(jù)劫持監(jiān)聽,proxy有個好的地方就是可以監(jiān)聽整個Object對象,不用單獨去監(jiān)聽單個對象屬性就可以檢測到數(shù)據(jù)的變化,比之前的單個屬性監(jiān)聽減少了性能上的開銷,還有就是可以監(jiān)聽數(shù)組,只不過穿的參數(shù)是一個數(shù)組但是返回的卻是對象形式;

舉個例子:

import { reactive } from 'vue';

const arr = reactive([1,2,3,4]);

console.log(arr); // 輸出的是代理之后的 Proxy {0: 1, 1: 2, 2: 3, 3: 4}

ref 原理和使用方式

ref官方文檔說明是:接受一個參數(shù)值并返回一個響應(yīng)式且可改變的 ref 對象。ref 對象擁有一個指向內(nèi)部值的單一屬性 .value。那它又是怎么實現(xiàn)的呢

// 可以是對象的屬性
export function ref<T extends object>(
  value: T
): T extends Ref ? T : Ref<UnwrapRef<T>>
// 也可以是一個任意值
export function ref<T>(value: T): Ref<UnwrapRef<T>>

export function ref<T = any>(): Ref<T | undefined>
// ref的值是可選的,非必填項
export function ref(value?: unknown) {
  return createRef(value)
}

從上邊代碼我們可以看到ref的參數(shù)可以是一個任意值,返回的值是當(dāng)前的參數(shù),或者是一個undefined;
并且如果你什么也不傳也可以,會返回一個響應(yīng)式對象但是value是undefined;

舉個例子:

import { ref } from 'vue';
const refParam = ref();

console.log(refParam) // 返回是一個RefImpl {_rawValue: undefined, _shallow: false, __v_isRef: true, _value: undefined}

我們接著往下分析

// 如果是一個對象,則使用reactive做深度代理,否則直接返回
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
// shallow 用來表示是淺層代理還是深度代理
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  let value = shallow ? rawValue : convert(rawValue) // convert 如果是一個對象,則使用reactive做深度代理,否則直接返回
  const r = {
    __v_isRef: true,
    get value() {
      // 方法追蹤
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      // 判斷新值和舊值是否是一致的,如果不是一致的就進(jìn)行更新操作
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        value = shallow ? newVal : convert(newVal)
        trigger(r, TriggerOpTypes.SET, 'value', newVal)
      }
    }
  }
  return r
}

從上邊的代碼我們可以分析出來,我們傳進(jìn)來的參數(shù)會被判斷解析;
1.看第一條判斷,如果是被代理過的不會再做更多操作,將直接返回這個值;
2.如果shallow表示淺代理value則直接使用這個參數(shù)當(dāng)做返回值;
3.如果是深度代理會執(zhí)行convert方法,這個時候會做判斷是不是一個對象,是的話就使用reactive進(jìn)行遞歸深度代理,否則使用當(dāng)前值,reactive下邊會進(jìn)行詳細(xì)分析;
4.從返回值就能看的出來,無論是對象還是數(shù)組亦或者其他的任意值,在ref處理的過程中都會當(dāng)做是一個對象返回回來

computed 計算機(jī)屬性3.0實現(xiàn)源碼


// 返回的值是只讀屬性,并且是響應(yīng)式
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
// 增加了計算機(jī)屬性的讀寫操作
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>

// 參數(shù)可以是一個對象形式,也可以是一個方法
export function computed<T>(
  // 參數(shù)是一個方法或者是一個對象參數(shù),如果是方法執(zhí)行只讀,如果是對象可以操作讀寫
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  // 如果是方法的話,不可以修改屬性值,讀取,如果是一個對象的話,則可以操作讀寫功能
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true
  let value: T
  let computed: ComputedRef<T>

  // effect 傳進(jìn)來一個getter,這個getter = 如果當(dāng)前參數(shù)是一個方法,值直接賦值當(dāng)前的function,否則賦值這個對象的get方法
  const runner = effect(getter, {
    lazy: true, // 懶執(zhí)行
    scheduler: () => {
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    // 標(biāo)記為這個是需要代理的響應(yīng)式
    __v_isRef: true,
    // 是否是只讀標(biāo)記
    [ReactiveFlags.IS_READONLY]:
      isFunction(getterOrOptions) || !getterOrOptions.set,

    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}

從上述代碼其實可以看出來,computed支持讀寫操作,如果我們在使用寫操作的時候比如下邊這個例子,當(dāng)我們給computed傳遞的是一個方法的時候默認(rèn)就是只讀模式,不可以進(jìn)行修改操作,但是我們?nèi)绻胍薷倪@個值的話就需要使用對象形式的參數(shù),注意{get() =>{}, set() => {}}get和set是搭配使用的,否則會拋出錯誤;
看下邊這個例子:

// 只讀模式操作
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 錯誤!
// 讀寫模式操作
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  },
})

plusOne.value = 1
console.log(count.value) // 0

reactive

接收一個普通對象然后返回該普通對象的響應(yīng)式代理,響應(yīng)式轉(zhuǎn)換是“深層的”:會影響對象內(nèi)部所有嵌套的屬性?;?ES2015 的 Proxy 實現(xiàn),返回的代理對象不等于原始對象。建議僅使用代理對象而避免依賴原始對象。
下邊直接看源碼中怎么寫的,然后咱們分析一下

// only unwrap nested ref
// 解嵌套
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 如果當(dāng)前的目標(biāo)對象存在并且是只讀則直接返回當(dāng)前對象
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // 創(chuàng)建一個響應(yīng)式對象
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

上邊代碼我們可以看到reactive 接收的實際上是個對象,當(dāng)然也可以接收一個數(shù)組,如果目標(biāo)對象存在并且是一個只讀的會直接返回這個對象,否則會創(chuàng)建一個reactive 對象;
那么這個創(chuàng)建的過程都發(fā)生了些什么呢?接著往下分析

function createReactiveObject(
  target: Target,                         // 需要代理的目標(biāo)對象
  isReadonly: boolean,                    // 是不是只讀對象
  baseHandlers: ProxyHandler<any>,        // 基礎(chǔ)的處理方法
  collectionHandlers: ProxyHandler<any>   // 收集依賴
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  // 當(dāng)前對象是不是原始對象,并且是被代理過的只讀對象
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  // 如果已經(jīng)有了對應(yīng)的代理對象就返回這個代理的對象
  const reactiveFlag = isReadonly
    ? ReactiveFlags.READONLY
    : ReactiveFlags.REACTIVE
  if (hasOwn(target, reactiveFlag)) {
    return target[reactiveFlag]
  }
  // only a whitelist of value types can be observed.

  if (!canObserve(target)) {
    return target
  }
  const observed = new Proxy(
    target,
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  )
  def(target, reactiveFlag, observed)
  return observed
}

從上邊代碼不難看出,reactive接收的是一個對象形式,如果不是對象形式就會拋出一個錯誤,可能大家到這會有個疑問為什么數(shù)組也可以呢,這個是因為isObject不是強(qiáng)校驗,看下邊;

export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'

通過上述的分析,我們不難看出,ref和reactive,computed的區(qū)別還是蠻大的;

首先ref對比reactive,reactive只能接收一個引用對象,不可以傳遞普通類型的數(shù)據(jù),比如字符串、數(shù)字等,但是ref可以,因為ref返回的是一個響應(yīng)式代理對象,這個對象value值就是我們的傳遞參數(shù);所以如果是想代理一個對象或者是數(shù)組還是使用reactive更合適,如果我們想要代理一個普通類型的值就需要使用ref去代理更合理;

computed接收一個響應(yīng)式的對象或者值,并且還可以對這個接收的目標(biāo)進(jìn)行操作,但是他本身并不是一個響應(yīng)式的信息,只不過是對這個響應(yīng)式的參數(shù)進(jìn)行了追蹤,獲取和修改的操作;

今天的分享就到這了,如果有哪些地方說的不對,還望在下邊的評論區(qū)發(fā)表出來,大家一起討論;
如果想要體驗vue3.0的同學(xué)也可以參考一下我的這篇文章vue3 學(xué)習(xí) 之 vue3使用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容