温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么在vue中实现一个MVVM框架

发布时间:2021-04-13 17:33:21 来源:亿速云 阅读:235 作者:Leah 栏目:web开发

这篇文章给大家介绍怎么在vue中实现一个MVVM框架,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

<body>  <div id="mvvm-app">   {{name}}  </div>  <script src="./js/observer.js"></script>  <script src="./js/watcher.js"></script>  <script src="./js/compile.js"></script>  <script src="./js/mvvm.js"></script>  <script>   let vm = new MVVM({    el: "#mvvm-app",    data: {     name: "hello world"    },     })  </script> </body>

数据代理

1、什么是数据代理

在vue里面,我们将数据写在data对象中。但是我们在访问data里的数据时,既可以通过vm.data.name访问,也可以通过vm.name访问。这就是数据代理:在一个对象中,可以动态的访问和设置另一个对象的属性。

2、实现原理

我们知道静态绑定(如vm.name = vm.data.name)可以一次性的将结果赋给变量,而使用Object.defineProperty()方法来绑定则可以通过set和get函数实现赋值的中间过程,从而实现数据的动态绑定。具体实现如下:

let obj = {}; let obj1 = {  name: 'xiaoyu',  age: 18, } //实现origin对象代理target对象 function proxyData(origin,target){  Object.keys(target).forEach(function(key){   Object.defineProperty(origin,key,{//定义origin对象的key属性    enumerable: false,    configurable: true,    get: function getter(){     return target[key];//origin[key] = target[key];    },    set: function setter(newValue){     target[key] = newValue;    }   })  }) }

vue中的数据代理也是通过这种方式来实现的。

function MVVM(options) {  this.$options = options || {};  var data = this._data = this.$options.data;  var _this = this;//当前实例vm  // 数据代理  // 实现 vm._data.xxx -> vm.xxx   Object.keys(data).forEach(function(key) {   _this._proxyData(key);  });  observe(data, this);  this.$compile = new Compile(options.el || document.body, this); } MVVM.prototype = { _proxyData: function(key) {  var _this = this;  if (typeof key == 'object' && !(key instanceof Array)){//这里只实现了对对象的监听,没有实现数组的   this._proxyData(key);  }  Object.defineProperty(_this, key, {   configurable: false,   enumerable: true,   get: function proxyGetter() {    return _this._data[key];   },   set: function proxySetter(newVal) {    _this._data[key] = newVal;   }  }); }, };

实现Observe

1、双向数据绑定

数据变动  --->  视图更新

视图更新  --->  数据变动

要想实现当数据变动时视图更新,首先要做的就是如何知道数据变动了,可以通过Object.defineProperty()函数监听data对象里的数据,当数据变动了就会触发set()方法。所以我们需要实现一个数据监听器Observe,来对数据对象中的所有属性进行监听,当某一属性数据发生变化时,拿到最新的数据通知绑定了该属性的订阅器,订阅器再执行相应的数据更新回调函数,从而实现视图的刷新。

当设置this.name = 'hello vue'时,就会执行set函数,通知订阅器里的订阅者执行相应的回调函数,实现数据变动,对应视图更新。

function observe(data){  if (typeof data != 'object') {   return ;  }  return new Observe(data); } function Observe(data){  this.data = data;  this.walk(data); } Observe.prototype = {  walk: function(data){   let _this = this;   for (key in data) {    if (data.hasOwnProperty(key)){     let value = data[key];     if (typeof value == 'object'){      observe(value);     }     _this.defineReactive(data,key,data[key]);    }   }  },  defineReactive: function(data,key,value){   Object.defineProperty(data,key,{    enumerable: true,//可枚举    configurable: false,//不能再define    get: function(){     console.log('你访问了' + key);return value;    },    set: function(newValue){     console.log('你设置了' + key);     if (newValue == value) return;     value = newValue;     observe(newValue);//监听新设置的值    }   })  } }

2、实现一个订阅器

要想通知订阅者,首先得要有一个订阅器(统一管理所有的订阅者)。为了方便管理,我们会为每一个data对象的属性都添加一个订阅器(new Dep)。

订阅器里存着的是订阅者Watcher(后面会讲到),由于订阅者可能会有多个,我们需要建立一个数组来维护。一旦数据变化,就会触发订阅器的notify()方法,订阅者就会调用自身的update方法实现视图更新。

function Dep(){  this.subs = []; } Dep.prototype = {  addSub: function(sub){this.subs.push(sub);  },  notify: function(){   this.subs.forEach(function(sub) {    sub.update();   })  } }

每次响应属性的set()函数调用的时候,都会触发订阅器,所以代码补充完整。

Observe.prototype = {  //省略的代码未作更改  defineReactive: function(data,key,value){   let dep = new Dep();//创建一个订阅器,会被闭包在key属性的get/set函数内,因此每个属性对应唯一一个订阅器dep实例   Object.defineProperty(data,key,{    enumerable: true,//可枚举    configurable: false,//不能再define    get: function(){     console.log('你访问了' + key);     return value;    },    set: function(newValue){     console.log('你设置了' + key);     if (newValue == value) return;     value = newValue;     observe(newValue);//监听新设置的值     dep.notify();//通知所有的订阅者    }   })  } }

实现Complie

compile主要做的事情是解析模板指令,将模板中的data属性替换成data属性对应的值(比如将{{name}}替换成data.name值),然后初始化渲染页面视图,并且为每个data属性添加一个监听数据的订阅者(new Watcher),一旦数据有变动,收到通知,更新视图。

遍历解析需要替换的根元素el下的HTML标签必然会涉及到多次的DOM节点操作,因此不可避免的会引发页面的重排或重绘,为了提高性能和效率,我们把根元素el下的所有节点转换为文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中。

注:文档碎片本身也是一个节点,但是当将该节点append进页面时,该节点标签作为根节点不会显示html文档中,其里面的子节点则可以完全显示。

Compile解析模板,将模板内的子元素#text添加进文档碎片节点fragment。

function Compile(el,vm){  this.$vm = vm;//vm为当前实例  this.$el = document.querySelector(el);//获得要解析的根元素   if (this.$el){   this.$fragment = this.nodeToFragment(this.$el);   this.init();   this.$el.appendChild(this.$fragment);  }  } Compile.prototype = {  nodeToFragment: function(el){   let fragment = document.createDocumentFragment();   let child;   while (child = el.firstChild){    fragment.appendChild(child);//append相当于剪切的功能   }   return fragment;     }, };

compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:

因为我们的模板只含有一个文本节点#text,因此compileElement方法执行后会进入_this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'

Compile.prototype = {  nodeToFragment: function(el){   let fragment = document.createDocumentFragment();   let child;   while (child = el.firstChild){    fragment.appendChild(child);//append相当于剪切的功能   }   return fragment;     },    init: function(){   this.compileElement(this.$fragment);  },    compileElement: function(node){   let childNodes = node.childNodes;   const _this = this;   let reg = /\{\{(.*)\}\}/g;   [].slice.call(childNodes).forEach(function(node){        if (_this.isElementNode(node)){//如果为元素节点,则进行相应操作     _this.compile(node);    } else if (_this.isTextNode(node) && reg.test(node.textContent)){     //如果为文本节点,并且包含data属性(如{{name}}),则进行相应操作     _this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'    }        if (node.childNodes && node.childNodes.length){     //如果节点内还有子节点,则递归继续解析节点     _this.compileElement(node);         }   })  },  compileText: function(node,exp){//#text,'name'    compileUtil.text(node,this.$vm,exp);//#text,vm,'name'  },};

CompileText()函数实现初始化渲染页面视图(将data.name的值通过#text.textContent = data.name显示在页面上),并且为每个DOM节点添加一个监听数据的订阅者(这里是为#text节点新增一个Wather)。

let updater = {  textUpdater: function(node,value){    node.textContent = typeof value == 'undefined' ? '' : value;  }, }   let compileUtil = {  text: function(node,vm,exp){//#text,vm,'name'   this.bind(node,vm,exp,'text');  },    bind: function(node,vm,exp,dir){//#text,vm,'name','text'   let updaterFn = updater[dir + 'Updater'];   updaterFn && updaterFn(node,this._getVMVal(vm,exp));   new Watcher(vm,exp,function(value){    updaterFn && updaterFn(node,value)   });   console.log('加进去了');  } };

现在我们完成了一个能实现文本节点解析的Compile()函数,接下来我们实现一个Watcher()函数。

实现Watcher

我们前面讲过,Observe()函数实现data对象的属性劫持,并在属性值改变时触发订阅器的notify()通知订阅者Watcher,订阅者就会调用自身的update方法实现视图更新。

Compile()函数负责解析模板,初始化页面,并且为每个data属性新增一个监听数据的订阅者(new Watcher)。

Watcher订阅者作为Observer和Compile之间通信的桥梁,所以我们可以大致知道Watcher的作用是什么。

主要做的事情是:

在自身实例化时往订阅器(dep)里面添加自己。

自身必须有一个update()方法 。

待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调。

先给出全部代码,再分析具体的功能。

//Watcher function Watcher(vm, exp, cb) {  this.vm = vm;  this.cb = cb;  this.exp = exp;  this.value = this.get();//初始化时将自己添加进订阅器 }; Watcher.prototype = {  update: function(){   this.run();  },  run: function(){   const value = this.vm[this.exp];   //console.log('me:'+value);   if (value != this.value){    this.value = value;    this.cb.call(this.vm,value);   }  },  get: function() {    Dep.target = this; // 缓存自己   var value = this.vm[this.exp] // 访问自己,执行defineProperty里的get函数      Dep.target = null; // 释放自己   return value;  } } //这里列出Observe和Dep,方便理解 Observe.prototype = {  defineReactive: function(data,key,value){   let dep = new Dep();   Object.defineProperty(data,key,{    enumerable: true,//可枚举    configurable: false,//不能再define    get: function(){     console.log('你访问了' + key);     //说明这是实例化Watcher时引起的,则添加进订阅器     if (Dep.target){      //console.log('访问了Dep.target');      dep.addSub(Dep.target);     }     return value;    },   })  } } Dep.prototype = {  addSub: function(sub){this.subs.push(sub);  }, }

我们知道在Observe()函数执行时,我们为每个属性都添加了一个订阅器dep,而这个dep被闭包在属性的get/set函数内。所以,我们可以在实例化Watcher时调用this.get()函数访问data.name属性,这会触发defineProperty()函数内的get函数,get方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcher实例就能收到更新通知。

那么Watcher()函数中的get()函数内Dep.taeger = this又有什么特殊的含义呢?我们希望的是在实例化Watcher时将相应的Watcher实例添加一次进dep订阅器即可,而不希望在以后每次访问data.name属性时都加入一次dep订阅器。所以我们在实例化执行this.get()函数时用Dep.target = this来标识当前Watcher实例,当添加进dep订阅器后设置Dep.target=null。

实现VMVM

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

function MVVM(options) {  this.$options = options || {};  var data = this._data = this.$options.data;  var _this = this;  // 数据代理  // 实现 vm._data.xxx -> vm.xxx   Object.keys(data).forEach(function(key) {   _this._proxyData(key);  });  observe(data, this);  this.$compile = new Compile(options.el || document.body, this); }

关于怎么在vue中实现一个MVVM框架就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI