Vue响应式原理-get/set

vue响应式原理—–get/set

什么是响应式?

要说什么是响应式, 那么就要知道什么不是响应式,看个例子

1
2
3
4
5
let a = 1;
let b = a * 5;

a = 2;
b; // 5 我们期望, 如果a改变,那么基于a的计算都可以同步, 我们希望b为10

这里就要提出一个属性Object.defineProperty()

使用方法为:

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
Object.defineProperty(obj, 'keyName', { 
// 这里就是配置obj这个对象的keyName这个属性的地方
get () {

},
set() {

}
})

// 首先是四个属性值的属性
// enumerable : 是否可枚举 // [].length
// writeable : 是否可写 // a.prop1 = 1;
// configurable : 是否可配置
// value : 我获取值时返回的value

// 两个方法: get/set
// 挑战1: 我需要一个fn,输入obj把里面的每一个属性都转换为可响应式,配上get/set方法
obj = {
a: 1, // get: "a is 1" set: "a is 2"
b: 1 // "b is 1"
}
const transform = (obj, i) => {
var unKnow = obj[i]
Object.defineProperty(obj, i, {
get() {
console.log(`getting key "${i}": ${unKnow}`, unKnow)
return unKnow
},
set(value) {
console.log(`setting key "${i}" to: ${value}`)
unKnow = value
}
})
}

function convert (obj) {
// Implement this!
for (let i in obj) {
transform(obj, i)
}
}

// test
let a = {
a: 1,
b: 2
}

convert(a)

依赖跟踪

data

每个vue的实例都会有一个watcher对象,这个对象中提供提供增加依赖和发布依赖的方法。增加依赖在于getter部分把当数据更新的操作增加, 而在单个属性发生改变,也就是set的时候触发notify, 执行更新操作。在这里用了设计模式中的发布-订阅模式

每个vue实例就是我们的订阅者, 这些订阅者的更新事件都放到watcher对象里区管理。当我们增加一个依赖项的时候把依赖项放入订阅队列, 然后在每次更新的时候区触发对应的事件。完成更新。

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
// 一个依赖项就是一个watch
// watch 中的subscibers是订阅者的一个队列,里面存放每个依赖事件
// 方法:
// depend: 为该属性增加依赖项
// notify: 数据更新时通知依赖项更新
window.Dep = class Dep {
constructor () {
// 订阅队列
this.subscribers = new Set()
// new Set([1, 2, 3, 1])
}

depend () {
if (activeUpdate) {
this.subscribers.add(activeUpdate)
}
}

notify () {
this.subscribers.forEach(sub => sub())
}
}

let activeUpdate = null

function autorun (update) {
const wrappedUpdate = () => {
activeUpdate = wrappedUpdate // 这个就是一个依赖, 注意,这个其实是是外层函数
update() // update里面要执行一个依赖收集
activeUpdate = null
}
wrappedUpdate()
}

结合get/set与依赖跟踪,完成一个小型观察者对象

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
class Dep {
constructor () {
this.subscribers = new Set()
}

depend () {
if (activeUpdate) {
this.subscribers.add(activeUpdate)
}
}

notify () {
this.subscribers.forEach(sub => sub())
}
}

// 批量对Obj的属性赋予响应式的能力
function observe (obj) {
// iterate through all properties on the object
// and convert them into getter/setters with
// Object.defineProperty()
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
// each property gets a dependency instance
// 在进行响应式的同时初始化依赖项实例, 之后再对应的getter/setter方法中形成闭包, 把依赖状态持久化
const dep = new Dep()
Object.defineProperty(obj, key, {
// The getter is responsible for registering subscribers
get () {
// 每次进行get方法的时候, 都进行一次依赖收集
dep.depend()
return internalValue
},
// The setter is responsible for notifying change
set (newVal) {
// 检测值是否改变, 如果没有改变, 那么不做处理(为了性能)
const changed = internalValue !== newVal
internalValue = newVal
// triggering re-computation
// 如果发生了改变, 那么在依赖class触发依赖项的更新
if (changed) {
dep.notify()
}
}
})
})
return obj
}

let activeUpdate = null

// 这里要注意一点, 你所有的, 对依赖项相关的操作,都要以匿名函数的方式传入这个函数, 才能完成依赖的收集
function autorun (update) {
// wrap the raw update function into a "job" function that registers and
// unregisters itself as the current active job when invoked
const wrappedUpdate = () => {
activeUpdate = wrappedUpdate
update()
activeUpdate = null
}
wrappedUpdate()
}

const state = {
count: 0
}



observe(state)

autorun(() => {
// 在state.count就触发了getter操作, 继而触发了依赖收集
a = state.count // 0
})

// 对state.count = 1触发了setter操作, 继而触发了依赖更新, 真他妈天才
state.count = 1

a // 1

2019.08.16
很震撼啊,这东西写得好精巧啊.

重新理一下,如果你要做到响应式.那么你应该有什么?

  1. 你要有监测机制, 因为如果你不知道什么时候改变,那么你就不会知道啥时候响应
  2. 你要有依赖收集, 因为你不能预知依赖到底有多少, 那么你就得管理依赖项
  3. 你要有响应机制, 你检测到了更新, 继而触发依赖收集, 下一步就是在数据更新的时候, 根据收集到的依赖, 去触发响应, 更新依赖项

以上这三点, 的实现分别为:

  • 检测机制用 get/set方法进行检测, 作为依赖收集, 触发响应的事件分发点
  • 依赖收集和响应机制我们使用dep这个类来完成, 供检测机制调用
  • 使用autorun包裹存在依赖的操作, 并生成引用, 供dep依赖收集
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
// 1. get/set批量依赖检测
const scan = (obj) => {
for(let i in obj) {
let prop = obj[i]

Object.defineProperty(obj, i, {
get() {
console.log('get')
return prop
},
set(value) {
console.log('set')
prop = value
}
})
}
}

// 2. 依赖项
class DepObj {
constructor() {
this.taskList = new Set()
}

getDep() {
if (activeUpdate) {
// 为啥这个要使用外部变量不用传参的方式传进来呢?
// 依赖收集的时候,在get方法内部, 在内部我们怎么访问到
this.taskList.add(activeUpdate)
}
}

notify() {
this.taskList.forEach(item => item())
}
}

let activeUpdate = null

function autorun (update) {
const wrappedUpdate = () => {
activeUpdate = wrappedUpdate // 这个就是一个依赖, 注意,这个其实是是外层函数, 在dep类里, 我们会把它存进taskList, 供通知的时候使用
update() // update里面要执行一个依赖收集
activeUpdate = null
}
wrappedUpdate()
}

// 3. 结合数据变化检测 + 依赖项收集
const scan = (obj) => {
for(let i in obj) {
let prop = obj[i]
let dep = new DepObj()
Object.defineProperty(obj, i, {
get() {
console.log('get')
dep.getDep()
return prop
},
set(value) {
console.log('set')
let change = value === prop
prop = value

if(change) {
dep.notify()
}
}
})
}
return obj
}