深入解析Vue2和Vue3的响应式原理
一、Vue2 解析
本次分析的Vue2版本为2.6。
1. 响应式原理
1、从 new Vue
开始,经过了一系列调用 init、initState、initData、observe、new Obsever、defineReactive,来看下defineReactive
的实现。
2、通过 get、set 监听 Data
中的数据变化,同时为每一个属性创建 Dep 用来搜集使用该 Data
的 Watcher
,来看下Dep
的实现。
3、编译模板,创建 Watcher,并将 Dep.target
标识为当前 Watcher
,来看下Watcher
的实现。
4、编译模板时,如果使用到了 Data
中的数据,就会触发 Data
的 get
方法,然后调用 Dep.addSub
将 Watcher
搜集起来。
5、数据更新时,会触发 Data
的 set
方法,然后调用 dep.notify,进而调用 watcher.update 方法,将所有使用到这个 Data
的 Watcher
加入一个异步队列。
6、最终执行 _render 方法完成页面更新
流程图如下:
2. watch
原理
computed
和watch
内部都是利用了watcher
,user watcher
的过程如下:
- Vue 在 initWatch 过程中,创建
Watcher
,并设置标志位 user 为true
,并判断用户是否设置了immediate
为true
,如果是,立即执行回调; watch
的对象 update 时,判断是否设置了sync
为true
,如果是,不加入异步队列,直接更新;Watcher
更新时判断标志位user
是否为true
,如果是,则执行用户传入的 cb,把newVal
和oldVal
传入。
3. computed
原理
看这个例子:
computed watcher
的过程如下:
- Vue 在 initComputed 过程中,创建标志位
lazy
为true
的Watcher
: - 因为初始化的时候
dirty=lazy=true
,会调用 watcher.evaluate 方法进行一次求值 this.getter.call(vm, vm) ,此时会访问this.user.name
,所以会触发其依赖收集。这时候Dep.target
的值为computed watcher
,依赖收集完后,this.user.name
的dep
中就有了computed watcher
; - 然后在
watcher.evaluate
中将dirty
设置为false
; - 如果
Dep.target
存在,则调用 watcher.depend 进行一次render watcher
的收集;
- 当
name
值改变时,会触发set
,然后通知computed watcher
,执行 update 方法,并将dirty
设置为true
。 - 再次访问
computed
属性时,如果dirty
为false
,则不会执行 watcher.evaluate 方法,直接返回之前缓存的值,如果dirty
为true
,则重新计算。
二、Vue3 解析
Vue3 的代码在 vue-next 仓库中,本次分析的版本是3.2。响应式部分在reactivity
文件夹中,并且可独立引用。
Vue3 的响应式多了一个副作用函数,即effect
函数,指的是响应式数据在发生变更的时候,要执行的函数。
1. 响应式原理
- 依赖收集在 targetMap 中,其是一个
WeakMap
,key
是响应式对象,value
是Map
类型的 depsMap。depsMap
的key
是响应式对象的key
,value
是 effect 函数。
- 响应式Data更新的时候会触发 trigger,然后从
targetMap
中取出对应的依赖进行更新。 effect
函数的创建时机包括 mountComponent、computed、watch 等。
流程图如下:
2. ref
原理
ref 是一个语法糖,返回一个对象,其在get
中调用track
,set
中调用trigger
。
3. Vue3 的computed
原理
- computed 内部用
effect
函数包裹传入的函数getter
,并执行getter
,拿到value
; - 内部
effect
中调用了trigger
,这样computed依赖的值变化的时候,会触发此effect
函数执行,也就能够触发依赖computed
的effect
函数也得到执行; - 构造一个对象,对象的
get
方法中调用了track
,进行了一次依赖收集; - 最后返回构造的对象
三、总结
对比 Vue2 和 Vue3 的响应式实现方式的不同,可以看出
1. Vue2 使用Object.defineProperty
进行数据劫持,Vue3 使用Proxy
,后者优势在于可以劫持push
、pop
等方法,也因为在顶层对象直接劫持,可以提高性能。
2. 依赖收集器的数据结构有变化,Vue2 的依赖收集在Dep.subs
中,也就是一个类的数组中,Vue3 的依赖收集在targetMap
中,其是一个WeakMap
。
3. Vue3 的响应式结构更加简单,与其他部分耦合性小,并已经独立成包,即可以和其他框架进行结合。
四、相关资料
- 图解 Vue 响应式原理
- 搞懂computed和watch原理,减少使用场景思考时间
- Vue源码之computed和watch
- Vue.js 技术揭秘
- vue3.0响应式函数原理
- 手写Vue3 响应式(Reactivity)模块
- vue3源码分析(三)—— 响应式系统(reactivity)
- Vue3 深度解析
20240301 更新
为什么引入 Watcher
Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释:
当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
「依赖收集的目的是:」 将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。
Watcher 和 Observer 的理解
Watcher 是依赖,Dep 是依赖列表,Observer 是观察者
Watcher 是靠近模版一侧,或者是自定义的 Watcher。而 Observer 是对开发者声明的 data,进行响应式劫持,get 中会把 Watcher 放到 dep.subs中, set 时会依次触发 sub 的 notify,来更新视图
data 的每一个 key,在 Observer 劫持时,都有一个独立的 Dep 对象
Watcher 只在构造函数中对 Dep.target 赋值一次 this,也就可以防止重复被收集。
「依赖的本质:」
所谓的依赖,其实就是Watcher。
至于如何收集依赖,总结起来就一句话:
在getter中收集依赖(收集Watch当如Dep中),在setter中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。
Dep 和 Watcher 的关系
Observer 负责将数据转换成 getter/setter 形式; Dep 负责管理数据的依赖列表;是一个发布订阅模式,上游对接 Observer,下游对接 Watcher Watcher 是实际上的数据依赖,负责将数据的变化转发到外界(渲染、回调); 首先将 data 传入 Observer 转成 getter/setter 形式;当 Watcher 实例读取数据时,会触发 getter,被收集到 Dep 仓库中;当数据更新时,触发 setter,通知 Dep 仓库中的所有 Watcher 实例更新,Watcher 实例负责通知外界
- Dep 负责收集所有相关的的订阅者 Watcher ,具体谁不用管,具体有多少也不用管,只需要根据 target 指向的计算去收集订阅其消息的 Watcher 即可,然后做好消息发布 notify 即可。
- Watcher 负责订阅 Dep ,并在订阅的时候让 Dep 进行收集,接收到 Dep 发布的消息时,做好其 update 操作即可。