vue 中 MVVM 原理及其实现
一、了解 MVVM
- MVVM – Model View ViewModel:数据,视图,视图模型。
- 三者与 Vue 的对应:view 对应 template,vm 对应 new Vue({…}),model 对应 data。
- 三者的关系:view 能够通过事件绑定的形式影响 model,model 能够通过数据绑定的模式影响到 view,viewModel 是把 model 和 view 连起来的连接器。
MVVM 框架的三大因素:
- 响应式:Vue 如何监听到 data 的每个属性变动。
数据劫持: 应用 Object.defineProperty(obj, ‘property’,{})来定义属性,将对象属性值的设置和拜访 (get,set) 都变成函数,别离在设置和获取该对象的该属性时调用执行。
- 模板引擎:Vue 的模板如何被解析,指令如何解决
- 渲染:Vue 的模板如何被渲染成 html,渲染过程是怎么的
二、实现办法
- 实现 compile, 进行模板的编译,包含编译元素(指令)、编译文本等,达到初始化视图的目标,并且还须要绑定好更新函数;
- 实现 Observe, 监听所有的数据,并对变动数据公布告诉;
- 实现 watcher, 作为一个中枢,接管到 observe 发来的告诉,并执行 compile 中相应的更新办法。
- 联合上述办法,向外裸露 mvvm 办法。
首先编辑一个 html 文件,如下:
<div id="app">
<input type="text" v-model="obj.name">
<div> {{obj.name}}</div>
{{message}}{{obj.name}}
</div>
<script src="index.js"></script>
<script>
let vm = new MVVM({el: '#app',// 或 document.querySelector('#app')
data: {
message: 'hello',
obj: {name: 'susu'}
}
});
</script>
1. 创立类 MVVM
class MVVM {constructor(options) {
this.$el = options.el;
this.$data = options.data;
if (this.$el) {
// 数据劫持 把对象所有的属性 减少 get set 办法
new Observer(this.$data);
this.proxyData(this.$data);
// 用数据和元素进行编译
new Compile(this.$el, this);
}
}
// 把对象的属性全副绑定在实例上,this.xx
proxyData(data) {
// defineProperty 解析 : https://www.jianshu.com/p/8fe1382ba135
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {get() {return data[key];
},
set(newValue) {data[key] = newValue;
}
})
})
}
}
2. 实现 compile(编译模板)
1. 把实在 DOM 移入到内存中 fragment, 因为 fragment 在内存中,操作比拟快
2. 编译 : 提取想要的元素节点 v-model 和文本节点 {{}}
3. 把编译好的 fragment,增加到 DOM 中
class Compile {constructor(el, vm) {this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 1. 把实在 DOM 移入到内存中 fragment, 因为 fragment 在内存中,操作比拟快
let fragment = this.node2fragment(this.el);
// 2. 编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
this.compile(fragment);
// 3. 把编译好的 fragment,增加到 DOM 中
this.el.appendChild(fragment);
}
}
// 是否是元素节点
isElementNode(el) {
//nodeType : 1 Element 代表元素节点
return el.nodeType == 1;
}
node2fragment(el) {
// 在内存中, 创立一个新的文档片段,
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {fragment.appendChild(firstChild);
}
// 返回虚构的节点对象,节点对象蕴含所有属性和办法。return fragment;
}
// 编译
compile(fragment) {
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {if (this.isElementNode(node)) {
// 元素节点, 编译元素
this.compileElement(node);
// 如果有子节点,再次执行
this.compile(node);
} else {
// 文本节点, 编译文本
this.compileText(node);
}
})
}
// 编译文本 {{msg}}
compileText(node) {let expr = node.textContent; //{{msg}}
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(expr)) {CompileUtil.text(node, this.vm, expr);
}
}
// 编译元素 v-
compileElement(node) {
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
// 是否是指令
if (attr.name.includes('v-')) {
let expr = attr.value;// message
let type = attr.name.split('-')[1];//model
CompileUtil[type](node, this.vm, expr);
}
})
}
}
CompileUtil = {
// 文本处理
text(node, vm, expr) {
let updaterFn = this.updater.textUpdater;
let value = this.getTextVal(vm, expr);
// 监控数据变动 eg:因为{{message}} {{obj.name}} , 所以须要循环
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {new Watcher(vm,arguments[1],newValue => {updaterFn && updaterFn(node, this.getTextVal(vm, expr));
})
})
updaterFn && updaterFn(node, value);
},
// 获取文本的 key eg:message
getTextVal(vm, expr) {return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {return this.getValue(vm, arguments[1]);// message
})
},
// 获取 data 中对象的值
getValue(vm, expr) {let arr = expr.split('.'); // 解决对象 obj.name =>[obj,name]
return arr.reduce((prev, next) => {return prev[next]
}, vm.$data);
},
// 设置 data 中对象的值
setValue(vm, expr, value) {let arr = expr.split('.'); // 解决对象 obj.name =>[obj,name]
arr.reduce((prev, next, curIndex) => {if (curIndex == arr.length - 1) {return prev[next] = value;
}
return prev[next];
}, vm.$data);
},
// 输入框解决
model(node, vm, expr) {
let updaterFn = this.updater.modalUpdater;
let value = this.getValue(vm, expr);
node.addEventListener('input', (event) => {this.setValue(vm, expr, event.target.value);
})
// 监控数据变动
new Watcher(vm,expr,newValue => {updaterFn && updaterFn(node, this.getValue(vm, expr));
})
updaterFn && updaterFn(node, value);
},
updater: {textUpdater(node, value) {node.textContent = value;},
modalUpdater(node, value) {node.value = value;}
}
}
3. 实现 observe(数据监听 / 劫持)
vue 采纳的 observe + sub/pub 实现数据的劫持,通过 js 原生的办法 Object.defineProperty()来劫持各个属性的 setter,getter,在属性对应数据扭转时,公布音讯给订阅者,而后触发相应的监听回调。
为何要监听 get,而不是间接监听 set?
- 因为 data 中有很多属性,有些被用到,有些可能不被用到
- 只有被用到的才会走 get
- 没有走到 get 中的属性,set 的时候咱们也无需关怀
- 防止不必要的从新渲染
次要内容:observe 的数据对象进行递归遍历,包含子属性对象的属性,都加上 setter 和 getter。
class Observer {constructor(data) {this.observe(data);
}
// 获取 data 的 key value
observe(data) {if (!data || typeof data != 'object') return;
Object.keys(data).forEach(key => {this.defineReactive(data, key, data[key]);
this.observe(data[key]);
})
}
// 定义响应式
defineReactive(obj, key, value) {
let that = this;
// 每个变动的数据 都会对应一个数组,这个数组是寄存所有更新的操作
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {console.log('observe',key,Dep.target);
Dep.target && dep.addSubs(Dep.target);
return value;
},
set(newValue) {if (value != newValue) {
// 如果是对象持续劫持
that.observe(newValue);
value = newValue;
// 告诉更新
dep.notify();}
}
})
}
}
实现数据劫持后,接下来的工作怎么告诉订阅者了,咱们须要在监听数据时实现一个音讯订阅器,具体的办法是:定义一个数组,用来寄存订阅者,数据变动告诉(notify)订阅者,再调用订阅者的 update 办法。
增加 Dep 类:
class Dep {constructor() {this.subs = [];
}
// 增加订阅
addSubs(watcher) {this.subs.push(watcher);
}
// 告诉更新
notify() {
this.subs.forEach(watcher => {watcher.update();
})
}
}
4. 实现 watcher(订阅核心)
Observer 和 Compile 之间通信的桥梁是 Watcher 订阅核心,其主要职责是:
1、在本身实例化时往属性订阅器 (Dep) 外面增加本人,与 Observer 建设连贯;
2、本身必须有一个 update()办法,与 Compile 建设连贯;
3、当属性变动时,Observer 中 dep.notify()告诉,而后能调用本身(Watcher)的 update()办法,并触发 Compile 中绑定的回调,实现更新。
// 观察者的目标就是给须要变动的那个元素减少一个观察者,当数据变动后执行对应的办法
class Watcher {constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 获取老的值
this.value = this.get();}
// 获取 data 中对象的值
getValue(vm, expr) {let arr = expr.split('.');
return arr.reduce((prev, next) => {return prev[next]
}, vm.$data);
}
get() {
Dep.target = this;
let value = this.getValue(this.vm, this.expr);
Dep.target = null;
return value;
}
// 更新, 内部调用的办法
update() {let newValue = this.getValue(this.vm, this.expr);
if (newValue != this.value) {this.cb(newValue);
}
}
}