手写实现vue的MVVM响应式原理
MVVM响应式实现原理:
1.模板编译
2.数据劫持
3.watcher
MVVM------------------视图-----模型----视图模型
三者与Vue的对应:view对应template,vm对应newVue({…}),model对应data
文中应用到的数据名词:
nodeType判断节点是否是元素节点
querySelector创建一个元素节点
createDocumentFragment文档碎片
attributes获取元素属性集合
textContent获取文本内容
reduce(prev,next,currentIndex){}一个可以用上一个元素和当前元素做处理的方法
defineProperty(obj,key,value){}数据拦截的主要方法
![](https://img2018.cnblogs.com/i-beta/1757059/202002/1757059-20200229095355227-1557906193.png)
首先建立一个vue的实例,建立mvvm.js,构建mvvm类。获取el的节点和data放入实例中,在将Observer.js(数据劫持)和Compile.js(模板编译)放入mvvm的js,全部在index页面运行.
第一步:模板编译
我首先制作Compile.js,也就是模板编译。
首先需要获取el 这个属性的值 用nodeType === 1判断是不是元素节点. 如果不是则用 queryselector() 生成一个节点 。 这样做的目的是,有些人el:#app 有些人是document.getElementById('app')。 不管俩者如何,我们都要生成一个节点来供后续使用。
随后判断el节点是否存在,如果存在。则进行编译 , 这里编译最好不要在dom里进行遍历编译,非常耗性能 。 我推荐的是用 createDocumentFragment() 方法. 建立一个虚拟节点对象, 在这个虚拟节点对象里进行遍历以及对应的操作。
那么说到虚拟节点, 我们需要将我们获取的el节点整个放入进去 ,进行遍历,将app里的每一个子节点都搬到fragement 变量中。
然后进行节点的编译。这里的节点又分为元素节点和文本节点。 还是用刚刚的nodeType判断区分吗,然后做对应的操作。
接下来我们先编译元素节点首先我们需要知道,获取元素节点要做什么,为什么获取元素节点。我是希望通过获取元素节点上的关于vue的指令,比如:v-model,v-html,v-for。等等...那么这些指令是放在元素节点上的属性里,所以我们用attributes获取元素节点的属性名的集合,也就是我们说的v-model。通过遍历这个attr属性名的集合,获取每个属性名。通过isDirective函数判断attrName包含v-的属性,这里我做给假设,好方便理解。这里通过上面的过滤,可以得出attrName是一个指令名的集合。那我假设这个指令名为v-model。我首先获取v-model的值,也就是expr。然后做一个解耦对象CompileUtil,方便后面制作其他的指令。所以这里调用的是CompileUtil[model]{node,this.v,,expr};
调用model的指令后,在model这个函数里做相对应的处理。这里的watcher构造函数先不用管,后面的事情。这里的uptate['modelUptate']和model一样放在CompileUtil中,方便管理。如果updateFn存在的话,则执行updateFn(),将v-model的值赋予input节点的value.下图中的getVal是防止v-model=’messge.a'这种嵌套对象的。这个函数里,首先利用split将messge.a拆分成[messge,a]数组。然后利用reduce方法返回上一个元素[当前元素],而最下面的vm.$data是reduce方法遍历的初始值。也就是data。
因为data:{messge:{a:'hello.world'}}.这样的编译,元素节点就可以编译出来了,可以将data的值编译到元素节点上了。
接下来编译文本节点,那文本节点,我们首先获取文本节点里的值,然后利用正则的test找{{a}},和之前的元素节点一样,执行对应的函数。,执行对应的行数。这里第86-90可以先不管,不过这里的textVal和上面的getVal函数不一样,首先是需要将符合条件的元素里的变量取出来也就是{{a}}里的a,argments[1]就是a变量。在考虑到对象嵌套,就执行上面的getVal。然后就可以将data里的值替换到文本里了。
这样元素节点和文本节点都编译完成了。然后将整个虚拟节点丢回dom树里去 。MVVM的编译就结束了
第二步:数据劫持,函数很少。但比较绕.这里执行observe,利用递归遍历,将data里的键值对全部拿出来处理,执行defineReactive函数,这里18行可以先不看。 看下面的最重点的Object.defineProperty()。这里要传入劫持的对象,劫持的键,以及回调函数。这里回调函数里俩个参数在下图。
然后,get函数是取值是做对应的操作,set函数是设置值做对应的操作。至此数据劫持就完成了
第三步:watcher 监察者 ,一旦变化执行对应的操作。也就是将模板编译和数据劫持俩个函数联系在一起。有衔接。
这里创建watcher类,将需要的参数获取。 vm是实例,expr是值,cb是回调函数callback。watcher实例里的value = get方法的返回值,value执行一次嵌套处理返回。这里监察者作用主要是 一 更新值,二是执行callback回调函数cb。三将自己的实例,放入dep的target里。那么watcher监察者就制作好了。
最后的连接部分,首先data里的每个属性值都被加上了set和get
1.获取值
在最开始编译的时候,编译节点的文本节点处理和元素节点处理的时候执行watcher函数,在watcher函数里的get函数中将 watcher函数自己放入de问值的时候,则会执行get函数,将 每个watcher放入dep数组中 。
2.修改值
在修改值的时候,会触发Observer.js 的defineProperty的set函数,set函数里比较新的值和旧的值,value是编译时候的值,newValue是set函数的第一个参数,也就是修改后的新值 。 将俩者比较,如果不同,就执行Dep构造函数的notify函数。notify则会遍历全部存在的dep数组里的watcher的update方法。在watcher的update方法中,比较值的不同,如果不同就则执行回调函数,将视图更新。这个回调函数是嵌套在处理文本节点和元素节点的方法里。
v-model的双向绑定
至于v-model的双向绑定,其实是绑定输入框的输入事件。将输入事件新的值赋值给input节点的value值,然后值的改变,执行set函数,将视图改变。视图的改变,会执行wacther的回调函数,文本节点也会重新赋值。
最终效果:
这就是mvvm响应式原理的实现,如果有残缺讲不清楚的地方,欢迎指出。谢谢。