Vue响应式原理

本文详细介绍响应式原理

内容包括

  • 什么是响应式原理
  • 响应式原理的实现
  • 什么是依赖跟踪
  • 依赖跟踪的实现
  • 小型响应式系统的实现
    • 迷你观察者

响应式

什么是响应式?

响应式:当某一状态更新,系统会自动更新与其关联的状态

  • web:不断变化的状态反应到DOM上的变化

Vue是如何跟踪变化的?

场景:实现一个程序,变量b总数a的10倍

方案一:

1
2
3
4
5
6
7
8
let a = 1;
let b = a * 10;

// 当a状态改变时
a = 2;
console.log(b); // >10 当a改变时,b并没有随之改变,因为上面代码是命令性的,并不会保持同步
// 每次修改a时都手动更新b
b = a * 10;

方案二:方案一显然不是很好,我们希望他们的关系是声明式的,系统可以自动关系

1
2
3
4
// 我们希望有以下函数,在a修改后自动调用
function onAChange(){
b = a *10;
}

defineProperty

使用ES5新增的对象方法defineProperty进行响应式设计

MDN 关于 Object.defineProperty的说明

预期结果

reactive.js

1
2
3
4
5
6
7
8
9
const convert = require('./convert.js')
let state = {
count:0,
}

convert(state);

state.count; // > getting key "count": 0
state.count = 1; // > setting key "count" to: 1

代码实现

convert.js

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
module.exports = function convert(obj) {
// 判断是否为对象
if (!isObject(obj)) {
throw new TypeError()
}
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get() {
console.log(`getting key "${key}": ${internalValue}`)
return internalValue
},
set(newValue) {
console.log(`setting key "${key}" to: ${newValue}`)
internalValue = newValue
}
})
})

}

function isObject(obj) {
return typeof obj === 'object'
&& !Array.isArray(obj)
&& obj !== null
&& obj !== undefined
}

运行代码

1
node .\reactive.js

控制台输出

getting key “count”: 0
setting key “count” to: 1

依赖跟踪

什么是依赖跟踪?

dependency tracking

依赖:找到一种方式去建立关联(订阅者模式)

  • depend:正在执行的代码,收集依赖
  • notify:依赖发生改变,任何之前被定义为依赖都会被通知重新执行
  • aoturun:接收一个更新函数,进入该函数表示进入响应空间,可以注册依赖
    • 将依赖项和更新函数建立依赖
    • 调用notify后会自动执行这段逻辑

代码实现

Dep类

  • 依赖收集(depen)
  • 依赖通知(notify)

autorun

  • 接收一个update函数,在收集依赖时就是收集这个update函数

预期结果

depenTest.js

1
2
3
4
5
6
7
8
9
10
const {Dep,autorun} = require('./depen.js')

const dep = new Dep()

autorun(()=>{
dep.depen();
console.log("update");
})

dep.notify()

代码实现

Dep使用了发布订阅模式(学过发布订阅者模式的很容易就能看懂Dep类)

  • 订阅:depen
  • 发布:notify
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
exports.Dep = class Dep {
constructor() {
this.subscribers = new Set()
}
depen() {
// 依赖收集
if (activeUpdate) {
// 将正在运行的函数加入依赖
this.subscribers.add(activeUpdate)
}
}
notify() {
// 通知依赖更新
this.subscribers.forEach(sub => sub())
}
}
// 因为JavaScript是单线程,所以定义一个全局变量,用于标记正在执行的函数,以便于将次函数加入依赖
let activeUpdate

exports.autorun = function autorun(update) {
function wrappedUpdate() {
activeUpdate = wrappedUpdate
update()
activeUpdate = null
}
wrappedUpdate()
}

运行代码

1
node .\depenTest.js

以下写法好像也可以,但不知道为什么尤雨溪要那样写

1
2
3
4
5
exports.autorun = function autorun(update) {
activeUpdate = update
update()
activeUpdate = null
}

图示说明

执行autorun后,从图中的1开始执行,将wrappedUpdate存入全局变量中

  • 因为JavaScript是单线程,所以可以用一个全局变量监测目前正在执行的函数

image-20211119161332805

在执行dep.notify时,会将所有订阅函数执行一遍,即每次add进去的activeUpdate函数

迷你观察者

实现目标

1
2
3
4
5
6
7
8
9
10
11
12
13
const state = {
count: 0
}

observe(state)

autorun(() => {
console.log(state.count)
})
// should immediately log "count is: 0"

state.count++
// should log "count is: 1"

代码

需要用到上面的convert(函数名改为observe)、Dep和autorun的代码

  • 修改observe
    • 在get中收集依赖
    • 在set中通知依赖更新

observe.js

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
const { Dep } = require('./depen.js')
module.exports = function observe(obj) {
// 判断是否为对象
if (!isObject(obj)) {
throw new TypeError()
}
let dep = new Dep()
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get() {
dep.depen()
return internalValue
},
set(newValue) {
if (newValue !== internalValue) {
internalValue = newValue
dep.notify()
}
}
})
})
}

function isObject(obj) {
return typeof obj === 'object'
&& !Array.isArray(obj)
&& obj !== null
&& obj !== undefined
}

mini-observer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const observe = require('./observe.js')
const { autorun } = require('./depen.js')
const state = {
count: 0
}
observe(state)

autorun(() => {
console.log(`count is: ${state.count}`);
})
// should immediately log "count is: 0"

state.count = 1
// should log "count is: 1"

运行测试

1
node .\mini_observe.js

控制台输出

count is: 0
count is: 1

源码地址:Github

参考