剖析Vue数据劫持的实现原理

原文: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()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。因此可以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入口函数,整合以上三者 

如图

剖析Vue数据劫持的实现原理_第1张图片

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;
            }
        });
	}
};

只是对this上面绑定data(key)的Object.defineProperty

对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:里面主要是放的的通过表达式来修改页面的方法

 

 

你可能感兴趣的:(vue)