# 7.响应式原理
# 要点:
- 依赖:我们把用了某个数据 a 的组件,称作 a 的依赖。即 watcher,谁用到了数据,谁就是依赖
- 每个数据,都有一个
Dep
依赖管理器,来管理哪些组件用了它。(即收集依赖 wathcer) - getter 中收集组件(依赖),setter 中更新组件(依赖)
# 原理
- 视图(view)变化更新数据(data)。这个很简单,通过事件监听就可以实现
- 数据(data)变化更新视图(view)。通过数据劫持(即Object.defineProperty( )方法)结合发布者-订阅者模式
总流程,三步走:
- Observer
- Observe 一个对象 obj
- 判断是否ob已经被响应化了,判断是否为数组,对数组七种方法进行特殊处理
- 给对象 obj 新增一个 dep,依赖数组
- 调用 defineReactive 递归监听所有属性 Object.defineProperty()
- 在子数据的 getter 里面将 Dep.target(即 watcher,订阅者,可能为一个 Vue 组件),dep.addSub 添加到 dep 里面
- 在子数据的 setter 里面,添加 dep.notify 方法,通知 watcher(即 Vue 组件)要更新了
- dep.notify:遍历 dep 实例中的 subs(即订阅者列表,watcher),subs[i].update()
- Watcher
this.callback.call(this.vm, value, oldVal);
触发 watcher 实例化时传入的回调(一般是更新 DOM 的函数)- 如:
new Watcher(this.vm, exp, function(value) {
self.updateText(node, value);
});
2
3
- Complie
- 解析 Dom 树,找到 v-modal, {{}}, ::data = ,这种属性并且初始化数据。
- 使用 DocumentFragement,对 DOM 节点进行文档拆分
- 同时在里面 new Watcher 进行订阅数据变化
# 实现过程
实现一个监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
实现一个解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
Ps:因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者
# 1. 实现一个 Observer(Object.defineProperty( ))
Observer = (Object.defineProperty() 监听 + 递归 ) + 订阅器
Observer 是一个数据监听器,其实现核心方法就是前文所说的 Object.defineProperty( )。 如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行 Object.defineProperty( )处理。
- 思路分析中,需要创建一个可以容纳订阅者的消息订阅器 Dep,订阅器 Dep 主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数。
- 所以显然订阅器需要有一个容器,这个容器就是list,将上面的 Observer 稍微改造下,植入消息订阅器: Observer 一旦有了 set 触发,就会通知到 Dep
/**
* Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
*/
export class Observer {
constructor(value) {
this.value = value;
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了
// 作用:
// 1. 避免重复操作
// 2. 重写数组push、unshift、splice方法的时候,可以通过this.__ob__获取到当前的实例,给新push的数据设置响应式
def(value, "__ob__", this);
// value.__ob__ = this
if (Array.isArray(value)) {
// 当value为数组时的逻辑
// 重写数组的七种方法
} else {
this.walk(value);
}
}
walk(obj: Object) {
var self = this;
Object.keys(obj).forEach(function (key) {
self.defineReactive(obj, key, obj[key]);
});
}
}
function defineReactive(data, key, val) {
// 这里用到闭包,将val闭包到函数里面,与newVal进行比较
// 递归遍历所有子属性
if (typeof val === "object") {
new Observer(val);
}
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
//我们将订阅器Dep添加一个订阅者设计在getter里面,
//这是为了让Watcher初始化进行触发,
//因此需要判断是否要添加订阅者。
// Dep.target 就是 一个watcher,也是一个回调函数
if (Dep.target) {
// 判断是否需要添加订阅者
dep.addSub(Dep.target); // 在这里添加一个订阅者
}
return val;
},
set: function (newVal) {
if (val === newVal) {
return;
}
val = newVal;
console.log(
"属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”"
);
dep.notify(); // 如果数据变化,通知所有订阅者
},
});
}
Dep.target = null;
var library = {
book1: {
name: "",
},
book2: "",
};
function obeserve(value) {
if (typeof value != "object") return;
let ob;
if (typeof value.__ob__ !== "undefined") {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
// 关键是new Observer(library);
}
observe(livrary);
library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍”
//下面是订阅器
export default class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
// 删除一个依赖
removeSub(sub) {
remove(this.subs, sub);
}
// 通知所有依赖更新
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
/**
* Remove an item from an array
*/
export function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
在 setter 函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。到此为止,一个比较完整 Observer 已经实现了,接下来我们开始设计 Watcher。
# 2. 实现 Watcher
我们说:谁用到了数据,谁就是依赖。这个“谁”,就是 watcher,他可以是一个 dom 节点中的 text 文本。 Vue1 中, 用到数据的 Dom 都是依赖 Vue2 中, 用到数据的组件都是依赖 getter 中收集依赖, setter 中触发依赖
- 订阅者 Watcher 在初始化的时候需要将自己添加进订阅器 Dep 中
- 而监听器 Observer 是在 get 函数执行了添加订阅者 Wather 的操作的,所以我们只要在订阅者 Watcher 初始化的时候触发对应的 get 函数去执行添加订阅者操作即可
- 这里还有一个细节点需要处理,我们只要在订阅者 Watcher 初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在 Dep.target 上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者 Watcher 的实现如下:
// 如
// new Watcher(this.vm, exp, function(value) {
// self.updateText(node, value);
// });
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function () {
this.run();
},
run: function () {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function () {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
},
};
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
这时候,我们需要对监听器 Observer 也做个稍微调整,主要是对应 Watcher 类原型上的 get 函数。需要调整地方在于 defineReactive 函数:
到此为止,简单版的 Watcher 设计完毕,这时候我们只要将 Observer 和 Watcher 关联起来,就可以实现一个简单的双向绑定数据了。
# 3. 实现 Compile(事件监听)
虽然上面已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析 dom 节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器 Compile 来做解析和绑定工作。解析器 Compile 实现步骤:
- 解析模板指令,并替换模板数据,初始化视图
- 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器
- 为了解析模板,首先需要获取到 dom 元素,然后对含有 dom 元素上含有指令的节点进行处理
function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
}
Compile.prototype = {
init: function () {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
},
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
},
compileElement: function (el) {
var childNodes = el.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function (node) {
var reg = /\{\{(.*)\}\}/;
var text = node.textContent;
if (self.isElementNode(node)) {
self.compile(node);
} else if (self.isTextNode(node) && reg.test(text)) {
self.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
});
},
compileText: function (node, exp) {
var self = this;
var initText = this.vm[exp];
this.updateText(node, initText);
new Watcher(this.vm, exp, function (value) {
self.updateText(node, value);
});
},
updateText: function (node, value) {
node.textContent = typeof value == "undefined" ? "" : value;
},
};
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
大概总结到这里,详情还请看:vue 的双向绑定原理及实现 (opens new window)
# Object.defineProperty 缺点
- 无法监听数组变化
// 解决方案:AOP思想
// 新建一个空数组,在该空数组上面创建七种方法,然后讲要监听的数组的原型指向该空数组
const aryMethods = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
const arrayAugmentations = [];
aryMethods.forEach((method) => {
// 这里是原生Array的原型方法
let original = Array.prototype[method];
// 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
// 注意:是属性而非原型属性
arrayAugmentations[method] = function () {
console.log("我被改变啦!");
// 调用对应的原生方法并返回结果
return original.apply(this, arguments);
};
});
let list = ["a", "b", "c"];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 别忘了这个空数组的属性上定义了我们封装好的push等方法
// Object.setPrototypeOf(list, arrayAugmentations)
list.__proto__ = arrayAugmentations;
list.push("d"); // 我被改变啦! 4
// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ["a", "b", "c"];
list2.push("d"); // 4
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
- 只能劫持对象属性,无法深层监听
- 无法对对象新增的 key/value 进行响应式处理
- 如果一个数据有 1000 个属性,那么就要给这 1000 个属性使用 Object.defineProperty,这样在初始化页面的时候会造成卡顿。如果用代理的话,那么只需要执行一次就可以了
# dep 和 watcher 为什么要互相收集
因为他们是多对多的关系
- dep 是一个数组,在 defineproperty 中监听数据变化,使用 notify 方法通知 watcher 去更新
- watcher 收集 dep
- 为了 computed 这个属性
- 假设:计算属性在 template 上面,但是依赖的 data 不在 template 上,那么 vnode 不会访问到所依赖的 data,依赖这些 data 的 dep 则无法更新 watcher
- 所以在 computed 属性执行的时候,会让 person 所依赖的 firstName 和 lastName 的 dep 去收集当前的 watcher
- 用于解除 watcher 的订阅
- 为了 computed 这个属性
← 6.组件插槽 8.Vue2 中的缺陷 →