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的幾種watchervue裏面主要有三種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 };複製代碼