再次看了上次写的博客关于Vue的MVVM,发现虽然介绍了MVVM的原理,但是感觉还不够详细,现在就再次根据这篇博客写详细一点,来看看new Vue
的时候Vue究竟做了些什么事。
我想,以需求作为出发点来理解原理会比较容易,所以这篇博客会以提出需求 -> 解决需求的方式来写。
Vue中的MVVM原理介绍
可以先阅读我的这篇博客了解一下关于Vue的MVVM,另外需要记住这一幅图(很重要),这张图就是本篇博客的概括:
回顾
继上一篇文章Vue中的MVVM--model -> view的绑定,我们完成了对页面的初始化渲染,达成了如下要求:
image.png
但还未完成
view -> model
的绑定,所以不能通过修改数据来触发视图的更新,今天就来完成剩余部分
提出需求
继上图:
image.png
我们需要做到的是:当修改输入框的数据时,上面的文字也随之进行刷新。
分析
先来看看还未完成的部分:
其中包含观察者
Observer
,监听器Watcher
,然后还有一个Dep
,过程是:
- 在
Compiler
中监听数据的变化并绑定监听器; - 在观察者
Observer
中实现对所有数据的getter
和setter
; - 监听器
Watcher
把更新事件添加进Dep
的事件队列中; - 观察者
Observer
发现数据产生变化的时候通知Dep
; -
Dep
把事件队列中的更新事件全部执行一遍;
总结下来就是实现两个事情:
- 添加数据依赖;
- 触发数据变更事件;
接下来就先创建Watcher
,Observer
,Dep
三个类;
image.png
image.png
image.png
先来看看Dep
是什么
根据上面的的步骤描述,很容易感觉到Dep
像是一个容器,存储着对视图的更新事件,是的,这是一个发布订阅模式的实现,该模式包含事件队列subs
,添加事件方法addSub
,执行事件队列函数notify
,移除事件队列里的事件removeSub
看完发布订阅模式后,继续我们的流程。
添加依赖
-
在Compiler中监听数据的变化并绑定监听器
image.png
在上一篇对model -> view
的绑定中我们有一个针对数据和指令统一进行绑定的方法bind
,为了不和后面的v-bind
指令冲突,现在改为了bindData
;
在这个函数承载的功能有获取tag文本和执行视图的更新,所以我们可以在这个函数中添加对数据的监听器Watcher
,因为后面需要把更新视图的事件添加进Dep
中,所以Watcher
中需要的参数要有一个更新事件也就是更新器updater
中的视图更新函数,此外将当前vm实例和得到的data
键值也传进去备用;
compile中添加数据监听器
更新器
watcher
接着 -
在观察者Observer中实现对所有数据的getter和setter
image.png
注意这一步需要考虑到数据中含有嵌套的对象,需要进行递归操作才能全部添加getter
和setter
,使用的是Object.definedProperty
,Observer
接受的参数是data
对象:
然后在MVVM
类中代入data
并执行observer
:
image.png
接着对所有data
中的属性绑定getter
和setter
,这一步需要进行递归操作:
image.png
最后回到Compiler
中,实现视图对数据的修改:
比如在输入框中修改数据直接反应到data
上
image.png
image.png
来看看成果:
image.png
这个时候在视图上对数据进行的修改就可以反映到data
上,并触发该数据的setter
函数; -
监听器
image.pngWatcher
把更新事件添加进Dep
的事件队列中;
这一步需要考虑一个问题:在什么时候怎么样把更新事件添加到Dep
中去?
回顾上面所写的,data
中的每一个属性都有一个对应的Watcher
,可以在Watcher
中获取得到对应的data
中的属性。那么在这个获取的过程中,又会触发该属性的getter
,就可以考虑在该属性的getter
中添加,分解成一下步骤就是:
① 把这个Watcher
通过构造函数本身的属性target保留在Dep
中,然后去data
中取值;
② 取值的时候触发Observer
中该属性的getter
,在Observer
中new一个Dep
实例出来,判断如果Dep类的target
非空(也就是该属性已被有监听器),则触发依赖添加事件depend
;
image.png
③ 这时候的Dep.target
就是被监听属性的Watcher
,在Dep类中添加一个方法depend
,用来把该属性的Watcher
添加进事件队列subs
中,但是这一步要当前的Watcher
,需要在Watcher类中进行触发,所以在Watcher
中创建一个函数addDep
,把Dep
的实例作为参数放进去,然后在addDep
中进行更新事件的添加:
image.png
image.png
然后置空Dep.target
,用于下一个数据的依赖添加
image.png
现在我们来看看subs
中有些什么
image.png
可见msg
被引用了两次就被监听了两次,这时候只要当msg
这个数据发生变化并触发setter
时,将subs
中所有的watcher
实例里的更新回调update
拉出来执行即可 -
更新视图
- 更新视图的时候,我们先要获取当前的数据新值,然后作为参数放进回调函数中,并且还要对新的数据进行上面的依赖添加步骤,那么
Watcher
还需要一个update
函数用来统一做这个事:
image.png - 在
Observer
的setter
中触发Dep
的notify
方法,进行视图的更新:
image.png -
到了这步其实就已经达成效果了:
image.png
- 修复bug
虽然MVVM双向绑定的功能已经达成,但是还是有不少bug的,其中最严重的有两个
-
当我们多次更新数据的时候,会发现添加进
image.pngsubs
的watcher
发生了递增的现象,所以当快速更新数据时就会导致执行函数过多而页面崩溃;
造成这个现象的原因是在进行第一次的更新时,watcher
将同一个数据的新值也进行了依赖添加,也就是let newVal = this.get()
这一段;
image.png
既然知道了原因,那么解决起来也很简单,给每一个被监听的对象都添加一个id即可。
因为添加sub的操作是在Watcher
中进行的,所以在Watcher
中创建一个对象depIds
image.png
然后给每一个Dep
都添加一个不同的id
image.png
最后在Watcher
中判断depIds
是否已经有这个id的Dep实例存在,如果没有则添加进去并执行addSub
,否则不执行:
image.png
效果,无论怎么修改,都只会有固定数量的Watcher
存在:
image.png -
当修改数据为对象的时候,这个对象没有进行监听,这个也好解决,只要在
image.pngsetter
中进行判断即可,若为对象则针对该对象重新进行监听
总结
到这里为止,我们就完成了view -< model
的绑定,并且知道在new Vue
的时候大致做了一些什么事了,剩下的就是逐步完善,例如对更多指令的支持,对methods
以及computed
和watch
的支持。
网友评论