宝哥软件园

详细说明vue双向绑定的原理和实现

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

前言

我使用vue已经有一段时间了。虽然我对它的双向约束原理有一个大致的了解,但是我并没有探究它的原理实现。所以这次花了几个晚上查数据,看相关源代码,也实现了vue双向绑定版本的简单版本。首先,我将在结果图上画出你:

代码:渲染:

它看起来像vue的使用方式吗?接下来,我们将从原理到实现,从简单到困难,一步步实现这个SelfVue。因为这篇文章只是为了学习和分享,所以它简单地实现了这个原则,而没有考虑太多的情况和设计。如有建议,请提出。

本文主要介绍两个主要内容:

1.vue数据双向绑定的原理。

2.vue简易版的实现过程主要实现了{{}}、v-model、事件指令等功能。

相关代码地址:https://github.com/canfoo/self-vue

vue数据的双向绑定原理

vue数据的双向绑定是通过数据劫持结合发布者-订阅者模式实现的,所以如果vue劫持数据,我们可以先看看通过控制台输出一个定义在vue初始化数据上的对象是什么。

代码:

var VM=new Vue({ data : { obj : { a : 1 } },create d : function(){ console . log(this . obj);}});结果:

我们可以看到属性A有两个对应的get和set方法。为什么有两种以上的方法?因为vue通过Object.defineProperty()实现了数据劫持。

Object.defineProperty()用于什么?它可以控制一个对象属性的一些唯一操作,比如读写权限,是否可以枚举。这里,我们主要研究它对应的两个描述属性get和set。如果你不熟悉它的用法,请点击这里阅读更多用法。

在正常情况下,我们可以轻松打印出对象的属性数据:

Var Book={name: 'vue权威指南' };控制台日志(book . name);//vue权威指南如果在执行console.log(book.name)时,想直接在书名中添加书名,应该怎么做?或者听什么对象Book的属性值。这时,Object.defineProperty()就派上了用场,代码如下:

var Book={ } var name=object . definepreproperty(Book,' name ',{ set : function(value){ name=value;Console.log('您取了一个名为' value '的标题);},get : function(){ return ' 《' + name + '》 ' })book . name=' vue权威指南';//你取了一个标题叫vue权威指南console . log(book . name);//《vue权威指南》我们通过Object.defineProperty()设置对象Book的名称属性,重写其get和set。顾名思义,get是读取name属性值触发的函数,set是设置name属性值触发的函数。因此,当执行语句Book.name=' vue authoritative guide '时,控制台将打印出'您取了一个名为vue authorized guide '的标题,然后,在读取该属性时,它将输出' 《vue权威指南》 ',因为我们在get函数中处理了该值。如果我们此时执行以下语句,控制台将输出什么?

控制台日志(书籍);

结果:

乍一看,它看起来有点类似于我们打印vue数据,表明vue确实通过这种方法劫持了数据。接下来,我们通过其原理实现一个简单版本的mvvm双向绑定代码。

思维分析

mvvm的实现主要包括两个方面:数据变更更新视图,以及查看变更更新数据:

关键是数据如何更新视图,因为视图实际上可以通过事件监控来更新数据,例如,输入标签可以监控“输入”事件。所以我们重点分析当数据发生变化时如何更新视图。

数据更新视图的重点是如何知道数据已经更改。只要知道数据发生了变化,接下来的事情就好办了。其实我们已经给出了如何知道数据发生了变化的答案,那就是通过Object.defineProperty()在属性上设置一个set函数,当数据发生变化时就会触发这个函数,所以我们只需要把一些需要更新的方法放在这里就可以实现数据更新视图了。

有了思路,下一步就是实现过程。

实施程序

我们已经知道,要实现数据的双向绑定,必须先劫持和监控数据,所以需要设置一个监听器Observer来监控所有属性。如果属性改变了,你需要告诉订阅者观察者它是否需要更新。因为有许多订阅者,所以我们需要一个消息订阅者Dep来收集这些订阅者,然后在侦听器观察者和订阅者观察者之间统一管理它们。然后,我们还需要有一个指令解析器Compile来扫描和解析每个节点元素,将相关指令初始化为订阅者Watcher,并替换模板数据或绑定相应的函数。此时,当订阅者Watcher收到相应属性的变化时,会执行相应的更新功能来更新视图。因此,我们将执行以下三个步骤来实现数据的双向绑定:

1.实现一个监听器观察器,用于劫持和监听所有属性,并在发生任何变化时通知订阅者。

2.实现一个订阅者Watcher,它可以接收属性更改的通知,并执行相应的函数来更新视图。

3.实现一个解析器Compile,可以扫描解析各个节点的相关指令,并根据初始化模板数据初始化对应的订阅者。

流程图如下:

1.实现一个观察者

Observer是一个数据监听器,其实现的核心方法是前面提到的Object.defineProperty()。如果要监视所有属性,可以通过递归方法遍历所有属性值,并使用Object.defineProperty()进行处理。下面的代码实现了一个观察者。

函数defineReactive(数据、键、值){ observe(val);//递归遍历object.defineproperty (data,key,{enumerable:true,configurable:true,get : function(){ returnval;},set:函数(NewVal){ Val=NewVal;console . log(' attribute ' key '已被侦听,现在其值为:“' NewVal . ToString()' '”);} });}函数observe(data) { if(!数据||数据类型!==' object '){ return;} Object.keys(数据)。forEach(函数(键){ defineReactive(数据,键,数据[键]);});};var library={ book 1: { name : ' ' },book 2: ' ' };观察(图书馆);Library.book1.name='vue权威指南';//属性名已经被监控,现在它的值是:“vue权威指南”library.book2=“没有这样的书”;//属性book2已经被监控,现在它的值是:“没有这样的book”。在思想分析中,有必要创建一个可以容纳订阅者的消息订阅者Dep。订阅者Dep主要负责收集订阅者,然后在属性发生变化时执行相应订阅者的更新功能。因此,很明显,订阅者需要有一个容器,这就是列表,上面的观察者被稍微修改并植入到消息订阅者中:

函数defineReactive(数据、键、值){ observe(val);//递归遍历所有子属性var Dep=new Dep();Object.defineproperty (data,key,{enumerable:true,configurable:true,get:function () {if(需要添加订阅方){dep.addSub(观察方));//在此添加订户} return val},set:函数(NewVal){ if(Val===NewVal){ return;} val=newValconsole . log(' attribute ' key '已被侦听,现在其值为:“' NewVal . ToString()' '”);dep . notify();//如果数据发生变化,通知所有订阅者} });} function Dep(){ this . subs=[];} dep . prototype={ addsub : function(sub){ this . subs . push(sub);},notify : function(){ this . subs . foreach(function(sub){ sub . update();});}};从代码来看,我们设计了subscriber Dep,在getter中添加一个subscriber,就是触发Watcher的初始化,所以需要判断是否添加subscriber。至于具体的设计方案,下面会详细介绍。在setter函数中,如果数据发生变化,将通知所有订阅者,订阅者将执行相应的更新函数。到目前为止,已经实现了一个相对完整的观察者,然后我们开始设计观察者。

2.实现观察器

订阅者观察者在初始化时需要将自己添加到订阅者Dep中,那么如何添加呢?我们已经知道监听器Observer在get函数中执行添加订阅者watcher的操作,所以我们只需要在订阅者Watcher初始化时启动相应的get函数来执行添加订阅者的操作。如何触发get函数可以像获取相应的属性值一样简单。核心原因是我们使用Object.defineProperty()进行数据监控。这里还有一个细节需要处理。我们只需要在subscriber Watcher初始化的时候添加订阅者,所以我们需要做一个判断操作,这样我们就可以对订阅者做一些事情:在Dep.target上缓存订阅者,然后在添加成功之后删除他们。订户观察器实现如下:

函数Watcher(vm,exp,CB){ this . CB=CB;this.vm=vmthis.exp=expthis . value=this . get();//将您自己添加到订阅者}观察器。prototype={ update : function(){ this。run();},run : function(){ var value=this . VM . data[this . exp];var oldVal=this.valueif(值!==old VaL){ this . value=value;this.cb.call(this.vm,value,oldVal);} },get : function(){ Dep . target=this;//缓存自己的var值=this . VM . data[this . exp]//在侦听器中强制get函数Dep.target=null//释放你的返回值;}};此时,我们需要对监听器Observer做一个细微的调整,主要对应Watcher类原型上的get函数。调整需求在于定义活动功能:

函数defineReactive(数据、键、值){ observe(val);//递归遍历所有子属性var Dep=new Dep();Object.defineproperty (data,key,{enumerable:true,configurable:true,get : function(){ if(dep . target)})。//确定是否需要添加subscriber dep . addsub(Dep . target);//在此添加订户} return val},set:函数(NewVal){ if(Val===NewVal){ return;} val=newValconsole . log(' attribute ' key '已被侦听,现在其值为:“' NewVal . ToString()' '”);dep . notify();//如果数据发生变化,通知所有订阅者} });} Dep.target=null到目前为止,简单版的Watcher已经设计好了。此时,我们只需要将Observer与Watcher关联起来,就可以实现一个简单的双向数据绑定。因为还没有尚未设计好的解析器Compile,我们将把模板数据写死。假设模板上还有一个节点,id号为‘name’,双向绑定的绑定变量也是‘name’,用两个花括号包装(这只是为了掩盖,暂时没用)。模板如下:

正文h1 id=' name ' { name } }/h1/正文

此时,我们需要将观察者与观察者联系起来:

函数SelfVue (data,el,exp){ this . data=data;观察(数据);El . innerHTMl=this . data[exp];//初始化模板数据newwatcher的值(this,exp,function(value){ El . innerhtml=value;});归还这个;}然后在页面上添加以下SelfVue类,就可以实现数据的双向绑定:

body h1 id=' name“{ name } }/h1/body script src=' http : js/observer . js '/script script src=' http : js/watcher . js '/script script src=' http : js/index . js '/script script type=' text/JavaScript ' var ele=document . queryselector(# name ');var self vue=new self vue({ name : ' hello world ' },ele,' name ');window . settimeout(function(){ console . log('名称值已更改'));self vue . data . name=' canfoo ';}, 2000);/script此时打开页面,可以看到页面开头显示‘hello world’,2s后变成‘canfoo’。最后,我们已经完成了一半,但是还有一个细节问题。当我们赋值时,我们使用这个形式‘selfvue . data . name=‘canfoo’’,我们的理想形式是‘selfvue . name=‘canfoo’。为了实现这种形式,我们需要在新的自拍时充当代理。要让访问selfVue的属性代理访问selfVue.data的属性,实现原理是使用Object.defineProperty()将属性值再包装一层:

函数SelfVue (data,el,exp){ var self=this;this.data=数据;Object.keys(数据)。forEach(函数(键){ self.proxyKeys(键);//绑定代理属性});观察(数据);El . innerHTMl=this . data[exp];//初始化模板数据newwatcher的值(this,exp,function(value){ El . innerhtml=value;});归还这个;} self vue . prototype={ ProxyKey s 3360 function(key){ var self=this;object . defineperOperty(this,key,{ enumerable: false,configurable: true,get : function proxyGetter(){ return self . data[key];},set:函数proxy setter(NewVal){ self . data[key]=NewVal;} });}}现在我们可以直接以' selfVue.name='canfoo '的形式更改模板数据。如果你想要渴望看到现象的童鞋,赶快去获取代码吧!

3.实现编译

虽然上面已经实现了一个双向数据绑定的例子,但是在整个过程中并不解析dom节点,而是直接固定一个节点来替换数据,所以需要实现一个解析器Compile来做解析和绑定。解析器编译的实现步骤:

1.解析模板指令,替换模板数据并初始化视图

2.将模板指令对应的节点绑定到对应的更新函数,并初始化对应的订阅者

为了解析模板,您需要首先获取dom元素,然后处理包含dom元素指令的节点。因此,这个链接需要频繁操作dom,所以可以先构建一个片段,在处理之前将需要解析的dom节点存储在片段中:

函数nodeToFragment(El){ var fragment=document . createdocumentfragment();var child=el.firstChildWhile (child) {//将Dom元素移动到fragment.appendChild(child)中;child=el.firstChild }返回片段;}接下来,我们需要遍历所有节点,对有相关规范的节点进行特殊处理。这里先处理最简单的情况,只处理形式为“{variable}}”的指令。很难先谈,然后再考虑更多的说明:

函数compileElement(El){ var child nodes=El . child nodes;var self=这个;[].slice.call(childNodes)。forEach(函数(节点){ var reg=/{{(。*)}}/;var text=node.textContent如果(自我。是文本节点reg。test(text)){//判断是否是指令本身。编译文本(节点,reg。exec (text) [1])符合此格式{ { } };} if(node . child nodes node . child nodes . length){ self.compileelement(node);//继续递归遍历子节点} });},函数compileText (node,exp){ var self=this;var initText=this . VM[exp];this.updateText(节点,init text);//将初始化的数据初始化到新的观察器中(这。VM,exp,函数(值){//生成订阅者并绑定更新函数self.updateText(节点,值);});},函数(节点,值){ node . textcontent=type of value==' undefined '?' :值;}获取最外层节点后,调用compileElement函数判断所有子节点。如果节点是文本节点,并且与{{}}形式的指令匹配,则编译过程从初始化视图数据开始,对应于上面的步骤1,然后需要生成与更新函数绑定的订阅者,对应于上面的步骤2。这样就完成了指令解析、初始化和编译三个过程,一个解析器Compile就可以正常工作了。为了将解析器编译与监听器观察器和订阅者观察器相关联,我们需要再次修改类SelfVue函数:

函数SelfVue(选项){ var self=thisthis.vm=thisthis.data=optionsobject . key(this . data)。forEach(函数(键){ self.proxyKeys(键);});观察(this . data);新建编译(选项,this . VM);归还这个;}更改后,我们不需要像以前一样通过传入固定的元素值进行双向绑定,而是可以为双向绑定命名各种变量:

body div id=' app ' H2 { { title } }/H2 h1 { { name } }/h1/div/body script src=' http : js/observer . js '/script script src=' http : js/watcher . js '/script script src=' http : js/compile . js '/script script src=' http : js/index . js '/script script type=' text/JavaScript ' var selfwindow . settimeout(function(){ self vue . title=' hello ';}, 2000);window . settimeout(function(){ selfvue . name=' canfoo ';}, 2500);/script有上面的代码,在页面上可以观察到titile和name最初分别初始化为‘hello world’和空,2s后title替换为‘hello’,3s后name替换为‘canfoo’。废话不多说,给你另一个版本的代码(v2),获取代码!

至此,一个双向的数据绑定功能已经基本完成。接下来,有必要改进更多指令的解析和编译。哪里可以处理更多的指令?答案很明显,只需在上面提到的compileElement函数中给其他指令节点添加判断,然后遍历它的所有属性,看看是否有匹配的指令属性,如果有,就解析编译。在这里,我们添加了另一个v-model指令和事件指令的解析编译,我们使用函数编译来解析这些节点:

函数compile(node){ var nodeAttrs=node . attributes;var self=这个;array . prototype . foreach . call(nodeAttrs,function(attr){ var attrName=attr . name;if(self . isdireactive(attrName)){ var exp=attr . value;var dir=attrname . substring(2);如果(自我。iseventection(dir)){//事件指令self。compileevent(节点,自身。虚拟机、扩展、目录);} else {//v-model指令self.compile model (node,self.vm,exp,dir);} node . remove attribute(AttRName);} });}上述编译函数安装在编译原型上。它首先遍历所有节点属性,然后判断该属性是否为指令属性。如果是,它会区分这是哪条指令,然后相应地处理它。处理方法比较简单,这里不一一列举。想要立即阅读代码的同学可以点击这里获取。

最后,我们稍微修改了类SelfVue,使其更像Vue的用法:

函数SelfVue(选项){ var self=thisthis . data=options . data;this . methods=options . methods;object . key(this . data)。forEach(函数(键){ self.proxyKeys(键);});观察(this . data);new Compile(options.el,this);options . mounted . call(this);//处理完一切后执行挂载的功能}此时,我们可以真正测试一下,并在页面上设置以下内容:

body div id='app' h2{{title}}/h2输入v-model='name' h1{{name}}/h1按钮on:click=' clickMe 'click me!/button/div/body script src=' http : js/observer . js '/script script src=' http : js/watcher . js '/script script src=' http : js/compile . js '/script script src=' http : js/index . js '/script script type=' text/JavaScript ' new self vue({ El : ' # app ',data: { title:}},mount ed : function(){ window . settimeout(()={ this . title=' hello ';}, 1000);} });/脚本看起来和vue一样吗?哈,你真的完了!如果你想要代码,只要点击这里得到它!你还没有描述这个现象吗?直接上图!请注意

其实这个渲染就是本文开头贴的渲染。之前的文章说要和大家一起实现,所以我在这里再贴一次渲染。这被称为端到端回声。

最后,希望这篇文章对你有所帮助。如果你有任何问题,请留言一起讨论。

以上就是边肖介绍的vue双向绑定的原理以及详细讲解和集成的实现。希望对大家有帮助。如果你有任何问题,请给我留言,边肖会及时回复你。非常感谢您对我们网站的支持!

更多资讯
游戏推荐
更多+