Vue响应式原理及手动实现一个简易的MVVM
本文于950天之前发表,文中内容可能已经过时。
前置知识点:
MVVM、MVC思想:
- 了解web前端发展历史有助于理解MVVM、MVC思想,历史发展总是朝着不断优化代码组织结构、易维护、封装复用的方向。揭开面纱,一切还是基于浏览器提供的API进行DOM操作。事实证明为项目开发效率的提升带来了重大意义。
推荐阅读:使用MVVM框架,只要改变JavaScript对象的状态,就会导致DOM结构作出对应的变化!这让我们的关注点从如何操作DOM变成了如何更新JavaScript对象的状态,而操作JavaScript对象比DOM简单多了!
- 什么是MVVM,MVC和MVVM的区别,MVVM框架VUE实现原理
- mvc和mvvm的区别
JS设计模式(观察者模式、发布-订阅模式):
- 观察者模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Observerable {
this.eventObj = {};
on(evName, fn) {
this.eventObj[evName] = this.eventObj[evName] ? [...this.eventObj[evName], fn] : [fn];
}
emit(evName, ...args) {
this.eventObj[evName].forEach((fn) => {
fn.apply(null, args);
});
}
}
// 实例化一个可观察对象
const event = new Observerable();
event.on('error', () => {
console.log('error1');
})
event.on('error', () => {
console.log('error2');
})
event.emit('error');
代码参考自: Node.js && JavaScript 面试常用的设计模式二
- 发布-订阅模式
观察者模式与发布-订阅模式核心思想非常类似,观察者模式维护一个
Observerable
对象,发布-订阅模式集合所有Publisher
和Subscriber
对象形成EventHub
信息中心,由信息中心负责通知订阅者,做到代码组织结构优化。
推荐阅读: 观察者模式 vs 发布-订阅模式
JS原生API:
- Object.defineProperty()
- js属性描述: getters与setters
- Document.createDocumentFragment()
- Node.childNodes|nodeType|textContent|innerHtml|firstChild|firstElementChild
AngularJs响应式原理:
通过
脏值检测
的方式比对数据是否有变更,来决定是否更新视图。最简单的方式就是通过setInterval()
定时轮询检测数据变动,当然Google不会这么low。Angular只有在指定的事件触发时进入脏值检测,大致如下:DOM事件(譬如用户输入文本、点击按钮ng-click
)、XHR响应事件$http
、浏览器Location变更事件$location
、Timer事件($timeout
、$interval
)、执行$digest()
或$apply()
。在Angular中组件是以树的形式组织起来的,相应地,检测器也是一棵树的形状。当一个异步事件发生时,脏检查会从根组件开始,自上而下对树上的所有子组件进行检查,这种检查方式的性能存在很大问题。
Vue响应式原理:
通过
Object.defineProperty()
API监听对象的变化,实现操作js对象自动更新DOM(调用预先绑定的回调函数)。由于Object.defineProperty
是ES5中一个无法shim的特性,这也就是Vue不支持IE8以及更低版本浏览器的原因,而且不能监听到数组的变化(Vue3.0开始使用ES6提供的Proxy
)。
推荐阅读:
手动实现一个简易的MVVM:
Vue响应式原理简单明了,难点在于Vue内部做了很多事情来实现双向绑定、性能优化等,通过算法模型减少DOM树更新和最大范围的重用。
实现Vue构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// Vue 构造函数
function Vue(options = {}) {
this.$options = options;
this._data = this.$options.data;
// 实例化一个发布者(可观察对象)
new Publisher(this._data);
// 实现vm实例对data对象中的属性直接读取和赋值
for (let key in this._data) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: false,
get() {
return this._data[key];
},
set(newValue) {
this._data[key] = newValue;
}
});
}
new Compile(this.$options.el, this);
}实现Compile简单编译DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// Compile 简单编译DOM
function Compile(el, vm) {
vm.$el = document.querySelector(el);
const fragment = document.createDocumentFragment();
let child = null;
while(child = vm.$el.firstChild) {
fragment.appendChild(child); // vm.$el的子元素被转移到fragment
}
function replace(fragment) {
const pattern = /\{\{(.*)\}\}/;
Array.from(fragment.childNodes).forEach((node) => {
let text = node.textContent;
if (node.nodeType === 3 && pattern.test(text)) {
const key = RegExp.$1.trim();
// 为每一个{{ xxx }}节点实例化一个订阅者,订阅data对象中的属性值变化
// 利用闭包,将node节点作为私有变量在内存中保存起来
new Subscriber(vm, key, (newValue) => {
node.textContent = text.replace(pattern, newValue);
});
// vm[key]对data对象取值触发get函数,此时EventHub静态属性target指向Subscriber实例,并被添加到events中
node.textContent = text.replace(pattern, vm[key])
}
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment);
vm.$el.appendChild(fragment);
}发布-订阅模式实现响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51// 发布-订阅设计模式
// EventHub信息中心
function EventHub() {
this.events = [];
}
EventHub.prototype.on = function(evName) {
this.events.push(evName);
}
EventHub.prototype.notify = function() {
this.events.forEach((event) => {
event.update();
});
}
// Publisher发布
function Publisher(data) {
const eventHub = new EventHub();
for (let key in data) {
let value = data[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
EventHub.target && eventHub.on(EventHub.target);
EventHub.target = null;
return value;
},
set(newValue) {
if (newValue === value) {
return;
}
value = newValue;
eventHub.notify();
}
});
}
}
// Subscriber订阅
function Subscriber(vm, key, fn) {
this.vm = vm;
this.key = key;
this.fn = fn;
EventHub.target = this;
}
Subscriber.prototype.update = function () {
this.fn(this.vm[this.key]);
}在发布-订阅模式中
Publisher
相当于Observerable
可观察对象,传入Vue构造函数options中的data对象通过定义getters
与setters
变成可观察对象,任何取值赋值操作都可以发布信息到EventHub信息中心
,由EventHub
负责通知订阅者。
完整代码: github/simple-mvvm
参考文章:
赞赏是不耍流氓的鼓励