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自动解包装功能,以提高开发者开发体验。

相关文章
相关标签/搜索