vue之mvvm原理解析
title: vue之mvvm原理解析
mvvm 原理解析
文章目录
mvvm 面试论述
MVVM分为Model、View、ViewModel三者
-
Model
:代表数据模型,数据和业务逻辑都在Model
层中定义; -
View
:代表UI视图,负责数据的展示; -
ViewModel
:负责监听Model
中数据的改变并且控制视图的更新,处理用户交互操作;
这种模式实现了Model
和View
的数据自动同步,也就是双向绑定,mvvm
双向绑定,采用的是数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的 setter、getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
大致的过程:
-
实现一个指令解析器
Compile
,对每个元素节点的之类进行解析,根据指令模板替换数据,以及搬到相应的更新函数 -
实现一个数据监控器
Observer
,将所有数据设置成响应式,并进行监听,如有变动可以拿到最新值并通知订阅者 -
实现一个订阅者
Watcher
,作为连接Observer(数据劫持)
与Compile(模板)
的桥梁,在对应模板数据更新处,添加监听数据的订阅者,并将其添加到订阅者容器Dep
中,当属性变动时,通过Dep
发布通知,执行指令绑定的相应回调函数,从而更新视图 -
mvvm的入口函数,主要是整合调控以上的,模板编译(compile)、数据劫持(Observe)、订阅者(Watcher),
mvvm的编译过程以及使用
- 编译的流程图
- 整体分析
过程分析
当 new MVVM()
后的编译主要分为两个部分
-
一部分是模板的编译
Compile
- 编译元素和文本,将插值表达式进行替换
- 编译模板指令的标签,例如:
v-model
-
一部分是数据劫持
Observer
- 将所有的数据响应式处理
- 给模板的每个编译处设置一个观察者,并将观察者存放在Dep中
-
Watcher
如果数据发生改变,在Object
的defineProperty
的set
函数中调用Watcher的update方法 -
Dep
发布订阅,将所有需要通知变化的data
添加到一个数组中
具体步骤
1、需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter,这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
2、 compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
3、 Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个 update() 方法
- 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。
4、MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过Observer来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
分解vue实例
vue的使用,
let vm = new Vue({
el:"#app",
data:{
school:{
name:"beida",
age:100
}
},
})
首先使用
new
创建一个vue
实例,传递一个对象参数,包含el
和data
实现Complie编译模板
index.html
页面的使用
vue类-入口文件
在入口之处,先处理了模板的编译(Compile),数据劫持(Observe)在后期进行使用。
编译模板
编译模板的主要的入口,分为,将节点转成文档碎片,替换模板中的常量数据
节点转文档碎片
将节点转换成文档碎片,然后返回,
编译模板
//编译模板
class Compiler{
//判断一个节点是否是元素节点
isElementNode(node){
return node.nodeType ```= 1;
}
//判断一个属性是否是一个指令
isDirective(attrName){
return attrName.startsWith("v-");
}
//编译模板
compile(node){
//node.childNodes 包含了元素节点与文本节点
//得到的是一个伪数组
let childNodes = node.childNodes;
[...childNodes].forEach(child=>{
if(this.isElementNode(child)){ //元素节点
//编译元素节点
this.compileElementNode(child)
//递归编译所有的节点
this.compile(child)
}else{ //文本节点
//编译文本节点
this.compileText(child)
}
})
}
//编译元素节点
compileElementNode(node){
//获取元素的属性节点(伪数组)
let attributes = node.attributes;
[...attributes].forEach(attr=>{
let {name,value: expr} = attr;
//判断是否是一个指令
if(this.isDirective(name)){
let [,directive] = name.split("-")
//根据指令,调用对呀的指令方法
CompilerUtil[directive](node,expr,this.vm);
}
})
}
//编译文本节点
compileText(node){
//得到所有的文本节点
let content = node.textContent;
//使用正则得到所有文本里面的内容
let reg = /\{\{(.+?)\}\}/;
if(reg.test(content)){
//{{}} 是v-text的语法糖,所有调用text指令
CompilerUtil['text'](node,content,this.vm)
}
}
//将节点转成文档碎片
node2fragme(node){
//创建键一个文档碎片
let fragment = document.createDocumentFragment();
let firstChild ;
while(firstChild = node.firstChild){
fragment.appendChild(firstChild)
}
return fragment;
}
}
//编译指令处理对象--处理不同的指令
CompilerUtil = {
//获取到data中对应的数据
getVal(vm,expr){
return expr.split(".").reduce((data,current)=>{
return data[current]
},vm.$data)
},
//设置$data中的数据
setVal(vm,expr,value){
expr.split(".").reduce((data,current,index,arr)=>{
if(index ```arr.length -1){
return data[current] = value;
}
return data[current]
},vm.$data)
},
//处理 v-model 指令的数据
model(node,expr,vm){
//更新模板中在data中对应的数据
let fn = this.updater['modelUpdater']
//当input 框的数据相互绑定
node.addEventListener("input",(e)=>{
let value = e.target.value;
//当输入框数据改变时,同步更改$data中的数据
this.setVal(vm,expr,value);
})
let value = this.getVal(vm,expr)
//替换模板中的数据
fn(node,value)
},
// 处理v-text指令的数据
text(node,expr,vm){
let fn = this.updater['textUpdater'];
//获取到要替换的内容
let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(vm,args[1])
})
fn(node,content)
},
//更新模板中的数据
updater:{
//将v-model绑定的数据进行替换
modelUpdater(node,value){
node.value = value;
},
//将v-text绑定的数据进行替换
textUpdater(node,content){
node.textContent = content;
}
}
}
编译模板,主要是对模板中的一些数据常量进行替换,对于一些指令进行相关的处理,特别是指令v-model的数据的绑定。
数据劫持
数据劫持,实在模板进行编译之前进行,将data中的所有的数据都变成响应式数据,
Observer
的调用 vue类
数据劫持 observer类
//数据劫持,将数据变成响应式数据
class Observer{
constructor(data){
//将数据变成响应式数据
this.observer(data)
}
//将数据变成响应式数据
observer(data){
//判断数据是否是一个对象
if(data && typeof data ```'object'){
for(let key in data){
//设置响应式
this.defindReactive(data,key,data[key])
}
}
}
//设置响应式
defindReactive(obj,key,value){
//如果数据是一个对象,继续递归设置
this.observer(value);
let dep = new Dep(); //不用的watcher存放到不同的dep中
Object.defineProperty(obj,key,{
//当获取数据时会调用get
get(){
return value;
},
//当设置数据时会调用set
set: (newValue)=>{
if(newValue != value){
//将新数据设置成响应式
this.observer(newValue);
value = newValue;
}
}
})
}
}
订阅者的Watcher的实现
订阅者watcher
//观察者
class Watcher{
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb; //状态改变后要进行的操作
//获取老数据--保存一个老状态
this.oldValue = this.get();
}
//获取状态的方法
get(){
Dep.target = this;
//当获取旧的值得时候便已经触发响应式数据
let value = CompilerUtil.getVal(this.vm,this.expr)
Dep.target = null;
return value;
}
//当状态发生改变的时候,观察者更新当前的状态
update(){
let newVal = CompilerUtil.getVal(this.vm,this.expr);
if(this.oldValue !```newVal){
this.cb(newVal)
}
}
}
存放订阅者Dep
//存储观察者的类
class Dep {
constructor(){
this.subs = []; //存放所有的watcher
}
//添加watcher 订阅
addSub(watcher){
this.subs.push(watcher)
}
//通知发布
notify(){
this.subs.forEach(watcher=>watcher.update())
}
}
订阅者,连接编译模板与数据劫持
编译模板处
//编译指令处理对象--处理不同的指令
CompilerUtil = {
// 此处省略若该代码 ......
//处理 v-model 指令的数据
model(node,expr,vm){
//更新模板中在data中对应的数据
let fn = this.updater['modelUpdater']
//给输入框添加一个观察者,如果数据改变,通知data数据改变
new Watcher(vm,expr,(newValue) =>{
fn(node,newValue)
})
// 此处省略若该代码 ......
},
// 处理v-text指令的数据
text(node,expr,vm){
let fn = this.updater['textUpdater'];
let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
//添加一个订阅者
new Watcher(vm,args[1],()=>{
fn(node,this.getVal(vm,args[1]))
})
return this.getVal(vm,args[1])
})
fn(node,content)
},
// 此处省略若该代码 ......
}
数据劫持处
//数据劫持,将数据变成响应式数据
class Observer{
// 此处省略若该代码 ...
//设置响应式
defindReactive(obj,key,value){
// 此处省略若该代码 ...
Object.defineProperty(obj,key,{
//当获取数据时会调用get
get(){
Dep.target && dep.subs.push(Dep.target)
return value;
},
//当设置数据时会调用set
set: (newValue)=>{
if(newValue != value){
//将新数据设置成响应式
this.observer(newValue);
value = newValue;
//当数据发生改变时,通知观察者
dep.notify();
}
}
})
}
}
总述:订阅者是,编译模板与数据劫持之间的桥梁,模板编译之处添加订阅者,并将订阅者存储在Dep中,在数据劫持处添加发布者,当数据发生改变的时候,通知订阅者。
参考文档
上一篇: Vue笨蛋学原理:数据(data)是如何出现在页面上的?
下一篇: 【MVVM】WPF