1. MVVM的概念
model-view-viewModel,通过数据劫持+发布订阅模式来实现。
mvvm是一种设计思想。Model代表数据模型,可以在model中定义数据修改和操作的业务逻辑;view表示ui组件,负责将数据模型转换为ui展现出来,它做的是数据绑定的声明、 指令的声明、 事件绑定的声明。而viewModel是一个同步view和model的对象。在mvvm框架中,view和model之间没有直接的关系,它们是通过viewModel来进行交互的。mvvm不需要手动操作dom,只需要关注业务逻辑就可以了。
mvvm和mvc的区别在于:mvvm是数据驱动的,而MVC是dom驱动的。mvvm的优点在于不用操作大量的dom,不需要关注model和view之间的关系,而MVC需要在model发生改变时,需要手动的去更新view。大量操作dom使页面渲染性能降低,使加载速度变慢,影响用户体验。
2. mvvm的优点
- 1、低耦合性 view 和 model 之间没有直接的关系,通过 viewModel 来完成数据双向绑定。
- 2、可复用性 组件是可以复用的。可以把一些数据逻辑放到一个 viewModel 中,让很多 view 来重用。
- 3、独立开发 开发人员专注于 viewModel ,设计人员专注于view。
- 4、可测试性 ViewModel 的存在可以帮助开发者更好地编写测试代码。
3. mvvm的缺点
- 1、bug很难被调试,因为数据双向绑定,所以问题可能在 view 中,也可能在 model 中,要定位原始bug的位置比较难,同时view里面的代码没法调试,也添加了bug定位的难度。
- 2、一个大的模块中的 model 可能会很大,长期保存在内存中会影响性能。
- 3、对于大型的图形应用程序,视图状态越多, viewModel 的构建和维护的成本都会比较高。
4. mvvm的双向绑定
mvvm 的核心是数据劫持、数据代理、数据编译和"发布订阅模式"。
4.1 数据劫持--就是给对象属性添加get,set钩子函数
- 1、观察对象,给对象增加 Object.defineProperty
- 2、vue的特点就是新增不存在的属性不会给该属性添加 get 、 set 钩子函数。
- 3、深度响应。循环递归遍历 data 的属性,给属性添加 get , set 钩子函数。
- 4、每次赋予一个新对象时(即调用 set 钩子函数时),会给这个新对象进行数据劫持( defineProperty )。
1 //通过set、get钩子函数进行数据劫持
2 function defineReactive(data){
3 Object.keys(data).forEach(key=>{
4 const dep=new Dep();
5 let val=data[key];
6 this.observe(val);//深层次的监听
7 Object.defineProperty(data,key,{
8 get(){
9 //添加订阅者watcher(为每一个数据属性添加订阅者,
以便实时监听数据属性的变化——订阅)
10 Dep.target&&dep.addSub(Dep.target);
11 //返回初始值
12 return val;
13 },set(newVal){
14 if(val!==newVal){
15 val=newVal;
16 //通知订阅者,数据变化了(发布)
17 dep.notify();
18 return newVal;
19 }
20 }
21 })
22 })
23 }
4.2 数据代理
将 data
,methods, compted
上的数据挂载到vm
实例上。让我们不用每次获取数据时,都通过 mvvm._data.a.b 这种方式,而可以直接通过 mvvm.b.a 来获取。
1 class MVVM{
2 constructor(options){
3 this.$options=options;
4 this.$data=options.data;
5 this.$el=options.el;
6 this.$computed=options.computed;
7 this.$methods=options.methods;
8 //劫持数据,监听数据的变化
9 new Observer(this.$data);
10 //将数据挂载到vm实例上
11 this._proxy(this.$data);
12 //将方法也挂载到vm上
13 this._proxy(this.$methods);
14 //将数据属性挂载到vm实例上
15 Object.keys(this.$computed).forEach(key=>{
16 Object.defineProperty(this,key,{
17 get(){
//将vm传入computed中
18 return this.$computed[key].call(this);
19 }
20 })
21 })
22 //编译数据
23 new Compile(this.$el,this)
24 };
25 //私有方法,用于数据劫持
26 _proxy(data){
27 Object.keys(data).forEach(key=>{
28 Object.defineProperty(this,key,{
29 get(){
30 return data[key]
31 }
32 })
33 })
34
35 }
36 }
4.3 数据编译
把 { {}}
, v-model , v-html , v-on
,里面的对应的变量用data里面的数据进行替换。
class Compile{
2 constructor(el,vm){
3 this.el=this.isElementNode(el)?el:document.querySelector(el);
4 this.vm=vm;
5 let fragment=this.nodeToFragment(this.el);
6 //编译节点
7 this.compile(fragment);
8 //将编译后的代码添加到页面
9 this.el.appendChild(fragment);
10 };
11 //核心编译方法
12 compile(node){
13 const childNodes=node.childNodes;
14 [...childNodes].forEach(child=>{
15 if(this.isElementNode(child)){
16 this.compileElementNode(child);
17 //如果是元素节点就还得递归编译
18 this.compile(child);
19 }else{
20 this.compileTextNode(child);
21 }
22 })
23
24 };
25 //编译元素节点
26 compileElementNode(node){
27 const attrs=node.attributes;
28 [...attrs].forEach(attr=>{
29 //attr是一个对象
30 let {name,value:expr}=attr;
31 if(this.isDirective(name)){
32 //只考虑到v-html和v-model的情况
33 let [,directive]=name.split("-");
34 //考虑v-on:click的情况
35 let [directiveName,eventName]=directive.split(":");
36 //调用不同的指令来进行编译
37 CompileUtil[directiveName](node,this.vm,expr,eventName);
38 }
39 })
40 };
41 //编译文本节点
42 compileTextNode(node){
43 const textContent=node.textContent;
44 if(/\{\{(.+?)\}\}/.test(textContent)){
45 CompileUtil["text"](node,this.vm,textContent)
46 }
47 };
48 //将元素节点转化为文档碎片
49 nodeToFragment(node){
50 //将元素节点缓存起来,统一编译完后再拿出来进行替换
51 let fragment=document.createDocumentFragment();
52 let firstChild;
53 while(firstChild=node.firstChild){
54 fragment.appendChild(firstChild);
55 }
56 return fragment;
57 };
58 //判断是否是元素节点
59 isElementNode(node){
60 return node.nodeType===1;
61 };
62 //判断是否是指令
63 isDirective(attr){
64 return attr.includes("v-");
65 }
66 }
67 //存放编译方法的对象
68 CompileUtil={
69 //根据data中的属性获取值,触发观察者的get钩子
70 getVal(vm,expr){
71 const data= expr.split(".").reduce((initData,curProp)=>{
72 //会触发观察者的get钩子
73 return initData[curProp];
74 },vm)
75 return data;
76 },
77 //触发观察者的set钩子
78 setVal(vm,expr,value){
79 expr.split(".").reduce((initData,curProp,index,arr)=>{
80 if(index===arr.length-1){
81 initData[curProp]=value;
82 return;
83 }
84 return initData[curProp];
85 },vm)
86 },
87 getContentValue(vm,expr){
88 const data= expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
89 return this.getVal(vm,args[1]);
90 });
91 return data;
92 },
93 model(node,vm,expr){
94 const value=this.getVal(vm,expr);
95 const fn=this.updater["modelUpdater"];
96 fn(node,value);
97 //监听input的输入事件,实现数据响应式
98 node.addEventListener('input',e=>{
99 const value=e.target.value;
100 this.setVal(vm,expr,value);
101 })
102 //观察数据(expr)的变化,并将watcher添加到订阅者队列中
103 new Watcher(vm,expr,newVal=>{
104 fn(node,newVal);
105 });
106 },
107 text(node,vm,expr){
108 const fn=this.updater["textUpdater"];
109 //将{
{person.name}}中的person.james替换成james
110 const content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
111 //观察数据的变化
112 new Watcher(vm,args[1],()=>{
113 // this.getContentValue(vm,expr)获取textContent被编译后的值
114 fn(node,this.getContentValue(vm,expr))
115
116 })
117 return this.getVal(vm,args[1]);
118 })
119 fn(node,content);
120 },
121 html(node,vm,expr){
122 const value=this.getVal(vm,expr);
123 const fn=this.updater["htmlUpdater"];
124 fn(node,value);
125 new Watcher(vm,expr,newVal=>{
126 //数据改变后,再次替换数据
127 fn(node,newVal);
128 })
129 },
130 on(node,vm,expr,eventName){
131 node.addEventListener(eventName,e=>{
132 //调用call将vm实例(this)传到方法中去
133 vm[expr].call(vm,e);
134 })
135 },
136 updater:{
137 modelUpdater(node,value){
138 node.value=value
139 },
140 htmlUpdater(node,value){
141 node.innerHTML=value;
142 },
143 textUpdater(node,value){
144
145 node.textContent=value;
146 }
147 }
148 }
4.4、发布订阅
发布订阅主要靠的是数组关系,订阅就是放入函数(就是将订阅者添加到订阅队列中),发布就是让数组里的函数执行(在数据发生改变的时候,通知订阅者执行相应的操作)。消息的发布和订阅是在观察者的数据绑定中进行数据的——在get钩子函数被调用时进行数据的订阅(在数据编译时通过 new Watcher() 来对数据进行订阅
),在set钩子函数被调用时进行数据的发布。
1 //消息管理者(发布者),在数据发生变化时,通知订阅者执行相应的操作
2 class Dep{
3 constructor(){
4 this.subs=[];
5 };
6 //订阅
7 addSub(watcher){
8 this.subs.push(watcher);
9 };
10 //发布
11 notify(){
12 this.subs.forEach(watcher=>watcher.update());
13 }
14 }
15 //订阅者,主要是观察数据的变化
16 class Watcher{
17 constructor(vm,expr,cb){
18 this.vm=vm;
19 this.expr=expr;
20 this.cb=cb;
21 this.oldValue=this.get();
22 };
23 get(){
24 Dep.target=this;
25 const value=CompileUtil.getVal(this.vm,this.expr);
26 Dep.target=null;
27 return value;
28 };
29 update(){
30 const newVal=CompileUtil.getVal(this.vm,this.expr);
31 if(this.oldValue!==newVal){
32 this.cb(newVal);
33 }
34 }
35 }
36 //观察者
37 class Observer{
38 constructor(data){
39 this.observe(data);
40 };
41 //使数据可响应
42 observe(data){
43 if(data&&typeof data==="object"){
44 this.defineReactive(data)
45 }
46 };
47 defineReactive(data){
48 Object.keys(data).forEach(key=>{
49 const dep=new Dep();
50 let val=data[key];
51 this.observe(val);//深层次的监听
52 Object.defineProperty(data,key,{
53 get(){
54 //添加订阅者watcher(为每一个数据属性添加订阅者,
以便实时监听数据属性的变化——订阅)
55 Dep.target&&dep.addSub(Dep.target);
56 //返回初始值
57 return val;
58 },set(newVal){
59 if(val!==newVal){
60 val=newVal;
61 //通知订阅者,数据变化了(发布)
62 dep.notify();
63 return newVal;
64 }
65 }
66 })
67 })
68 }
69 }