首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Vue 响应式原理的 10 个细节

在讲解之前,我们先了解一下数据响应式是什么?所谓数据响应式就是建立与(调用了响应式数据的操作)之间的关系,当响应式数据发生变化时,可以通知那些使用了这些响应式数据的依赖操作进行相关更新操作,可以是DOM更新,也可以是执行一些回调函数。从Vue2到Vue3都使用了响应式,那么它们之间有什么区别?

Vue2响应式:基于实现的。

Vue3响应式:基于实现的。

那么它们之间有什么区别?为什么Vue3会选择Proxy替代defineProperty?我们先看看下面两个例子:

从上图可以看出使用defineProperty定义了包含name属性的对象obj,然后添加age属性、添加hobby属性(数组)、添加student属性并分别访问,都没有触发obj对象中的get、set方法。也就是说,我们再看看这样一个例子:

image.png

假如我们一开始就为obj添加hobby属性,我们发现修改数组下标0的值,并没有触发obj里的set方法,也就是说。那么我们继续看一下Proxy代理的对象例子:

从上图是不是发现了Proxy与defineProperty的明显区别之处了,Proxy能支持对象添加或修改触发get、set方法,不管对象内部有什么属性。所以

Object.defineProperty():defineProperty定义对象不能监听添加额外属性修改额外添加的属性的变化;defineProperty定义对象不能监听根据自身数组下标修改数组元素的变化。我们看看Vue里的用法例子:

从图中确实可以修改data里的属性,但是不能及时渲染,所以Vue2提供了两个属性方法解决了这个问题:和。

注意不能直接this._ data.age这样去添加age属性,也是不支持的。

image.png

Proxy:解决了上面两个弊端,proxy可以实现:

可以直接监听对象而非对象属性,可以监听对象添加额外属性的变化;

可以直接监听数组的变化

Proxy 返回的是一个新对象,而 Object.defineProperty 只能遍历对象属性直接修改。

支持多达13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是Object.defineProperty 不具备的。

总的来说,Vue3响应式使用Proxy解决了Vue2的响应式的诟病,从原理上说,它们所做的事情都是一样的,依赖收集依赖更新

1 Vue2响应式原理

这里基于Vue2.6.14版本进行分析

Vue2响应式:通过Object.defineProperty()对每个属性进行监听,当对属性进行读取的时候就会触发getter,对属性修改的时候就会触发setter。首先我们都知道Vue实例中有data属性定义响应式数据,它是一个对象。我们看看下面例子的data:

从上图我们可以看到,data中的每一个属性都会带  属性,它是一个Observer对象,其实Vue2中响应式的关键就是这个对象,在data中的每一个属性都会带get、set方法,而Vue源码中其实把get、set分别定义为reactiveGetter、reactiveSetter,这些东西怎么添加进去的。Vue2又是怎么数据变化同时实时渲染页面?先看看下面的图:

给data属性创建Observer实例:通过初注册响应式函数initState中调用了initData函数实现为data创建Observer实例。

image.png

通过Observer实例把data中所有属性转换成getter/setter形式来实现响应性:对data属性分为两种情况处理:(defineReactive实现)和。数组怎么处理(后面再详细说明)

注意地,由于Vue实例的data永远都是一个对象,所以data里面包含的数组类型只有对象属性数组属性

在getter收集依赖,在setter中触发依赖:当读取data中的数据时,会在get方法中收集依赖,当修改data中的数据时,会在set方法中通知依赖更新。defineReactive方法中主要是做四件事情:、、、。

image.png

从上面我们知道了dep.depend()实现了依赖收集,dep.notify()实现了通知依赖更新,那么Dep类究竟做了什么?我们先看看下面的图:

从图中我们得明确一点,,也就是说哪个依赖使用了变化的数据,其实就是Dep.taget,它就是我们需要收集的依赖,是一个Watcher实例对象,其实Watcher对象有点类似watch监听器,我们先看一个例子:

怎么监听多层嵌套的对象,其实就是通过.分割为对象,循环数组一层层去读数据,最后一后拿到的就是想要对的数据。

当嵌套对象a.b.c属性发生变化时,就会触发第二个参数中的函数。也就是说a.b.c就是变化的数据,当它的值发生变化时,通知Watcher,接着Watcher触发第二个参数执行回调函数。我们看看Watcher类源码,是不是发现了cb其实就与watch的第二参数有异曲同工之妙。

实现对数组的监听:从最开始的例子,我们了解对象以及嵌套对象的监听,但是Object.defineProperty是用来监听对象指定属性的变化,不支持数组监听,那么数组又是怎么监听?我们上面说了data中的数据被赋予响应性都是在Observer中实现的,那么监听的实现也是在Observer对象中实现的,先对数组的特定方法做自定义处理,为了拦截数组元素通知依赖更新,然后才通过observeArray函数遍历创建Observer实例,主要分为两种情况:

当浏览器支持__ proto __ 对象:强制赋值当前arrayMethods给target的__ proto __ 对象,直接给当前target数组带上自定义封装的数组方法,从而实现监听数组变化。其实arrayMethods处理后就是下面这样一个对象:

image.png

当浏览器不支持__ proto __ 对象:遍历数组元素通过defineProperty定义为元素带上自定义封装的原生数组方法,由于自定义数组方法中做了拦截通知依赖更新,从而实现监听数组的变化。

对数组的Array原生方法做了自定义封装的源码如下,在自定义方法中拦截通知依赖更新。

Vue2响应式原理小结:

给data创建Observer实例

Observer类实现对数据封装getter、setter的响应性

针对数组类型数据,自定义封装Array原生方法,在封装过程中拦截执行通知依赖更新

真正通过Watcher通知依赖更新,通过run方法中的cb回调函数,实现类似watch侦听器第二参数中监听变化后的操作

Vue3响应式原理

这里基于Vue3.2.41版本进行分析

其实Vue3的响应原理与Vue2的响应原理都差不多,唯一不同的就是它们的实现方式,Vue3通过创建Proxy的实例对象而实现的,它们都是收集依赖、通知依赖更新。而Vue3中把依赖命名为副作用函数effect,也就是数据改变发生的副作用,我们先来看一下例子:

通过Proxy创建一个代理对象,把house代理给obj,obj是代理对象,house是被代理对象。house对象中数据改变,由于effect函数读取了对象属性,所以当数据改变,也需要及时更新副作用函数effect。但是问题来了,假如对象中多个属性的,依赖于数据变化的多个副作用函数,数据变化一次都需要执行一次,代码写起来就会很冗余,所以我们需要这样处理:

把副作用函数都存到Set实例中,Set可以过滤重复数据,然后在获取数据中收集副作用函数,在修改数据中遍历执行副作用函数,这样就简化了代码,不需要每次改变都要执行一次了,也就是修改一次数据及时更新effect。虽然上面已经实现了响应式的雏形了,但是还需要解决很多问题:

假如这个副作用函数是一个匿名函数,这时候需要怎么处理?添加一个全局变量临时存储。

假如读取不存在的属性的时候,副作用函数发生什么?副作用函数会被重新执行,由于。所以这就需要引入唯一key辨识每一个数据的副作用函数,以target(目标数据)、key(字段名)、effectFn(依赖)。看下图:

分三种情况分析副作用函数存储数据唯一标识

[x] 两个副作用函数同时读取同一个对象的属性值

image.png

[x] 一个副作用函数中读取了同一个对象不同属性

image.png

[x] 不同副作用函数中读取两个不同对象的相同属性

所以为了解决这些不同情况的副作用保存问题,所以Vue3引入了Weak、Map、Set三个集合方法来保存对象属性的相关副作用函数:

image.png

WeakMap与Map的区别是?区别就是垃圾回收器是否回收的问题,WeakMap对象对key是弱引用,如果target对象没有任何引用,可以被垃圾回收器回收,这就需要它了。相对于WeakMap,不管target是否引用,Map都不会被垃圾回收,容易造成内存泄露。我们看一下下面例子:

假如在一个副作用函数中调用了对象的两个属性,但是有布尔值控制,按正常来说,副作用函数只能执行一次get获取值的,但是我们现有的实现方法还实现不了,我们看看下面例子。

通过这个例子,我们是不是需要解决这个问题,也就是当每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。我们看看源码例子:

嵌套副作用函数处理:由于副作用函数可能是嵌套,比如副作用函数中effectFn1中有还有一个副作用函数effectFn2,以上面的方法对于嵌套函数的处理用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。看了很多资料举例用effect栈存储,是的没错,当执行副作用函数的时候把它入栈,执行完毕后把它出栈。现在我们一起看一下源码怎么处理的:

按位跟踪标记递归深度方式(优化方案):通过用二进制位标记当前嵌套深度的副作用函数是否记录过,如果记录过就,如果已经超过最大深度,因为采用降级方案,是全部删除然后重新收集副作用函数的。

为什么需要设置30位,因为31位会溢出。

为什么是左移一位,是因为第一位也就是说当前深度只是1,所以保持不变,不用管,从第二位开始。

如何判断当前依赖是否已记录过,通过按位与判断是否有位已经标识,有就大于0:

清理依赖

完全清理方式(降级方案):逐个清理掉当前依赖集合deps中每个依赖。

响应式可调度性scheduler:trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。

Vue3响应式的6个细节我们都了解了,我们可以对副作用工作流做一个全面总结如图:

Vue3响应式的关键在于两个函数:(收集依赖)和(触发依赖)。

Vue3响应式原理小结:

Vue3中的副作用函数其实就是Vue2的依赖

activeEffect解决匿名函数问题

WeakMap、Map、Set存储对象属性的相关副作用函数

处理副作用函数时,假如有多个响应式属性,控制只触发生效的属性或用到的属性

嵌套副作用函数,使用二进制位记录嵌套副作用,通过控制二进制位是否清理嵌套副作用实现层级追踪

track()实现依赖收集、层级依赖追踪、依赖清理(解决嵌套副作用)

trigger()当某个依赖值发生变化时触发的, 根据依赖值的变化类型, 会收集与依赖相关的不同副作用处理对象, 然后逐个触发他们的 run 函数, 通过执行副作用函数获得与依赖变化后对应的最新值

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20230214A040AE00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券