一、如何理解MVVM模式
mvvm模式字面可理解为数据-视图-数据驱动,本质上就是MVC模式在前端的体现,而vue正是运用这种模式,看vue源码可了解到,vue核心即采用数据劫持结合发布者-订阅者模式,通过ES5中Object.defineProperty()的特性来劫持各个属性的setter,getter,在数据变动时发消息给订阅者,触发对应watcher的回调,以致于view更新的效果。这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
二、总体实现思路
借助上图来理解会更清晰,首先Mvue类接收一个参数对象作为初始输入,接收data,一方面在Observer观察者中,运用Object.defineProperty()方法把data数据对象里属性全部转成setter、getter方法,并初始化各自的订阅器Dep(用来存放watcher);另一方面创建Compile指令解析器,作用是对每个元素节点进行解析,替换模板数据,调用updater更新view(同时也是生成 虚拟dom 到真实dom的过程),与此同时对data的每个属性绑定一个Watcher订阅者,并将各个Watcher push到对应的Dep订阅器中; 当data中的属性被改变时,触发setter,并在对应的Dep容器中调用Dep.notify()通知订阅者Watcher,订阅者收到消息后,将要更新的watcher依赖push到更新队列中,等在下一个事件循环中刷新队列执行自身的update方法,来触发render进而更新view;而对于双向绑定,无非是运用input表单的addEventListener事件,来更新data以至于达到更新view的目的。
三、上代码
- index.js
let vm = new MVVM({
el: '#app',
data: {
person: {
name: '大神',
age: 18,
fav: 'film'
},
msg: 'msg的内容',
htmlStr: '<h3>这是v-html</h3>'
}
- MVVM实例
class MVVM {
constructor(options) {
// 初始元素与数据通过options对象绑定
this.$el = options.el;
this.$data = options.data;
this.$options = options;
// 通过Compiler对象对模版进行编译,例如{{}}插值、v-text、v-html、v-model等Vue语法
if (this.$el) {
// 1. 创建观察者
new Observer(this.$data);
// 2. 编译模版
new Compiler(this.$el, this);
// 3. 通过数据代理实现 this.person.name = '海贼王——路飞'功能,而不是this.$data.person.name = '海贼王——路飞'
this.proxyData(this.$data);
}
}
//用vm代理vm.$data
proxyData(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
})
}
}
}
- 编译HTML模版对象
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1. 将预编译的元素节点放入文档碎片对象中,避免DOM频繁的回流与重绘,提高渲染性能 ---vue中采用虚拟dom
const fragments = this.node2fragments(this.el);
// 2. 编译模版
this.compile(fragments);
// 3. 追加子元素到根元素
this.el.appendChild(fragments);
}
compile(fragments) {
// 1.获取子节点
const childNodes = fragments.childNodes;
// 2.递归循环编译
[...childNodes].forEach(child => {
// 如果是元素节点
if (this.isElementNode(child)) {
this.compileElement(child);
} else {
// 文本节点
this.compileText(child);
}
//递归遍历
if (child.childNodes && child.childNodes.length) {
this.compile(child);
}
})
}
compileElement(node) {
let attributes = node.attributes;
// 对于每个属性进行遍历编译
// attributes是类数组,因此需要先转数组
[...attributes].forEach(attr => {
let { name, value } = attr; // v-text="msg" v-html=htmlStr type="text" v-model="msg"
if (this.isDirector(name)) { // v-text v-html v-mode v-bind v-on:click v-bind:href=''
let [, directive] = name.split('-');
let [compileKey, detailStr] = directive.split(':');
// 更新数据,数据驱动视图
compileUtil[compileKey](node, value, this.vm, detailStr);
// 删除有指令的标签属性 v-text v-html等,普通的value等原生html标签不必删除
node.removeAttribute('v-' + directive);
}
})
}
compileText(node) {
// 编译文本中的{{person.name}}--{{person.age}}
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](node, content, this.vm);
}
}
rector(attrName) {
// 判断是否为Vue特性标签
return attrName.startsWith('v-');
}
node2fragments(el) {
// 创建文档碎片对象
const f = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 元素节点的nodeType属性为 1
return node.nodeType === 1;
}
}
// 编译模版具体执行
const compileUtil = {
getValue(expr, vm) {
// 处理 person.name 这种对象类型,取出真正的value
return expr.split('.').reduce((data, currentVal) => {
return data[currentVal];
}, vm.$data)
},
setVal(expr, vm, inputValue) {
expr.split('.').reduce((data, currentVal) => {
data[currentVal] = inputValue;
}, vm.$data)
},
getContent(expr, vm) {
// {{person.name}}--{{person.age}}
// 防止修改person.name使得所有值全部被替换
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
},
text(node, expr, vm) {
let value;
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// text的 Watcher应在此绑定,
// Watcher的构造函数的 getOldVal()方法需要接受数据或者对象,而{{person.name}}不能接收
new Watcher(args[1], vm, () => {
//更新view
this.updater.textUpdater(node, this.getContent(expr, vm));
});
return this.getValue(args[1], vm);
});
} else {
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
let value = this.getValue(expr, vm);
// html对应的 Watcher
new Watcher(expr, vm, (newVal) => {
this.updater.htmlUpdater(node, newVal);
})
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
// v-model绑定对应的 Watcher, 数据驱动视图
new Watcher(expr, vm, (newVal) => {
this.updater.modelUpdater(node, newVal);
});
// 视图 => 数据 => 视图
node.addEventListener('input', (e) => {
this.setVal(expr, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
// 视图更新函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
}
}
}
- 定义观察者
class Observer {
constructor(data) {
this.observe(data);
}
// data是一个对象,可能嵌套其它对象,需要采用递归遍历的方式进行观察者绑定
observe(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
}
// 通过 object.defineProperty方法对对象属性进行劫持
defineReactive(obj, key, value) {
// 递归观察
this.observe(value);
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
// 首次取data属性时拦截,收集依赖,Dep.target为对应的Watcher,并往Dev中添加订阅者
Dep.target && dep.addWatcher(Dep.target);
return value;
},
// 采用箭头函数在定义时绑定this的定义域
set: (newVal) => { //数据更新即触发
if (value === newVal) return;
this.observe(newVal);
value = newVal;
// 通知对应watcher数据发生改变
dep.notify();
}
})
}
}
- 如何实现一个 watcher
// 订阅者
class Watcher {
// 通过回调函数实现更新的数据通知到视图
constructor(expr, vm, cb) {
this.expr = expr;
this.vm = vm;
this.cb = cb;
this.oldVal = this.getOldVal(); // 获取旧数据
}
// 在初始化new Watcher时触发,即在利用getValue()获取数据调用getter()方法时立即把当前订阅者挂载,收集到Dep容器中
getOldVal() {
Dep.target = this;
const oldVal = compileUtil.getValue(this.expr, this.vm);
// 挂载完毕需要注销,防止重复挂载
Dep.target = null;
return oldVal;
}
// 通过回调函数更新数据
update() {
const newVal = compileUtil.getValue(this.expr, this.vm);
if (newVal !== this.oldVal) {
//若在vue中,这个回调将调用render函数,会返回一个newVnode,进而调用patch(oldVnode,newVnode)函数来更新dom
this.cb(newVal);
}
}
}
- Dep实例
// Dep类 容器 收集watcher对象,并在数据变化时通知watcher
class Dep {
constructor() {
this.watcherCollector = [];
}
// 添加watcher
addWatcher(watcher) {
this.watcherCollector.push(watcher);
}
// 数据变化时通知watcher更新
notify() {
this.watcherCollector.forEach(w => w.update());
}
}
四、总结
以上便是对mvvm设计模式及vue的双向绑定原理做的概要解析,主要还是对 Watcher, Observer , Dep 的关系做梳理,
Observer, 观察者,用来观察数据源变化.
Dep, 观察者和订阅者是典型的 一对多 的关系,所以这里设计了一个依赖中心,来管理某个观察者和所有这个观察者对应的订阅者的关系, 消息调度和依赖管理都靠它。
Watcher, 订阅者,当某个观察者观察到数据发生变化的时候,这个变化经过消息调度中心,最终会传递到所有该观察者对应的订阅者身上,然后这些订阅者分别执行自身的业务回调即可