vue响应式原理的核心之一就是发布订阅模式。它定义的是一种依赖关系,当一个状态发生改变的时候,全部依赖这个状态的对象都会获得通知。vue
比较典型的就是买东西,好比A想买一个小红花,可是它缺货了,因而A就留下联系方式,等有货了商家就经过A的联系方式通知他。后来又来了B、C、D...,他们也想买小红花,因而他们都留下了联系方式,商家把他们的联系方式都存到小红花的通知列表,等小红花有货了,一并通知这些人。数组
在上面这个例子中,能够抽象出来发布订阅的两个类:缓存
class Dep { constructor(){ this.subs = [] //存放订阅者信息 } addSub(watcher){ //添加订阅者 this.subs.push(watcher) } notify(){ //通知全部订阅者 this.subs.forEach((sub) => { sub.update() }) } } class Watcher{ constructor(cb){ this.cb = cb //订阅者在收到通知要执行的操做 } update(){ this.cb && this.cb() } } const a = new Watcher(()=>{ console.log('A收到,小红花到货了') }) const b = new Watcher(()=>{ console.log('B收到,小红花到货了') }) const dep = new Dep() dep.addSub(a) dep.addSub(b) dep.notify()复制代码
在vue中,响应式数据能够类比成上面例子中的小红花,经过发布订阅的模式来监听数据状态的变化,通知视图进行更新。那么,是在什么时候进行订阅,什么时候进行发布,这就要用到数据劫持。app
vue使用Object.defineProperty()进行数据劫持。ide
let msg = "hello" const data = {}; Object.defineProperty(data, 'msg', { enumerable: true, configurable: true, get() { //读取data.msg时会执行get函数 console.log('get msg') return msg; }, set(newVal) { //为data.msg赋值时会执行set函数 console.log('set msg') msg = newVal; } }); data.msg //'get msg' data.msg = 'hi' //'set msg'复制代码
经过Object.defineProperty定义的属性,在取值和赋值的时候,咱们均可以在它的get、set方法中添加自定义逻辑。当data.msg的值更新时,每个取值data.msg的地方也须要更新,可视为此处要订阅data.msg,所以 在get方法中添加watcher。data.msg从新赋值时,要通知全部watcher进行相应的更新,所以 在set方法中notify全部watcher。函数
在vue中,定义在data中的数据都是响应式的,由于vue对data中的全部属性进行了数据劫持。oop
function initData (vm) { var data = vm.$options.data; observe(data, true); } function observe (value, asRootData) { var ob = new Observer(value); return ob } //Observer的做用就是对数据进行劫持,将数据定义成响应式的 var Observer = function Observer (value) { if (Array.isArray(value)) { //当数据是数组,数组劫持的方式与对象不一样 if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { //当数据是对象,递归对象,将对象的每一层属性都使用Object.defineProperty劫持,如 {a: {b: {c: 1}}} this.walk(value); } };复制代码
使用vue时,data中常常会有数组,和对象不一样,它的数据劫持不能经过Object.defineProperty来实现,下面咱们分别来简单实现一下。测试
对象的数据劫持,首先遍历对象的全部属性,对每个属性使用Object.defineProperty劫持,当属性的值也是对象时,递归。优化
function observeObject(obj){ //递归终止条件 if(!obj || typeof obj !== 'object') return Object.keys(obj).forEach((key) => { let value = obj[key] //递归对obj属性的值进行数据劫持 observeObject(value) let dep = new Dep() //每一个属性都有一个依赖数组 Object.defineProperty(obj,key,{ enumerable: true, configurable: true, get(){ dep.addSub(watcher) //伪代码, 添加watcher return value }, set(newVal){ value = newVal //obj属性从新赋值后,对新赋的值也进行数据劫持,由于新赋的值可能也是一个对象 / ** let a = { b: 1 } a.b = {c: 1} **/ observeObject(value) dep.notify() //伪代码, 通知全部watcher进行更新 } }) }) }复制代码
数组状态的变化主要有两种: 一是数组的项的变化,二是数组长度的变化。所以数组的数据劫持也是考虑这两方面。this
function observeArr(arr){ for(let i=0; i<arr.length; i++){ observe(arr[i]) //伪代码,对每一项进行劫持 } }复制代码
vue对于数组项是简单数据类型的状况没有劫持,这也致使了vue数组使用的一个问题,当数组项是简单数据类型时,修改数据项时视图并不会更新。
<div><span v-for="item in arr">{{item}}</span></div> <button @click="changeArr">change array</button> <!--点击按钮视图不会更新成523-->复制代码
data:{ arr: [1,2,3] }, methods:{ changeArr(){ this.arr[0] = 5 } }复制代码
let arrayProto = Array.prototype; let arrayMethods = Object.create(arrayProto); //arrayMethods继承自Array.prototype let methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; methodsToPatch.forEach((method) => { //重写这7个方法 arrayMethods[method] = function(...args) { let result = arrayProto[method].apply(this,args) //调用原有的数组方法 let inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { //push、unshift、splice可能加入新的数组元素,这里也要对新元素进行劫持 observeArray(inserted); } dep.notify(); //伪代码, 通知全部watcher进行更新 return result } }) arr.__proto__ = arrayMethods //arr是须要进行劫持的数组,修改它原有的原型链方法。复制代码实现一个简单的双向数据绑定
class Vue { constructor(options){ this.$data = options.data this.$getterFn = options.getterFn observe(this.$data) // 将定义在options.data中的数据做响应式处理 //options.getterFn是一个取值函数,模拟页面渲染时要作的取值操做 new Watcher(this.$data, this.$getterFn.bind(this), key => { console.log(key + "已修改,视图刷新") }) } }复制代码
function observe(data){ if(!data || typeof data !== 'object') return let ob; //为数据建立observer时,会将observer添加到数据属性,若是数据已经有observer,会直接返回该observer if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observer) { ob = data.__ob__; }else{ ob = new Observer(data) } return ob } class Observer { constructor(data){ this.dep = new Dep() //将dep挂载到observer上,用于处理data是数组的状况 Object.defineProperty(data, '__ob__', { //将observer挂载到要data上,方便经过data访问dep属性和walk、observeArray方法 enumerable: false, configurable: false, value: this }) if(Array.isArray(data)){ //若是是数组,重写数组的7个方法,对数组的每一项做响应式处理 data.__proto__ = arrayMethods this.observeArray(data) }else{ this.walk(data) } } walk(data){ let keys = Object.keys(data) keys.forEach((key) => { defineReactive(data, key) }) } observeArray(data){ data.forEach((val) => { observe(val) }) } } //重写数组的7个方法 let arrayProto = Array.prototype; let arrayMethods = Object.create(arrayProto); //arrayMethods继承自Array.prototype let methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; methodsToPatch.forEach((method) => { arrayMethods[method] = function(...args) { //将一个不定数量的参数表示为一个数组 let result = arrayProto[method].apply(this,args) //调用原有的数组方法 let inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { //push、unshift、splice可能加入新的数组元素,这里也要对新元素进行劫持 this.__ob__.observeArray(inserted); } this.__ob__.dep.notify('array') //触发这个数组dep的notify方法 return result } }) function defineReactive(data,key){ let dep = new Dep() //每一个属性对应一个dep,来管理订阅 let value = data[key] //当value是数组时,不会为数组的每一个属性添加dep,而是为整个数组添加一个dep。 //当数组执行上面那7个方法时,就触发这个dep的notify方法 this.__ob__.dep.notify('array') let childOb = observe(value) Object.defineProperty(data,key,{ enumerable: true, configurable: true, get(){ //添加订阅者。Dep.target是一个全局对象。它指向当前的watcher Dep.target && dep.addSub(Dep.target) if(Array.isArray(value)) { Dep.target && childOb.dep.addSub(Dep.target) } return value }, set(newVal){ if(newVal === value) return value = newVal observe(value) dep.notify(key) } }) }复制代码
什么时候触发watcher仍是明显的。添加watcher就有点不太明显了。这里对watcher的构造函数做了一些修改。
Dep.target = null class Watcher{ constructor(data,getterFn,cb){ this.cb = cb Dep.target = this getterFn() Dep.target = null } update(key){ this.cb && this.cb(key) } }复制代码
关键就是:
Dep.target = this getterFn() Dep.target = null复制代码
在new Watcher()时,就会执行这三行代码。Dep.target = this将当前建立的watcher赋值给Dep.target这个全局变量,执行getterFn()时,会对取vm.$data中的值,上面已经将vm.$data做了响应式处理,因此取它值的时候就会执行各属性的get方法。
get(){ //此时Dep.target指向当前的watcher,此时就将当前watcher添加到这个属性对应的订阅数组里。 Dep.target && dep.addSub(Dep.target) if(Array.isArray(value)) { Dep.target && childOb.dep.addSub(Dep.target) //若是属性对应的值是数组,就将当前watcher添加到该数组对应的订阅数组里。 } return value },复制代码
这样就完成了对须要访问的属性添加watcher的操做,而后将Dep.target还原成null。
测试代码:(渲染视图也是对data里的属性取值,如{{msg.m}},添加watcher,完成订阅。这里咱们就简单访问取值来进行模拟)
let vm = new Vue({ el: '#root', data:{ msg: { m: "hello world" }, arr: [ {a: 1}, {a: 2} ] }, getterFn(){ console.log(this.$data.msg.m) this.$data.arr.forEach((item) => { console.log(item.a) }) } })复制代码
效果:能够看到,getterFn访问过的数据,在修改值时就会触发watcher的回调函数。
vue里面主要有三种watcher:
渲染watcher是在vm.$mount()方法执行时建立的。
Vue.prototype.$mount = function () { var updateComponent = function () { vm._update(vm._render(), hydrating); }; //updateComponent就是进行视图渲染的函数,对data中数据的取值的操做就是在该函数中完成 new Watcher(vm, updateComponent, noop, options,true); };复制代码
Watcher的构造函数:
var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) { this.vm = vm; if (options) { ... this.lazy = !!options.lazy; //主要用于computed watcher } else { this.deep = this.user = this.lazy = this.sync = false; } this.cb = cb; if (typeof expOrFn === 'function') { this.getter = expOrFn; //expOrFn对应上面的updateComponent方法 } else { this.getter = parsePath(expOrFn); } //若是this.lazy为false,就当即执行this.get() //因此在建立watcher的时候就会执行updateComponent方法 this.value = this.lazy? undefined: this.get(); }; Watcher.prototype.get = function get () { pushTarget(this); //类比上面简易版的Dep.target = this var value; var vm = this.vm; value = this.getter.call(vm, vm); //执行取值函数,完成watcher订阅 popTarget(); //类比上面简易版的Dep.target = null return value };复制代码
在渲染watcher建立的时候,就当即执行取值函数,完成响应式数据的依赖收集。能够看出,定义在data中的数据,它们的watcher都是同一个,就是在vm.$mount()方法执行时建立的watcher。watcher的update方法:
Watcher.prototype.update = function update () { if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); //渲染watcher会走这里的逻辑,其实最终都会执行this.run(),只是这里用队列进行优化 } }; Watcher.prototype.run = function run () { var value = this.get(); //又会执行updateComponent方法 }复制代码
定义在data中的数据,它们的watcher都是同一个,当data每一次数据中数据更新时,都会执行watcher.update()。渲染watcher的update()最终会执行updateComponent方法,若是一次性修改N个data属性时,好比下面例子中的change,理论上会执行N次updateComponent(),很明显,这是不科学的。
做为优化,维护一个watcher队列,每次执行watcher.update()就尝试往队列里面添加watcher(queueWatcher(this)),若是当前watcher已经存在于队列中,就再也不添加。最后在nextTick中一次性执行这些watcher的run方法。
这样,若是一次性修改N个data属性时,实际上只会执行一次updateComponent()
data:{ msg: "hello", msg2: "ni hao" }, methods:{ change(){ this.msg = "hi" this.msg2 = "hi" } },复制代码
data:{ msg: "hello" }, computed: { newMsg(){ return this.msg + ' computed' } },复制代码
<div>{{newMsg}}</div>复制代码
当msg更新时,newMsg也会更新。由于computed会对访问到的data数据(这里是msg)进行订阅。
function initComputed (vm, computed) { var watchers = vm._computedWatchers = Object.create(null); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; watchers[key] = new Watcher( //watcher的取值函数就是咱们在computed中定义的函数 vm, getter || noop, noop, computedWatcherOptions // { lazy: true } ); if (!(key in vm)) { defineComputed(vm, key, userDef); } } }复制代码
在initComputed的时候,建立了watcher,它有个属性lazy: ture。在watcher的constructor中,lazy: ture表示建立watcher的时候不会执行取值函数,因此,此时watcher并无加入msg的订阅数组。
this.value = this.lazy? undefined: this.get(); 复制代码
只有在页面对computed进行取值{{newMsg}}的时候,watcher才会加入msg的订阅数组。这里主要来看看defineComputed方法,它的大体逻辑以下:
function defineComputed (target,key,userDef) { // target:vm, key: newMsg Object.defineProperty(target, key, { enumerable: true, configurable: true, get: function computedGetter () { //当视图对newMsg进行取值的时候会执行这里 var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { //这里要对照Watcher的构造函数来看,默认watcher.dirty = watcher.lazy,首次执行为true watcher.evaluate(); //会执行watcher.evaluate() } if (Dep.target) { watcher.depend(); } return watcher.value } }, set: userDef.set || noop }); } Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); //执行watcher的取值函数,返回取值函数执行的结果,并将watcher添加到msg的订阅数组 this.dirty = false; //this.dirty置为false,用于缓存。 };复制代码
computed watcher有个属性dirty,用于标记是否执行取值函数。
一、初始化watcher时,watcher.dirty = watcher.lazy,值为true。页面第一次访问newMsg时就会执行watcher.evaluate()
二、取值完成后,watcher.dirty = false。下一次页面再取值就会直接返回以前计算获得的值 watcher.value 。
三、若是watcher订阅的 msg 发生变化,就会通知执行watcher的 watcher.update()。lazy属性为true的watcher执行update方法是watcher.dirty = true,这样页面取值newMsg就会从新执行取值函数,返回新的值。这样就实现了computed的缓存功能。
Watcher.prototype.update = function update () { if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } };复制代码
watch:{ msg(newValue,oldValue){ console.log(newValue,oldValue) } },复制代码
或者这样:
mounted(){ this.$watch('msg',function(newValue,oldValue){ console.log(newValue,oldValue) }) }复制代码
user watcher的核心方法就是vm.$watch:
Vue.prototype.$watch = function (expOrFn,cb,options) { //核心就是这里 //expOrFn ---> msg //cb ---> 用户本身定义的回调函数,function(oldValue,newValue){console.log(oldValue,newValue)} var watcher = new Watcher(vm, expOrFn, cb, options); }; }复制代码
和渲染watcher、 computed watcher的expOrFn不一样,user watcher 的expOrFn是个表达式。
//watcher的构造函数中 if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); }复制代码
建立user watcher时,会根据这个表达式完成取值操做,添加watcher到订阅数组。
expOrFn: 'msg' -----> vm.msg expOrFn: 'obj.a' -----> vm.obj ----->vm.obj.a复制代码
deep:true时,会递归遍历当前属性对应的值,将watcher添加到全部属性上,每一次修改某一个属性都会执行watcher.update()。
Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; value = this.getter.call(vm, vm); if (this.deep) { traverse(value); //递归遍历取值,每次取值都添加该watcher到取值属性的订阅数组。 } popTarget(); return value };复制代码