前言
首先,相当一部分人没有把响应式原理和双向数据绑定区分开。响应式原理和双向数据绑定并不是完全相同的概念。响应式原理是Vue的一大特性,官网的描述数据模型仅仅是普通的JavaScript对象。而当你修改它们时,视图会进行更新。双向数据绑定是通过V-model指令来实现的。v-model指令可以用于在表单元素(如input、textarea等)和组件上创建双向数据绑定。
响应式原理
响应式简介
响应式原理是Vue的一大特性,官网的描述数据模型仅仅是普通的JavaScript对象。而当你修改它们时,视图会进行更新。这就是响应式。
MVVM模型
我们都知道MVVM是基于一下三个部分
- 数据层(Model):应用的数据及业务逻辑
- 视图层(View):应用的展示效果,各类UI组件
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来 我们需要知道这三个部分真正对应Vue中的哪些部分
- 数据层(Model):指的是data部分
- 视图层(View):指的是template部分
- 业务逻辑层(ViewModel):指的是VUE实例 更重要的是知道VM如何讲Model和View连接。
VUE响应式组成
![[Pasted image 20230401201214.png]]
Observer
我们常说的Object.defineproperty()就是在这里进行的。 Observer会递归遍历对象的所有属性,对每个属性使用Object.defineProperty()方法进行重写,给每个属性添加getter和setter方法,当数据发生变化时setter方法会通知Dep(依赖)对象进行更新。同时,在getter方法中会进行依赖收集,即将观察者对象添加到当前属性的Dep中,当数据发生变化时,Dep对象就可以通知所有观察者进行更新操作了
Observer实现
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep() // 依赖收集器
this.walk(value)
}
// 遍历对象的所有属性,将它们转化为getter/setter形式
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
// 将对象的属性转化为getter/setter形式
function 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()
}
})
}
// 依赖收集器
class Dep {
constructor() {
this.subs = []
}
// 添加一个观察者对象
addSub(sub) {
this.subs.push(sub)
}
// 移除一个观察者对象
removeSub(sub) {
remove(this.subs, sub)
}
// 将当前的Dep与观察者对象关联
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有观察者对象进行更新
notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 移除数组中指定的元素
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
// 将观察者对象与Dep关联起来
Dep.target = null
const targetStack = []
function pushTarget(target) {
targetStack.push(target)
Dep.target = target
}
function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Dep
Dep是发布订阅模式的实现,在set时将依赖收集到Dep的subs数组中,在get时更新依赖。不了解发布订阅模式的建议去看看有关JS手写发布订阅的文章。
Compile
compile函数也就是vue的编译过程,主要是将vue模板(template)转化为可执行的JavaScript代码。这个过程由Vue的编译器(compiler)完成。
编译器会将Vue模板解析成一颗抽象语法树(AST),然后通过AST进行静态分析,并且对一些静态节点进行标记,从而确定模板中需要进行动态数据绑定的部分。如{{value}}这样的插值文本就会在这时被解析标记。
接着,编译器会根据AST生成渲染函数,这个渲染函数是由render函数和静态节点数组组成的。在执行渲染函数时,Vue会根据渲染函数中的数据和静态节点数组生成虚拟DOM,并将虚拟DOM渲染成真实DOM。
最后,编译器会将生成的渲染函数和一些必要的工具函数封装在一个JavaScript模块中,并返回这个模块的引用。
总结来说,Vue的编译过程将模板转化为可执行的JavaScript代码,其中包括将模板解析成抽象语法树、进行静态分析、生成渲染函数和静态节点数组,并最终将它们封装在一个JavaScript模块中。这个过程主要是为了提高Vue的性能和开发效率,使得Vue能够更加高效地处理数据绑定和DOM渲染。
Compile实现
真正的Compile函数很复杂,这里只简单写个开头
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
Watcher
Watcher类是用来监听数据变化并执行相应回调函数的一个核心类。Watcher实例会将自己添加到响应式数据的依赖列表中,并在数据变化时执行更新操作。同时,Watcher实例还支持异步更新和其他一些配置选项,从而满足不同场景的需求。
watcher实现
class Watcher {
constructor(data, key, cb) {
this.data = data; // 监听的数据对象
this.key = key; // 监听的数据属性名
this.cb = cb; // 数据变化时执行的回调函数
// 在这里立即调用一次get方法,可以将Watcher实例添加到Dep类中
// 从而在数据发生变化时能够及时通知到Watcher实例
this.value = this.get();
}
// 获取监听的数据属性值,并将当前Watcher实例添加到Dep类中
get() {
Dep.target = this; // 设置Dep.target静态属性为当前Watcher实例
const value = this.data[this.key]; // 获取监听的数据属性值
Dep.target = null; // 获取完成后将Dep.target静态属性清空
return value;
}
// 数据变化时触发的更新操作
update() {
const oldValue = this.value;
const newValue = this.data[this.key];
if (oldValue !== newValue) {
this.value = newValue;
this.cb(newValue, oldValue); // 执行回调函数
}
}
}
// Dep类用于管理所有的Watcher实例
class Dep {
constructor() {
this.subs = [];
}
// 添加一个Watcher实例
addSub(sub) {
this.subs.push(sub);
}
// 通知所有的Watcher实例进行更新
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
// 在getter方法中添加依赖
function defineReactive(obj, key, value) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
dep.notify(); // 数据变化时通知所有的Watcher实例进行更新
}
}
});
}
// 用于初始化Watcher实例并触发依赖添加过程
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
});
}
// 示例代码
const data = { count: 0 };
observe(data);
new Watcher(data, 'count', (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`);
});
data.count = 1; // count changed from 0 to 1
data.count = 2; // count changed from 1 to 2
理解Observer、Dep、Watcher 这篇文章对Watcher的理解就很不错,Watcher是实现响应式的核心类,理解它的实现与作用很重要。 每个组件至少都有一个watcher它的回调用来通知执行render函数,watcher的第三个参数还可以用来执行用户自定义的操作如watch和computed中的代码。
数据双向绑定
双向数据绑定是通过V-model指令来实现的。v-model指令可以用于在表单元素(如input、textarea等)和组件上创建双向数据绑定。
实现原理
- 对于<input>元素,v-model会在其上添加一个value属性和一个input事件监听器,用于实现双向绑定。
- 对于<textarea>元素,v-model会在其上添加一个value属性和一个input事件监听器,用于实现双向绑定。
- 对于<select>元素,v-model会将其value绑定到选中的<option>元素的value上,并添加一个change事件监听器,用于实现双向绑定。
v-model实现
Vue.directive('model', {
bind: function (el, binding, vnode) {
var value = binding.value;
var vm = vnode.context;
// 根据标签类型分别绑定不同的事件
if (el.tagName === 'INPUT') {
el.addEventListener('input', function () {
vm[value] = el.value;
});
} else if (el.tagName === 'SELECT') {
el.addEventListener('change', function () {
vm[value] = el.value;
});
} else if (el.tagName === 'TEXTAREA') {
el.addEventListener('input', function () {
vm[value] = el.value;
});
}
// 在元素上存储指令相关信息,便于后续更新
el._vModel = {
value: value,
callback: function (value) {
if (el.tagName === 'INPUT') {
el.value = value;
} else if (el.tagName === 'SELECT') {
// 根据value值选中对应的选项
var options = el.options;
for (var i = 0, l = options.length; i < l; i++) {
var option = options[i];
if (option.value === value) {
option.selected = true;
break;
}
}
} else if (el.tagName === 'TEXTAREA') {
el.value = value;
}
}
};
},
update: function (el, binding) {
var value = binding.value;
el._vModel.callback(value);
},
unbind: function (el) {
// 移除事件监听和相关信息
var info = el._vModel;
if (el.tagName === 'INPUT') {
el.removeEventListener('input', info.callback);
} else if (el.tagName === 'SELECT') {
el.removeEventListener('change', info.callback);
} else if (el.tagName === 'TEXTAREA') {
el.removeEventListener('input', info.callback);
}
delete el._vModel;
}
});