Vue2响应式原理-高级
前言
思考两个问题,在Vue2中的data的数据,如何在视图中展示?当data变化时,如何使视图同步更新展示?这里涉及两个点:数据和视图。
一、思考
要完成上面的功能,需要解决两个问题:
- 监听
data数据的变化。- 此时数据的变化无非就是
获取对象属性和对对象属性赋值。 - 可以使用
Object.defineProperty做数据劫持,监听对象属性的改变。
- 此时数据的变化无非就是
- 当数据改变时,通知
视图刷新。- 使用一种监听机制,那就是
观察者模式。
- 使用一种监听机制,那就是
一句话总结:Object.defineProperty数据劫持测试数据变化+观察者模式进行依赖收集和视图更新。
二、过程
1.任务拆解
- 给
Vue类传入两个内容,一个是要展示的数据,一个是数据要挂载的视图。 - 数据处理:
- 拿到数据,遍历对象的每个属性,使用
Object.defineProperty将它们变成getter/setter模式,意思是可以劫持到属性被获取和赋值。 - 在数据的
getter过程,可以将那些获取数据的主体(依赖)保存下来。 - 在数据的
setter过程,可以通知那些依赖去同步更新视图。 - 意味着数据中每个属性都对应一个
Dep(发布者),才能细粒度的保存到每一次getter操作(观察者)。
- 拿到数据,遍历对象的每个属性,使用
- 视图处理:
- 使用编译器(
compiler)编译模板(template模块),解析指令(v-text/v-model)/插值表达式({{}})。 - 在编译过程
订阅数据的变化(watcher),同时绑定更新函数(update)。 - 展示视图。
- 使用编译器(

一句话总结:首先做到在页面上展示变量,然后知道当数据改变时怎么更新展示,最后根据展示类型做不同的展示方案。所以需要知道哪个地方使用了变量,怎么通知更新。
2.实现
- 接收初始化参数。
- 把
data中的属性注入到Vue中,并且转成getter/setter形式,方便通过vm[key]的方式访问。 - 调用
Observer监听data中所有属性的变化(变成响应式数据)。 - 调用
Dep收集每个属性的依赖。 - 调用
Watcher订阅通知。 - 调用
compiler解析指令(v-text/v-model)/差值表达式({{}})。
2.1 使用方式
明确输入输出和展示效果。将数据项和视图传给Vue类。
<div id="app">
<h1>表达式</h1>
<h1>{{message}}</h1>
<h1>{{age}}</h1>
v-text:message <h1 v-text="message"></h1>
v-model:message <input type="text" v-model="message" />
v-model:count <input type="text" v-model="count" />
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
message: 'why',
age: 18,
count: 'count11',
person: {
name: 7
}
}
})
// 给属性重新赋值为对象,也是响应式的
vm.message = 'hhh'
// 给Vue实例新增属性,不是响应式的(Object.defineProperty的弊端)
// 解决方案:
// 1. Vue.set(vm.someObject, 'b', 2)
// 2. this.$set(this.someObject,'b',2)
vm.testPro = 'ttt'
</script>
2.2 创建Vue类
- 接收初始化选项,选项中包括
数据项和视图内容。 - 使用
_proxyData函数将属性注入到Vue实例中,方便之后能直接通过vm[key]获取到值。因为现在数据都被包裹到data中,取值不方便。
// 0.创建Vue构造函数
class Vue {
constructor(options) {
// 1.接收传递过来的选项,并保存
this.$options = options || {};
// 获取选项参数中的data
this.$data = options.data || {};
// el传过来的可能是字符串'#app',也可能是一整个节点数据
this.$el =
typeof options.el === "string"
? document.querySelector(options.el)
: options.el;
// 2.把data转换成getter/setter,并注入到Vue实例中
this._proxyData(this.$data);
// 3.调用Observer对象,监听数据的变化(把$data数据变成响应式)
new Observer(this.$data);
// 4.调用compiler解析指令/差值表达式
new Compiler(this);
}
_proxyData(data) {
Object.keys(data).forEach((key) => {
// 第一个参数为this,将属性代理到Vue实例上
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newValue) {
if (data[key] === newValue) {
return;
}
data[key] = newValue;
},
});
});
}
}
2.3 创建Observer类
使用Object.defineProperty劫持所有data的属性,监听它们的变化(变成响应式数据)。属性改变时通知订阅者。
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
if (!data || typeof data !== "object") {
return;
}
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
// 把数据变成响应式
defineReactive(obj, key, value) {
let that = this;
// 在使用前先创建Dep类,每个属性对应一个Dep对象
let dep = new Dep();
// 将对象中的属性也变成响应式
this.walk(value);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 添加一个观察者watcher
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue === value) {
return;
}
value = newValue;
// 新增加的数据也应该是响应式的
that.walk(newValue);
// 发送通知,更新视图
dep.notify();
},
});
}
}
2.4 创建Compiler类
编译模板,把变量变成数据,绑定更新函数。添加订阅者,收到通知就执行更新函数。
class Compiler {
constructor(vm) {
// 保存视图
this.el = vm.$el;
// 保存Vue实例
this.vm = vm;
this.compile(this.el);
}
// 编译模板
compile(el) {
// 获取el的所有子节点
let childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
if (this.isTextNode(node)) {
// 处理文本节点
this.compileText(node);
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node);
}
// 判断node是否还有子节点
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 编译文本节点,处理差值表达式
compileText(node) {
// 匹配{{variable}}
let reg = new RegExp(/\{\{(.+)\}\}/);
let value = node.textContent;
if (value.match(reg)) {
let key = reg.exec(value)[1];
// 使用Vue实例上的值替换掉节点的内容
node.textContent = value.replace(reg, this.vm[key]);
// 订阅数据的变化
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
}
// 编译元素节点,处理指令
compileElement(node) {
Array.from(node.attributes).forEach((attr) => {
// 获取属性的名字
let attrName = attr.name;
// 判断是否是指令
if (this.isDirective(attrName)) {
// 有可能是, v-text:text,v-model:model
attrName = attrName.substr("2");
// 获取指令中的值,v-text:msg,v-model:count
let key = attr.value;
this.update(node, key, attrName);
}
});
}
// 更新展示
update(node, key, attrName) {
let updateFn = this[`${attrName}Updater`];
// 需要改变this的指向
updateFn && updateFn.call(this, node, this.vm[key], key);
}
// 处理v-text指令
textUpdater(node, value, key) {
node.textContent = value;
// 订阅数据的变化
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
// 处理v-model指令
modelUpdater(node, value, key) {
node.value = value;
// 订阅数据的变化
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
// 数据双向绑定
node.addEventListener("input", () => {
this.vm[key] = node.value;
});
}
// 判断元素的属性是否为指令
isDirective(attrName) {
return attrName.startsWith("v-");
}
// 判断节点是否为文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 判断节点是否为元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
2.5 创建Dep类
收集依赖。
// 发布者,作用是收集依赖(watcher),在getter中收集依赖,在setter中通知依赖
class Dep {
constructor() {
this.sbus = [];
}
// 保存订阅者
addSub(sub) {
this.sbus.push(sub);
}
// 通知订阅者
notify() {
this.sbus.forEach((sub) => {
// 执行更新
sub.update();
});
}
}
2.6 创建Watcher类
订阅Observer属性变化的消息,触发Compiler更新函数。
// 订阅者,作用是数据变化后触发依赖更新视图
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// data中属性名称
this.key = key;
// 回调函数,负责更新视图
this.cb = cb;
Dep.target = this;
// 获取更新前的旧值
// 这里的vm[key]被Observer中的setter捕获,所以下一步需要对Dep.target初始化
this.oldValue = vm[key];
// 初始化
Dep.target = null;
}
// 更新操作
update() {
let newValue = this.vm[this.key];
if (newValue === this.oldValue) {
return;
}
this.cb(newValue);
}
}
2.7 双向数据绑定
// 在Compiler类中的modelUpdater函数实现数据双向绑定
node.addEventListener("input", () => {
this.vm[key] = node.value;
});
三、总结
- 将要展示的数据和要挂载的节点传给
Vue类,在Vue内部通过_proxyData函数将数据转换成getter/setter形式,同时把数据注入到Vue实例中,可以通过实例访问。 - 使用
Observer类把数据变成响应式的,具体使用Object.defineProperty劫持。对象中属性也要变成响应式的,新增数据也变成响应式的。其中使用Dep类在数据getter中收集依赖,在setter中通知依赖更新视图。 - 使用
Compiler类编译模板并展示视图。递归遍历所有节点包括子节点,判断节点类型,当前案例需要处理的是文本节点({{}})和元素节点(元素节点上会有v-text、v-model指令)。- 如果是文本节点,需要处理插值表达式的情况(
{{}}),所以需要使用正则判断是否是差值表达式。如果是表达式,则需要在Vue实例中拿到变量的值,并替换掉这个节点的内容(node.textContent)为变量的值。此时还需要订阅数据变化,方便变量值改变时更新视图。 - 如果是元素节点,需要拿到节点上的属性,遍历判断是不是指令(简单判断),是指令则执行对应的更新函数。
v-text指令则替换整个节点的内容(node.textContent),同时订阅数据变化,方便变量值改变时更新视图。v-model指令则改变节点的值(node.value),同时订阅数据变化,方便变量值改变时更新视图。
- 如果是文本节点,需要处理插值表达式的情况(
- 使用
Dep类充当发布者,在数据getter中收集依赖,在setter中通知依赖更新视图。 - 使用
Watcher类充当订阅者/观察者,在数据变更时触发更新视图。 - 在可以使用
v-model的节点上,监听input操作,回调为更新Vue实例的值为节点的值,完成数据双向绑定。此时,数据赋值触发setter,则Dep类会发送通知让所有依赖更新视图。
总的来看就是把要做的事情拆开来,一步步完成。就像如何把一头大象放进冰箱,打开冰箱,把大象塞进去,关上冰箱。
附录
转载自:https://juejin.cn/post/7241875961130319927