一、MVVM模式
MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定。
二、为什么会出现 MVVM 呢?
-
Model:代表数据模型,数据和业务逻辑都在Model层中定义;
-
View:代表UI视图,负责数据的展示;
-
ViewModel:就是与界面(view)对应的Model。因为,数据库结构往往是不能直接跟界面控件一一对应上的,所以,需要再定义一个数据对象专门对应view上的控件。而ViewModel的职责就是把model对象封装成可以显示和接受输入的界面数据对象。
三、MVVM 的出现,完美解决了以上三个问题。
-
MVVM 由 Model、View、ViewModel 三部分构成,Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。
-
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
-
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
四、Vue.js 的细节
-
Vue.js 可以说是MVVM 架构的最佳实践,专注于 MVVM 中的 ViewModel,不仅做到了数据双向绑定,而且也是一款相对来比较轻量级的JS 库,API 简洁,很容易上手。Vue的基础知识网上有现成的教程,此处不再赘述, 下面简单了解一下 Vue.js 关于双向绑定的一些实现细节:
-
Vue.js 是采用 Object.defineProperty 的 getter 和 setter,并结合观察者模式来实现数据绑定的。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
五、如何实现MVVM
- Observer :数据监听器,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用Object.defineProperty的getter和setter来实现
- Compile :指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
- Watcher :订阅者,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数
- Dep :消息订阅器,内部维护了一个数组,用来收集订阅者(Watcher),数据变动触发notify 函数,再调用订阅者的 update 方法
- 从封面图中可以看出,当执行 new Vue() 时,Vue 就进入了初始化阶段,一方面Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;另一方面,Vue 的指令编译器Compile 对元素节点的指令进行扫描和解析,初始化视图,并订阅 Watcher 来更新视图, 此时Wather 会将自己添加到消息订阅器中(Dep),初始化完毕
- 当数据发生变化时,Observer 中的 setter 方法被触发,setter 会立即调用Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。
- 代码如下:
class Dep{//观察者模式代码
constructor(){
this.subs = [] //存储订阅者,存储传进来的观察者
}
addSub(watcher){//订阅 let watcher = new Wathcer()
this.subs.push(watcher)//存储观察者
}
notify(){//发布
this.subs.forEach(watcher => {
watcher.update()
})
}
}
class Watcher{
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.oldData = this.get()
}
get(){
Dep.target = this
let value = ComplieUtil.getVal(this.vm,this.expr);
Dep.target = null
return value
}
update(){
let newVal = ComplieUtil.getVal(this.vm,this.expr);
if(newVal !== this.oldData){
this.cb(newVal)
}
}
}
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
if (data && typeof data === 'object') {
for (let key in data) {
this.defineReactive(data, key, data[key]);
}
}
}
defineReactive(obj, key, value) {
this.observer(value);
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newVal) => {
// console.log(newVal,"newVal")
if (newVal !== value) {
this.observer(newVal)//补充
value = newVal;
dep.notify();//发布功能
}
}
})
}
}
//基类调度
class Complie {
constructor(el, vm) {
//判断el属性是不是⼀个元素 如果不是元素 那就获取它
this.el = this.isElementNode(el) ? el : document.querySelector(el); //1
//保证所有⼈拿到实例vm
this.vm = vm;
//把当前节点中的元素放到内存中
let fragment = this.node2fragment(this.el); //2
//⽤数据编译模板
this.compile(fragment); //4
//把内容塞到⻚⾯中
this.el.appendChild(fragment) //3
}
//判断是不是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
//把每个节点都放⼊到内存中
node2fragment(node) {
//创建⼀个⽂档碎⽚
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = node.firstChild) {
//appendChild具有移动性
fragment.appendChild(firstChild)
}
return fragment
}
//核⼼的编译⽅法
compile(node) { //⽤来编译内存的dom节点
let childNodes = node.childNodes;
[...childNodes].forEach(child => {
//判断是否为节点
if (this.isElementNode(child)) {
// 属性节点方法
//type="text" v-model="className.name"
this.compileElement(child);
//如果是元素的话 需要传⼊⾃⼰然后遍历⼦节点
this.compile(child)
} else {
//处理文本节点
//{{className.name}}
this.compileText(child);
}
});
}
//编译元素节点下的属性节点
//type="text" v-model="className.name"
isDirective(attrName){
return attrName.startsWith('v-')
}
//元素节点 <input type v-model/> <h1> <h3> <ul>
compileElement(node){
let attributes = node.attributes;
[...attributes].forEach(attr => {
//[type:'text',v-model:'className.name']
let {name,value:expr} = attr;//重命名
// console.log(expr)
if(this.isDirective(name)){//v-model: v-html
let [,directive] = name.split('-');
// [,directive] = [v,model]
// [a,b]=[b,a]
// console.log(directive)
let [directiveName,] = directive.split(":")
//这个方法为了复用 代码优化 model
//对应 129行 <input />
ComplieUtil[directiveName](node,expr,this.vm)
}
})
}
//文本节点
//{{className.name}}
compileText(node){
let content = node.textContent;
// console.log(content)正则表达式 {{className.name}}
if(/\{\{(.+?)\}\}/.test(content)){
//"{{className.name}}"
ComplieUtil['text'](node,content,this.vm)
}
}
}
ComplieUtil = {
getVal(vm,expr){//className.name
return expr.split('.').reduce((data,current)=>{
return data[current]//1707班
},vm.$data)
},
setVal(vm,expr,value){
// [className,name]
expr.split('.').reduce((data,current,index,arr)=>{
if(index === arr.length - 1){
return data[current] = value//1707班
}
return data[current]
},vm.$data)
},
model(node,expr,vm){
let fn = this.updater['modelUpdater']
new Watcher(vm,expr,(newVal)=>{
// 给输入框添加观察者,如果数据更新就会触发这个方法,会拿到新的值
fn(node,newVal)
})
let value = this.getVal(vm,expr)
fn(node,value)
node.addEventListener('input',(e)=>{
let value = e.target.value;
this.setVal(vm,expr,value)
})
},
text(node,expr,vm){
let fn = this.updater["textUpdater"]
let value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
new Watcher(vm,args[1],(newVal)=>{
fn(node,newVal)
})
return this.getVal(vm,args[1])
})
fn(node,value)
},
updater:{
modelUpdater(node,value){
node.value = value
},
textUpdater(node,value){
node.textContent = value
},
}
}