自 Backbone 以后前端框架就如同雨后春笋般出现,咱们已经习惯了用各类框架进行开发,可是前端框架出现的意义是什么?咱们为何要选择前端框架进行开发呢?javascript
提早声明: 咱们没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现.html
最开始学习前端框架的时候(我第一个框架是 React)并不理解框架能带来什么,只是由于你们都在用框架,最实际的一个用途就是全部企业几乎都在用框架,不用框架就 out 了.前端
随着使用的深刻我逐渐理解到框架的好处:vue
上一节咱们只说了前端框架的好处,可是并无指出根本问题,直到我看到这篇文章(中文版)。java
简单来讲,前端框架的根本意义是解决了UI 与状态同步问题。react
在 Vue 中咱们若是要在todos
中添加一条,只须要app4.todos.push({ text: '新项目' })
,这时因为 Vue 内置的响应式系统会自动帮咱们进行 UI 与状态的同步工做.git
<div id="app-4">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ol>
</div>
复制代码
var app4 = new Vue({
el: '#app-4',
data: {
todos: [
{ text: '学习 JavaScript' },
{ text: '学习 Vue' },
{ text: '整个牛项目' }
]
}
})
复制代码
若是咱们用 JQuery 或者 JS 进行操做,免不了一大堆li.appendChild
、document.createElement
等 DOM 操做,咱们须要一长串 DOM 操做保证状态与 UI 的同步,其中一个环节出错就会致使 BUG,手动操做的缺点以下:github
不论是 vue 的数据劫持、Angular 的脏检测仍是 React 的组件级 reRender都是帮助咱们解决 ui 与状态同步问题的利器。面试
这也解释了Backbone做为前端框架鼻祖在以后落寞的缘由,Backbone只是引入了 MVC 的思想,并无解决 View 与 Modal 同步的问题,相比于现代的三大框架直接操做 Modal 就能够同步 UI 的特性, Backbone 仍然与 JQuery 绑定,在 View 里操做 Dom来达到同步 UI 的目的,这显然是不符合现代前端框架设计要求的。算法
UI 在 MVVM 中指的是 View,状态在 MVVM 中指的是 Modal,而保证 View 和 Modal 同步的是 View-Modal。
Vue 经过一个响应式系统保证了View 与 Modal的同步,因为要兼容IE,Vue 选择了 Object.defineProperty
做为响应式系统的实现,可是若是不考虑 IE 用户的话,Object.defineProperty
并非一个好的选择,具体请看面试官系列(4): 基于Proxy 数据劫持的双向绑定优点所在。
咱们将用 Proxy 实现一个响应式系统。
建议阅读以前看一下面试官系列(4): 基于Proxy 数据劫持的双向绑定优点所在中基于
Object.defineProperty
的大体实现。
一个响应式系统离不开发布订阅模式,由于咱们须要一个 Dep
保存订阅者,并在 Observer 发生变化时通知保存在 Dep 中的订阅者,让订阅者得知变化并更新视图,这样才能保证视图与状态的同步。
发布订阅模式请阅读面试官系列(2): Event Bus的实现
/** * [subs description] 订阅器,储存订阅者,通知订阅者 * @type {Map} */
export default class Dep {
constructor() {
// 咱们用 hash 储存订阅者
this.subs = new Map();
}
// 添加订阅者
addSub(key, sub) {
// 取出键为 key 的订阅者
const currentSub = this.subs.get(key);
// 若是能取出说明有相同的 key 的订阅者已经存在,直接添加
if (currentSub) {
currentSub.add(sub);
} else {
// 用 Set 数据结构储存,保证惟一值
this.subs.set(key, new Set([sub]));
}
}
// 通知
notify(key) {
// 触发键为 key 的订阅者们
if (this.subs.get(key)) {
this.subs.get(key).forEach(sub => {
sub.update();
});
}
}
}
复制代码
咱们在订阅器 Dep
中实现了一个notify
方法来通知相应的订阅这们,然而notify
方法到底何时被触发呢?
固然是当状态发生变化时,即 MVVM 中的 Modal 变化时触发通知,然而Dep
显然没法得知 Modal 是否发生了变化,所以咱们须要建立一个监听者Observer
来监听 Modal, 当 Modal 发生变化的时候咱们就执行通知操做。
vue 基于Object.defineProperty
来实现了监听者,咱们用 Proxy 来实现监听者.
与Object.defineProperty
监听属性不一样, Proxy 能够监听(实际是代理)整个对象,所以就不须要遍历对象的属性依次监听了,可是若是对象的属性依然是个对象,那么 Proxy 也没法监听,因此咱们实现了一个observify
进行递归监听便可。
/** * [Observer description] 监听器,监听对象,触发后通知订阅 * @param {[type]} obj [description] 须要被监听的对象 */
const Observer = obj => {
const dep = new Dep();
return new Proxy(obj, {
get: function(target, key, receiver) {
// 若是订阅者存在,直接添加订阅
if (Dep.target) {
dep.addSub(key, Dep.target);
}
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
// 若是对象值没有变,那么不触发下面的操做直接返回
if (Reflect.get(receiver, key) === value) {
return;
}
const res = Reflect.set(target, key, observify(value), receiver);
// 当值被触发更改的时候,触发 Dep 的通知方法
dep.notify(key);
return res;
},
});
};
/** * 将对象转为监听对象 * @param {*} obj 要监听的对象 */
export default function observify(obj) {
if (!isObject(obj)) {
return obj;
}
// 深度监听
Object.keys(obj).forEach(key => {
obj[key] = observify(obj[key]);
});
return Observer(obj);
}
复制代码
咱们目前已经解决了两个问题,一个是如何得知 Modal 发生了改变(利用监听者 Observer 监听 Modal 对象),一个是如何收集订阅者并通知其变化(利用订阅器收集订阅者,并用notify通知订阅者)。
咱们目前还差一个订阅者(Watcher)
// 订阅者
export default class Watcher {
constructor(vm, exp, cb) {
this.vm = vm; // vm 是 vue 的实例
this.exp = exp; // 被订阅的数据
this.cb = cb; // 触发更新后的回调
this.value = this.get(); // 获取老数据
}
get() {
const exp = this.exp;
let value;
Dep.target = this;
if (typeof exp === 'function') {
value = exp.call(this.vm);
} else if (typeof exp === 'string') {
value = this.vm[exp];
}
Dep.target = null;
return value;
}
// 将订阅者放入待更新队列等待批量更新
update() {
pushQueue(this);
}
// 触发真正的更新操做
run() {
const val = this.get(); // 获取新数据
this.cb.call(this.vm, val, this.value);
this.value = val;
}
}
复制代码
咱们在上一节中实现了订阅者( Watcher),可是其中的update
方法是将订阅者放入了一个待更新的队列中,而不是直接触发,缘由以下:
所以这个队列须要作的是异步且去重,所以咱们用 Set
做为数据结构储存 Watcher 来去重,同时用Promise
模拟异步更新。
// 建立异步更新队列
let queue = new Set()
// 用Promise模拟nextTick
function nextTick(cb) {
Promise.resolve().then(cb)
}
// 执行刷新队列
function flushQueue(args) {
queue.forEach(watcher => {
watcher.run()
})
// 清空
queue = new Set()
}
// 添加到队列
export default function pushQueue(watcher) {
queue.add(watcher)
// 下一个循环调用
nextTick(flushQueue)
}
复制代码
咱们梳理一下流程, 一个响应式系统是如何作到 UI(View)与状态(Modal)同步的?
咱们首先须要监听 Modal, 本文中咱们用 Proxy 来监听了 Modal 对象,所以在 Modal 对象被修改的时候咱们的 Observer 就能够得知。
咱们得知Modal发生变化后如何通知 View 呢?要知道,一个 Modal 的改变可能触发多个 UI 的更新,好比一个用户的用户名改变了,它的我的信息组件、通知组件等等组件中的用户名都须要改变,对于这种状况咱们很容易想到利用发布订阅模式来解决,咱们须要一个订阅器(Dep)来储存订阅者(Watcher),当监听到 Modal 改变时,咱们只须要通知相关的订阅者进行更新便可。
那么订阅者来自哪里呢?其实每个组件实例对应着一个订阅者(正由于一个组件实例对应一个订阅者,才能利用 Dep 通知到相应组件,否则乱套了,通知订阅者就至关于间接通知了组件)。
当订阅者得知了具体变化后它会进行相应的更新,将更新体如今 UI(View)上,至此UI 与 Modal 的同步完成了。
完整代码已经在 github 上,目前只实现了一个响应式系统,接下来会逐步实现一个完整的迷你版 mvvm 框架,因此你能够 star 或者 watch 来关注进度.
响应式系统虽然是 Vue 的核心概念,可是一个响应式系统并不够.
响应式系统虽然得知了数据值的变化,可是当值不能完整映射 UI 时,咱们依然须要进行组件级别的 reRender,这种状况并不高效,所以 Vue 在2.0版本引入了虚拟 DOM, 虚拟 DOM进行进一步的 diff 操做能够进行细粒度更高的操做,能够保证 reReander 的下限(保证不那么慢)。
除此以外为了方便开发者,vue 内置了众多的指令,所以咱们还须要一个 vue 模板解析器.
老掉牙问题,虚拟 DOM, 虚拟 DOM 的实现以及 diff 算法的优化。