如何理解vue中的mvvm模式,剖析vue原理&实现双向绑定MVVM

  如何理解vue中的mvvm模式,剖析vue原理&实现双向绑定MVVM

  本文主要介绍对vue的MVVM响应原理的深入理解。Vue采用发布者-订阅者模型,通过Object.defineProperty()劫持每个属性的getter和setter,在数据发生变化时向订阅者释放消息,触发相应的监控回调。

  

目录

  前言Vue的MVVM原理创建一个html示例在MVue.js中创建MVue条目Create Compile1。流程元素节点compileElement(子元素)2。处理文本节点compileText(子节点)3。实现compileUtil指令到进程更新器以更新数据实现数据观察器数据相关Dep观察器观察器实现视图驱动数据驱动视图摘要

  

前言

  这几天在面试,每当被面试官问到Vue的响应式原理,答案都是肤浅的。如果你只停留在MVVM框架是模型层、视图层和viewmodel层的双向数据绑定的回答上,那么建议你彻底处理好Vue的MVVM响应式原理。(全文约13900字,阅读时间约25分钟。建议有一定vue基础后再阅读)

  怎么来的?

  想清楚一件事情的原理,就要追根溯源,刨根问底。在Vue之前,所有框架如何实现MVVM的双向绑定?

  大致分为以下几种:

  发布者-订阅者模式(backbone.js)脏值检查(angular.js)数据劫持(vue.js)发布者-订阅者模式,通过sub和pub实现视图的监控绑定。通常的方式是VM。$ set (property ,value)。脏值检查,内部其实是setnterval。当然为了节省性能,也没那么低。通常,脏值检查在特定事件时执行:

  DOM事件,比如输入文本,单击按钮(ng-click)XHR响应事件($http)浏览器位置更改事件($location)定时器事件($timeout,$interval)执行$digest()或$apply()

  Vue采用发布者-订阅者模式,通过Object.defineProperty()劫持每个属性的getter和setter,当数据发生变化时向订阅者释放消息,触发相应的监控回调。

  

Vue的MVVM原理

  话不多说,就上图吧。

  首先,请尽量记住这张图,并且能够自己画出来。下面所有的原理都是围绕这张图的。感觉很蠢吧?不过,我相信很多人在Vue的官方文档里看到过这张图:

  其实这两个数字是想表达一个意思:3354。两者都代表了双向数据绑定的原理流程,官方文档展示的更为简洁。看你更能接受哪种描述。自己实施响应式原则后,可以记住这两张图。

  这是第一张图。当我们创建一个vue实例时,vue实际上做了以下事情:

  创建入口函数,编译数据观测器和指令解析器;分别添加。Compile解析所有DOM节点上的vue指令,提交给Updater(实际上是一个对象);Updater替换数据(如{{}},msg,@click)完成页面初始化渲染;Observer通过使用Object.defineProperty劫持数据,其中getter和setter通知依赖Dep的变化;将Watcher添加到Dep,并在数据发生变化时通知Watcher进行更新;Watcher获取旧值和新值,通知更新器在回调函数中更新视图;Compile中的每条指令都有一个新的Watcher,用来触发Watcher的回调函数进行更新。简单Vue响应式原理完整源代码:详情请见。

  按照前面的思路,让我们一步步实现一个简单的MVVM响应式系统,加深对响应式原理的理解。

  

创建一个html示例

  现在我们已经创建了一个简单的Vue渲染示例。我们要做的就是用自己的MVue把data,msg,htmlStr,methods中的数据渲染到标签上。完整的数据驱动视图,视图驱动数据驱动视图。

  !声明文档类型

  html lang=en

  头

  meta charset=UTF-8

  meta name= viewport content= width=device-width,initial-scale=1.0

  meta http-equiv= X-UA-Compatible content= ie=edge

  标题文档/标题

  /头

  身体

  div id=应用程序

  H2 { { person . name } }-{ { person . age } }/H2

  h3{{person.fav}}/h3

  保险商实验所

  li1/李

  li2/李

  李/李

  /ul

  h3{{msg}}/h3

  div v-text=person.fav/div

  div v-text=msg/div

  div v-html=htmlStr/div

  输入类型=text v-model=msg

  按钮v-on:click= handle click click/按钮

  button @ click= handle click @ click 2/button

  /div

  脚本src=。/observer . js /脚本

  脚本src=。/mvue . js /脚本

  脚本

  //创建Vue实例以获取ViewModel

  const vm=新MVue({

  埃尔: #app ,

  数据:{

  人:{

  名称:“我的vue”,

  年龄:18,

  最喜欢的:《坦克世界》

  },

  消息:“学习MVVM框架的原则”,

  HtmlStr: h3爱前端,金子总会发光/h3

  },

  方法:{

  handleClick() {

  console . log(this);

  }

  }

  });

  /脚本

  /body

  /html

  

在MVue.js中创建MVue入口

  MVue类{

  构造函数(选项){

  这个。$ el=options.el

  这个。$ data=options.data

  这个。$ options=options

  如果(这个。$el) {

  //1.实现一个数据观察器。

  //2.实现一个指令观察器。

  新编译(这个。$el,这个);

  }

  }

  思路:

  自然首先要构建MVue类,需要在MVue类构造函数中使用options参数以及其中的el和数据。

  那么,在el存在的情况下,我们需要先实现一个指令解析器编译,然后实现Observer观察器。

  显然,Compile应该需要传入MVue实例的el和整个MVue实例来解析标签的指令。

  

创建Compile

  思路:在解析标签指令之前,我们做的第一件事是:

  判断el是否是元素节点,如果不是,获取标签el,然后传入vm实例;递归获取所有子节点,便于下一步解析。【注意:这一步会频繁触发页面的回流和重绘,所以我们需要先将节点存储在文档片段对象中,相当于把它们放在内存中,从而减少页面的回流和重绘。】编译文档片段对象中的模板;最后,文档片段对象被附加到根元素。

  类编译{

  构造函数(el,vm) {

  this.el=this.isElementNode(el)?El:document . query selector(El);

  this.vm=vm

  //获取文档片段对象,放入内存,会减少页面的回流和重绘。

  const fragment=this . node 2 fragment(this . El);

  //编译模板

  this.compile(片段);

  //将子元素追加到根元素

  this.el.appendChild(片段);

  }

  这里我们先自己定义了几个方法:

  判断是否是元素节点isElementNode(el)、存储的文档片段对象node2Fragment(el)和编译模板compile(fragment)分别在构造函数之后去实现:

  节点2片段(el) {

  //创建文档片段对象

  const f=document . createdocumentfragment();

  //递归放入

  让第一胎;

  while((first child=El . first child)){

  f.appendChild(第一个孩子);

  }

  返回f;

  }

  isElementNode(node) {

  return node . nodetype===1;

  }

  编译模板compile(fragment)的思路是递归获取所有子节点,确定节点是元素节点还是文本节点,然后定义两种方法,compileElement(child)和compileText(child),来处理这两种节点。

  编译(片段){

  //获取子节点

  const child nodes=fragment . child nodes;

  [.子节点]。forEach((子级)={

  if (this.isElementNode(child)) {

  //是元素节点。

  //编译元素节点

  //console.log (element node ,子);

  this.compileElement(子级);

  }否则{

  //是文本节点。

  //编译文本节点

  //console.log (text node ,child);

  this . compile text(child);

  }

  //逐层递归遍历

  if(child . child nodes child . child nodes . length){

  this.compile(子);

  }

  });

  }

  好了,现在编译的基本框架已经建立了。希望你在这里不困,振作起来!现在,让我们继续处理元素节点和文本节点。

  

1.处理元素节点compileElement(child)

  思路:

  拿到标签里的每个某视频剪辑软件指令,如v-文本v-html v-模型v-on:点击,显然它们都是以五-开头的,当然还有@开头的指令也不要忘记把节点、节点值、虚拟机实例、(开的事件名)传入compileUtil对象,后面用它处理每个指令,属性对应指令方法;别忘了,最后的视图标签上是没有某视频剪辑软件指令的,所以我们要把它们从节点属性中删去。

  编译元素(节点){

  常量属性=节点.属性

  [.属性】forEach((属性)={

  const { name,value }=属性

  if (this.isDirective(name)) {

  //是一个指令文本html模型v-on:点击

  const [,directive]=名称。拆分(-);//文本超文本标记语言模型打开:单击

  const [dirName,event name]=指令。拆分(:);//文本超文本标记语言模型打开

  //更新数据数据驱动视图

  compileUtil[目录名](节点,值,this.vm,事件名);

  //删除有指令标签上的属性

  node.removeAttribute(v-指令);

  } else if(这个。iseventname(name)){

  //@click=handleClick

  让[,事件名称]=名称。拆分(“@”);

  compileUtil[on](节点,值,this.vm,事件名称);

  }

  });

  }

  判断是否是指令,以五-开头

  isDirective(attrName) {

  返回姓名。以( v-)开头;

  }

  

2.处理文本节点compileText(child)

  主要使用正则匹配双大括号即可:

  编译文本(节点){

  //{{}} v文本

  常量内容=node.textContent

  if (/\{\{(.)\}\}/.测试(内容)){

  compileUtil[text](节点,内容,this。VM);

  }

  }

  

3.实现compileUtil指令处理

  思路:

  每个指令对应各自方法,除了在需要额外传入事件名称,其他的指令处理函数只需要传节点、值(或表达式expr)、vm

  实例:

  const compileUtil={

  text(node,expr,vm) {

  },

  html(节点,表达式,虚拟机){

  },

  模型(节点、表达式、虚拟机){

  },

  在(节点,表达式,虚拟机,事件名称){

  }

  };

  没有一下子放出代码来的话,骨架原来这么简单啊,继续逐个击破它们!

  虚拟超文本标记语言指令处理,思路:拿到值,把值传给更新器更新器,更新,完事儿。

  html(节点,表达式,虚拟机){

  const value=this.getVal(expr,VM);

  this.updater.htmlUpdater(节点,值);

  },

  v型车指令处理,同上。先实现数据=视图这条线,双向绑定最后实现。

  模型(节点、表达式、虚拟机){

  const value=this.getVal(expr,VM);

  this.updater.modelUpdater(节点,值);

  },

  比较复杂的,v-on,思路:获取事件名,从方法中取到对应的函数,添加到事件中,注意这要绑定给伏特计实例,假的默认事件冒泡。

  在(节点,表达式,虚拟机,事件名称){

  //获取事件名,从方法里面取函数

  设fn=vm .$options.methods vm .$ options。方法[expr];

  节点。addevent侦听器(事件名称,fn.bind(vm),false)

  },

  v-text指令处理:

  text(node,expr,vm) {

  //expr:msg:学习视图模型框架原理

  //对传入不同的字符串不同操作div v-text=person.name/div

  //{{}}

  让价值;

  if (expr.indexOf({{ )!==-1) {

  //{ {人。name } }-{ { person。年龄} }

  value=expr.replace(/\{\{(.)\}\}/g,(.args)={

  返回this.getVal(args[1],vm)

  })

  }否则{

  value=this.getVal(expr,VM);

  }

  this.updater.textUpdater(节点,值);

  },

  用到args这个数组,console.log一下args,发现args[1]就有我们要找的具体属性:

  例如,取到人名后,传入到this.getVal(person.name ,vm),最后能取到虚拟机.$data.person.name。

  怎么拿到它们对应的值呢?

  显然,不论是htmlStr、msg、person、它们都在实例伏特计的数据内,在自定义方法getVal中,可以使用使分离分割小圆点"."得到数组,再用高逼格的减少方法去遍历找到数据每个属性(对象)下的每个属性的值,像这样:

  getVal(expr,vm) {

  return expr.split( . ).reduce((data,currentVal)={

  返回数据[当前值];

  },vm .$ data);

  },

  (不记得怎么用reduce?请在右上角新建标签页,去CSDN上补一补。)进阶拿到双大括号内对应的属性的值:

  getContentVal(expr,vm) {

  return expr.replace(/\{\{(.)\}\}/g,(.args)={

  控制台。日志(参数);

  返回this.getVal(args[1],VM);

  });

  },

  

更新器Updater更新数据

  在指令方法的后面,创建一个updater属性,它实际上是一个类。我们亲切地称它为更新程序,它是如此的明显,以至于你可以立即记住它的样子:

  //更新的函数

  更新程序:{

  textUpdater(节点,值){

  node.textContent=值;

  },

  htmlUpdater(节点,值){

  node.innerHTML=value

  },

  模型更新程序(节点,值){

  node.value=value

  }

  },

  每个指令方法得到值后,更新到node节点。

  至此,我们已经完成了原理图上的MVVM到Compile到Updater这一条线:

  

实现数据观察者Observer

  MVue类{

  构造函数(选项){

  这个。$ el=options.el

  这个。$ data=options.data

  这个。$ options=options

  如果(这个。$el) {

  //1.实现一个数据观察器。

  新观察家(这个。$ data);

  //2.实现一个指令观察器。

  新编译(这个。$el,这个);

  }

  }

  观察者类构造函数应该传递给它什么?是的,观察者想要监听所有的数据,所以我们将vm实例的数据作为参数传入。

  递归,遍历所有属性、对象、子对象.在数据中。对于每个键,用Object.defineProperty()劫持数据的作用是直接在对象上定义一个新的属性,或者修改一个已有的属性。)Object.defineProperty下有get方法和set方法,也就是官方原理图上的getter和tester。在劫持数据之前,创建一个依赖dep实例Dep。对于GETTER,在订阅数据变更时,向dep添加观察器;对于setter,当数据发生变化时,将newVal赋给一个新值,并用notify通知dep这一变化。(这只是对应官方示意图)最后三点,4、5、6,可以说是MVVM实施过程中最关键、最巧妙的三步。正是这三个点睛之笔,成功地将整个系统桥接起来,并注意它们各自在代码中的位置。

  课堂观察者{

  构造函数(数据){

  this.observer(数据);

  }

  观察者(数据){

  /**

  {

  人:{

  姓名:张三,

  收藏:{

  答:“爱好1”,

  乙:“爱好二”

  }

  }

  }

  */

  if (data typeof data===object) {

  Object.keys(数据)。forEach((key)={

  this.defineReactive(data,key,data[key]);

  });

  }

  }

  defineReactive(对象,键,值){

  //递归遍历

  this.observer(值);

  const Dep=new Dep();

  //劫持数据

  Object.defineProperty(obj,key,{

  //是否可以遍历

  可枚举:真,

  //能不能改一下写法?

  可配置:假,

  //编译前,初始化时

  get() {

  //当订阅数据更改时向Dep添加观察器

  dep . target dep . add sub(dep . target);

  返回值;

  },

  //当数据被外界修改时

  set: (newVal)={

  //新值也会被劫持。

  this.observer(纽瓦尔);//这里的this指向当前实例,所以改用arrow函数查找。

  //判断新值是否有变化。

  如果(newVal!==值){

  value=newVal

  }

  //告诉Dep通知更改

  dep . notify();

  },

  });

  }

  }

  

数据依赖器Dep

  主要作用:

  想要更新集合的观察者通知每个观察者更新//数据依赖性。

  类Dep

  构造函数(){

  this . subs=[];

  }

  //收集观察者

  addSub(观察器){

  this.subs.push(守望者);

  }

  //通知观察者更新

  通知(){

  Console.log(通知了观察者,this . subs);

  this.subs.forEach(w=w.update())

  }

  }

  

观察者Watcher

  注意Dep.target=this这一步是在Dep实例上挂载观察器并关联它。因此,当观察器Watcher获得旧值时,它应该解除关联,否则它将重复添加观察器。以下是不解除关联的错误演示:

  最后,使用回调函数将要处理的新值传递给更新程序。

  类监视器{

  构造函数(虚拟机、表达式、回调){

  //通过cb发送新值

  this.vm=vm

  this.expr=expr

  this.callback=回调;

  //首先保存旧值

  这个。老瓦尔=这个。getold val();

  }

  getOldVal() {

  //把观察者挂载到资料执行防止实例上,关联起来

  Dep.target=这

  const old val=compileutil。getval(这个。expr,这个。VM);

  //获取旧值后,取消关联,就不会重复添加

  Dep.target=null

  返回奥尔德瓦尔

  }

  update() {

  //更新,要取旧值和新值

  const new val=compileutil。getval(这个。expr,这个。VM);

  如果(纽瓦尔!==this.oldVal) {

  这个。回调(新val);

  }

  }

  }

  如何更新程序如何接收从看守人传来的新值做回调处理呢?

  只需要在刚刚写好的compileUtil对象的每个指令处理方法内都新(添加)一个看守人实例即可。注意文本指令方法下新观察者实例的价值参数,可以用参数[1]传入,重新处理纽瓦尔。

  const compileUtil={

  getVal(expr,vm) {

  return expr.split( . ).reduce((data,currentVal)={

  返回数据[当前值];

  },vm .$ data);

  },

  setVal(expr,vm,inputVal) {

  return expr.split( . ).reduce((data,currentVal)={

  数据[当前值]=输入值;//把当前新值复制给旧值

  },vm .$ data);

  },

  getContentVal(expr,vm) {

  return expr.replace(/\{\{(.)\}\}/g,(.args)={

  控制台。日志(参数);

  返回this.getVal(args[1],VM);

  });

  },

  text(node,expr,vm) {

  //expr:msg:学习视图模型框架原理

  //对传入不同的字符串不同操作div v-text=person.name/div

  //{{}}

  让价值;

  if (expr.indexOf({{ )!==-1) {

  //{ {人。name } }-{ { person。年龄} }

  value=expr.replace(/\{\{(.)\}\}/g,(.args)={

  新观察器(vm,args[1],()={

  //额外处理expr:{ { person。name } }-{ { person。年龄} }

  //还要重新处理纽瓦尔

  this.updater.textUpdater(node,this.getContentVal(expr,VM));

  });

  返回this.getVal(args[1],vm)

  })

  }否则{

  value=this.getVal(expr,VM);

  }

  this.updater.textUpdater(节点,值);

  },

  html(节点,表达式,虚拟机){

  const value=this.getVal(expr,VM);

  //绑定观察者,将来数据发生变化出发这里的回调进行更新

  新观察器(vm,expr,newVal={

  this.updater.htmlUpdater(node,newVal);

  })

  this.updater.htmlUpdater(节点,值);

  },

  模型(节点、表达式、虚拟机){

  const value=this.getVal(expr,VM);

  //绑定更新函数数据=驱动视图

  新观察器(vm,expr,(newVal)={

  this.updater.modelUpdater(node,new val);

  });

  //视图=数据=视图

  node.addEventListener(input ,e={

  //设置值

  this.setVal(expr,vm,e . target。值);

  })

  this.updater.modelUpdater(节点,值);

  },

  在(节点,表达式,虚拟机,事件名称){

  //获取事件名,从方法里面取函数

  设fn=vm .$options.methods vm .$ options。方法[expr];

  节点。addevent侦听器(事件名称,fn.bind(vm),false)

  },

  绑定(节点,表达式,虚拟机,属性名称){

  //类似2007年11月25日。

  },

  //更新的函数

  更新程序:{

  文本更新器(节点,值){

  node.textContent=值;

  },

  htmlUpdater(节点,值){

  node.innerHTML=value

  },

  模型更新程序(节点,值){

  节点值=值

  }

  },

  };

  

实现视图驱动数据驱动视图

  还是借着上面这个代码块,我们只需要在模型指令方法下,为投入标签绑定事件,并自定义塞特瓦尔方法为结节赋值即可。

  到这里,我们已经基本完整实现了Vue的MVVM双向数据绑定

  小改进:

  在偏估计量实例中,我们一开始使用的是$数据获取到数据,这里可以做一层代理代理人,便于我们省略$数据

  方法:{

  handleClick() {

  //控制台。日志(这个);

  this.person.name=这是做了一层代理

  //把这个。$数据代理成这

  这个 data.person.name=数据更改了

  }

  }

  还是使用对象。定义属性数据劫持,遍历数据下的每个钥匙,让吸气剂返回数据[键],设置器设置数据[关键字]直接等于纽瓦尔即可。

  偏估计量类{

  构造函数(选项){

  这个。$ el=options.el

  这个。$ data=options.data

  这个。$ options=options

  如果(这个。$el) {

  //1.实现一个数据观察器。

  新观察家(这个。$ data);

  //2.实现一个指令观察器。

  新编译(这个。$el,这个);

  this.proxyData(this。$ data);

  }

  }

  代理数据(数据){

  for(数据中的常量键){

  Object.defineProperty(this,key,{

  get() {

  返回数据[键]

  },

  set(newVal) {

  data[key]=new val;

  }

  })

  }

  }

  }

  

总结

  再次体会官方文档对响应式原理的描述:

  当我们将一个普通的JavaScript对象作为数据选项传递给Vue实例时,Vue将遍历这个对象的所有属性,并使用object。定义属性,将所有这些属性转换为getter/setter。Object.defineProperty是ES5中无法填补的特性,这也是Vue不支持IE8及更早版本浏览器的原因。

  这些getter/setter对用户来说是不可见的,但是在内部,它们使Vue能够跟踪依赖关系

  当访问和修改时通知更改。这里需要注意的是,在控制台上打印数据对象时,不同的浏览器对getter/setter的格式是不同的,建议安装。

  Vue-devtools获得一个更友好的用户界面来检查数据。

  每个组件实例对应一个watcher实例,它将“被触摸”的数据属性记录为组件渲染过程中的依赖项。之后,当

  当设置器被触发时,它将通知观察器,以便其关联的组件可以被重新呈现。

  以及开头时我自己总结的原理描述:

  在我们创建一个vue实例时,其实vue做了这些事情:

  创建了入口函数,分别new了一个数据观察者

  观测器和指令解析器编译;Compile解析所有DOM节点上的vue指令,提交给Updater(实际上是一个对象);Updater替换数据(如{{}},msg,@click)完成页面初始化渲染;Observer通过使用Object.defineProperty劫持数据,其中getter和setter通知依赖Dep的变化;将Watcher添加到Dep,并在数据发生变化时通知Watcher进行更新;Watcher获取旧值和新值,通知更新器在回调函数中更新视图;Compile中的每条指令都有一个新的Watcher,用来触发Watcher的回调函数进行更新。在实现代码的过程中,可以深刻体会到视图驱动数据驱动视图的核心Vue的数据驱动视图的匠心,了解Object.defineProperty的具体应用场景。

  这就是这篇关于彻底理解Vue的MVVM响应原理的文章。有关Vue的MVVM响应的更多信息,请搜索我们以前的文章或继续浏览下面的相关文章。希望大家以后能多多支持我们!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: