Vue Composition API 響應式包裝對象原理

上一篇文章Vue 3.0 最新進展,Composition API中,筆者經過描述Vue Composition API 的最新修正,本文經過解析@vue/composition-api的響應式原理部分代碼,以便在解讀學習過程當中,加深對 Vue Composition API 的理解。vue

若是讀者對 Vue Composition API 還不太熟悉,建議在閱讀本文以前先了解 Vue 3.0 即將帶來的Composition API,能夠查閱@vue/composition-api相關文檔,或查看筆者以前寫過的文章:react

本文主要分如下兩個部分對 Composition API 的原理進行解讀:git

  • reactive API 原理
  • ref API 原理

reactive API 原理

打開源碼能夠找到reactive的入口,在composition-api/src/reactivity/reactive.ts,咱們先從函數入口開始分析reactive發生了什麼事情,經過以前的學習咱們知道,reactive用於建立響應式對象,須要傳遞一個普通對象做爲參數。github

export function reactive<T = any>(obj: T): UnwrapRef<T> {
  if (process.env.NODE_ENV !== 'production' && !obj) {
    warn('"reactive()" is called without provide an "object".');
    // @ts-ignore
    return;
  }

  if (!isPlainObject(obj) || isReactive(obj) || isNonReactive(obj) || !Object.isExtensible(obj)) {
    return obj as any;
  }
  // 建立一個響應式對象
  const observed = observe(obj);
  // 標記一個對象爲響應式對象
  def(observed, ReactiveIdentifierKey, ReactiveIdentifier);
  // 初始化對象的訪問控制,便於訪問ref屬性時自動解包裝
  setupAccessControl(observed);
  return observed as UnwrapRef<T>;
}
複製代碼

首先,在開發環境下,會進行傳參檢驗,若是沒有傳遞對應的obj參數,開發環境下會給予開發者一個警告,在這種狀況,爲了避免影響生產環境,生產環境下會將警告放過。typescript

函數入口會檢查類型,首先調用isPlainObject檢查是不是對象。若是不是對象,將會直接返回該參數,由於非對象類型並不可觀察。api

而後調用isReactive判斷對象是否已是響應式對象,下面是isReactive原型:數組

import {
  AccessControlIdentifierKey,
  ReactiveIdentifierKey,
  NonReactiveIdentifierKey,
  RefKey,
} from '../symbols';
// ...
export function isReactive(obj: any): boolean {
  return hasOwn(obj, ReactiveIdentifierKey) && obj[ReactiveIdentifierKey] === ReactiveIdentifier;
}
複製代碼

經過上面的代碼咱們知道,ReactiveIdentifierKeyReactiveIdentifier都是一個Symbol,打開composition-api/src/symbols.ts能夠看到,ReactiveIdentifierKeyReactiveIdentifier是已經定義好的Symbol安全

import { hasSymbol } from './utils';

function createSymbol(name: string): string {
  return hasSymbol ? (Symbol.for(name) as any) : name;
}

export const WatcherPreFlushQueueKey = createSymbol('vfa.key.preFlushQueue');
export const WatcherPostFlushQueueKey = createSymbol('vfa.key.postFlushQueue');
export const AccessControlIdentifierKey = createSymbol('vfa.key.accessControlIdentifier');
export const ReactiveIdentifierKey = createSymbol('vfa.key.reactiveIdentifier');
export const NonReactiveIdentifierKey = createSymbol('vfa.key.nonReactiveIdentifier');

// must be a string, symbol key is ignored in reactive
export const RefKey = 'vfa.key.refKey';
複製代碼

在這裏咱們大體能夠猜出來,在定義響應式對象時,Vue Composition API 會在響應式對象上設定一個Symbol的屬性,屬性值爲Symbol(vfa.key.reactiveIdentifier)。從而咱們能夠經過對象上是否具備Symbol(vfa.key.reactiveIdentifier)來判斷這個對象是不是響應式對象。app

同理,由於 Vue Composition API 內部使用的nonReactive,用於保證一個對象不可響應,與isReactive相似,也是經過檢查對象是否具備對應的Symbol,即Symbol(vfa.key.nonReactiveIdentifier)來實現的。ide

function isNonReactive(obj: any): boolean {
  return (
    hasOwn(obj, NonReactiveIdentifierKey) && obj[NonReactiveIdentifierKey] === NonReactiveIdentifier
  );
}
複製代碼

此外,由於建立響應式對象須要拓展對象屬性,經過Object.isExtensible來判斷到,當對象是不可拓展對象,也將不可建立響應式對象。

接下來,在容錯判斷邏輯結束後,經過observe來建立響應式對象了,經過文檔和源碼咱們知道reactive等同於 Vue 2.6+ 中Vue.observable,Vue Composition API 會盡量經過Vue.observable來建立響應式對象,但若是 Vue 版本低於2.6,將經過new Vue的方式來建立一個 Vue 組件,將obj做爲組件內部狀態來保證其響應式。關於 Vue 2.x 中如何實現響應式對象,筆者以前也有寫過一篇文章,在這裏就不過多闡述。感興趣的朋友,能夠翻閱筆者兩年前的文章Vue源碼學習筆記之observer與變異方法

function observe<T>(obj: T): T {
  const Vue = getCurrentVue();
  let observed: T;
  if (Vue.observable) {
    observed = Vue.observable(obj);
  } else {
    const vm = createComponentInstance(Vue, {
      data: {
        $$state: obj,
      },
    });
    observed = vm._data.$$state;
  }

  return observed;
}
複製代碼

接下來,會在對象上設置Symbol(vfa.key.reactiveIdentifier)屬性,def是一個工具函數,其實就是Object.defineProperty

export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}
複製代碼

接下來,調用setupAccessControl(observed)就是reactive的核心部分了,經過以前的文章咱們知道:直接獲取包裝對象的值必須使用.value,可是,若是包裝對象做爲另外一個響應式對象的屬性,訪問響應式對象的屬性值時, Vue 內部會自動展開包裝對象。同時,在模板渲染的上下文中,也會被自動展開。setupAccessControl就是幫助咱們作這件事:

/** * Proxing property access of target. * We can do unwrapping and other things here. */
function setupAccessControl(target: AnyObject): void {
  // 首先須要保證設定訪問控制參數的合法性
  // 除了與前面相同的保證響應式對象target是對象類型和不是nonReactive對象外
  // 還須要保證保證對象不是數組(由於沒法爲數組元素設定屬性描述符)
  // 也須要保證不是ref對象(由於ref的value屬性用於保證屬性的響應式),以及不能是Vue組件實例。
  if (
    !isPlainObject(target) ||
    isNonReactive(target) ||
    Array.isArray(target) ||
    isRef(target) ||
    isComponentInstance(target)
  ) {
    return;
  }
  // 一旦初始化了該屬性的訪問控制,也會往響應式對象target上設定一個Symbol(vfa.key.accessControlIdentifier)的屬性。
  // 用於標記該對象以及初始化完成了自動解包裝的訪問控制。
  if (
    hasOwn(target, AccessControlIdentifierKey) &&
    target[AccessControlIdentifierKey] === AccessControlIdentifier
  ) {
    return;
  }

  if (Object.isExtensible(target)) {
    def(target, AccessControlIdentifierKey, AccessControlIdentifier);
  }
  const keys = Object.keys(target);
  // 遍歷對象自己的可枚舉屬性,這裏注意:經過def方法定義的Symbol標記並不是可枚舉屬性
  for (let i = 0; i < keys.length; i++) {
    defineAccessControl(target, keys[i]);
  }
}
複製代碼

首先須要保證設定訪問控制參數的合法性,除了與前面相同的保證響應式對象target是對象類型和不是nonReactive對象外,還須要保證保證對象不是數組(由於沒法爲數組元素設定屬性描述符),也須要保證不是ref對象(由於refvalue屬性用於保證屬性的響應式),以及不能是Vue組件實例。

與上面相同的是,一旦初始化了該屬性的訪問控制,也會往響應式對象target上設定一個Symbol(vfa.key.accessControlIdentifier)的屬性。用於標記該對象以及初始化完成了自動解包裝的訪問控制。

下面來看核心部分:經過Object.keys(target)獲取到對象自己非繼承的屬性,以後調用defineAccessControl,這裏須要注意的一點是,Object.keys只會遍歷響應式對象target自己的非繼承的可枚舉屬性,經過def方法定義的Symbol標記Symbol(vfa.key.accessControlIdentifier)等,並不是可枚舉屬性,於是不會受到訪問控制的影響。

const keys = Object.keys(target);
// 遍歷對象自己的可枚舉屬性,這裏注意:經過def方法定義的Symbol標記並不是可枚舉屬性
for (let i = 0; i < keys.length; i++) {
  defineAccessControl(target, keys[i]);
}
複製代碼

defineAccessControl會建立響應式對象的屬性的代理,以便ref自動進行解包裝,方便開發者在開發過程當中用到ref時,手動執行一次.value的解封裝:

/** * Auto unwrapping when access property */
export function defineAccessControl(target: AnyObject, key: any, val?: any) {
  // 每個Vue可觀察對象都有一個__ob__屬性,這個屬性用於收集watch這個狀態的觀察者,這個屬性是一個內部屬性,不須要解封裝
  if (key === '__ob__') return;

  let getter: (() => any) | undefined; let setter: ((x: any) => void) | undefined; const property = Object.getOwnPropertyDescriptor(target, key); if (property) { // 保證能夠改變目標對象屬性的自有屬性描述符:若是對象的自有屬性描述符的configurablefalse,沒法爲該屬性設定屬性描述符,沒法設定gettersetter if (property.configurable === false) { return; } getter = property.get; setter = property.set; // arguments.length === 2表示沒有傳入val參數,而且不是readonly對象,這時該屬性的值:響應式對象的屬性能夠直接取值拿到 // 傳入val的狀況是使用vue.setcomposition 也提供了set api if ((!getter || setter) /* not only have getter */ && arguments.length === 2) { val = target[key]; } } // 嵌套對象的狀況,實際上setupAccessControl是遞歸調用的 setupAccessControl(val); Object.defineProperty(target, key, { enumerable: true, configurable: true, get: function getterHandler() { const value = getter ? getter.call(target) : val; // if the key is equal to RefKey, skip the unwrap logic // 對ref對象取值時,屬性名不是ref對象的Symbol標記RefKey,getterHandler返回包裝對象的值,即`value.value` if (key !== RefKey && isRef(value)) { return value.value; } else { // 不是ref對象,getterHandler直接返回其值,即`value` return value; } }, set: function setterHandler(newVal) { // 屬性沒有setter,證實這個屬性不是被Vue觀察的,直接返回 if (getter && !setter) return; // 給響應式對象屬性賦值時,先拿到 const value = getter ? getter.call(target) : val; // If the key is equal to RefKey, skip the unwrap logic // If and only if "value" is ref and "newVal" is not a ref, // the assignment should be proxied to "value" ref. // 對ref對象賦值時,而且屬性名不是ref對象的Symbol標記RefKey,若是newVal不是ref對象,setterHandler將代理到對ref對象的value屬性賦值,即`value.value = newVal` if (key !== RefKey && isRef(value) && !isRef(newVal)) { value.value = newVal; } else if (setter) { // 該對象有setter,直接調用setter便可 // 會通知依賴這一屬性狀態的對象更新 setter.call(target, newVal); } else if (isRef(newVal)) { // 既沒有getter也沒有setter的狀況,普通鍵值,直接賦值 val = newVal; } // 每次從新賦值,考慮到嵌套對象的狀況:對newVal從新初始化訪問控制 setupAccessControl(newVal); }, }); } 複製代碼

經過上面的代碼,咱們能夠看到,爲了給ref對象自動解包裝,defineAccessControl會爲reactive對象從新設置gettersetter,考慮到嵌套對象的狀況,在初始化響應式對象和從新爲響應式對象的某個屬性賦值時,會深遞歸執行setupAccessControl,保證整個嵌套對象全部層級的ref屬性均可以自動解包裝。

ref API 原理

ref的入口在composition-api/src/reactivity/ref.ts,下面先來看ref函數:

class RefImpl<T> implements Ref<T> {
  public value!: T;
  constructor({ get, set }: RefOption<T>) {
    proxy(this, 'value', {
      get,
      set,
    });
  }
}

export function createRef<T>(options: RefOption<T>) {
  // seal the ref, this could prevent ref from being observed
  // It's safe to seal the ref, since we really shoulnd't extend it.
  // related issues: #79
  // 密封ref,保證其安全性
  return Object.seal(new RefImpl<T>(options));
}

export function ref(raw?: any): any {
  // 先建立一個可觀察對象,這個value其實是一個 Vue Composition API 內部使用的局部變量,並不會暴露給開發者
  const value = reactive({ [RefKey]: raw });
  // 建立ref,對其取值其實最終代理到了value
  return createRef({
    get: () => value[RefKey] as any,
    set: v => ((value[RefKey] as any) = v),
  });
}
複製代碼

看到ref的入口首先調用reactive來建立了一個可觀察對象,這個value其實是一個 Vue Composition API 內部使用的局部變量,並不會暴露給開發者。它具備一個屬性值RefKey,其實也是個Symbol,而後調用createRefref返回createRef建立的ref對象,ref對象實際上經過gettersetter代理到咱們經過const value = reactive({ [RefKey]: raw });建立的局部變量value的值,便於咱們獲取ref包裝對象的值。

另外爲了保證ref對象的安全性,不被開發者意外篡改,也爲了保證 Vue 不會再爲ref對象再建立代理(由於包裝對象的value屬性確實沒有必要再另外被觀察),所以調用Object.seal將對象密封。保證只能改變其value,而不會爲其拓展屬性。

isRef很簡單,經過判斷傳遞的參數是否繼承自RefImpl

export function isRef<T>(value: any): value is Ref<T> {
  return value instanceof RefImpl;
}
複製代碼

toRefsreactive對象轉換爲普通對象,其中結果對象上的每一個屬性都是指向原始對象中相應屬性的ref引用對象,這在組合函數返回響應式狀態時很是有用,這樣保證了開發者使用對象解構或拓展運算符不會丟失原有響應式對象的響應。其實也只是遞歸調用createRef

export function toRefs<T extends Data = Data>(obj: T): Refs<T> {
  if (!isPlainObject(obj)) return obj as any;

  const res: Refs<T> = {} as any;
  Object.keys(obj).forEach(key => {
    let val: any = obj[key];
    // use ref to proxy the property
    if (!isRef(val)) {
      val = createRef<any>({
        get: () => obj[key],
        set: v => (obj[key as keyof T] = v),
      });
    }
    // todo
    res[key as keyof T] = val;
  });

  return res;
}
複製代碼

小結

本文主要描述 Vue Composition API 響應式部分的代碼,reactiveref都是基於 Vue 響應式對象上作再次封裝,ref的內部實際上是一個響應式對象,refvalue屬性將代理到這個響應式對象上,這個響應式對象對開發者是不可見的,使得調用過程相對友好,而reactive提供了對ref自動解包裝功能,以提高開發者開發體驗。

相關文章
相關標籤/搜索