剖析Vue实现原理 - 如何实现双向绑定mvvm
原文:https://github.com/DMQ/mvvm (包含原文源码)
由于源码的注释比较少,我自己加了注释的地址:https://github.com/zengqingxiao/MVVM/tree/master
几种实现双向绑定的做法
- 发布者-订阅者模式(backbone.js)
- 脏值检查(angular.js)
- 数据劫持(vue.js)
发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value)
,
脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval()
定时轮询检测数据变动,当然Google不会这么low,(用一些监听)angular只有在指定的事件触发时进入脏值检测,大致如下
- DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
- XHR响应事件 ( $http )
- 浏览器Location变更事件 ( $location )
- Timer事件( $timeout , $interval )
- 执行 $digest() 或 $apply()
数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。因此可以vm.property = value
这种方式更新数据,同时自动更新视图,于是有了下面两种方式
发现:react和微信小程序好像就是用的发布者-订阅者模式,而vue用的是数据劫持,我认为其主要原因是vue用了Object.defineProperty()给data对象中的每一个值添加了访问器属性(get,set),但是是放弃了对IE8的兼容,而React为了兼容IE,所以只可以吧修改修改的写到一个方法中,而不是像vue一样改变data去自动响应(通过对data添加的set访问器属性)
VUE的数据劫持是实现过程
1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 (Watcher)
2. 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数(Updater)
3. 实现一个Watcher,作为连接Observer和Compile的桥梁,
3-1:在初始化的时候通过Compile(解析指令)来绑定和订阅器Dep的关系
3-2:在数据变化的时候能够并收订阅到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4. mvvm入口函数,整合以上三者
如图
1.数据代理
实现vm.aa 就可以修改vm.data.aa,获取vm.aa,就可以找到vm.data.aa的值
方便了vue的使用值少写一些代码,注意:这里的(set,get是绑定在vm.data这个整体上面的,而data中的每一个属性值也要绑定访问器属性,但是目的是为了完成数据劫持)
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data;
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data, me = this;
// 属性代理,实现 vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function(key) {
me._proxy(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
_proxy: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
};
对data数据处理
按照vue代码也可以从上面的图我们可以知道代码会先执行Observer来对data中的每一个数据进行Object.defineProperty()来添加get和set访问器属性,目的就是了当我们访问data或者修改data的时候我们对其做出响应的操作,例如:set的时候我们去修改页面,那么我们如果修改页面我们后面来讲解,并且每一个data中的数据都有一个Dep对象,这个对象中包含了他们的ID和一个Subs数组,这个数组是来放这个data值和那些wacther对象的,
页面初始化和监听变化
通过Compile来初始化页面对node中的文本节点和元素节点进行解析,通过Updater方法来对node进行渲染,在初始化的过程中我们对那些文本节点和元素节点进行Watcher监听,构造相关Watcher对象(包括了当前表达式exp对应的值,和vm对象,和回调函数),而在构架watcher的时候我们会对表达式中的值读取一遍,为什么呢?其实就是为了触发我们前面在observer中对每一个data设置的访问器属性set,作用就是吧当前watcher,通过原型链放入到相关联的Dap的sunbs数组中,(每一个data都触发一个函数这个函数中定义了相关Dep和访问器函数,那么触发git访问器函数的时候就会找到相关Dep),那么我们每一个data就有一个Dep,而这个Dep中又包含了subs数组中包含了Watcher,当某一个值发生改变遍历subs,找到和data相关的watcher,而这个watcher在触发他相关的回调函数,这个回调函数中通过就会去找到相关的node去和改变表达式的值
注意:例如一个表达式{{A.B.C}}我们只是获取c的值但是这个c这个值相关的watcher分别会放入到a,a.b,a.b.c的Dpe中的subs数组中,主要当a修改那么a.b.c相关的watcher就会被触发,那么相关的回调函数触发修改node,那么是如果做到的呢,其实就是我们如果要获取a.b.c要先获取到a的值,在获取到a.b的值,那么在获取那么值的时候触发了相应的Dep,而中国Dpe就会把当前表达式{{a.b.c}}所关联的Watcher放入到对应的Dep的subs数组中
ubdater:里面主要是放的的通过表达式来修改页面的方法
上一篇: 微信支付回调验证签名处理