从 Proxy 到 Vue3 响应式

2022-12-05 09:40:40 浏览数 (1)

前言

最近想再回顾下 Proxy 这一部分的内容, 顺便也看看他的应用场景, 刚好在 Vue3 的响应式 API 中有使用, 所以就结合着一起复习下, 顺便总结记录一番. 如果只对 Vue3 的响应式感兴趣的, 可以直接跳到文章的第二部分.

一、Proxy 和 Reflect

Proxy 和 Reflect 是 ES6中出来的, 已经很久了, 但是平时工作中写一些业务代码基本都不会去考虑用这两个语法 (不是业务太low了, 就是自己太low了), 太久了容易生疏, 这里结合 Vue3 来系统性的整理一下.

可以说 Proxy 和 Reflect 是贴近了函数式的编程思想, 特别是 Reflect, 均是采用函数式调用的写法, 下面先来看下这两者的概念.

1、Proxy

话不多说, 先上语法

代码语言:javascript复制
/**
 * target: 目标对象
 * handler: 一个对象, 是操作target时所对应的某些处理函数
 */
new Proxy(target, handler)

Proxy顾名思义是代理的意思, 其功能也名副其实, 在目标对象之前设置一层代理, 进行对象访问的拦截, 由此提供了一种机制,就是可以对外界的访问进行过滤和改写. 这个功能很强大, 等于可以改变一些对象原来底层的访问, 从而修改某些操作的默认行为.

具体可以拦截或修改对象的哪些访问? 目前官方提供了13个拦截操作, 均可以在参数 handler 对象中定义, 具体如下:

方法

说明

返回值

get(target, propKey, receiver)

拦截对象属性的读取

属性值

set(target, propKey, value, receiver)

拦截对象属性的设置

布尔值

has(target, propKey)

拦截propKey in proxy的操作,以及对象的hasOwnProperty方法

布尔值

deleteProperty(target, propKey)

拦截delete proxy[propKey]的操作

布尔值

ownKeys(target)

拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)

数组

getOwnPropertyDescriptor(target, propKey)

拦截Object.getOwnPropertyDescriptor(proxy, propKey)

属性的描述对象

defineProperty(target, propKey, propDesc)

拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs)

布尔值

preventExtensions(target)

拦截Object.preventExtensions(proxy)

布尔值

getPrototypeOf(target)

拦截Object.getPrototypeOf(proxy)

对象

isExtensible(target)

拦截Object.isExtensible(proxy)

布尔值

setPrototypeOf(target, proto)

拦截Object.setPrototypeOf(proxy, proto)

布尔值

apply(target, object, args)

拦截Proxy实例作为函数调用的操作

construct(target, args)

拦截Proxy实例作为构造函数调用的操作

具体的用法不一一介绍, 具体可以参见MDN, 选 get 方法重点的说下

用于拦截属性的读取操作, 可以在读取过程中进行一系列的逻辑执行, 比如:

1) 可以拦截数组下标读取, 以及倒序获取数组元素

代码语言:javascript复制
const arr = new Proxy([1, 2, 3], {
  get(target, p, receiver) {
    return Reflect.get(target, p < 0 ? `${ p   target.length}` : p, receiver);
  },
});
console.log(arr[-1]); // 3

2) 函数名链式调用

代码语言:javascript复制
// 定义全局方法
const globalFunc = {
  double: (n) => n * 2,
  pow: (n) => Math.pow(n, 2),
  round: (n) => Math.round(n),
};
const pip = (value) => {
  let funcList = [];
  const func = new Proxy({}, {
      get(target, p) {
        if (p === "exec") {
          return funcList.reduce((val, fn) => fn(val), value);
        }
        funcList.push(globalFunc[p]);
        return func;
      },
    }
  );
  return func;
};
console.log(pip(3.4).double.pow.round.exec); // 46

3) get 函数有第三个参数 receiver , 可以用来改变读取的函数中 this 的指向

代码语言:javascript复制
const testA = { _m: "m_A", _n: "n_A" };
const testB = {
  _m: "m_B",
  _n: "n_B",
  get m() { return this._m; },
  get n() { return this._n; },
};

const proxy = new Proxy(testB, {
  get(target, p, receiver) {
    if (p === 'm') return Reflect.get(target, p, testA);
    return Reflect.get(target, p, receiver);
  },
});
console.log(proxy.m, proxy.n); // m_A, n_B

4) 可以定义对象的私有属性, 禁止被外界直接访问, 这里有个小问题, 下面代码虽然禁止了私有属性的访问, 但是涉及到私有属性相关的方法也无法正常使用, 这个是否有好的解法?

代码语言:javascript复制
const obj = { 
  _name: 'test',
  getName() { return this._name; } // 这个也没有办法拿到_name
 };

const proxy = new Proxy(obj, {
  get(target, p, receiver) {
    if (p.startsWith('_')) {
      console.warn('cannot read private prop redirectly')
      return null
    }
    return Reflect.get(target, p, receiver);
  },
});
console.log(proxy._name); // warning

5) get 方法可以被继承

代码语言:javascript复制
const proxy = new Proxy({ a: 1 }, {
  get(target, p, receiver) {
    console.log(`GET ${p}`);
    return Reflect.get(target, p, receiver);
  },
});
const obj = Object.create(proxy);
console.log(obj.a);
// GET a
// 1

2、Reflect

英文大多翻译成反射的意思, 但是理解起来比较别扭, 其实就是替代了现有 Object 对象的某些方法, 对这些方法做了一些优化, 使这些方法更容易理解, 更函数式. 其目前实现的方法与 Proxy 中 handler 的方法一一对应, 都是13个

Reflect 是一个对象, 所有属性和方法都是静态的, 为什么有了 Object 还需要 Reflect ? 他与 Proxy 有什么关系?

1) 提升 Object 方法的合理性, 使方法的返回更加友好. Object.defineProperty 在出错时会跑出一个异常, 我们要手动去捕获他, 否则程序会中断, 而 Reflect.defineProperty 采用函数返回值的形式告诉调用方结果.

代码语言:javascript复制
const obj = Object.freeze({});
try {
 Object.defineProperty(obj, 'a', { value: 1 });
} catch (error) {
  console.log('Object: ', error);
  // Object:  TypeError: Cannot define property foo, object is not extensible
}

const res = Reflect.defineProperty(obj, 'a', { value: 1 });
console.log('Reflect: ', res); // Reflect:  false

2) 函数式编程思想, 不再采用 Object 的一些命令式语法

代码语言:javascript复制
const obj = { a: 1,  b: 2 };

console.log('Object: ', 'a' in obj);
console.log('Reflect: ', Reflect.has(obj, 'a'));

delete obj.a;
Reflect.deleteProperty(obj, 'b');

3) 为 Proxy 提供运行对象默认行为的方法, 作为修改行为的基础. 因为 Proxy 可以拦截修改对象的一些行为方法, 而这些方法都能在 Reflect 上找到, 保证原生的行为能力可以正常运行.

代码语言:javascript复制
const proxy = new Proxy(obj, {
  get(target, name) {
    console.log('get ', target, name);
    // 调用原生方法
    return Reflect.get(target, name); 
  },
  deleteProperty(target, name) {
    console.log('delete '   name);
    // 调用原生方法
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log('has '   name);
    // 调用原生方法
    return Reflect.has(target, name);
  }
});

有了上面的基础, 我们接下来看下 Vue3 是如何使用的

二、Vue3中的响应式

众所周知, Vue3 使用 Proxy 替代了 Object.defineProperty 来做响应式. 因为 Object.defineProperty 的功能有限 (无法监听删除、数组下标、in事件、apply等), 所以 Vue2 做了很多功能补齐, 甚至有的就不支持. 而到了 Vue3 使用 Proxy 带来了全新的响应式解决方案, 我们来看看其中的核心: 响应式API (官网传送), 篇幅原因, 先介绍一部分

1、reactive、readonly、shallowReactive、shallowReadonly

这几个方法先放一起说, 大多数响应式 API 都会以 reactive 为基础, 他返回一个对象的响应式代理. 直接源码看一下, reactvie 最终是使用 createReactiveObject 来创建一个响应式代理

代码语言:javascript复制
function reactive(target) {
    // 如果target已经是只读的响应式对象, 则直接返回只读的版本, 例如一般的computed类型
    if (isReadonly(target)) {
        return target;
    }
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

说明:

  • 针对于 target 的类型, Vue3 中区分为 普通 COMMON 类型和 集合 COLLECTION 类型, 不同类型的 Proxy handlers 的配置是不一样的, 这里先有个这个概念, 下面会具体说到.
  • 针对于只读的对象, reactive 不会进行处理, 一般是继承于 ComputedRefImpl 的类型, 即不带有 setter 的 computed, 后面会说到. ComputedRefImpl 类中会将属性 __v_isReadonly 设为 true, isReadonly 函数就是判断 __v_isReadonly 属性真假与否的
  • mutableHandlers, mutableCollectionHandlers 两个对象分别定义了 Proxy 中的 handler, 即代理需要拦截监听的事件, 他们区别是监听事件方法不同, reactive 也会根据要创建对象的不同类型, 在两者中选择一个, 具体如何选择, 结合下面的代码说明
  • reactiveMap 是个 WeakMap, 存储着原始对象 target 和其代理 porxy 的映射关系, 避免进行重复代理

另外三种类型 readonly、shallowReactive、shallowReadonly 与 reactive 类似, 都是调用 createReactiveObject 方法进行代理创建, 只是传入的参数不同, 处理逻辑稍有差异. 不同代理创建、内部属性标识、缓存对象、拦截方法都不一样, 这里归纳整理一下

类型

创建函数

是否只读

缓存对象

普通类型handlers

集合类型handlers

响应式代理

reactive

false

reactiveMap

get, set, deleteProperty, has, ownKeys

get

只读代理

readonly

true

readonlyMap

readonlyGet、set、deleteProperty

get

浅层响应式代理

shallowReactive

false

shallowReactiveMap

shallowGet, shallowSet, deleteProperty, has, ownKeys

get

浅层只读代理

shallowReadonly

true

shallowReadonlyMap

shallowReadonlyGet、set、deleteProperty

get

下面就来看下 createReactiveObject 这个方法具体是怎么创建这四种类型的 Proxy 的

createReactiveObject

再来看下 createReactiveObject 的实现

代码语言:javascript复制
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    // 只有非空的对象类型, 才会去创建代理, 否则会直接返回原始值
    if (!shared.isObject(target)) {
        return target;
    }
    // 如果目标对象已经是响应式的也直接返回, 除非是创建一个他的只读副本
    if (target["__v_raw" /* RAW */] && !(isReadonly && target["__v_isReactive"])) {
        return target;
    }
    // proxyMap即reactiveMap, 上面说了缓存了对象代理状态, 已代理过的就从缓存中直接获取, 并返回
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy;
    }
    // 只有符合条件的对象, 才会去进行代理监听, 具体哪些类型, 下面说明
    const targetType = getTargetType(target);
    if (targetType === 0) {
        return target; 
    }
    // 根据不同类型的对象, 创建不同程度的监听方法
    const proxy = new Proxy(target, targetType === 2 ? collectionHandlers : baseHandlers);
    // 缓存当前原始对象和代理对象之间的映射关系
    proxyMap.set(target, proxy);
    return proxy;
}

函数流程图如下

说明:

getTargetType: Vue3 会根据原始对象的类型对其进行归类, 并根据类型设置代理对象的 handler, 其依据用一张表来描述,

原始对象类型

返回值

返回值含义

handler取值

markRaw 标记不可被转为代理

0

SKIP (无效)

-

对象不可扩展

0

INVALID (无效)

-

Object 类型

1

COMMON

mutableHandlers

Array 类型

1

COMMON

mutableHandlers

Map 类型

2

COLLECTION

mutableCollectionHandlers

Set 类型

2

COLLECTION

mutableCollectionHandlers

WeakMap 类型

2

COLLECTION

mutableCollectionHandlers

WeakSet 类型

2

COLLECTION

mutableCollectionHandlers

不属于以上任何情况

0

INVALID (无效)

-

baseHandlers: 针对于普通 (COMMON) 类型的 handlers, 他定义了 get、set、deleteProperty、has、ownKeys 拦截方法. 也由此可见, 在 Vue2 的基础上扩展了除get、set的其他响应式控制.

collectionHandlers: 针对于集合 (COLLECTION) 类型的 handlers, 其只定义了 get 方法, 因为集合都是以函数式的方式调用, 例如 set.has、set.add, 所以只需要拦截 get 方法, 然后再到 get 方法中代理各集合的内建方法即可

下面分开解释, 先来看下 COMMON 的 handlers

baseHandlers

1) get 拦截方法

Vue3 中针对于普通类型的对象一共有 4 种类型的 get 拦截, 除了普通响应式的 get, 还包括 shallowGet (浅层响应式)、readonlyGet (只读代理)、shallowReadonlyGet (浅层只读代理). 浅层的含义就是说所有的效果只作用域对象的根层级, 不做深层级的处理. 例如浅层响应式, 只有根层级被转化成了响应式, 对其深层级属性不做转换, 例如下面这个例子

代码语言:javascript复制
const state = shallowReactive({ a: 1, b: { c: 2 } });
isReactive(state); // true, 当 state.a的值变化时, 可以被监听
isReactive(state.b); // false, 内层对象非响应式, 会原样返回

这几种类型都是通过 createGetter 来创建, 他接收两个布尔类型参数, 这两个参数的取值组合控制了不同的类型的 get 定义, 普通的 reactive 使用的是函数默认值创建, 一起来看下区别

代码语言:javascript复制
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    // 获取响应式状态: 一个对象不是只读的, 就是响应式的
    if (key === '__v_isReactive' /* IS_REACTIVE */) {
      return !isReadonly;
    }
    // 获取只读状态
    if (key === '__v_isReadonly' /* IS_READONLY */) {
      return isReadonly;
    }
    // 获取浅层状态
    if (key === '__v_isShallow' /* IS_SHALLOW */) {
      return shallow;
    }
    // 获取target的原始对象
    if (
      key === '__v_raw' /* RAW */ && receiver
            === (isReadonly
              ? shallow
                ? shallowReadonlyMap
                : readonlyMap
              : shallow
                ? shallowReactiveMap
                : reactiveMap
            ).get(target)
    ) {
      return target;
    }
    // 调用非只读数组的内置代理方法, arrayInstrumentations里有Vue3代理的方法, 下面详细说
    const targetIsArray = shared.isArray(target);
    if (!isReadonly && targetIsArray && shared.hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver);
    }
    // 直接先获取target对象的属性值
    const res = Reflect.get(target, key, receiver);
    // 获取的是默认Symbol或者内置的Symbol属性, 则直接返回
    if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res;
    }
    // 如果不是只读代理, 则有可能会改变属性值, 要进行依赖收集
    if (!isReadonly) {
      track(target, 'get' /* GET */, key);
    }
    // 如果是浅层代理, 则直接返回
    if (shallow) {
      return res;
    }
    // 如果获取的属性值是ref对象, 则判断是否需要自动解包
    if (isRef(res)) {
      // 数组或者数值类型, 则不解包
      const shouldUnwrap = !targetIsArray || !shared.isIntegerKey(key);
      return shouldUnwrap ? res.value : res;
    }
    // 获取的属性值是个对象, 因为不是浅层代理, 所以返回值也需要转为代理对象
    if (shared.isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}

说明:

  • track 是 Vue3 中用来进行依赖收集的函数, 当 target 的值发生了改变, 例如调用了 set、deleteProperty 等就会去触发 trigger, 沿着收集的依赖进行处理
  • arrayInstrumentations 是一个代理数组某些函数方法的对象, 包括两组, 查找方法'includes', 'indexOf', 'lastIndexOf' 和 改变长度方法'push', 'pop', 'shift', 'unshift', 'splice'. 具体代码在 createArrayInstrumentations 函数中实现, 这里不详细解析代码. 要知道的就是为什么要分两组. 数组的查找方法是需要对数组的每个元素进行依赖收集的, 即针对于每个元素都要 track(arr, "get" /* GET */, i '') 一下, 原因很简单, 因为数组元素的改变会直接影响这几个方法的返回值, 所以是有副作用的, 必须得监听一下. 而改变长度方法,在执行期间会触发 length 的get 和 set , 而数组长度变化真正影响的是正在使用该数组对象的地方, 例如获取数组元素值, 这已经在当时 get 的时候收集过依赖了, 所以为了避免重复, 在改变长度的过程中需要暂停依赖收集 (即调用pauseTracking函数), 等执行完毕后, 再恢复(即调用resetTracking函数).
  • 对于 key 是Symbol对象, 并且是 Symbol 的内建方法, 则不进行依赖收集, 因为响应式只对对象进行处理
  • 如果 key 是内置的属性 (__proto__、__v_isRef、__isVue) 也不进行依赖收集, 这里理解__proto__原型的方法一般不会影响到代理的属性, 而__v_isRef、__isVue 都是自定义的一些私有的布尔值属性, 不需要对其进行监听. 对于 Symbol 上的一些内置方法的调用.
  • 对于 Vue3 中的 ref 对象, 这里说的不是模版引用的 ref, 是响应式的 ref, 一般是用响应式代理属性的 __v_isRef 标识位来区分, 一般通过 ref()、customRef()、toRef()、computed()、deferredComputed() 创建的对象, 都属于 ref 对象

同样来归纳一下不同代理之间 get 拦截方法的差异性

get拦截类型

对应函数

内部标识取值

是否依赖收集

__v_isReactive

__v_isReadonly

__v_isShallow

响应式代理

get

true

false

false

除改变数组长度方法 和 内建Symbol方法外

只读代理

readonlyGet

false

true

false

浅层响应式代理

shallowGet

true

false

true

除改变数组长度方法 和 内建Symbol方法外

浅层只读代理

shallowReadonlyGet

false

true

true

用四种方法创建一个 { a: 1 } 对象的响应式对象, 看下不同代理类型的区别

四种响应式类型区别四种响应式类型区别

2) set 拦截方法

和 get 一样, set 有有一个公共方法, 用来创建不同类型的 set, 他叫 createSetter, set 主要分为3 种, 因为两种只读代理共用一个. 对于这种代理, 由于是只读, 所以不会进行任何设置的操作, 其set 就是一个空壳, 像下面这样

代码语言:javascript复制
set(target, key) { return true; }

而另外两种响应式的代理, 则通过 createSetter 函数的参数来区分为普通 set 和 浅层 set (shallowSet), 下面来重点看下 createSetter

代码语言:javascript复制
function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    // 先获取一下对应key原来的值
    let oldValue = target[key];
    // 对于原来值是只读ref对象, 是不允许改变其本来的ref属性的
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false;
    }
    // 如果target是非浅层响应式代理, 并且新值value非只读
    if (!shallow && !isReadonly(value)) {
      // 如果新值value是非浅层响应式的, 则对新旧值进行解包, 拿到原始对象
      if (!isShallow(value)) {
        value = toRaw(value);
        oldValue = toRaw(oldValue);
      }
      // 非数组, 值由响应式变为非响应式, 则直接赋值, 退出
      if (!shared.isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value;
        return true;
      }
    }
    // 用于判断是不是要往数组或对象里添加新元素
    const hadKey = shared.isArray(target) && shared.isIntegerKey(key)
      ? Number(key) < target.length
      : shared.hasOwn(target, key);
    // 设置target的key值
    const result = Reflect.set(target, key, value, receiver);
    // 只对当前实例属性做依赖处理, 如果是原型链中的某个元素,则不要触发
    if (target === toRaw(receiver)) {
      // 如果是新增元素, 则激活add类型的触发器,进行依赖处理
      if (!hadKey) {
        trigger(target, 'add' /* ADD */, key, value);
      } else if (shared.hasChanged(value, oldValue)) {
        // 如果是修改元素值, 并且新旧值不一样, 则激活set类型的触发器,进行依赖处理
        trigger(target, 'set' /* SET */, key, value);
      }
    }
    return result;
  };
}

说明:

  • 代码中判断的只读的ref对象, 一般会是 computed 计算类型的属性, 如果一个 proxy 包含该属性, 是不能对其重新赋值成非计算属性的, 直接来看个例子
代码语言:javascript复制
const state = reactive({ a: 1 });
const comp = computed(() => state.a   1); // 只读的ref对象
const test = ref({ c: comp }); // 将其初始化给ref属性的c
test.value.c = 2; // 把c的值动态改为数字类型, 触发 proxy 的set
// 赋值不被允许, 直接返回 false, 立即报错 
// TypeError: 'set' on proxy: trap returned falsish for property 'c'
计算属性值无法被非计算属性值覆盖计算属性值无法被非计算属性值覆盖
  • 因为 Proxy 是可以继承的, set 有可能在原型链上, 而且可以通过 receiver 来改变 set 函数的指针, 或以其他方式被间接地调用(因此不一定是 proxy 本身, 所以有可能会改变非自身属性的值. 代码里通过 target === toRaw(receiver) 来确定只对当前本身属性, 而不对原型链上的属性变更进行依赖处理, 其他情况交给指向的对象去处理.
  • 处理依赖的原则同样是监听元素的变化, 包括新增元素和元素的值更新, 这个和 Vue2 的思路差不多. hasChanged 使用的是 Object.is 的方法进行判断的
  • 浅层响应式与非浅层的区别就是 set 过程中是否会去对新旧值进行自动解包, 即拿到原始对象

同样来归纳一下不同代理之间 set 拦截方法的差异性

get拦截类型

对应函数

是获取原始值

处理trigger的add

处理trigger的set

响应式代理

set

响应式对象值新增

响应式对象值变化

只读代理

readonlySet

浅层响应式代理

shallowSet

响应式对象值新增

响应式对象值变化

浅层只读代理

readonlySet

3) deleteProperty、has、ownKeys 拦截方法

这几个代理方法比较简单, 放一起说, 在实现上, 主要区分了只读属性和非只读属性两种代理

只读属性的代理: 对 deleteProperty 删除属性这种操作都是禁止的, has、ownKeys没有进行拦截

非只读属性的代理: 会对三个方法进行依赖收集, 然后调用 Reflect 对应方法返回数据

get拦截类型

deleteProperty

has

ownKeys

响应式代理

拦截, trigger delete

拦截, track hase

拦截, track iterate

只读代理

拦截, return true

浅层响应式代理

拦截, trigger delete

拦截, track hase

拦截, track iterate

浅层只读代理

拦截, return true

collectionHandlers

下面来看下 COLLECTION 的 handlers

Set 和 Map 这类的都是通过实例化对象的方式使用, 所以要对里面的值进行操作, 都是调用对象的一些属性和方法, 因此他们代理的 handlers 只需要进行 get 函数的实现. 但是在 get 函数中, 分别实现了集合方法的代理.

mutableCollectionHandlers 同样分了四种类型, 与 ceateGetter 一样都是通过一个公共函数实现, 这里是叫 createInstrumentationGetter 的函数. 这个函数很简单, 主要是进行了不同代理类型的分发处理, 分别给到 mutableInstrumentations、readonlyInstrumentations、shallowReadonlyInstrumentations、shallowInstrumentations 进行集合方法对代理, 其余的内部标识取值与 createGetter 类似.

这里不贴源码了, 用一张函数调用图来表示, 所有方法的实现都区分了四种类型

集合类型的 handlers集合类型的 handlers

Vue3 代理了集合的 10 个方法和 1 个属性的获取, 其实现不复杂, 可以直接查看源码理解, 这里不做分析, 主要说明几点

1) 所有非只读的代理对象, 都会进行 track 的依赖收集

2) add、set、delete、clear 这种涉及到结合元素变更的, 都会进行 trigger 触发依赖处理

2、ref、shallowRef

ref 函数创建一个响应式的、可更改的 ref 对象, 他是 RefImpl 类的实例化对象, 其对外只暴露一个 value 属性用户获取和更改.

shallowRef 函数是创建一个浅层 ref 对象, 浅层的函数在上面已经说过了, 浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式.

ref 和 shallowRef 都是通过 createRef(rawValue, shallow) 来创建的, 而 createRef 最终是进行 new RefImpl(rawValue, shallow) 实例化, 根据参数 shallow 区分是否是浅层对象, 下面来重点看下 RefImpl

代码语言:javascript复制
class RefImpl {
  constructor(value, __v_isShallow) {
    this.__v_isShallow = __v_isShallow; // 浅层对象标识
    this.dep = undefined; // 依赖收集器
    this.__v_isRef = true; // ref标识
    // 浅层对象会原样存储和暴露, 不转为响应式
    this._rawValue = __v_isShallow ? value : toRaw(value);
    this._value = __v_isShallow ? value : toReactive(value);
  }

  get value() {
    trackRefValue(this); // 当有调用方时, 往this.dep中添加依赖
    return this._value; // 返回对应响应式值
  }

  set value(newVal) {
    // 设置新值
    newVal = this.__v_isShallow ? newVal : toRaw(newVal);
    // 当值发生变化的时候, 更新相关属性, 并且会根据收集的依赖进行逐个触发, 通知值变化
    if (shared.hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = this.__v_isShallow ? newVal : toReactive(newVal);
      triggerRefValue(this);
    }
  }
}

来看两个示例, 示例代码如下

代码语言:javascript复制
const refA = ref({ a: 1 });
const shallowRefB = shallowRef({ a: 1 });
const c = computed(() => (refA.value.a   1));
被引用的非浅层对象被引用的非浅层对象
未被引用的浅层对象未被引用的浅层对象

说明

  • 可以看到浅层和非浅层 ref 对象的区别, 除了__v_isShallow 标识位之外, value 存储也不一样, 非浅层的已经转换成 Proxy 的响应式对象了, 而浅层的则按原对象的类型存储
  • 对于 refA, 由于他被一个 computed 对象的回调使用, 相当于调用了 ref 对象的 get value, 所以向其 dep 中添加了 computed 的依赖, dep 是一个 Set 类型, 具体到依赖处理的时候再详细介绍

3、computed

比较熟悉的计算属性, 必然也是响应式的. 他可以接收一个函数或一个带有 get、set 的对象作为第一参数, 他返回一个只读的 ref 对象. computed 内部是通过实例化 ComputedRefImpl 对象创建的, 所以重点看下 ComputedRefImpl 类.

代码语言:javascript复制
class ComputedRefImpl {
  // 如果初始化computed传入的是函数, 则getter就是该函数, _setter是个空函数, isReadonly为true
  // 如果初始化computed传入的是对象, 则分别将get、set赋值给getter、setter, isReadonly取决于是否定义了set
  constructor(getter, _setter, isReadonly, isSSR) {
    this._setter = _setter; // 初始化setter函数
    this.dep = undefined; // 依赖收集齐
    this.__v_isRef = true; // ref标识
    this._dirty = true; // 是否被引用, true为未被引用
    // 副作用处理器件, 主要是当有地方引用computed时, 进行依赖收集和处理
    this.effect = new ReactiveEffect(getter, () => {
      // 当有被其他对象引用, 且触发副作用时, 会执行这里
      if (!this._dirty) {
        this._dirty = true;
        triggerRefValue(this);
      }
    });
    this.effect.computed = this; // 在副作用处理器中关联
    // 副作用处理器是否有效
    this.effect.active = this._cacheable = !isSSR; 
    // 只读标识
    this['__v_isReadonly' /* IS_READONLY */] = isReadonly;
  }

  get value() { 
    // 针对于只读的对象, reactive不会进行处理, 需要在对象自身中进行响应式处理
    const self = toRaw(this);
    trackRefValue(self);
    // 如果存在被引用的情况, 则进行依赖处理, 并计算computed的value值
    if (self._dirty || !self._cacheable) {
      self._dirty = false;
      self._value = self.effect.run();
    }
    return self._value;
  }

  set value(newValue) {
    this._setter(newValue);
  }
}

同样用两个示例说明, 示例代码如下, 在上面创建的 refA 的基础上, 生成计算属性

代码语言:javascript复制
const computedC = computed(() => (refA.value.a   1));
const computedD = computed(() => (refA.value.a   2));
被引用的计算属性被引用的计算属性
未引用的计算属性未引用的计算属性

说明

  • 这里有两个依赖收集器, 一个是 computed 本身类里面的 dep, 一个是 effect 副作用处理器中的 deps. 本身的 dep 是用来收集其他对象对自己的依赖, 比如 dom 上的使用. deps 是由于当 computedC 被使用时, 会触发 effect 副作用的 run 方法, 该方法会将自身赋值给向全局的 activeEffect 变量, 而 computedC 是从 refA 值计算出来的, 所以在 refA 中会把当前的 activeEffect 加到 refA 的 dep中, 在这个过程中调用的 trackEffects 函数会向 activeEffect.deps 的数组中推入 refA 的 dep. 关键代码如下, 这块后续到副作用里详细介绍
代码语言:javascript复制
class ReactiveEffect {
    ...
    run() {
        ...
        try {
            this.parent = activeEffect;
            activeEffect = this;
            ...
        }
    }
}       
function trackEffects(dep, debuggerEventExtraInfo) {
    ...
    if (shouldTrack) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
    }
}
  • 未被视图使用到的计算属性, 不会在副作用处理器和自身 dep 中添加依赖, 以提高性能
  • 一般 setter 都不会配置, 即使传入了 setter, 也无法改变计算属性内部的值 _value, 因此 computed 被认为是个只读对

结语

Vue3 中初始化响应式对象的 API 大致先介绍到这里, 还有一些其他的响应式相关的工具、副作用等 API 有机会一起学习分享

0 人点赞