Vue响应式原理

1. 认识响应式逻辑

抽象描绘

  • 假如你有一个变量m , 你的某一段代码使用了变量m , 如果某个时刻变量m发生了改变, 那么这段代码也会重新执行.
  • 但实际上执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中, 于是就变成了, 你有一个对象, 你的某个函数使用了对象的某个属性, 如果某个时候这个属性发生改变, 这个函数也会重新执行

具体描述

数据驱动视图

简洁版 :

在 Vue 实例创建过程中,首先对所有属性进行劫持 ( vue2 / vue3 方法和原理都不同, 注意辨别 ) , 同时会为每个数据属性创建一个 Dep(依赖),Dep 用于收集所有订阅了该属性的 Watcher。当属性值发生变化时,Vue 会通知该属性对应的这些 Watcher 实例进行相应的更新操作。

  • 数据响应式

Vue采用的是数据劫持结合发布和-订阅者模式的方式

通过拦截对数据的操作,在数据变动时发布消息给订阅者,触发相应的监听回调。

  • 数据劫持

vue2数据劫持

vue2通过Object.definePropertydata上的数据递归地进行(转为)gettersetter操作。也就是对属性的读取、修改进行拦截(数据劫持)

注意是将 data 中的所有属性进行监听

vue3 数据劫持

vue3通过Proxy对象创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy的监听是深层次的,监听整个对象,而不是某个属性。

这里是将 reactive () 中的所有属性进行监听

  • 发布者-订阅者模式

简单地说,发布者-订阅者模式的流程就是,监听器**Observer**监听数据状态变化, 一旦数据发生变化,则会通知(数据)对应的订阅者**Watcher**,让订阅者执行对应的业务逻辑 。

  • 整个响应式执行过程
  1. 在 Vue 实例初始化过程中, 我们设置了一个监听器**Observer**,此实例的所有相关属性会被监听(也就是上方的数据劫持) . 在此过程中,Vue 会对每个属性创建一个 dep 实例
  • vue2 对 data 中的属性进行遍历生成 dep 实例
  • vue3 中对 reactive() 中的对象进行遍历生成 dep 实例
  • dep 实例会收集所有订阅了该属性的 Watcher 订阅者, 并将该 Watcher 绑定更新函数
  • 这些更新函数可能会执行一些操作,比如更新模板中的文本、计算新的值等
  1. 通过Compile解析模板指令,将模板中的数据和方法与真实 DOM 节点关联起来,使得数据和方法能够被视图访问和使用, 然后再初始化渲染页面视图.
  2. 一旦属性发生变化,Vue 会通知所有订阅了当前属性的的订阅者Watcher(这些 Watcher 放在属性对应的 dep 实例当中), 来执行此订阅者对应的更新函数, 从而更新视图.

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

image-20230921094628751

2. 响应式依赖收集

2.1. 响应式函数的实现watchFn

我们现在实现一个响应式函数

◼ 但是我们怎么区分呢?

  • 这个时候我们封装一个新的函数watchFn
  • 凡是传入到watchFn的函数,就是需要响应式的;
  • 其他默认定义的函数都是不需要响应式的;

img

如图, foo以及bar都是需要响应式的

我们将这些函数在响应之前先执行一次

当obj的某个属性发生变化时, 就依次执行reactiveFns中的函数

目前当然是极为不完善, 待后续优化hhh

2.2. 响应式依赖的收集__类

◼ 目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:

  •  我们在实际开发中需要监听很多对象的响应式;
  •  这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
  •  我们不可能在全局维护一大堆的数组来保存这些响应函数;

◼ 所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:

  •  相当于替代了原来的简单 reactiveFns 的数组;

img

这个类记住

img

当我们某个属性发生变化时, 只需执行dep.notify()就行了, 无需一个一个的遍历函数

这个obj对象就对应Depend类所创建出来的对象

当然, 还需大大的优化

现在我们每次修改了一个属性之后都是手动的去调用, 这样太麻烦了, 我们想要让它自动去调用响应函数,

因此我们要去监听属性的变化

3. Object.defineProperty()监听属性变化

img

img

再记一遍这个类

img

注意看注释

img

这里在属性变化后就不用手动给它通知了, 自动响应实现

不过这种收集方式其实是错误的hhhhh

4. 自动收集依赖 🔥

我们现在发现了一个问题 , obj 对象的两个属性都依赖于 同一个dep对象的reactiveFns,

这样就会造成我们根本没办法区分它们

对于同一个对象来说, 我们还是给它放到了同一个dep里面, 这会造成无法将它的属性区分

如何解决这个问题呢 ?

img

img

比如我们要获取obj对象的name属性的依赖

1
2
const dep = objMap.get(obj).get(name);
dep.notifiy();

img

  • dep对象数据结构的管理

  • 每一个对象的每一个属性都会对应一个dep对象

  • 同一个对象的多个属性的dep对象是存放一个map对象中

  • 多个对象的map对象, 会被存放到一个objMap的对象中

  • 当执行get函数, 自动的添加fn函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class Depend {
constructor() {
this.reactiveFns = []
}

addDepend(fn) {
if (fn) {
this.reactiveFns.push(fn)
}
}

notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}

const obj = {
name: "why",
age: 18
}

// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null //
function watchFn(fn) {
reactiveFn = fn
fn() // 执行下方的get方法 , 使得间接的把函数加到特定的dep对象上
//(因为函数中会有一些对象会有一些属性)
reactiveFn = null // 使用完后再置为null, 不然可能会造成影响
}

// 封装一个函数: 负责通过obj的key获取对应的Depend对象
// 我们用一个WeakMap对所有对象进行管理, 我们不需要对里面进行强引用,
// 如果有一天obj为null,那就无法销毁了
const objMap = new WeakMap()


// 这个函数的作用是通过对象的key找到它的dep对象
function getDepend(obj, key) {

// 1.根据对象obj, 找到对应的map对象
let map = objMap.get(obj) // 没有就new一个
if (!map) {
map = new Map()
objMap.set(obj, map)
}

// 2.根据key, 找到对应的depend对象
let dep = map.get(key)
if (!dep) {
dep = new Depend()
map.set(key, dep)
}
return dep
}

Object.keys(obj).forEach(key => {
let value = obj[key]

Object.defineProperty(obj, key, {
set: function(newValue) {
value = newValue
// 拿到这个对象的属性的dep对象
const dep = getDepend(obj, key)
dep.notify() // 对这些
},
get: function() {
// 在函数中拿到obj 和 key 就会调用这个get方法
// console.log("get函数中:", obj, key)
// 找到对应的obj对象的key对应的dep对象
const dep = getDepend(obj, key)
// 意味着我们只会创建obj对象的key属性的dep对象
dep.addDepend(reactiveFn)
// 精髓 : 在这个dep对象上放入跟特有对象的特有属性相关的函数,即reactiveFn
// dep这个对象就是保存了跟这个特有对象特有属性相关的一些函数

return value
}
})
})

watchFn(function foo() {
console.log("foo function")
console.log("foo:", obj.name)
console.log("foo", obj.age)
})

watchFn(function bar() {
console.log("bar function")
console.log("bar:", obj.age + 10)
})

注意看注释 !!!!!

你用了我的数据, 我就收集你的依赖, 你没用, 我就不收集 —

我们还有可以优化的点 :

当我们执行下面这段代码时

img

age发生变化时, 它会执行两次函数

我们可以这样操作

img

我们的reactiveFns是一个Set, 这样它就不会添加相同的函数到这里面去

然后下方的push改为add

还有一个地方是我们可以给img这个地方换个写法

我们并不希望将reactiveFn添加放到get中,因为它是属于Dep的行为 (也可以不换, 看自己 )

img

我们直接在类中添加一个方法自动获取收集的函数depend()

相当于利用到这个自由变量reactiveFn

img

然后直接dep.depen()调用即可

不过这两种写法都可以 hhh

但这个代码还有不足, 关于多个对象, 我们如何给它挨个来自动收集依赖, 因为我们这里是写死给obj对象自动收集依赖的

如何解决呢?

方法如下hhh —- 多个对象响应式

5. 多个对象响应式

我们只需要将监听对象属性的这一串代码封装为一个函数

img

这里我们将这一串代码封装为一个函数reactive, 我们创建的对象就可以作为参数传递进去, 那么这个对象的所有属性就能被监听到了, 然后我们返回一个被监听的对象

注意 : 需要返回这个对象哈, 别搞忘了

然后我们在创建对象时使用这个函数, 那么对象的所有属性就能被监听到了

img

img

以上都是vue2响应式原理(也就是defineProperty), 接下来我们用vue3对代码进行重构

特别easy

6. vue3__监听对象__proxy

我们直接将

Object.defineProperty

img

这部分代码改成

proxy

img

在我们调用了Proxy的get捕获器时, 收集依赖

因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖;

这部分代码即可

都是学过的东西 , 忘了就翻翻前面的笔记

7. 完整响应式代码

完整响应式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Depend {
constructor() {
this.reactiveFns = new Set()
}

addDepend(fn) {
if (fn) {
this.reactiveFns.add(fn)
}
}

depend() {
if (reactiveFn) {
this.reactiveFns.add(reactiveFn)
}
}

notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}

let reactiveFn = null
function watchFn(fn) {
reactiveFn = fn
fn()
reactiveFn = null
}

const objMap = new WeakMap()
function getDepend(obj, key) {
// 1.根据对象obj, 找到对应的map对象
let map = objMap.get(obj)
if (!map) {
map = new Map()
objMap.set(obj, map)
}

// 2.根据key, 找到对应的depend对象
let dep = map.get(key)
if (!dep) {
dep = new Depend()
map.set(key, dep)
}
return dep
}

function reactive(obj) {
const objProxy = new Proxy(obj, {
set: function(target, key, newValue, receiver) {
// target[key] = newValue
Reflect.set(target, key, newValue, receiver)
const dep = getDepend(target, key)
dep.notify()
},
get: function(target, key, receiver) {
const dep = getDepend(target, key)
dep.depend()
return Reflect.get(target, key, receiver)
}
})
return objProxy
}

业务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const obj = reactive({
name: "why",
age: 18,
address: "广州市"
})

watchFn(function() {
console.log(obj.name)
console.log(obj.age)
console.log(obj.age)
})

// 修改name
console.log("--------------")
// obj.name = "kobe"
obj.age = 20
// obj.address = "上海市"


console.log("=============== user =================")
const user = reactive({
nickname: "abc",
level: 100
})

watchFn(function() {
console.log("nickname:", user.nickname)
console.log("level:", user.level)
})

user.nickname = "cba"

执行结果

img

总结 :

  • Vue3主要是通过Proxy来监听数据的变化以及收集相关的依赖的;
  • ue2中通过我们前面学习过的Object.defineProerty的方式来实现对象属性的监听;

Vue响应式原理
http://example.com/2023/09/12/Vue的响应式原理/
作者
weirdo
发布于
2023年9月12日
更新于
2023年10月24日
许可协议