关于vue3中的Reactive

464 阅读5分钟

前言

前不久聊了下调试vue的方法(原来调试vue源码这么简单!),既然掌握了调试的方法,自然就要探索一番,vue的核心自然是它的响应式,也是双向绑定,表现出来的api自然是reactive,虽然大家常用的是ref这个api,但实际上ref也是基于reactive去实现的,那么本文就来聊聊reactive。

ps:本文并不会针对reactive聊得多深,比如依赖收集以及依赖执行部分,这部分会是下一篇文章的主要内容,也就是vue3的另外一个api---effect。

Proxy与Reflect

接触过vue3的,自然不会对Proxy感到陌生,随便打印一个响应式数据都是Proxy,毕竟vue3就是基于Proxy去做的响应式。

但,不知道大家是否听说过一句话,Proxy一般都会搭配Reflect去使用。其实这句话的根源在于this的指向问题。

我们通过例子来了解这个问题。

const person = {
  name: "猪头切图仔",
  age: 18,
};

const proxyPerson = new Proxy(person, {
  get(target, key) {
    console.log(`读取了${key}`);
    return target[key];
  },
});

proxyPerson.name;

这里我们写下一个很简单的proxy例子,这段代码的意思是,使用proxy去拦截了person对象的读取,只要读取了person对象的属性,都会打印读取了key

image.png

好,现在我们修改一下例子。

const person = {
  name: "猪头切图仔",
  age: 18,
  get nameWithAge() {
    return `${this.name}今年${this.age}岁~`;
  },
};

// proxyPerson部分不变

proxyPerson.name;
proxyPerson.nameWithAge;

修改后的代码,控制台会打印什么?

image.png

这个结果好像对?但我们仔细思考一下,这真的符合我们的预期吗?这真的对吗?

我们来分析下nameWithAge这个方法,这个方法里我们读取了person的name和age,那么理论上我们是否就希望他在执行nameWithAge方法时,然后执行读取name和age时也触发Proxy中的get呢?

这么说可能有点绕,换个大家很熟悉的例子,我们可以把这个nameWithAge看成了computed,现在我们的视图上用到了nameWithAge,然后我们修改了name或者age,但现在视图上用了nameWithAge的地方却不会更新了。

为什么?可以简单理解为,既然在get的时候都不触发proxy的捕获器,set的时候自然也不会。

当然,完整的理解是双向绑定的原理是get的时候收集 ,set的时候触发依赖,既然get不执行,那么也就不会收集到nameWithAge这个依赖,那么set的时候自然不会触发nameWithAge这个依赖。

造成这个现象的根本原因是因为nameWithAge中的this指向的是person而不是proxyPerson,所以它读的是person.name和person.age,那自然不会触发proxyPerson的get和set。

此时,我们就需要Reflect来解决这个问题了。

在proxy的捕获器中,他们的get和set其实接收3个参数:

  • target:被Proxy的对象,也就是例子中的person。
  • key:读取或者设置的属性名
  • receiver:代理对象,也就是例子中的proxyPerson。

我们只要将这三个参数原封不动的传给Reflect,问题就解决了。

const proxyPerson = new Proxy(person, {
  get(target, key, receiver) {
    console.log(`读取了${key}`);
    return Reflect.get(target, key, receiver);
  },
});

这时,我们再执行一次。

image.png

这样就合理了。

reactive

有了上面的前置知识后,我们现在就来实现一下reactive。

rective这个api抛开effect部分,那么就非常简单了,不过还是会存在很多细节,本文会讲一些常见的疑问与细节的实现,至于全部的细节,还是调试一遍源码比较好。

ok,接下来我们一步一步来,先完成最简单的proxy逻辑。

function reactieve(target) {
  // isObject就是一个判断是否是对象的方法,自行实现吧
  // 只代理对象,这也是为什么使用ref的时候非要.value
  if (!isObject(target)) return target;
  
  const proxy = new Proxy(target, mutableHandlers);
  return proxy;
}

const mutableHandlers = {
  get(target, key, receiver) {
    // 使用proxy的时候要搭配reflect,用来解决this问题
    const res = Reflect.get(target, key, receiver);
    return res;
  },
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);
    return result;
  }
};

到这就是最基本的部分了,一个非常简单的数据代理。

后面我们结合例子一点一点完善reactive。

image.png

这里我们引入vue的reactive,打印一下state和state.info。

image.png

从这里可以看出,它进行了层层代理,所以reactive里面理应有个递归逻辑,如果取值时发现仍然是个对象应该再继续代理。

get(target, key, receiver) {
  // 如果在取值的时候发现出来的值是对象,那么再次进行代理,直到非对象为止
  if (isObject(target[key])) {
    return reactive(target[key]);
  }
  // 使用proxy的时候要搭配reflect,用来解决this问题
  const res = Reflect.get(target, key, receiver);
  return res;
}

ok,我们继续看例子。

image.png

这里我们分别定义了三个state:

  • state:针对原person对象进行代理
  • state1:针对state对象进行代理
  • state2:再对person对象进行代理

最后他们的结果:

image.png

都是true,但显然,我们目前写的还不满足,所以可以看出来两点:

  • 针对已经是代理对象的,有一个标识会标记他们为代理对象,再执行代理的话,如果发现有这个标识,就不继续进行。
  • reactive必然针对已代理过的对象进行记录,如果在记录内,那么就不再进行代理。
// ts文件,所以这里用的枚举,大家在js文件中定义一个常亮即可
const enum ReactiveFlags {
  IS_REACTIVE = "__v_isReactive",
}

function isReactive(value) {
  return value[ReactiveFlags.IS_REACTIVE];
}

// 记录对象是否被代理过
const reactiveMap = new WeakMap();

get(target, key, receiver) {
  const existedProxy = reactiveMap.get(target);
  // 代理过了,直接返回代理对象
  if (existedProxy) return existedProxy;
  
  // 如果代理的是代理对象,那么直接返回该代理对象
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }

  // 代理对象读取标识,返回true
  if (key === ReactiveFlags.IS_REACTIVE) {
    return true;
  }

  // 如果在取值的时候发现出来的值是对象,那么再次进行代理,直到非对象为止
  if (isObject(target[key])) {
    return reactive(target[key]);
  }
  // 使用proxy的时候要搭配reflect,用来解决this问题
  const res = Reflect.get(target, key, receiver);
  
  // 记录当前对象,表示已对该对象进行代理
  reactiveMap.set(target, proxy);

  return res;
},

ok,到此,reactive基本就实现了,但这自然是不够的,仅仅只是代理的对对象,实际属于什么都没做,vue的主要是通过数据的双向绑定去实现数据的改变使得视图及时有反馈。

那么要如何实现这个双向绑定?如果看过我之前关于mobx的原理的文章,那么大家应该很简单就能写出来,其实他就是在get中进行依赖收集,在set中触发那些收集到的依赖。

这些所谓的依赖其实就是一个一个的effect(vue的核心api)。

大家可能对effect并不熟悉,但对computed,watch,watchEffect等应该非常熟悉,而他们都是基于effect去实现的。

总结下来也就是,reactive在get中会收集effect,看看都有哪些effect使用到了自己,然后在自己被变更时(也就是set的时候),执行这些effect,让这些effect拿着最新的自己去更新视图或者计算数据等。

结尾

以上便是本文的全部内容,本文属于vue3相关内容的开篇了,较为简单,后面会讲核心且最绕的api---effect,在完成effect的实现后,会基于effect去实现诸如computed,watch等api,敬请期待吧!

最后的最后,希望本文能帮助到各位,另外,觉得本文不错的话,请不要吝啬手中的赞哦🌹🌹🌹