如何理解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的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。