手机版

vue实现了一个简单的MVVM框架

时间:2021-08-30 来源:互联网 编辑:宝哥软件园 浏览:

不知不觉接触前端已经半年了。越来越意识到学习知识不应该只停留在有用的层面,这是我在学习jQuery一段时间后意识到的。

虽然jQuery只是一个JS代码库,但是只要学习一两天一些基本的JS操作,就可以快速掌握jQUery的基本语法,并熟练使用。但是,如果你不了解jQuery库背后的实现原理,我相信如果你停止使用jQuery一段时间,你就会忘记jQuery的一切,这可能是知道原因的结果。

最近在学vue的时候又经历了这样的困惑。虽然我能熟练掌握vue的基本使用方法,但也可以说说MV*模式、数据劫持、双向数据绑定、数据代理。但是如果再深入一点,就有点难了。所以这几天下定决心要研究大量的技术文章(刚开始试着看早期的源代码,但是vue和jQuery不在一个层次。与jQuery相比,vue才是真正的前端框架。我别无选择,只能弃坑改看技术博客。)我瞥见了vue。最后,尝试实践所学,实现了一个基于数据代理、数据劫持、模板解析和双向绑定的小vue框架。

Tips:文章是根据各个模块的实现依赖来分析的,但是阅读的时候可以根据vue的执行顺序来分析,对初学者来说比较友好。推荐的阅读顺序是:VMVM、数据代理、观察器、Complie和Watcher。

源代码:https://github.com/yuliangbin/MVVM

功能演示如下:

数据代理终端

以下面的模板为例,根元素“#mvvm-app”中只有一个文本节点#text需要替换,#text的内容为{{name}}。让我们用下面的模板详细看看VUE框架的一般实现过程。

body div id=' mvvm-app“{ name } }/div脚本src='http:/js/observer . js '/script script src=' http :/js/watcher . js '/script script src=' http :/js/compile . js '/script script src=' http :/js/mvvm . js '/script script letvm=new mvvm({ El : ' # mvvm-app ',data: {name:' hello world。

1.什么是数据代理

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

2.实施原则

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

let obj={ };Letobj1={name:' xiaoyu ',age:18,}//实现origin对象代理目标对象函数proxyData(origin,Target) {object。按键(目标)。foreach (function (key) {object。defineproperty (origin,key,{//)定义origin对象的键属性:可枚举3360 false,可配置3360 true,get : function getter(){ return target[key];//origin[key]=target[key];},set:函数setter(new value){ target[key]=new value;} })} } VUE的数据代理也是这样实现的。

功能MVVM(选项){这。$ options=options | | { };var数据=这个。_data=this。$ options.datavar _ this=this//当前实例vm //数据代理//实现vm。_ data.xxx-VM.xxxobject.keys(数据)。foreach (function (key) {_ this。_ proxydata(密钥);});观察(数据,这个);这个。$ Compile=new Compile(options . El | | document . body,this);} mvvm . prototype={ _ proxy data :函数(键){ var _ this=thisif (typeof key=='object '!(数组的关键实例){//这里,只监控对象,但是这个。未实现数组的_proxyData(键);} Object.defineProperty(_this,key,{ configurable: false,enumerable: true,get:函数proxyGetter() { return _this。_ data[key];},set:函数proxySetter(newVal) { _this。_ data[key]=NewVal;} });},};实施观察

1.双向数据绑定

数据更改-视图更新

查看更新-数据更改

为了在数据发生变化时更新视图,首先要做的是如何知道数据的变化。您可以通过Object.defineProperty()函数监控数据对象中的数据,并在数据发生变化时触发set()方法。因此,我们需要实现一个数据监听器Observe来监控数据对象中的所有属性。当某个属性的数据发生变化时,我们得到最新的数据通知与该属性绑定的订阅者,然后订阅者执行相应的数据更新回调函数来刷新视图。

当这个。name=' hellowue '设置完成后,将执行set函数,通知订阅者中的订阅者执行相应的回调函数,从而实现数据更改和相应的视图更新。

函数observe(data){ if (typeof data!=' object '){ return;}返回新的Observe(数据);}函数Observe(data){ this . data=data;this.walk(数据);} observe . prototype={ walk : function(data){ let _ this=this;for(数据中的键){ if (data.hasOwnProperty(键)){ let value=data[key];if(type of value==' object '){ observe(value);} _this.defineReactive(数据、键、数据[键]);}}},定义reactive :函数(数据、键、值){object。定义属性(数据,键,{可枚举: true,//可配置: false,///不能再定义get3360函数(){console.log('您访问过'键)。返回值;},设置:函数(新值){console.log ('you set '键);if (newValue==value)返回;value=newValue观察(新值);//收听新设置的值}})} 2。实现订户

要通知订户,您必须首先有一个订户(以统一的方式管理所有订户)。为了便于管理,我们将为每个数据对象的属性添加一个订阅者(新Dep)。

订户观察器(稍后描述)存储在订户中。由于可能有多个订阅者,我们需要创建一个数组来维护它。一旦数据发生变化,就会触发订阅者的notify()方法,订阅者会调用自己的update方法来更新视图。

function Dep(){ this . subs=[];} dep . prototype={ addsub : function(sub){ this . subs . push(sub);},notify : function(){ this . subs . foreach(function(sub){ sub . update();})}}每次调用response属性的set()函数都会触发订阅者,所以代码补全了。

Observe.prototype={//省略的代码不变。定义电抗:函数(数据、键、值){ let dep=new dep();//创建订户将在key属性的get/set函数中关闭,因此每个属性都对应于唯一的订户dep实例对象。defineproperty (data,key,{enumerable: true,//configuration ble : false,///您不能再定义get3360 function () {console.log('您可以访问。返回值;},设置:函数(新值){console.log ('you set '键);if (newValue==value)返回;value=newValue观察(新值);//收听新设置的值dep . notify();//通知所有订阅者}})}实现Complie

编译器主要做的是解析模板指令,用数据属性对应的值替换模板中的数据属性(例如,用data.name的值替换{{name}}),然后初始化呈现页面视图,为每个数据属性添加一个新的Watcher。一旦数据发生变化,它将收到通知并更新视图。

遍历和解析需要替换的根元素el下的HTML标签必然会涉及到很多dom节点操作,这必然会导致页面重排或者重绘。为了提高性能和效率,我们将根元素el下的所有节点转换为文档片段进行解析和编译操作,然后将片段添加回原始的真实DOM节点。

注意:文档片段本身也是一个节点,但是当节点追加到页面中时,作为根节点的节点标签不会显示在html文档中,但是其中的子节点可以完全显示。

Compile解析模板,并将模板中的子元素#text添加到文档片段节点片段中。

函数编译(el,vm){ this。$ vm=vm//vm是此的当前实例。$ El=document . queryselector(El);//获取要分析的根元素(如果这。$ El){这个。$ fragment=这个。nodetofragment(这。$ El);this . init();这个。$el.appendChild(这个。$ fragment);} } compile . prototype={ nodetofragments : function(El){ let fragment=document . createdocumentfragment();让孩子;while(child=El . first child){ fragment . appendchild(child);//append相当于cutting函数}返回片段;},};CompileElement方法将遍历所有节点及其子节点,进行扫描、解析和编译,调用对应的指令渲染函数进行数据渲染,调用对应的指令更新函数进行绑定。有关详细信息,请参见代码和注释:

我们的模板只包含一个文本节点#text,所以compileElement方法将输入_ this。compiletext(节点,reg。exec(节点。textcontent) [1])。//#文本,“名称”

compile . prototype={ nodetofragments : function(El){ let fragment=document . createdocumentfragment();让孩子;while(child=El . first child){ fragment . appendchild(child);//append相当于cutting函数}返回片段;},init:函数(){ this.compileElement(this。$ fragment);},compileElement:函数(节点){ let child nodes=node . child nodes;const _ this=this让reg=/\{\{(。*)\ } \ }/g;[].切片。调用(childnodes)。foreach(函数(节点){if (_ this。iselementnode(node)){//如果是元素节点,执行相应的操作_ this.compile(node);} else if (_ this。istex node(node)reg。测试(节点。text content)){//如果是文本节点且包含数据属性(如{{name}}),执行相应的操作_ this。compiletext(节点,reg。exec(节点。text content)[1]);//# text,' name'} if(节点。childnodes节点。child nodes . length){//如果节点中有子节点,递归继续解析node _ this.compileelement(node);} }) },compileText:函数(node,exp){//#text,' name' compileUtil.text(node,this。$vm,exp);//#text,vm,' name' },};CompileText()函数初始化呈现的页面视图(通过#text.textContent=data.name在页面上显示data.name的值),并添加一个订阅者来监听每个DOM节点的数据(这里,为#text节点添加一个Wather)。

让更新程序={ textUpdater:函数(节点,值){ node . textcontent=type of value==' undefined '?' :值;},}让compileUtil={ text : function(node,vm,exp){//#text,vm,' name' this.bind(node,vm,exp,' text ');},bind:函数(node,vm,exp,dir){//#text,vm,' name ',' text ' let updaterFn=Updater[dir ' Updater '];updaterFn updaterFn(节点,这个。_getVMVal(vm,exp));new Watcher(vm,exp,function(value){ updaterFn updaterFn(node,value)});Console.log('已添加');}};现在我们已经完成了一个可以解析文本节点的Compile()函数,然后我们实现了一个Watcher()函数。

实现观察器

前面我们说过,Observe()函数实现了数据对象的属性劫持,当属性值发生变化时,触发订阅者的notify()通知订阅者Watcher,订阅者会调用自己的update方法来更新视图。

Compile()函数负责解析模板,初始化页面,并为每个数据属性添加一个新的Watcher。

观察者订阅者充当观察者和编译者之间的桥梁,所以我们可以大致知道观察者的角色是什么。

主要要做的是:

实例化自己时,将自己添加到订阅服务器(dep)中。

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

当通知属性更改dep.notice()时,它可以调用自己的update()方法,并在Compile中触发回调绑定。

先给出所有代码,然后分析具体功能。

//Watcherfunction Watcher(vm,exp,CB){ this . VM=VM;this.cb=cbthis.exp=expthis . value=this . get();//在初始化期间将自己添加到订阅服务器};watcher . prototype={ update : function(){ this . run();},run : function(){ const value=this . VM[this . exp];//console.log('me: '值);if(值!=this . value){ this . value=value;this.cb.call(this.vm,value);} },get : function(){ Dep . target=this;//自己缓存var值=this.vm[this.exp] //自己访问并在defineProperty中执行get函数Dep.target=null//释放你的返回值;} }//这里列出了Observe和Dep,以便于理解observe.prototype={定义reactive :函数(数据、键、值){让Dep=new Dep();Object.defineproperty (data,key,{enumerable: true,//configuration ble : false,///不能再定义get 3360 function(){ console . log(' your visible ' key);//表示这是由实例化Watcher引起的,如果(Dep . target){//console . log(' Dep . target被访问')则将其添加到订阅服务器;dep . AddSub(Dep . target);}返回值;},})} } dep . prototype={ addsub : function(sub){ this . subs . push(sub);},}我们知道,在执行Observe()函数时,我们为每个属性添加一个subscriber dep,这个dep在属性的get/set函数中是关闭的。因此,在实例化Watcher时,我们可以调用这个. get()函数来访问data.name属性,这将触发defineProperty()函数中的get函数。当执行get方法时,当前的观察器实例将被添加到属性的subscriber dep中,这样当属性值更改时,观察器实例就可以收到更新通知。

那么Dep.taeger=this在Watcher()函数中get()函数的特殊意义是什么呢?我们想要的是在实例化Watcher时将相应的Watcher实例添加到dep订阅者中一次,并且我们不希望以后每次访问data.name属性时都添加dep订阅者一次。因此,我们在实例化this.get()函数时使用Dep.target=this来标识当前Watcher实例,并在将其添加到Dep订阅服务器后将其设置为Dep.target=null。

实施虚拟机管理程序

作为数据绑定的入口,MVVM集成了Observer、Compile和Watcher,通过Observer监控自身模型数据的变化,通过编译解析编译模板指令,最后利用Watcher在Observer和编译器之间搭建通信桥梁,实现数据的变化-视图更新。查看交互更改(输入)-数据模型更改的双向绑定效果。

功能MVVM(选项){这。$ options=options | | { };var数据=这个。_data=this。$ options.datavar _ this=this//数据代理//实现VM。_ data.xxx-VM.xxxobject.keys(数据)。foreach (function (key) {_ this。_ proxydata(密钥);});观察(数据,这个);这个。$ Compile=new Compile(options . El | | document . body,this);}

版权声明:vue实现了一个简单的MVVM框架是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。