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