Vue响应式原理
1. 认识响应式逻辑
抽象描绘
- 假如你有一个变量m , 你的某一段代码使用了变量m , 如果某个时刻变量m发生了改变, 那么这段代码也会重新执行.
- 但实际上执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中, 于是就变成了, 你有一个对象, 你的某个函数使用了对象的某个属性, 如果某个时候这个属性发生改变, 这个函数也会重新执行
具体描述
数据驱动视图
简洁版 :
在 Vue 实例创建过程中,首先对所有属性进行劫持 ( vue2 / vue3 方法和原理都不同, 注意辨别 ) , 同时会为每个数据属性创建一个 Dep
(依赖),Dep 用于收集所有订阅了该属性的 Watcher
。当属性值发生变化时,Vue 会通知该属性对应的这些 Watcher
实例进行相应的更新操作。
- 数据响应式
Vue采用的是数据劫持结合发布和-订阅者模式的方式
通过拦截对数据的操作,在数据变动时发布消息给订阅者,触发相应的监听回调。
- 数据劫持
vue2数据劫持
vue2通过Object.defineProperty
对data
上的数据递归地进行(转为)getter
和setter
操作。也就是对属性的读取、修改进行拦截(数据劫持)
注意是将 data 中的所有属性进行监听
vue3 数据劫持
vue3通过Proxy
对象创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy
的监听是深层次的,监听整个对象,而不是某个属性。
这里是将 reactive () 中的所有属性进行监听
- 发布者-订阅者模式
简单地说,发布者-订阅者模式的流程就是,监听器**Observer**
监听数据状态变化, 一旦数据发生变化,则会通知(数据)对应的订阅者**Watcher**
,让订阅者执行对应的业务逻辑 。
- 整个响应式执行过程
- 在 Vue 实例初始化过程中, 我们设置了一个监听器
**Observer**
,此实例的所有相关属性会被监听(也就是上方的数据劫持) . 在此过程中,Vue 会对每个属性创建一个dep
实例
- vue2 对 data 中的属性进行遍历生成
dep
实例 - vue3 中对
reactive()
中的对象进行遍历生成dep
实例 dep
实例会收集所有订阅了该属性的Watcher 订阅者
, 并将该Watcher
绑定更新函数- 这些更新函数可能会执行一些操作,比如更新模板中的文本、计算新的值等
- 通过
Compile
解析模板指令,将模板中的数据和方法与真实 DOM 节点关联起来,使得数据和方法能够被视图访问和使用, 然后再初始化渲染页面视图. - 一旦属性发生变化,Vue 会通知所有订阅了当前属性的的订阅者
Watcher
(这些Watcher
放在属性对应的dep
实例当中), 来执行此订阅者对应的更新函数, 从而更新视图.
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
2. 响应式依赖收集
2.1. 响应式函数的实现watchFn
我们现在实现一个响应式函数
◼ 但是我们怎么区分呢?
- 这个时候我们封装一个新的函数
watchFn
; - 凡是传入到watchFn的函数,就是需要响应式的;
- 其他默认定义的函数都是不需要响应式的;
如图, foo以及bar都是需要响应式的
我们将这些函数在响应之前先执行一次
当obj的某个属性发生变化时, 就依次执行reactiveFns中的函数
目前当然是极为不完善, 待后续优化hhh
2.2. 响应式依赖的收集__类
◼ 目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
- 我们在实际开发中需要监听很多对象的响应式;
- 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
- 我们不可能在全局维护一大堆的数组来保存这些响应函数;
◼ 所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:
- 相当于替代了原来的简单 reactiveFns 的数组;
这个类记住
当我们某个属性发生变化时, 只需执行dep.notify()就行了, 无需一个一个的遍历函数
这个obj对象就对应Depend类所创建出来的对象
当然, 还需大大的优化
现在我们每次修改了一个属性之后都是手动的去调用, 这样太麻烦了, 我们想要让它自动去调用响应函数,
因此我们要去监听属性的变化
3. Object.defineProperty()监听属性变化
再记一遍这个类
注意看注释
这里在属性变化后就不用手动给它通知了, 自动响应实现
不过这种收集方式其实是错误的hhhhh
4. 自动收集依赖 🔥
我们现在发现了一个问题 , obj 对象的两个属性都依赖于 同一个dep对象的reactiveFns,
这样就会造成我们根本没办法区分它们
对于同一个对象来说, 我们还是给它放到了同一个dep里面, 这会造成无法将它的属性区分
如何解决这个问题呢 ?
比如我们要获取obj对象的name属性的依赖
1 |
|
dep对象数据结构的管理
每一个对象的每一个属性都会对应一个dep对象
同一个对象的多个属性的dep对象是存放一个map对象中
多个对象的map对象, 会被存放到一个objMap的对象中
当执行get函数, 自动的添加fn函数
1 |
|
注意看注释 !!!!!
你用了我的数据, 我就收集你的依赖, 你没用, 我就不收集 —
我们还有可以优化的点 :
当我们执行下面这段代码时
age发生变化时, 它会执行两次函数
我们可以这样操作
我们的reactiveFns是一个Set, 这样它就不会添加相同的函数到这里面去
然后下方的push改为add
还有一个地方是我们可以给这个地方换个写法
我们并不希望将reactiveFn添加放到get中,因为它是属于Dep的行为 (也可以不换, 看自己 )
我们直接在类中添加一个方法自动获取收集的函数depend()
相当于利用到这个自由变量reactiveFn
然后直接dep.depen()
调用即可
不过这两种写法都可以 hhh
但这个代码还有不足, 关于多个对象, 我们如何给它挨个来自动收集依赖, 因为我们这里是写死给obj对象自动收集依赖的
如何解决呢?
方法如下hhh —- 多个对象响应式
5. 多个对象响应式
我们只需要将监听对象属性的这一串代码封装为一个函数
这里我们将这一串代码封装为一个函数reactive, 我们创建的对象就可以作为参数传递进去, 那么这个对象的所有属性就能被监听到了, 然后我们返回一个被监听的对象
注意 : 需要返回这个对象哈, 别搞忘了
然后我们在创建对象时使用这个函数, 那么对象的所有属性就能被监听到了
以上都是vue2响应式原理(也就是defineProperty), 接下来我们用vue3对代码进行重构
特别easy
6. vue3__监听对象__proxy
我们直接将
Object.defineProperty
这部分代码改成
proxy
在我们调用了Proxy的get捕获器时, 收集依赖
因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖;
这部分代码即可
都是学过的东西 , 忘了就翻翻前面的笔记
7. 完整响应式代码
完整响应式代码
1 |
|
业务代码
1 |
|
执行结果
总结 :
- Vue3主要是通过Proxy来监听数据的变化以及收集相关的依赖的;
- ue2中通过我们前面学习过的Object.defineProerty的方式来实现对象属性的监听;