相信大家对mvvm并不陌生吧,下面看下实现的代码,参考vue源码,整理出来的小demo。 想看整体代码 猛戳 github,如果要是觉得有对您有帮助 麻烦给个star。
<div id="app">
<input type="text" v-model="message.a" />
{{message.a}}
</div>
<script src="./mvvm/watcher.js"></script>
<script src="./mvvm/observer.js"></script>
<script src="./mvvm/compile.js"></script>
<script src="./mvvm/mvvm.js"></script>
<script>
//将标签放到内存中去,然后 编译 => 提前想要的元素元素节点 v-model 和文本节点 {{}}
let vm = new MVVM({
el : "#app",
data:{
message:{
a:'1212'
}
}
})
</script>
几种实现双向绑定的做法
1.发布订阅模式
一般通过sub,pub的方法实现数据和视图的绑定监听,更新数据方法通常做法是 vm.set('property', value)。
2.脏检查
angular.js 就是通过脏值检测的方法对数据是否有变更,来决定更新视图,最简单的方式就是setInterval()定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:
- DOM事件,比如用户输入文本,点击按钮等。(ng-click)
- XHR响应事件(
$http
) - 浏览器Location变更事件(
$location
) - Timer事件(
$timeout
,$interval
) - 执行
$digest()
或$apply()
3.数据劫持
vue.js 采用的就是数据劫持结合发布订阅模式,通过Object.defineProperty()
来劫持各个属性的 setter
, getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
思路整理
- mvvm 会初始化两个方法
Observer
- 劫持所有属性,Compile
- 解析指令 Compile
生成视图的同时 会订阅数据变化new Watcher
。生成更新视图的回调, (这个回调什么时候调用呢?)new Watcher
会添加订阅者到Dep数组
中,方便修改数据的时候通知变化。Observer
如果劫持到变化会通知Dep
,Dep
会运行Dep数组
里面所有的通知(new Watcher
)。

1. Observer
我们知道 可以利用 Obeject.defineProperty()
来监听 setter, getter。
class Observer{
constructor(data){
this.observer(data);
}
observer(data){
//要对这个data数据将原有的属性改成set和get的形式 所以必须要数组
if(!data || typeof data !== 'object'){
return;
}
//要将数据 一一劫持 先获取到 data 到 key 和 value
Object.keys(data).forEach(key => {
//劫持
this.defineReactive(data,key,data[key]);
this.observer(data[key]); //深度递归劫持
})
}
//定义响应式
defineReactive(obj,key,value){
let that = this;
//每个变化的数据,都会对应一个数组,这个数组是存放所有更新的操作
let dep = new Dep();
// 在获取某个值到时候,
Object.defineProperty(obj,key,{
enumerable : true,
configurable : true,
get(){ //当取值时调用到方法
Dep.target && dep.addSub(Dep.target); // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
return value;
},
set(newValue){ //当给data属性中设置值到时候,更改获取的属性到值
if(newValue != value){
//这里的this不是实例
that.observer(newValue);//如果是对象,继续劫持
value = newValue;
dep.notify(); //通知所有人数据更新了
}
}
})
}
}
class Dep{
constructor(){
//订阅的数组
this.subs = [];
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher => watcher.update());
}
}
要点总结
- 利用递归 深度监听(由于Object.defineProperty 无法深度监听)
- get() 的时候也就是谁需要展示的时候, 要把
new watcher
push到数组中去(订阅),方便修改值去通知所有的订阅者(发布) - set()的时候,要通知所有的订阅者,你们要修改值到视图啦(发布)
2. Compile
compile 主要做的事情就是解析模版指令,将模版中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

fragment
进行解析编译操作,解析完成,再将fragment
添加回原来的真实dom节点中
//将真实的DOM移入到内存中 fragment
//同样定义一个类
class Compile{
constructor(el,vm){
//el可能是 #app or dom,所以要进行判断
this.el = this.isElementNode(el)?el:document.querySelector(el);
this.vm = vm;
if(this.el){
//如果这个元素能获取到,我们才开始编译
//1.先把这些真实的DOM移入到内存中 fragment
//2、编译 =》 提前想要的元素元素节点 v-model 和文本节点 {{}}
//3、把编译好的 fragment 在塞回到页面里去
//1.先把这些真实的DOM移入到内存中 fragment
let fragment = this.node2fragment(this.el);
//2、编译 =》 提前想要的元素元素节点 v-model 和文本节点 {{}}
this.compile(fragment);
//3、把编译的fragment在赛回到页面中去
this.el.appendChild(fragment);
}
}
/*专门写一些辅助方法*/
//判断是否是元素节点
isElementNode(node){
return node.nodeType === 1;
}
isDirective(name){
return name.includes('v-');
}
/*核心的方法*/
//1、需要将el中的内容全部放到内存中
node2fragment(el){
//文档碎片 内存中的dom节点
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){
fragment.appendChild(firstChild);
}
return fragment; //内存中的节点
}
//2、编译 =》 提前想要的元素元素节点 v-model 和文本节点 {{}}
compile(fragment){
//需要递归
let childNodes = fragment.childNodes;
//
Array.from(childNodes).forEach(node => {
if(this.isElementNode(node)){
//是元素节点,还需要深入的检查
//这里需要编译元素
this.compileElement(node);//编译 带 v-model 的元素
this.compile(node);
}else{
//文本节点
//这里需要编译文本
this.compileText(node);
}
});
}
compileElement(node){
//带v-model
let attrs = node.attributes;//取出当前节点的属性
Array.from(attrs).forEach(attr => {
//判断属性名字是不是包含v-
let attrName = attr.name;
if(this.isDirective(attrName)){
//取到对应的值放到节点中
let expr = attr.value;
//解构负值,将v-model中的model截取处理
let [,type] = attrName.split('-');
//node this.vm.$data expr v-model v-text v-html
//todo ...
CompileUtil[type](node,this.vm,expr);
}
})
}
compileText(node){
//带{{}}
let expr = node.textContent;//取文本中的内容
let reg = /\{\{([^}]+)\}\}/g; //{{a}}、{{b}}、{{c}}
if(reg.test(expr)){
// node this.vm.$data text
//todo ...
CompileUtil['text'](node,this.vm,expr);
}
}
}
CompileUtil = {
//获取示例上对应的示例
getVal(vm,expr){
expr = expr.split('.');
return expr.reduce((prev,next) => {
return prev[next];
},vm.$data);
},
getTextVal(vm,expr){
return expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
return this.getVal(vm,arguments[1]);
});
},
text(node,vm,expr){ //文本处理
let updateFn = this.updater['textUpdater'];
//{{message.a}} => 'hello,123获取编译文本后的结果
let value = this.getTextVal(vm,expr);
expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{ // arguments ["{{message.a}}", "message.a", 9, "↵ {{message.a}}↵ "]
new Watcher(vm,arguments[1],(newValue)=>{
//如果数据变化了,文本节点需要重新依赖的属性更新文本中的内容
updateFn && updateFn(node,this.getTextVal(vm,expr));
})
return arguments[1];
});
updateFn && updateFn(node,value);
},
setVal(vm,expr,value){ //[message,a]
expr = expr.split('.');
//收敛
return expr.reduce((prev,next,currentIndex)=>{
if(currentIndex === expr.length-1){
return prev[next] = value;
}
return prev[next];
},vm.$data)
},
model(node,vm,expr){ //输入框处理
let updateFn = this.updater['modelUpdater'];
//这里应该加一个监控,数据变化了 应该调用这个watch的callback
new Watcher(vm,expr,(newValue)=>{
//当值变化后会调用 cb,将新的值传递过去 ()
updateFn && updateFn(node,this.getVal(vm,expr));
});
node.addEventListener('input',(e)=>{
let newValue = e.target.value;
this.setVal(vm,expr,newValue)
})
updateFn && updateFn(node,this.getVal(vm,expr));
},
updater:{
//文本更新
textUpdater(node,value){
node.textContent = value;
},
//输入框更新
modelUpdater(node,value){
node.value = value;
}
}
};
总结
先放入代码片段里面,在用 compile
方法遍历元素节点,解析文本节点, 而且在遍历节点的同时,会 new watcher
添加回调来接受数据变化的通知。
3. Watcher
Watcher订阅者作为Observer和Compile之间通信的桥梁,先看代码
// 观察者的目的就是给需要变化的那个元素增加一个观察这,
//当数据变化后,执行对应的方法
//目的:用新值和老值进行比对,如果发生变化,就调用更新方法
class Watcher{
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
//先获取一下老的值
this.value = this.get();
}
//获取示例上对应的示例
getVal(vm,expr){
expr = expr.split('.')
return expr.reduce((prev,next) => {
return prev[next];
},vm.$data);
}
get(){
Dep.target = this;
let value = this.getVal(this.vm,this.expr);
Dep.target = null;
return value;
}
//对外暴露的方法
update(){
let newValue = this.getVal(this.vm,this.expr);
let oldValue = this.value;
if(newValue != oldValue){
this.cb(newValue); //调用watch的callback
}
}
}
总结
- 在自身实例化的时候, 往Dep 里面 push 自身
- 自身有个 update() 方法 以供调用 更新视图回调
- 在 dep.notice() 通知的时候,能调用自身的update()方法,并且触发Compile中绑定的回调。
4. mvvm
//因为 MVVM 可以 new,所以 MVVM 肯定是一个类
//用 es6写法定义
class MVVM{
//在类里面接受参数,例如,el,和data
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);
}
}
proxyData(data){
Object.keys(data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newValue){
data[key] = newValue;
}
})
})
}
}
主要是还是用Object.defineProperty
方法来劫持数据,这边使用代理,实现 this.xxx 代替 this.data.xxx 的效果。
总结
本文主要是参考 vue源码 ,来写的一个mvvm 小demo, 相信文中肯定有一些不严谨的思考和错误, 希望大家指出来,和大家共同进步。