# 粗读响应式API

# reactive

在使用 Vue 时,正常来说只有在 data 中的属性是响应式,如果希望在后期添加可响应的元素就可以通过 reactive 方法,粟子

  created() {
    this.info = Vue.reactive({
      name: 'lanjz'
    })
  },

如果组件有用到 this.info,当 this.info 改变时,组件中使用到的地方也跟着更新

# 源码定义

function reactive(target) {
  // 如果是已经设置过只读的对象,则略过
  if (target && target["__v_isReadonly" /* IS_READONLY */]) {
    return target;
  }
  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
}

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
  // targe 必需是对象
  if (!isObject(target)) {
    {
      console.warn(`value cannot be made reactive: ${String(target)}`);
    }
    return target;
  }
  // target 已经是个被代理对象,则直接返回
  if (target["__v_raw" /* RAW */] &&
    !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
    return target;
  }
  // 根据是否只读使用 readonlyMap或reactiveMap 存储当前对象
  const proxyMap = isReadonly ? readonlyMap : reactiveMap;
  // 已经保存过则直接返回
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // 对于不可扩展的,获者是已经被标志不可用的对象,则略过
  const targetType = getTargetType(target);
  if (targetType === 0 /* INVALID */) {
    return target;
  }
  // 设置代理
  const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
  proxyMap.set(target, proxy);
  return proxy;
}

核心还是最后的 Proxy 代理的使用,看下这个代理具体配置了啥

get

// get
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    if (key === "__v_isReactive" /* IS_REACTIVE */) {
      return !isReadonly;
    }
    else if (key === "__v_isReadonly" /* IS_READONLY */) {
      return isReadonly;
    }
    else if (key === "__v_raw" /* RAW */ &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)) {
      return target;
    }
    const targetIsArray = isArray(target);
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver);
    }
    const res = Reflect.get(target, key, receiver);
    if (isSymbol(key)
      ? builtInSymbols.has(key)
      : key === `__proto__` || key === `__v_isRef`) {
      return res;
    }
    if (!isReadonly) {
      track(target, "get" /* GET */, key);
    }
    if (shallow) {
      return res;
    }
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key);
      return shouldUnwrap ? res.value : res;
    }
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}

先看下几个自定义属性的意思:

  • __v_isReadonly: 是否是已读的

  • __v_raw:属性是否响应式的

  • __v_isReactive: !isReadonly

之后分别判断对象是数组类型和 Symbol 类型分别走不同的分支

之后执行track(target, "get" /* GET */, key) 给当前属性收集事件

shallow 应该是表示是否要进一步处理得到的值?

isRef(res) 是否有属性 __v_isRef,表示是否是使用 refs 创建的对象

如果得到值是对象的话,继续调用 reactive 处理这个对象

总得来说, get 重点主要还是收集依赖的作用

set

function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];
    if (!shallow) {
      value = toRaw(value);
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value;
        return true;
      }
    }
    // 设置是否是已存在的属性
    const hadKey = isArray(target) && isIntegerKey(key)
      ? Number(key) < target.length
      : hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, "add" /* ADD */, key, value);
      }
      else if (hasChanged(value, oldValue)) {
        trigger(target, "set" /* SET */, key, value, oldValue);
      }
    }
    return result;
  };
}

set 被触发时通过 trigger 去执行收集的事件

# refs

refreactive 一样, 也是用来实现响应式数据的方法

作用?

由于 reactive 必须传递一个对象, 所以导致在企业开发中,如果我们只想让某个变量实现响应式的时候会非常麻烦 ,所以Vue3就给我们提供了 ref 方法, 为非对象的基础数据类型创建响应性

# ref

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value

源码定义:

 const convert = (val) => isObject(val) ? reactive(val) : val;
 function ref(value) {
    return createRef(value);
  }
function createRef(rawValue, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}

class RefImpl {
  constructor(_rawValue, _shallow = false) {
    this._rawValue = _rawValue; // 保存值
    this._shallow = _shallow;  // 是否深层处理
    this.__v_isRef = true; // 设置 ref 标识
    this._value = _shallow ? _rawValue : convert(_rawValue); // 添加代理
  }
  get value() {
    track(toRaw(this), "get" /* 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), "set" /* SET */, 'value', newVal);
    }
  }
}

当调用 ref 实际是生成了一个 RefImpl 实例,并且对外定义了 value 属性,并定义了存储描述符,当 setget 时,分别也会调用对应用 trigger 发布事件和 track 收集事件

并且如果使用的目标是对象的话,还会调用 reactive(val) 给这个对象添加响应劫持

# unref

如果参数为 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val

源码定义:

  function unref(ref) {
    return isRef(ref) ? ref.value : ref;
  }

# toRef

可以用来为源响应式对象上的 property 性创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接

源码:

function toRef(object, key) {
    return isRef(object[key])
      ? object[key]
      : new ObjectRefImpl(object, key);
  }
// ObjectRefImpl
  class ObjectRefImpl {
    constructor(_object, _key) {
      this._object = _object;
      this._key = _key;
      this.__v_isRef = true;
    }
    get value() {
      return this._object[this._key];
    }
    set value(newVal) {
      this._object[this._key] = newVal;
    }
  }

如果已经是 ref 对象则直接返回,否则调用 ObjectRefImpl 生成一个 ref,跟 RefImpl 相比少了在 get/set 方法触发了少了主动触发收集事件和派发事件的操作,因为 toRef 的对象本身就是响应式对象了

# toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。

源码:

function toRefs(object) {
    if ( !isProxy(object)) {
      console.warn(`toRefs() expects a reactive object but received a plain one.`);
    }
    const ret = isArray(object) ? new Array(object.length) : {};
    for (const key in object) {
      ret[key] = toRef(object, key);
    }
    return ret;
  }

先判断 object 是否被代理的,之后遍历属性调用 toRef ,将每个属性变成 ref 对象

# 总结

ref 本质是声明了一个类,这个类对外暴露了一个 value 属性指向当前 ref 目标,并给这个 value 属性设置了 getset 描述符

reactive 里面给对象设置 proxy 代理一样,类的 get/set 方法也定义了收集事件和派发事件的操作

如果 ref 目标是对象的话还会使用 reactive 处理这个对象

使用 ref 注意点:

  • 在Vue中使用 ref 的值不用通过 value 获取

  • 在JS中使用 ref 的值必须通过 value 获取

为什么一定要使用 value 访问目标?

因为需要通过对象的描述符来添加响应式的功能

换汤不换药~