Vue响应式原理由以下三个部分组成:
手写Vue响应式原理可以分为以下几个步骤:
总的来说,手写Vue响应式原理主要由Observer、Dep、Watcher、Compile、Vue这几个组成部分构成。其中Observer用于拦截数据变化,Dep用于管理Watcher对象,Watcher用于建立视图与数据之间的联系,Compile用于解析模板指令,Vue将这些类进行整合,实现了Vue的响应式更新机制。
我们使用递归来遍历数据对象中的所有属性,对每个属性使用Object.defineProperty()方法进行定义。在defineReactive()方法中,我们还创建了一个Dep类,Dep类用于管理所有订阅者(Watcher)和通知它们更新。
class Dep {
constructor() {
this.subs = []; // 存储依赖的数组
}
// 添加依赖
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 通知依赖更新
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
Dep.target = null; // 静态属性 target,用于保存当前的 Watcher 对象
上面的代码定义了一个名为Dep
的类,它有以下几个方法:
constructor()
:构造函数,初始化订阅者数组subs
为空数组。addSub(sub)
:添加订阅者的方法,将传入的订阅者对象sub
添加到subs
数组中。notify()
:通知所有订阅者更新的方法,遍历subs
数组,对每个订阅者调用其update()
方法。target
:定义一个全局变量target
,用于存储当前的订阅者对象。在Vue中,每个响应式数据(如data中的属性)都会对应一个Dep
对象。当这个属性被读取时,会将当前的订阅者对象存储到Dep.target
中,然后在属性的getter方法中,将Dep.target
添加到当前属性的Dep
对象的订阅者数组中;当属性的值被修改时,会调用该属性的Dep
对象的notify()
方法,通知所有订阅者更新。
接下来,我们需要创建一个Watcher类,它的主要作用是在数据发生变化时,触发视图的更新操作。在Watcher类中,我们首先需要保存更新视图所需的回调函数,并将Watcher实例添加到数据的订阅列表中。在数据发生变化时,我们遍历订阅列表,并依次调用回调函数来更新视图。
// 创建一个Watcher类,用于管理依赖与视图的更新
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 将当前Watcher实例指定为Dep.target
Dep.target = this;
// 获取数据的值,触发数据的get方法,从而将当前Watcher实例添加到Dep中
this.oldValue = vm[key];
Dep.target = null;
}
// 更新视图
update() {
const newValue = this.vm[this.key];
if (this.oldValue === newValue) {
return;
}
this.cb(newValue);
this.oldValue = newValue;
}
}
Observer 类:该类用于对数据进行监听和响应式处理,主要实现了 walk 和 defineReactive 两个方法。walk 方法遍历对象中所有属性,对每个属性调用 defineReactive 方法进行响应式处理;defineReactive 方法利用 Object.defineProperty 给每个属性添加 getter 和 setter,当属性被访问或修改时,会触发相应的依赖更新。
class Observer {
constructor(data) {
this.walk(data);
}
// 对数据对象进行递归遍历,为每个属性添加getter和setter
walk(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(obj, key, val) {
const dep = new Dep(); // 创建一个依赖收集器
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
get() {
// 添加依赖
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
// 触发依赖更新
dep.notify();
}
});
}
}
Compile类的代码。在Compile类中,我们首先需要遍历模板中的节点,并根据节点的类型来处理它们。对于普通节点,我们将对它们的文本内容进行处理,对于包含指令的节点,我们将创建Watcher实例,并将它们添加到订阅列表中。
class Compile {
constructor(el, vm) {
this.el = document.querySelector(el); // 获取根节点
this.vm = vm; // 保存 Vue 实例
this.compile(this.el); // 编译模板
}
compile(el) {
const childNodes = el.childNodes; // 获取根节点的子节点列表
Array.from(childNodes).forEach(node => {
if (node.nodeType === 1) { // 元素节点
this.compileElement(node);
} else if (this.isInterpolation(node)) { // 文本节点且包含插值语法
this.compileText(node);
}
// 递归编译子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
compileElement(node) {
const attrs = node.attributes; // 获取元素节点的属性列表
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const exp = attr.value;
if (attrName.startsWith("v-")) { // 匹配指令
const dir = attrName.substring(2); // 获取指令名称
this[dir] && this[dir](node, exp); // 调用对应的指令函数
}
});
}
compileText(node) {
const exp = node.textContent; // 获取插值语法中的表达式
node.textContent = this.getVMValue(exp); // 将插值语法替换为表达式的值
}
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent); // 文本节点且包含插值语法
}
getVMValue(exp) {
let value = this.vm;
exp.split(".").forEach(key => {
value = value[key];
});
return value;
}
// v-model 指令
model(node, exp) {
this.bind(node, exp, "model");
node.addEventListener("input", e => {
const newValue = e.target.value;
this.setVMValue(exp, newValue);
});
}
// v-bind 指令
bind(node, exp, dir) {
const updaterFn = this[dir + "Updater"];
updaterFn && updaterFn(node, this.getVMValue(exp));
new Watcher(this.vm, exp, value => {
updaterFn && updaterFn(node, value);
});
}
// model 指令更新视图
modelUpdater(node, value) {
node.value = value;
}
// v-text 指令
text(node, exp) {
this.bind(node, exp, "text");
}
// text 指令更新视图
textUpdater(node, value) {
node.textContent = value;
}
setVMValue(exp, value) {
let vm = this.vm;
const keys = exp.split(".");
keys.forEach((key, index) => {
if (index < keys.length - 1) {
vm = vm[key];
} else {
vm[key] = value;
}
});
}
}
Compile 类的实例化需要传入两个参数:el 和 vm。其中,el 是根节点的选择器,vm 是 Vue 实例。
Compile 类主要实现了以下功能:
创建一个Vue类,将Observer、Watcher和Compile类组合在一起,以创建一个完整的Vue实例。
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
// 将 Vue 实例的属性代理到 $data 对象上
this._proxyData(this.$data);
// 创建Observer实例,监听数据变化
new Observer(this.$data);
// 创建Compile实例,解析模板指令
new Compile(this.$el, this);
}
//使用_proxyData()方法来将数据代理到Vue实例中,就可以在Vue实例中通过this.key的方式来访问数据
_proxyData(data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newValue) {
if (newValue === data[key]) {
return;
}
data[key] = newValue;
},
});
});
}
}
到此为止,我们已经完成了手写代码来模拟Vue2.0的响应式数据实现的过程。我们可以通过这个过程来深入理解Vue2.0的响应式数据原理,从而更好地应用Vue2.0开发应用程序。
后续会继续更新vue2.0其他源码系列,包括目前在学习vue3.0源码也会后续更新出来,喜欢的点点关注。