深刻源碼理解reactive和ref

這是我參與8月更文挑戰的第4天,活動詳情查看:8月更文挑戰javascript

前言

最近一直從新學習Vue3,看到composition API了,嘗試結合源碼看看,理解深入一些。本文先來看看 reactiveref 兩個APIhtml

1、reactive

官方定義

咱們先來看官方對於reactive的解釋,官方的解釋也很是簡單vue

返回對象的響應式副本java

但從這句話咱們能夠獲得如下信息react

  1. reactive接受一個對象做爲參數
  2. 其返回值是經reactive函數包裝事後的數據對象,這個對象具備響應式

產生一些疑問

但一樣會有一些疑問web

好比,reactive的參數只能傳遞一個對象嗎,若是傳遞其餘值會怎麼樣?api

好比,返回的響應式數據的本質是什麼,爲啥就能讓數據變成響應式?markdown

好比,"副本"是否是意味着響應式數據與原始數據沒有關聯?app

好比,返回的響應式副本里頭的數據是深度響應式嗎,便是否遞歸監聽對象的全部屬性?等等函數

經過測試解決疑問

帶着這些疑問咱們一塊兒來看 首先,經過reactive建立一個響應數據

import { reactive } from "vue";
export default {
  setup() {  
    const state = reactive({
      count: 0,
    });
  },
};
複製代碼

如上代碼就能夠建立一個響應式數據state,我具體來看一下這個

console.log(state)
複製代碼

能夠看見,返回的響應副本state其實就是Proxy對象。因此reactive實現響應式就是基於ES2015 Proxy的實現的。那咱們知道Proxy有幾個特色:

  1. 代理的對象是不等於原始數據對象
  2. 原始對象裏頭的數據和被Proxy包裝的對象之間是有關聯的。即當原始對象裏頭數據發生改變時,會影響代理對象;代理對象裏頭的數據發生變化對應的原始數據也會發生變化。

須要記住:是對象裏頭的數據變化,並不能將原始變量的從新賦值,那是大換血了

所以,既然reactive實現響應式是基於Proxy的實現的,那咱們大膽猜想,原始數據與相應數據也是有關聯的。那咱們來測試一下

<template>
  <button @click="change"> {{ state.count }} </button>
</template>
<script> import { reactive } from "vue"; export default { setup() { const obj = { count: 0, }; const state = reactive(obj); function change(){ ++state.count console.log(obj); console.log(state); } return { state,change}; }, }; </script>
複製代碼

以上代碼測試結果以下

驗證,確實當響應式對象裏頭數據變化的時候原始對象的數據也會變化

若是反過來,結果也是同樣

// ++state.count
++obj.count;
複製代碼

當響應式對象裏頭數據變化的時候原始對象的數據也會變化

那問題來了,咱們操做數據的時候經過誰來操做呢?

官方的建議是

建議只使用響應式代理,避免依賴原始對象

再來解決另一個問題看看reactive是否會深度監聽每一層呢?

const state = reactive({
    a:{
        b:{
            c:{name:'c'}
        }
    }
});    
console.log(state);  
console.log(state.a);
console.log(state.a.b);  
console.log(state.a.b.c); 
複製代碼

能夠看到結果reactive是遞歸會將每一層包裝成Proxy對象的,深度監聽每一層的property

最後測試一下若是reactive傳遞是非對象而是原始值會怎麼樣

const state = reactive(0);  
console.log(state)
複製代碼

結果是,原始值並不會被包裝,因此也沒有響應式特色

源碼解析

下面,咱們看看reactive的源碼吧

源碼目錄位置:vue-next\packages\reactivity\src\reactive.ts

直接找到reactive的類型聲明:

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> 複製代碼

能夠看到reactive接受一個參數targettarget的類型是泛型T,而T類型是extends object,簡單來講接受的參數target的類型是object類型或者時繼承自object類的子類類型

返回值的類型的UnwrapNestedRefs<T>

看看UnwrapNestedRefs<T>類型

type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
複製代碼

使用type關鍵字聲明類型UnwrapNestedRefs<T>,這裏有個三目運算符,用於進一步判斷T;若是傳入的T屬於Refs類或者其子類,那麼返回傳入的T,否者就是UnwrapRef<T>

下面具體看看reactive方法的定義

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
複製代碼

接受一個類型爲object參數,當傳入對象是隻讀,返回自己。這裏的as關鍵字是斷言,表示傳入的值必定是Target類型,裏頭有個ReactiveFlags.IS_READONLY,用於判斷是不是隻讀的屬性

export interface Target {
  [ReactiveFlags.SKIP]?: boolean
  [ReactiveFlags.IS_REACTIVE]?: boolean
  [ReactiveFlags.IS_READONLY]?: boolean
  [ReactiveFlags.RAW]?: any
}
複製代碼

若是傳遞的對象是普通對象(不是readonly),則執行建立響應式對象函數createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers) 該方法比較長,是reactive的核心方法,因此仍是得讀一下源碼

function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, 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
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
複製代碼

能夠看到除了幾種特殊狀況返回target自己以外,就返回proxyproxy就是經過new Proxy構造函數構建出來的。這裏也進一步證實了reactive的響應式功能確實是經過Proxy實現的

能夠看同樣Proxy的定義

interface ProxyHandler<T extends object> {
    getPrototypeOf? (target: T): object | null;
    setPrototypeOf? (target: T, v: any): boolean;
    isExtensible? (target: T): boolean;
    preventExtensions? (target: T): boolean;
    getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
    has? (target: T, p: PropertyKey): boolean;
    get? (target: T, p: PropertyKey, receiver: any): any;
    set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
    deleteProperty? (target: T, p: PropertyKey): boolean;
    defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
    enumerate? (target: T): PropertyKey[];
    ownKeys? (target: T): PropertyKey[];
    apply? (target: T, thisArg: any, argArray?: any): any;
    construct? (target: T, argArray: any, newTarget?: any): object;
}
interface ProxyConstructor {
    revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; };
    new <T extends object>(target: T, handler: ProxyHandler<T>): T;
}
declare var Proxy: ProxyConstructor;
複製代碼

裏面的具體實現方法,在createReactiveObject傳參的時候就傳入進來了 mutableHandlers和mutableCollectionHandlers,具體能夠去`vue-next\packages\reactivity\src\baseHandlers.ts文件中看

小結

通過上面的瞭解,咱們能夠總結和回答一下最開始幾個疑問了

  1. reactive的參數能夠傳遞對象也能夠傳遞原始值。可是原始值並不會包裝成響應式數據

  2. 返回的響應式數據的本質Proxy對象

  3. 返回的響應式"副本"與原始數據有關聯,當原始對象裏頭的數據或者響應式對象裏頭的數據發生,會彼此相互影響。兩種均可以觸發界面更新,操做時建議只使用響應式代理對象

  4. 返回的響應式對象裏頭時深度遞歸監聽每一層的,每一層都會被包裝成Proxy對象

2、ref

官方定義

關於ref,官方的解釋是:

接受一個內部值並返回一個響應式且可變的 ref 對象

www.vue3js.cn/docs/zh/api…

爲了方便理解,下文中將內部值都稱爲原始數據(orgin

簡單來講ref就是:原始數據=>響應式數據 的過程

產生疑問

但有幾個問題得搞明白

  1. ref接受的原始數據是什麼類型?是原始值仍是引用值,仍是都行?
  2. 返回的響應式數據本質具體是什麼?根據傳遞的數據類型不一樣,返回的響應式對象是否不一樣?
  3. 響應式數據改變會觸發界面更新,那原始數據改變會觸發界面更新嗎?即原始數據和返回的響應式數據是否有關聯

測試解決疑問

示例代碼1:

let origin = 0; //原始數據爲原始值
let count = ref(origin);
function add() {
  count.value++;
}
複製代碼

示例代碼2:

let origin = { val: 0 };//原始數據爲對象
let count = ref(origin);
function add() {
  count.value.val++;
}
複製代碼

經測試,咱們發現,傳遞的原始數據orgin能夠是原始值也能夠是引用值,可是須要注意,若是傳遞的是原始值,指向原始數據的那個值保存在返回的響應式數據的.value中,如上count.value;若是傳遞的一個對象,返回的響應式數據的.value中對應有指向原始數據的屬性,如上count.value.val

爲了測試第二個問題,咱們將上述示例中的count打出來,看返回的具體是什麼

console.log(count)
console.log(count.constructor)
複製代碼

對比發現,無論傳遞數據類型的數據給ref,不管是原始值仍是引用值,返回的響應式數據對象本質都是由RefImpl類構造出來的對象。但不一樣的是裏頭的value,一個是原始值,一個是Proxy對象

源碼分析

到這裏,不妨來讀一下RefImpl類的源碼

目錄:vue-next\packages\reactivity\src\ref.ts

class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true
  constructor(private _rawValue: T, private readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    track(toRaw(this), TrackOpTypes.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), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}
複製代碼

能夠看見RefImpl class傳遞了一個泛型類型T,裏頭具體包含:

  1. 有個私有屬性_value,類型爲T,有個公開只讀屬性__v_isRef值爲true
  2. 有兩個方法,get value(){}set value(){},分別對應私有屬性的讀寫操做,用於供外界操做value
  3. 有一個構造函數constructor,用於構造對象。構造函數接受兩個參數:
    • 第一個參數_rawValue,要求是T類型
    • 第二個參數_shallow,默認值爲true

當經過它構建對象時,會給對象的_value屬性賦值爲 _rawValue或者convert(_rawValue) 再看convert源碼以下:

const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val 複製代碼

經過源碼咱們發現,最終Vue會根據傳入的數據是否是對象isObject(val),若是是對象本質調用的是reactive,不然返回原始數據

下面再來驗證最後一個問題就是:經過ref包裝的結果,當原始數據改變時會觸發界面更新嗎?即原始數據和返回的響應式數據是否有關聯?

示例代碼3

let origin = 0; //原始值
let count = ref(origin);
function add() {
  origin++
  console.log(count.value)
}
複製代碼

示例代碼4

let origin = { val: 0 }; //引用值
let count = ref(origin);
function add() {
  origin++
  console.log(count.value.val)
}
複製代碼

發現,不管傳入給ref的原始數據是原始值仍是引用值,當原始數據發生修改時,並不會影響響應式數據,更不會觸發界面UI的更新

實例代碼5

let origin = 0; 
let count = ref(origin);
function add() {
  count.value++
  console.log(origin)
}
複製代碼

上述代碼,不管count修改多少次,origin一直是0

若是響應式數據發生改變,對應界面UI是會自動更新的,注意不影響原始數據

小結

簡單小結一下:

  1. ref本質是將一個數據變成一個對象,這個對象具備響應式特色

  2. ref接受的原始數據能夠是原始值也能夠是引用值,返回的對象本質都是RefImpl類的實例`

  3. 不管傳入的原始數據時什麼類型,當原始數據發生改變時,並不會影響響應數據,更不會觸發UI的更新。但當響應式數據發生改變,對應界面UI是會自動更新的,注意不影響原始數據。因此ref中,原始數據和通過ref包裝後的響應式數據是無關聯的

END

以上就是關於和reactiveref全部內容~

源碼看得比較少,若有問題歡迎留言告知~

相關文章
相關標籤/搜索