因为 Vue 是数据双向绑定的框架,而整个框架的由三个部分组成:
- 数据层(Model):应用的数据及业务逻辑,为开发者编写的业务代码;
- 视图层(View):应用的展示效果,各类UI组件,由template 和 css 组成的代码;
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来;
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM。(详细的MVVM知识不在讲解范围内,只需要清楚双向绑定在此架构中作用和位置即可)
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它 是什么,便可以进一步了解数据绑定的原理。
理解ViewModel
它的主要职责就是:
- 数据变化后更新视图;
- 视图变化后更新数据;
那么,就可以得出它主要由两个部分组成:
- 监听器(Observer):观察数据,做到时刻清楚数据的任何变化,然后通知视图更新;
- 解析器(Compiler):观察UI,做到时刻清楚视图发生的一切交互,然后更新数据;
然后把二者组合起来,一个具有数据双向绑定的框架就诞生了。
双向绑定如何实现的?
实现监听器
确保它是一个独立的功能,它的任务就是监听数据的变化,并提供通知功能。
监听数据变化常见的方式有三种:
- 观察者模式(发布+订阅);
- 数据劫持;
- 脏检查;
第三种,是 AngularJS 最早所提供的监听数据变化方式,而 Vue 是采用前两者的组合,那么让我们来看看它们是怎么被运用的。
观察者模式
观察者模式是一种对象行为模式。它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
在观察者模式中,主导的是起通知作用的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅并接收通知。
举例代码如下:
/**
发布者
*/
function Subject() {
// 单个发布者的所有订阅者
this.observers = [];
// 添加一个订阅者
this.attach = function(callback) {
this.observers.push(callback);
};
// 通过所有的订阅者
this.notify = function(value) {
this.observers.forEach(callback => callback(value));
};
}
/**
* 订阅者
*/
function Observer(queue, key, callback) {
queue[key].attach(callback);
}
// ====
// 手动更新数据
function setData(data, key, value) {
data[key] = value;
// 通知此值的所有订阅者,数据发生了更新
messageQueue[key].notify(value);
}
// ====
// 消息队列
const messageQueue = {};
// 数据
const myData = { value: "" };
// 将每个数据属性添加可订阅的入口
for (let key in myData) {
messageQueue[key] = new Subject();
}
// 订阅 value 值的变化
Observer(messageQueue, "value", value => {
console.warn("value updated:", value);
});
// 更新数据
setData(myData, "value", "hello world.");
setData(myData, "value", 100);
setData(myData, "value", true);
消息队列(queue)
我们可以看到,单纯的订阅和发布功能,它们彼此是独立存在的,因此还需要一个消息队列来关联他们。
上面的例子中,消息队列是作为一个全局存储变量,而在框架中则是封装起来的,每个 new Vue() 都有独立的一个队列,在下面我们将具体演示。
数据劫持
其实,就数据监听来说,观察者就已经满足了需求。但是,为什么和Vue不一样呢?因为 Vue 进行了优化,添加了数据劫持。
2009 年发布的 ECMAScript 5 中新增了一个 Object.definePropotype 的特性(具体使用不在此文章讲解范围内,请自行了解 developer.mozilla.org/zh-CN/docs/… ),能够定义对象属性的 getter 和 setter ,这可就厉害了,要知道 JavaScript 中一切皆对象。
那么我们的 setData(myData, ‘value’, 100); 就可以替换成 myData.value = 100; 的编写方式。从语法和使用上都变的更简单。
以下是变动后的代码:
// 发布者
function Subject() {
this.observers = [];
this.attach = function(callback) {
this.observers.push(callback);
};
this.notify = function(value) {
this.observers.forEach(callback => callback(value));
};
}
// 订阅者
function Observer(queue, key, callback) {
queue[key].attach(callback);
}
// ====
// 数据拦截器
function Watcher(data, queue) {
for (let key in data) {
let value = data[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: () => value,
set: newValue => {
value = newValue;
// 通知此值的所有订阅者,数据发生了更新
queue[key].notify(value);
}
});
}
return data;
}
// ====
// 消息队列
const messageQueue = {};
// 数据
const myData = Watcher({ value: "" }, messageQueue);
// 将每个数据属性都添加到观察者的消息队列中
for (let key in myData) {
messageQueue[key] = new Subject();
}
// 订阅 value 值的变化
Observer(messageQueue, "value", value => {
console.warn("value updated:", value);
});
// 更新数据
myData.value = "hello world.";
myData.value = 100;
myData.value = true;
当然,ES2015 已经出来有点时间了,所以应尽用新的解决方案 Proxy(具体的使用请自行了解哦 developer.mozilla.org/zh-CN/docs/… ),讲到它主要是因为 Vue3 就采用了此方案。至于性能和具体的差异,之后有机会再单独分享,这里就不细说,让我们看看新的语法怎么写呢?
代码:
// 发布者
function Subject() {
this.observers = [];
this.attach = function(callback) {
this.observers.push(callback);
};
this.notify = function(value) {
this.observers.forEach(callback => callback(value));
};
}
// 订阅者
function Observer(queue, key, callback) {
queue[key].attach(callback);
}
// ====
// 数据拦截器 - 代理方式
function ProxyWatcher(data, queue) {
return new Proxy(data, {
get: (target, key) => target[key],
set(target, key, value) {
target[key] = value;
// 通知此值的所有订阅者,数据发生了更新
queue[key].notify(value);
}
});
}
// ====
// 消息队列
const messageQueue = {};
// 数据
const myData = ProxyWatcher({ value: "" }, messageQueue);
// 将每个数据属性都添加到观察者的消息队列中
for (let key in myData) {
messageQueue[key] = new Subject();
}
// 订阅 value 值的变化
Observer(messageQueue, "value", value => {
console.warn("value updated:", value);
});
// 更新数据
myData.value = "hello world.";
myData.value = 100;
myData.value = true;
以上,我们已经完成双向绑定中的数据层的功能,现在任何的数据变化,我们都可以及时的知道,并关联任何想要做的事情,比如更新视图。
接下来就是模板的操作了,也就是 Compile 模板解析功能。
模板解析(实现视图到数据的绑定)
对于DOM的操作,目前常见的方式有:
- 原生或者基于库的DOM操作;
- 将DOM转换为Virtual DOM,然后进行对比与更新;
- 使用原生的Web Component技术;
以上三种的差异与性能等,之后有机会再单独分享。在Vue中使用的是 Virtual DOM 的方式,因为它比直接操作 DOM 所消耗的性能要少很多,也不存在 Web Component 的兼容性。
这三中方式的本质都是更新 DOM 的展示效果,只是方式不同而已,为了更简单的说明双向绑定的原理,我们就采用第一种方式。虚拟DOM是有很多独立的第三方库,如果有兴趣同学可以去研究哦。
解析器与DOM操作
这里的主要任务是:
- 解析模板中所有的特定特性,例如:v-model、v-text、{{ }}语法等;
- 关联数据展示到DOM;
- 关联事件绑定到DOM;
- 因此,下面的功能只能需要满足:展示数据到模板上。
代码如下:
<div id="app">
<input v-model="value" />
<p v-text="value"></p>
</div>
// 模板解析
function Compile(el, data) {
// 关联自定义特性
if (el.attributes) {
[].forEach.call(el.attributes, attribute => {
if (attribute.name.includes('v-')) {
Update[attribute.name](el, data, attribute.value);
}
});
}
// 递归解析所有DOM
[].forEach.call(el.childNodes, child => Compile(child, data));
}
// 自定义特性对应的事件
const Update = {
"v-text"(el, data, key) {
// 初始化DOM内容
el.innerText = data[key];
},
"v-model"(input, data, key) {
// 初始化Input默认值
input.value = data[key];
// 监听控件的输入事件,并更新数据
input.addEventListener("keyup", e => {
data[key] = e.target.value;
});
}
};
// ====
// 数据
const myData = { value: "hello world." };
// 解析
Compile(document.querySelector("#app"), myData);
目前我们定义的 DOM 解析,并没有关联数据监听,让我们来完成它!
完整的双向绑定
代码如下:
// 发布者
function Subject() {
this.observers = [];
this.attach = function(callback) {
this.observers.push(callback);
};
this.notify = function(value) {
this.observers.forEach(callback => callback(value));
};
}
// 订阅者
function Observer(queue, key, callback) {
queue[key].attach(callback);
}
// ====
// 数据拦截器 - 代理方式
function ProxyWatcher(data, queue) {
return new Proxy(data, {
get: (target, key) => target[key],
set(target, key, value) {
target[key] = value;
// 通知此值的所有订阅者,数据发生了更新
queue[key].notify(value);
}
});
}
// ====
// 模板解析
function Compile(el, data) {
// 关联自定义特性
if (el.attributes) {
[].forEach.call(el.attributes, attribute => {
if (attribute.name.includes('v-')) {
Update[attribute.name](el, data, attribute.value);
}
});
}
// 递归解析所有DOM
[].forEach.call(el.childNodes, child => Compile(child, data));
}
// 自定义特性对应的事件
const Update = {
"v-text"(el, data, key) {
// 初始化DOM内容
el.innerText = data[key];
// 创建一个数据的订阅,数据变化后更新展示内容
Observer(messageQueue, key, value => {
el.innerText = value;
});
},
"v-model"(input, data, key) {
// 初始化Input默认值
input.value = data[key];
// 监听控件的输入事件,并更新数据
input.addEventListener("keyup", e => {
data[key] = e.target.value;
});
// 创建一个订阅
Observer(messageQueue, key, value => {
input.value = value;
});
}
};
// ====
// 消息队列
const messageQueue = {};
// 数据
const myData = ProxyWatcher({ value: "hello world." }, messageQueue);
// 将每个数据属性都添加到观察者的消息队列中
for (let key in myData) {
messageQueue[key] = new Subject();
}
// ====
// 解析+关联
Compile(document.querySelector("#app"), myData);
如此,一个非常简单的 MVVM 功能就完成了。当然,它仅仅是为了讲解原理非编写的,如果要做成向 Vue 这样的成熟框架,就需要将各个核心封装成模块,进行更扩展性的定义。
如果再简单的封装一下,看上去就是一个极简的Vue了。
代码如下:
// 观察者功能
// 发布者
function Subject() {
this.observers = [];
this.attach = function(callback) {
this.observers.push(callback);
};
this.notify = function(value) {
this.observers.forEach(callback => callback(value));
};
}
// 订阅者
function Observer(queue) {
this.queue = queue
this.add = function(key, callback) {
this.queue[key].attach(callback);
}
}
// ====
// 数据拦截器
// 监听数据更新 - 代理方式
function ProxyWatcher(data, queue) {
return new Proxy(data, {
get: (target, key) => target[key],
set(target, key, value) {
target[key] = value;
// 通知此值的所有订阅者,数据发生了更新
queue[key].notify(value);
}
});
}
// ====
// 模板解析
function Compile(el, vm) {
// 关联自定义特性
if (el.attributes) {
[].forEach.call(el.attributes, attribute => {
if (attribute.name.includes('v-')) {
Update[attribute.name](el, vm.data, attribute.value, vm);
}
});
}
// 递归解析所有DOM
[].forEach.call(el.childNodes, child => Compile(child, vm));
return el
}
// 自定义特性对应的事件
const Update = {
"v-text"(el, data, key, vm) {
// 初始化DOM内容
el.innerText = data[key];
// 创建一个数据的订阅,数据变化后更新展示内容
vm.observer.add(key, value => {
el.innerText = value;
});
},
"v-model"(input, data, key, vm) {
// 初始化Input默认值
input.value = data[key];
// 创建一个订阅
vm.observer.add(key, value => {
input.value = value;
});
// 监听控件的输入事件,并更新数据
input.addEventListener("keyup", e => {
data[key] = e.target.value;
});
}
};
// ====
// 封装
function Vue({ el, data }) {
// initProxy
this.messageQueue = {};
this.observer = new Observer(this.messageQueue)
this.data = ProxyWatcher(data, this.messageQueue);
// initState
for (let key in myData) {
this.messageQueue[key] = new Subject();
}
// initRender
// initEvents
this.el = Compile(el, this);
}
// ====
// 数据
const myData = { value: "hello world." };
// 实例
const vm = new Vue({
el: document.querySelector("#app"),
data: myData
});