水平有限,暂时还只会用Vue。斗胆用MVVM原理分析一下Vue。
什么是双向绑定
之前的一篇废话中介绍过发布订阅者模式,而Vue中也正是采用了 发布订阅者模式 + 数据劫持的方式实现双向绑定。而在这其中Object.defineProperty
起到了至关重要的作用,用法就不多说了。当设置value和获取value时会调用setter
和getter
。PS:听说还有人要为它申请专利…?
大致思路
数据监听器
Observer
,对数据对象的所有属性进行监听,如果有变动则获取到并通知订阅者。指令解析器
Compile
,对元素节点的指令进行扫描和解析,根据指令模板替换数据,绑定相应的回调函数。订阅者
Watcher
,关联以上两者,订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,更新视图。入口函数
mvvm
,整合以上。
结合内容来看,就是这个样子的:
Observer
数据劫持
假设你已经知道了Object.defineProperty()
可以用来监听属性变动,那么将数据对象,包括其子属性对象的属性,进行递归遍历,只要给对象的某个值赋值,就会触发setter
,监听到数据的变化。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let data = {
name: 'Corbusier'
}
Observer(data)
data.name = 'Gaudi'
function Observer(data) {
if(!data || typeof data !== 'object') {
return
}
/* 递归遍历到子属性对象 */
Object.keys(data).forEach( key => {
defineReactive(data, key, data[key])
function defineReactive(data,key,val) {
Observer(val) // 监听子属性
Object.defineProperty(data,key, {
enumerable: true, // 可枚举
configurable: false, // 不可再定义
get: function() {
return val
},
set: function(newVal) {
console.log(`检测到值变化,${val} ===> ${newVal}`)
val = newVal
}
})
}
})
}
// 检测到值变化,Corbusier ===> Gaudi
MVVM构造函数
MVVM构造函数作为一个构造器使用,模仿Vue构造器的功能使用。
1 |
|
这样写会有一个问题,监听数据对象为options.data
,更新视图时,必须通过let vm = new MVVM({data: {name: 'Corbusier'}}) vm._data.name = 'Gaudí'
的方式来改变。而我们期望的方式是:let vm = new MVVM({data: {name: 'Corbusier'}}) vm.name = 'Gaudí'
。
所以需要给MVVM
实例添加一个属性代理的方法,当访问vm属性时代理访问vm._data
属性,改造一下该构造函数:
1 | function MVVM(options = {}) { |
运用Object.defineProperty()
方法来劫持MVVM实例对象的属性的读写权,在读写MVVM实例时转为读写vm._data
的属性值。
Compile
现在来到了编译解析阶段。主要的任务是解析模板指令,将模板中的变量替换为数据,初始化渲染为View层,每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据变动则通知更新View。
遍历解析过程多次操作DOM
节点,为了提高效率、性能,先将el
节点转换为文档碎片fragment
进行解析编译,完成后其放回原真实DOM
节点中。
1 | function MVVM(options = {}) { |
现在我们得到的是一个可以编译,但不能在View层展示数据内容的Compile
编译器,先别着急,继续看下去,稍后给出解决方案。
Watcher
前面介绍过订阅者,Watcher
就是一个订阅者,作为Observer
和Compile
之间的关联。仍然拿设计模式之发布-订阅者模式中的栗子做说明。
你需要发布结婚举行婚礼的消息,这时候打开通讯录,挨个给朋友打电话。这样的过程简化一下就是:
MVVM实例化对象数组中添加订阅者。(在通讯录中的联系人名单)
订阅者添加
update
方法。(保存联系人对应的电话)订阅者添加监听属性,属性变动时调用自身的
update()
方法,并触发Compile
中绑定的回调函数。(打电话挨个通知联系人)
1 | function Dep() { |
在之前实现Compile
的过程中发现数据并没有更新到View,现在我们可以解决这个问题了。
更新视图
再一次通过这张图来理解这其中的逻辑,Watcher
需要做的是:将Compile
和Observer
联系起来。
作为订阅者,
Watcher
在Dep
数组中添加对数据的订阅,当数据变化时通知订阅者,Dep
本身作为Observer
和Watcher
的中间人,类似于之前所说的通讯录的功能。数据变化时,订阅者Watcher
可以直接获取到该变化。另一方面,
Compile
也需要知晓数据的变化,及时完成绑定其中的回调函数的执行,达到更新View
视图视图的目的。所以Compile
同样也要订阅数据的变化。当数据变化时调用update
方法,并触发Compile
中的回调函数。
针对1):在数据劫持Observer
中添加Dep
通知数据变化,发生变化时通知Watcher
,重写一下Watcher
构造函数:
1 | function Watcher(vm, exp, fn) { |
当获取值的时候,会调用defineProperty
的get
方法。给Dep
定义一个全局target
属性,暂存Watcher
,添加完之后移除:
1 | function Observer(data) { |
而在设置值时,需要经由Observer
的set
方法 ==> Dep
中的notify
通知订阅者Watcher
==> Watcher
中的update方法,达到更新视图的目的。
当需要notify
通知时,数据已经发生了变化,此时需要获取新值,并将Compile
里的替换逻辑Replace
去修改{{}}
中的内容。将这三个方法一并修改:
1 | // 接以上 |
现在数据的变化可以反应在视图上了,不信?在控制台输入mvvm.a = 'xx'
试试。是不是突然有变化了?
当然,现在还差一点。数据的双向绑定。
实现双向绑定
比如:在结构中有input
表单,需要做双向绑定。那么需要遍历的是文档中的元素节点。
从节点中找到v-model
表达式,将其初始值与MVVM
实例中的data
属性值绑定。同时键盘输入绑定事件,新值赋给MVVM
实例中的数据值,然后订阅者Watcher
订阅它的数据变化,及时作出更新,完成双向绑定。
听上去很简单,let’s do it!
1 | <div>{{input}}</div> |
1 | function Replace(frag) { |
后续
到这里基本已经结束了,但是发现一个问题:当文档元素中有相邻的moustache expression
时,后一个键值并不能被正确编译。例如:
1 | <div>{{input}} - {{div}}</div> |
改写Compile
中的Replace
逻辑,主要是用了数组的迭代方法,将实例上的所有属性赋值:
1 | function Replace(frag) { |
总结
MVVM主要包含了:1). Observer
利用set
和get
对数据进行劫持 2). Compile
对指令的解析,视图更新回调函数的绑定 3). Watcher
订阅数据的变化,当数据变化时,及时的调用其update
方法,并触发Compile
上的回调函数,及时更新视图,实现数据与视图的同步。