总结响应式.md

最近在重新梳理知识点,Vue写了那么久, 是时候给自己一个交代了.也不能每天画好玩的UI对吧.

从题目开始, 这篇文章的前置知识点有

Object.defineProperty

关于第一个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
var obj = {
a: 1,
b: 2
}
Object.defineProperty(obj, 'a', {
// 枚举----你使用for-in/Object.key()的时候会影响到
enumerable: false,
// 可配置---是否可以修改属性的属性, 这就是这个对象里所有的内容
configurable: false,
// 可写---是否可以在这个对象上修改value,比如常规的赋值,删除操作
writable: false,
// 值---你用点运算符操作的时候,读取到的值
value: "static"
})

obj.a // 'static'
Object.keys(obj) // []
for(let i in obj) {console.log(i)} // undefined
obj.a = 1
obj.a // 'static'
Object.defineProperty(obj, 'a', {
// 枚举----你使用for-in/Object.key()的时候会影响到
enumerable: false,
// 可配置---是否可以修改属性的属性, 这就是这个对象里所有的内容
configurable: false,
// 可写---是否可以在这个对象上修改value,比如常规的赋值,删除操作
writable: false,
// 值---你用点运算符操作的时候,读取到的值
value: "1"
}) // trow Error
var unknow = obj.b
Object.defineProperty(obj, 'b', {
// 当然我们也可以重写它的默认的get/set行为
get() {
return unknow * 4
},
set(value) {
unknow = value
}
})
obj.b // 8
obj.b = 2 // 2

这个方法相关的我们就复习完了.

响应式

先说定义, 通俗一点说的响应式是指, 当数据a变化了, 与这个数据a相关的操作都会更新. 来看个非响应式的例子可能会好理解一点.(反向操作)

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

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

那么怎么做到这一点?

我们监测变量a, 如果a触发了get操作, 那么很可能,进行了依赖性的运算, 在这个例子里就是

1
let b = a * 5

在这里b的计算依赖于a.噢, 所以,在这个地方我们获取了a, 并进行了计算, 那我就拿个小本子把这个操作记下来.

如果, 变量a触发了set操作, 也就是赋值操作, 那么我们就要拿出小本子, 在把上面记下来的依赖重新执行一遍就好.

其实这就是响应式的原理.

  1. 收集依赖
  2. 侦测变化
  3. 触发更新

之前我们也提到了, defineProperty我们通过get方法知道什么时候收集依赖, 通过set方法知道什么时候发生了变化, 触发更新.

接下来就是实操了.

把对象所有的属性转换为get/set

defineProperty不像proxy,它只能单个的去监听对象上的属性,而proxy这个小玩具.就很有意思.

那么请听题: 假设你有一个对象(你在骗自己), 你希望有一个函数, 这个函数把这个对象上所有的属性转换为可监测get/set。如果触发了get需要在控制台输出

1
`get: ${key}${value}`

一定要自己试一下.以下是答案

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
const transform = (obj, i) => {
var unKnow = obj[i]
Object.defineProperty(obj, i, {
get() {
console.log(`getting"${i}": ${unKnow}`, unKnow)
return unKnow
},
set(value) {
console.log(`setting"${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)

Vue中的依赖跟踪

data

每个vue的实例都会有一个watcher对象,这个对象中提供增加依赖和触发依赖更新的方法。

在getter操作时把依赖传入Watcher, 而在单个属性发生改变,也就是set的时候触发依赖更新notify, 继而Watcher执行视图更新操作。

注意, 在这里用了设计模式中的发布-订阅模式

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

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

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

let activeUpdate = null
// 上面的class部分简单易懂, 不做赘述
// 这个函数一定要明白
function autorun (update) {
const wrappedUpdate = () => {
activeUpdate = wrappedUpdate
update()
// update里面触发数据监听, 会先触发get, 而我们在get里做依赖收集,此时activeUpdate存的是 整个函数体
// 通过这种方式完成了依赖收集, 并且把activeUpdate置空,为下次使用做准备
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
// 2. 依赖项
class Dep {
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. 结合数据变化检测 + 依赖项收集
function scan (obj) {
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
// 在进行响应式的同时初始化依赖项实例, 之后再对应的getter/setter方法中形成闭包, 把依赖状态持久化
const dep = new Dep()
Object.defineProperty(obj, key, {
get () {
// 每次进行get方法的时候, 都进行一次依赖收集
dep.getDep()
return internalValue
},
set (newVal) {
// 检测值是否改变, 如果没有改变, 那么不做处理(为了性能)
const changed = internalValue !== newVal
internalValue = newVal
// 如果发生了改变, 那么在依赖class触发依赖项的更新
if (changed) {
dep.notify()
}
}
})
})
return obj
}



var state = {
count: 0
}

scan(state)

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

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

a // 1

state.count = 2

a // 2

看懂的时候觉得真的, 尤雨溪太他妈帅了。这东西写得好精巧啊。

总结

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

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

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

  • 检测机制用 get/set方法进行检测, 作为依赖收集, 触发响应的事件分发点
  • 依赖收集和响应机制我们使用dep这个类来完成, 供检测机制调用
  • 使用autorun包裹存在依赖的操作, 并生成引用, 供dep类的getDep依赖收

完结, 撒花, 满地打滚求点赞.