专栏前言
本文是vue3源码解析系列的第二篇文章,这一章我们主要学习vue3源码中涉及到的一些核心api。
后续的源码解读是非常复杂的,所以相关基础知识一定要牢固哦~
前言
大部分使用过vue3的同学都知道,vue3的底层的响应式实现由Object.defineProperty更换成了Proxy。
为什么vue3要更换呢?proxy相对于前者又有何优势呢?
接下来让我们通过案例去一探究竟吧!
当响应式不存在
我们先看一个例子
1 | let shoes = { |
第二次打印依旧是30,虽然我们的num发生了变化,但是下一次获取total的值依旧是之前的值,因为total已经被运算过了。
那应该怎么做,才能实时的获取到当前最新的total呢?
也很简单,我们每次获取之间,手动重新计算一次就好了。
1 | let shoes = { |
我们增加effect方法来手动触发依赖,这样我们实现了需求。
但是这样手动触发的方式,在真实业务中过于繁琐,难以维护,本质上依旧是命令式思维。
如何实现值的修改,后续逻辑的自动执行呢?
vue2的解决方案
通过Object.defineProperty来对字段进行代理,通过set,get方法,完成逻辑的自动触发。
1 | let num = 3 |
我们再以上代码,再次修改shoes.num,将触发代理中的set,进而触发effect,实现依赖的自动触发,vue2的底层也正是如此实现的,这样看起来我们的需求已经解决了,那为何vue3有放弃了Object.defineProperty呢?
接下来我们就要聊聊他的缺陷。
Object.defineProperty的缺陷
该API确实满足了我们上面提到的案例,但是他在一些场景也存在很多问题。
比如大家一定都遇到过的问题
- object中新增字段 没有响应性
- array中指定下标的方式增加字段 没有响应性的
为什么会这样呢?vue的官方解释是
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。
尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
那JavaScript到底限制了什么呢?
object.defineProperty只能监听到指定对象的指定属性的get set,这些工作其实是vue初始化阶段完成,所以指定对象的指定元素发生变化的时候,我们可以监听到变化,vue中也确实是这么表现的;
但是如果,我们在指定对象上面新增属性,object.defineProPerty是无法监听到的,无法监听则无法处理被新增的字段,自然字段就不具备响应式;
在vue2中,如果想解决以上问题,需要使用Vue.$set进行手动增加响应式字段,解决无法监听到字段新增的问题。
vue3的解决方案
vue3中改用了proxy,为什么响应式核心api做了修改,proxy是什么?我们先实现一个类似vue2的案例
1 | let shoes = { |
通过以上代码,我们可以看到一些差别
object.defineproperty
代理的并非对象本身,而是对象中的属性
只能监听到对象被代理的指定属性,无法监听到对象本身的修改
修改对象属性的时候,是对原对象进行修改的,原有属性,则需要第三方的值来充当代理对象
proxy
- proxy针对对象本身进行代理
- 代理对象属性的变化都可以被代理到
- 修改对象属性的时候,我们针对代理对象进行修改
无论是逻辑的可读性,还是API能力上,proxy都比object.defineProPerty要强很多,这也是vue3选择proxy的原因。
proxy的好兄弟Reflect
在vue3的源码中的**@vue/reactivity中,我们会经常看到在proxy的set、get中存在Reflect的身影,但是从我们上面对proxy的使用来看,赋值 读取都实现了,为什么vue3中使用了Reflect**呢?
首先我们了解一下Reflect是干嘛的
官方解释:Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
似乎比较难理解,我们举个例子吧
1 | let obj = { num:10 } |
这么来看,似乎这个api很普通啊,反而把简单的读取值写复杂了。
这时候我们就要提一下Reflect.get 的第三个参数了
1 | Reflect.get(target, propertyKey, receiver]) // receiver 如果target对象中指定了propertyKey,receiver则为getter调用时的this值。 |
这次我们知道了,第三个参数receiver具有强制修改this指向的能力,接下来我们来看一个场景
1 | let data = { |
打印情况如下
1 | 属性被读取 |
dataProxy.useinfo的get输出的值是正常的,但是get只被触发了一次,这是不正常的;
因为useinfo里面还读取了被代理对象data的name、age,理想情况应当是get被触发三次。
为什么会出现这样的情况呢,这是因为调用userinfo的时候,this指向了data,实际执行的是data.userinfo,此时的this指向data,而不是dataProxy,此时get自然是监听不到name、age的get了。
这时候我们就用到了Reflect的第三个参数,来重置get set的this指向。
1 | let dataProxy = new Proxy(data, { |
打印情况如下
1 | 属性被读取 |
现在打印就正常了,get被执行的3次,此时的this指向了dataProxy,Reflect很好的解决了以上的this指向问题。
通过以上案例,我们可以看到使用target[key]有些情况下是不符预期的,比如案例中的被代理对象this指向问题,而使用Reflect则可以更加稳定的解决这些问题,在vue3源码中也确实是这么用的。
补充章节(WeakMap)
通过以上文章,我们了解到了object.defineproperty相较于proxy的劣势,以及搭配proxy同时出现的Reflect的原因,这是vue3最核心的api。
但是仅仅知道理解proxy+reflect,还不太够,为了尽量轻松的阅读Vue3源码,我们还要学习一个原生API,那就是WeakMap。
weakMap和map一样都是key value格式,但是他们还是存在一些差别。
- weakMap的key必须是对象,并且是弱引用关系
- Map的key可以是任何值(基础类型+对象),但是key所引用的对象是强引用关系
通过查阅MDN我们可以发现,weakMap可以实现的功能,Map也是可以实现的,那为什么Vue3内部使用了WeakMap呢,问题就在引用关系上
强引用:不会因为引用被清除而失效
弱引用:会因为引用被清除而自动被垃圾回收
概念似乎还无法体现其实际作用,我们通过以下案例即可明白
1 | // Map |
通过以上案例我们可以了解到
- 弱引用在对象与key共存场景存在优势,作为key的对象被销毁的同时,WeakMap中的key value也自动销毁了。
- 弱引用也解释了为什么weakMap的key不能是基础类型,因为基础类型存在栈内存中,不存在弱引用关系;
在vue3的依赖收集阶段,源码中用到了WeakMap,具体什么作用?我们下一节进行解答。
结语
通过本篇文章,我们认识到了object.defineproperty相较于proxy的劣势,以及搭配proxy同时出现的Reflect的原因,还有一个Map的原生的API,WeakMap的作用。
接下来我们就可以正式走进vue3源码的世界~