关于vue.js:vue面试之CompositionAPI响应式包装对象原理

36次阅读

共计 8511 个字符,预计需要花费 22 分钟才能阅读完成。

本文次要分以下两个局部对 Composition API 的原理进行解读:

  • reactive API 原理
  • ref API 原理

reactive API 原理

关上源码能够找到 reactive 的入口,在 composition-api/src/reactivity/reactive.ts,咱们先从函数入口开始剖析 reactive 产生了什么事件,通过之前的学习咱们晓得,reactive用于创立响应式对象,须要传递一个一般对象作为参数。

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 参数,开发环境下会给予开发者一个正告,在这种状况,为了不影响生产环境,生产环境下会将正告放过。

函数入口会查看类型,首先调用 isPlainObject 查看是否是对象。如果不是对象,将会间接返回该参数,因为非对象类型并不可察看。

而后调用 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) 来判断这个对象是否是响应式对象。

同理,因为 Vue Composition API 外部应用的 nonReactive,用于保障一个对象不可响应,与isReactive 相似,也是通过查看对象是否具备对应的 Symbol,即Symbol(vfa.key.nonReactiveIdentifier) 来实现的。

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 中如何实现响应式对象,笔者之前也有写过一篇文章,在这里就不过多论述。

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) {
    // 保障能够扭转指标对象属性的自有属性描述符:如果对象的自有属性描述符的 configurable 为 false,无奈为该属性设定属性描述符,无奈设定 getter 和 setter
    if (property.configurable === false) {return;}
    getter = property.get;
    setter = property.set;
    // arguments.length === 2 示意没有传入 val 参数,并且不是 readonly 对象,这时该属性的值:响应式对象的属性能够间接取值拿到
    // 传入 val 的状况是应用 vue.set,composition 也提供了 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 主动解包装性能,以晋升开发者开发体验。

正文完
 0