首先歡迎你們關注個人Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵。接下來的日子我應該會着力寫一系列關於Vue與React內部原理的文章,感興趣的同窗點個關注或者Star。
以前的兩篇文章響應式數據與數據依賴基本原理和從Vue數組響應化所引起的思考咱們介紹了響應式數據相關的內容,沒有看的同窗能夠點擊上面的連接瞭解一下。若是你們都閱讀過上面兩篇文章的話,確定對這方面內容有了足夠的知識儲備,想來是時候來看看Vue內部是如何實現數據響應化。目前Vue的代碼很是龐大,但其中包含了例如:服務器渲染等咱們不關心的內容,爲了能集中於咱們想學習的部分,咱們此次閱讀的是Vue的早期代碼,你們能夠checkout
到這裏查看對應的代碼。
以前零零碎碎的看過React的部分源碼,當我看到Vue的源碼,以爲真的是很是優秀,各個模塊之間解耦的很是好,可讀性也很高。Vue響應式數據是在Observer
模塊中實現的,咱們能夠看看Observer
是如何實現的。
javascript
若是看過上兩篇文章的同窗應該會發現一個問題:數據響應化的代碼與其餘的代碼耦合太強了,好比說:
前端
//代碼來源於文章:響應式數據與數據依賴基本原理 //定義對象的單個響應式屬性 function defineReactive(obj, key, value){ observify(value); Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: function(newValue){ var oldValue = value; value = newValue; //能夠在修改數據時觸發其餘的操做 console.log("newValue: ", newValue, " oldValue: ", oldValue); }, get: function(){ return value; } }); }
好比上面的代碼,set
內部的處理的代碼就與整個數據響應化相耦合,若是下次咱們想要在set
中作其餘的操做,就必需要修改set
函數內部的內容,這是很是不友好的,不符合開閉原則(OCP: Open Close Principle)。固然Vue不會採用這種方式去設計,爲了解決這個問題,Vue引入了發佈-訂閱模式。其實發布-訂閱模式是前端工程師很是熟悉的一種模式,又叫作觀察者模式,它是一種定義對象間一種一對多的依賴關係,當一個對象的狀態發生改變的時候,其餘觀察它的對象都會獲得通知。咱們最多見的DOM事件就是一種發佈-訂閱模式。好比:
vue
document.body.addEventListener("click", function(){ console.log("click event"); });
在上面的代碼中咱們監聽了body
的click
事件,雖然咱們不知道click
事件何時會發生,可是咱們必定能保證,若是發生了body
的click
事件,咱們必定能獲得通知,即回調函數被調用。在JavaScript中由於函數是一等公民,咱們不多使用傳統的發佈-訂閱模式,多采用的是事件模型的方式實現。在Vue中也實現了一個事件模型,咱們能夠看一下。由於Vue的模塊之間解耦的很是好,所以在看代碼以前,其實咱們能夠先來看看對應的單元測試文件,你就知道這個模塊要實現什麼功能,甚至若是你願意的話,也能夠本身實現一個相似的模塊放進Vue的源碼中運行。java
Vue早期代碼使用是jasmine
進行單元測試,emitter_spec.js
是事件模型的單元測試文件。首先簡單介紹一下jasmine
用到的函數,能夠對照下面的代碼瞭解具體的功能:git
describe
是一個測試單元集合it
是一個測試用例beforeEach
會在每個測試用例it
執行前執行expect
指望函數,用做對指望值和實際值之間執行邏輯比較createSpy
用來建立spy,而spy的做用是監測函數的調用相關信息和函數執行參數
var Emitter = require('../../../src/emitter') var u = undefined // 代碼有刪減 describe('Emitter', function () { var e, spy beforeEach(function () { e = new Emitter() spy = jasmine.createSpy('emitter') }) it('on', function () { e.on('test', spy) e.emit('test', 1, 2 ,3) expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith(1, 2, 3) }) it('once', function () { e.once('test', spy) e.emit('test', 1, 2 ,3) e.emit('test', 2, 3, 4) expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith(1, 2, 3) }) it('off', function () { e.on('test1', spy) e.on('test2', spy) e.off() e.emit('test1') e.emit('test2') expect(spy.calls.count()).toBe(0) }) it('apply emit', function () { e.on('test', spy) e.applyEmit('test', 1) e.applyEmit('test', 1, 2, 3, 4, 5) expect(spy).toHaveBeenCalledWith(1) expect(spy).toHaveBeenCalledWith(1, 2, 3, 4, 5) }) })
能夠看出Emitter
對象實例對外提供如下接口:github
on
: 註冊監聽接口,參數分別是事件名和監聽函數 emit
: 觸發事件函數,參數是事件名 off
: 取消對應事件的註冊函數,參數分別是事件名和監聽函數 once
: 與on
相似,僅會在第一次時通知監聽函數,隨後監聽函數會被移除。看完了上面的單元測試代碼,咱們如今已經基本瞭解了這個模塊要幹什麼,如今讓咱們看看對應的代碼:數組
// 刪去了註釋而且對代碼順序有調整 // ctx是監聽回調函數的執行做用域(this) function Emitter (ctx) { this._ctx = ctx || this } var p = Emitter.prototype p.on = function (event, fn) { this._cbs = this._cbs || {} ;(this._cbs[event] || (this._cbs[event] = [])) .push(fn) return this } // 三種模式 // 不傳參狀況清空全部監聽函數 // 僅傳事件名則清除該事件的全部監聽函數 // 傳遞事件名和回調函數,則對應僅刪除對應的監聽事件 p.off = function (event, fn) { this._cbs = this._cbs || {} // all if (!arguments.length) { this._cbs = {} return this } // specific event var callbacks = this._cbs[event] if (!callbacks) return this // remove all handlers if (arguments.length === 1) { delete this._cbs[event] return this } // remove specific handler var cb for (var i = 0; i < callbacks.length; i++) { cb = callbacks[i] // 這邊的代碼之因此會有cb.fn === fn要結合once函數去看 // 給once傳遞的監聽函數其實已經被wrapped過 // 可是仍然能夠經過原來的監聽函數去off掉 if (cb === fn || cb.fn === fn) { callbacks.splice(i, 1) break } } return this } // 觸發對應事件的全部監聽函數,注意最多隻能用給監聽函數傳遞三個參數(採用call) p.emit = function (event, a, b, c) { this._cbs = this._cbs || {} var callbacks = this._cbs[event] if (callbacks) { callbacks = callbacks.slice(0) for (var i = 0, len = callbacks.length; i < len; i++) { callbacks[i].call(this._ctx, a, b, c) } } return this } // 觸發對應事件的全部監聽函數,傳遞參數個數不受限制(採用apply) p.applyEmit = function (event) { this._cbs = this._cbs || {} var callbacks = this._cbs[event], args if (callbacks) { callbacks = callbacks.slice(0) args = callbacks.slice.call(arguments, 1) for (var i = 0, len = callbacks.length; i < len; i++) { callbacks[i].apply(this._ctx, args) } } return this } // 經過調用on與off事件事件,在第一次觸發以後就`off`對應的監聽事件 p.once = function (event, fn) { var self = this this._cbs = this._cbs || {} function on () { self.off(event, on) fn.apply(this, arguments) } on.fn = fn this.on(event, on) return this }
咱們能夠看到上面的代碼採用了原型模式建立了一個Emitter
類。配合Karma跑一下這個模塊 ,測試用例所有經過,到如今咱們已經閱讀完Emitter
了,這算是一個小小的熱身吧,接下來讓咱們正式看一下Observer
模塊。
瀏覽器
按照上面的思路咱們先看看Observer
對應的測試用例observer_spec.js
,因爲Observer
的測試用例很是長,我會在代碼註釋中作解釋,並儘可能精簡測試用例,能讓咱們瞭解模塊對應功能便可,但願你能有耐心閱讀下來。
服務器
//測試用例是精簡版,不然太冗長 var Observer = require('../../../src/observe/observer') var _ = require('../../../src/util') //Vue內部使用工具方法 var u = undefined Observer.pathDelimiter = '.' //配置Observer路徑分隔符 describe('Observer', function () { var spy beforeEach(function () { spy = jasmine.createSpy('observer') }) //咱們能夠看到咱們經過Observer.create函數能夠將數據變爲可響應化, //而後咱們監聽get事件能夠在屬性被讀取時觸發對應事件,注意對象嵌套的狀況(例如b.c) it('get', function () { Observer.emitGet = true var obj = { a: 1, b: { c: 2 } } var ob = Observer.create(obj) ob.on('get', spy) var t = obj.b.c expect(spy).toHaveBeenCalledWith('b', u, u) expect(spy).toHaveBeenCalledWith('b.c', u, u) Observer.emitGet = false }) //咱們能夠監聽響應式數據的set事件,當響應式數據修改的時候,會觸發對應的時間 it('set', function () { var obj = { a: 1, b: { c: 2 } } var ob = Observer.create(obj) ob.on('set', spy) obj.b.c = 4 expect(spy).toHaveBeenCalledWith('b.c', 4, u) }) //帶有$與_開頭的屬性都不會被處理 it('ignore prefix', function () { var obj = { _test: 123, $test: 234 } var ob = Observer.create(obj) ob.on('set', spy) obj._test = 234 obj.$test = 345 expect(spy.calls.count()).toBe(0) }) //訪問器屬性也不會被處理 it('ignore accessors', function () { var obj = { a: 123, get b () { return this.a } } var ob = Observer.create(obj) obj.a = 234 expect(obj.b).toBe(234) }) // 對數屬性的get監聽,注意嵌套的狀況 it('array get', function () { Observer.emitGet = true var obj = { arr: [{a:1}, {a:2}] } var ob = Observer.create(obj) ob.on('get', spy) var t = obj.arr[0].a expect(spy).toHaveBeenCalledWith('arr', u, u) expect(spy).toHaveBeenCalledWith('arr.0.a', u, u) expect(spy.calls.count()).toBe(2) Observer.emitGet = false }) // 對數屬性的get監聽,注意嵌套的狀況 it('array set', function () { var obj = { arr: [{a:1}, {a:2}] } var ob = Observer.create(obj) ob.on('set', spy) obj.arr[0].a = 2 expect(spy).toHaveBeenCalledWith('arr.0.a', 2, u) }) // 咱們看到能夠經過監聽mutate事件,在push調用的時候對應觸發事件 // 觸發事件第一個參數是"",表明的是路徑名,具體源碼能夠看出,對於數組變異方法都是空字符串 // 觸發事件第二個參數是數組自己 // 觸發事件第三個參數比較複雜,其中: // method屬性: 表明觸發的方法名稱 // args屬性: 表明觸發方法傳遞參數 // result屬性: 表明觸發變異方法以後數組的結果 // index屬性: 表明變異方法對數組發生變化的最開始元素 // inserted屬性: 表明數組新增的元素 // remove屬性: 表明數組刪除的元素 // 其餘的變異方法: pop、shift、unshift、splice、sort、reverse內容都是很是類似的 // 具體咱們就不一一列舉的了,若是有疑問能夠本身看到所有的單元測試代碼 it('array push', function () { var arr = [{a:1}, {a:2}] var ob = Observer.create(arr) ob.on('mutate', spy) arr.push({a:3}) expect(spy.calls.mostRecent().args[0]).toBe('') expect(spy.calls.mostRecent().args[1]).toBe(arr) var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('push') expect(mutation.index).toBe(2) expect(mutation.removed.length).toBe(0) expect(mutation.inserted.length).toBe(1) expect(mutation.inserted[0]).toBe(arr[2]) }) // 咱們能夠看到響應式數據中存在$add方法,相似於Vue.set,能夠監聽add事件 // 能夠向響應式對象中添加新一個屬性,若是以前存在該屬性則操做會被忽略 // 而且新賦值的對象也必須被響應化 // 咱們省略了對象數據$delete方法的單元測試,功能相似於Vue.delete,與$add方法相反,能夠用於刪除對象的屬性 // 咱們省略了數組的$set方法的單元測試,功能也相似與Vue.set,能夠用於設置數組對應數字下標的值 // 咱們省略了數組的$remove方法的單元測試,功能用於移除數組給定下標的值或者給定的值,例如: // var arr = [{a:1}, {a:2}] // var ob = Observer.create(arr) // arr.$remove(0) => 移除對應下標的值 或者 // arr.$remove(arr[0]) => 移除給定的值 it('object.$add', function () { var obj = {a:{b:1}} var ob = Observer.create(obj) ob.on('add', spy) // ignore existing keys obj.$add('a', 123) expect(spy.calls.count()).toBe(0) // add event var add = {d:2} obj.a.$add('c', add) expect(spy).toHaveBeenCalledWith('a.c', add, u) // check if add object is properly observed ob.on('set', spy) obj.a.c.d = 3 expect(spy).toHaveBeenCalledWith('a.c.d', 3, u) }) // 下面的測試用例用來表示若是兩個不一樣對象parentA、parentB的屬性指向同一個對象obj,那麼該對象obj改變時會分別parentA與parentB的監聽事件 it('shared observe', function () { var obj = { a: 1 } var parentA = { child1: obj } var parentB = { child2: obj } var obA = Observer.create(parentA) var obB = Observer.create(parentB) obA.on('set', spy) obB.on('set', spy) obj.a = 2 expect(spy.calls.count()).toBe(2) expect(spy).toHaveBeenCalledWith('child1.a', 2, u) expect(spy).toHaveBeenCalledWith('child2.a', 2, u) // test unobserve parentA.child1 = null obj.a = 3 expect(spy.calls.count()).toBe(4) expect(spy).toHaveBeenCalledWith('child1', null, u) expect(spy).toHaveBeenCalledWith('child2.a', 3, u) }) })
能堅持看到這裏,咱們的長征路就走過了一半了,咱們已經知道了Oberver
對外提供的功能了,如今咱們就來了解一下Oberver
內部的實現原理。
Oberver
模塊實際上採用採用組合繼承(借用構造函數+原型繼承)方式繼承了Emitter
,其目的就是繼承Emitter
的on
, off
,emit
等方法。咱們在上面的測試用例發現,咱們並無用new
方法直接建立一個Oberver
的對象實例,而是採用一個工廠方法Oberver.create
方法來建立的,咱們接下來看源碼,因爲代碼比較多我會盡可能去拆分紅一個個小塊來說:
前端工程師
// 代碼出自於observe.js // 爲了方便講解我對代碼順序作了改變,要了解詳細的狀況能夠查看具體的源碼 var _ = require('../util') var Emitter = require('../emitter') var arrayAugmentations = require('./array-augmentations') var objectAugmentations = require('./object-augmentations') var uid = 0 /** * Type enums */ var ARRAY = 0 var OBJECT = 1 function Observer (value, type, options) { Emitter.call(this, options && options.callbackContext) this.id = ++uid this.value = value this.type = type this.parents = null if (value) { _.define(value, '$observer', this) if (type === ARRAY) { _.augment(value, arrayAugmentations) this.link(value) } else if (type === OBJECT) { if (options && options.doNotAlterProto) { _.deepMixin(value, objectAugmentations) } else { _.augment(value, objectAugmentations) } this.walk(value) } } } var p = Observer.prototype = Object.create(Emitter.prototype) Observer.pathDelimiter = '\b' Observer.emitGet = false Observer.create = function (value, options) { if (value && value.hasOwnProperty('$observer') && value.$observer instanceof Observer) { return value.$observer } if (_.isArray(value)) { return new Observer(value, ARRAY, options) } else if ( _.isObject(value) && !value.$scope // avoid Vue instance ) { return new Observer(value, OBJECT, options) } }
咱們首先從Observer.create
看起,若是value
值沒有響應化過(經過是否含有$observer
屬性去判斷),則使用new操做符建立Obsever實例(區分對象OBJECT與數組ARRAY)。接下來咱們看Observer
的構造函數是怎麼定義的,首先借用Emitter
構造函數:
Emitter.call(this, options && options.callbackContext)
配合原型繼承
var p = Observer.prototype = Object.create(Emitter.prototype)
從而實現了組合繼承Emitter
,所以Observer
繼承了Emitter
的屬性(ctx
)和方法(on
,emit
等)。咱們能夠看到Observer
有如下屬性:
id
: 響應式數據的惟一標識value
: 原始數據type
: 標識是數組仍是對象parents
: 標識響應式數據的父級,可能存在多個,好比var obj = { a : { b: 1}}
,在處理{b: 1}
的響應化過程當中parents
中某個屬性指向的就是obj
的$observer
。 咱們接着看首先給該數據賦值$observer
屬性,指向的是實例對象自己。_.define
內部是經過defineProperty
實現的:
define = function (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value : val, enumerable : !!enumerable, writable : true, configurable : true }) }
下面咱們首先看看是怎麼處理數組類型的數據的
if (type === ARRAY) { _.augment(value, arrayAugmentations) this.link(value) }
若是看過我前兩篇文章的同窗,其實還記得咱們對數組響應化當時還作了一個着重的原理講解,大概原理就是咱們經過給數組對象設置新的原型對象,從而遮蔽掉原生數組的變異方法,大概的原理能夠是:
function observifyArray(array){ var aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; var arrayAugmentations = Object.create(Array.prototype); aryMethods.forEach((method)=> { let original = Array.prototype[method]; arrayAugmentations[method] = function () { // 調用對應的原生方法並返回結果 // do everything you what do ! return original.apply(this, arguments); }; }); array.__proto__ = arrayAugmentations; }
回到Vue的源碼,雖然咱們知道基本原理確定是相同的,可是咱們仍然須要看看arrayAugmentations
是什麼?下面arrayAugmentations
代碼比較長。咱們會在註釋裏面解釋基本原理:
// 代碼來自於array-augmentations.js var _ = require('../util') var arrayAugmentations = Object.create(Array.prototype) // 這邊操做和咱們以前的實現方式很是類似 // 建立arrayAugmentations原型繼承`Array.prototype`從而能夠調用數組的原生方法 // 而後經過arrayAugmentations覆蓋數組的變異方法,基本邏輯大體相同 ['push','pop','shift','unshift','splice','sort','reverse'].forEach(function (method) { var original = Array.prototype[method] // 覆蓋arrayAugmentations中的變異方法 _.define(arrayAugmentations, method, function () { var args = _.toArray(arguments) // 這裏調用了原生的數組變異方法,並得到結果 var result = original.apply(this, args) var ob = this.$observer var inserted, removed, index // 下面switch這一部分代碼看起來很長,其實目的就是針對於不一樣的變異方法生成: // insert removed inserted 具體的含義對照以前的解釋,瞭解便可 switch (method) { case 'push': inserted = args index = this.length - args.length break case 'unshift': inserted = args index = 0 break case 'pop': removed = [result] index = this.length break case 'shift': removed = [result] index = 0 break case 'splice': inserted = args.slice(2) removed = result index = args[0] break } // 若是給數組中插入新的數據,則須要調用ob.link // link函數其實在上面的_.augment(value, arrayAugmentations)以後也被調用了 // 具體的實現咱們能夠先無論 // 咱們只要知道其目的就是分別對插入的數據執行響應化 if (inserted) ob.link(inserted, index) // 其實從link咱們就能夠猜出unlink是幹什麼的 // 主要就是對刪除的數據解除響應化,具體實現邏輯後面解釋 if (removed) ob.unlink(removed) // updateIndices咱們也先不講是怎麼實現的, // 目的就是更新子元素在parents的key // 由於push和pop是不會改變現有元素的位置,所以不須要調用 // 而諸如splce shift unshift等變異方法會改變對應下標值,所以須要調用 if (method !== 'push' && method !== 'pop') { ob.updateIndices() } // 一樣咱們先不考慮propagate內部實現,咱們只要propagate函數的目的就是 // 觸發自身及其遞歸觸發父級的事件 // 若是數組中的數據有插入或者刪除,則須要對外觸發"length"被改變 if (inserted || removed) { ob.propagate('set', 'length', this.length) } // 對外觸發mutate事件 // 能夠對照咱們以前講的測試用例'array push',就是在這裏觸發的,回頭看看吧 ob.propagate('mutate', '', this, { method : method, args : args, result : result, index : index, inserted : inserted || [], removed : removed || [] }) return result }) }) // 能夠回看一下測試用例 array set,目的就是設置對應下標的值 // 其實就是調用了splice變異方法, 其實咱們在Vue中國想要改變某個下標的值的時候 // 官網給出的建議無非是Vue.set或者就是splice,都是相同的原理 // 注意這裏的代碼忽略了超出下標範圍的值 _.define(arrayAugmentations, '$set', function (index, val) { if (index >= this.length) { this.length = index + 1 } return this.splice(index, 1, val)[0] }) // $remove與$add都是一個道理,都是調用的是`splice`函數 _.define(arrayAugmentations, '$remove', function (index) { if (typeof index !== 'number') { index = this.indexOf(index) } if (index > -1) { return this.splice(index, 1)[0] } }) module.exports = arrayAugmentations
上面的代碼相對比較長,具體的解釋咱們在代碼中已經註釋。到這裏咱們已經瞭解完arrayAugmentations
了,咱們接着看看_.augment
作了什麼。咱們在文章從Vue數組響應化所引起的思考中講過Vue是經過__proto__
來實現數組響應化的,可是因爲__proto__
是個非標準屬性,雖然普遍的瀏覽器廠商基本都實現了這個屬性,可是仍是存在部分的安卓版本並不支持該屬性,Vue必須對此作相關的處理,_.augment
就負責這個部分:
exports.augment = '__proto__' in {} ? function (target, proto) { target.__proto__ = proto } : exports.deepMixin exports.deepMixin = function (to, from) { Object.getOwnPropertyNames(from).forEach(function (key) { var desc =Object.getOwnPropertyDescriptor(from, key) Object.defineProperty(to, key, desc) }) }
咱們看到若是瀏覽器不支持__proto__
話調用deepMixin
函數。而deepMixin
的實現也是很是的簡單,就是使用Object.defineProperty
將原對象的屬性描述符賦值給目標對象。接着調用了函數:
this.link(value)
關於link
函數在上面的備註中咱們已經見過了:
if (inserted) ob.link(inserted, index)
當時咱們的解釋是將新插入的數據響應化,知道了功能咱們看看代碼的實現:
// p === Observer.prototype p.link = function (items, index) { index = index || 0 for (var i = 0, l = items.length; i < l; i++) { this.observe(i + index, items[i]) } } p.observe = function (key, val) { var ob = Observer.create(val) if (ob) { // register self as a parent of the child observer. var parents = ob.parents if (!parents) { ob.parents = parents = Object.create(null) } if (parents[this.id]) { _.warn('Observing duplicate key: ' + key) return } parents[this.id] = { ob: this, key: key } } }
其實代碼邏輯很是簡單,link
函數會對給定數組index(默認爲0)以後的元素調用this.observe
, 而observe
其實也就是對給定的val
值遞歸調用Observer.create
,將數據響應化,並創建父級的Observer與當前實例的對應關係。前面其實咱們發現Vue不只僅會對插入的數據響應化,而且也會對刪除的元素調用unlink
,具體的調用代碼是:
if (removed) ob.unlink(removed)
以前咱們大體講過其用做就是對刪除的數據解除響應化,咱們來看看具體的實現:
p.unlink = function (items) { for (var i = 0, l = items.length; i < l; i++) { this.unobserve(items[i]) } } p.unobserve = function (val) { if (val && val.$observer) { val.$observer.parents[this.id] = null } }
代碼很是簡單,就是對數據調用unobserve
,而unobserve
函數的主要目的就是解除父級observer
與當前數據的關係而且再也不保留引用,讓瀏覽器內核必要的時候可以回收內存空間。
在arrayAugmentations
中其實還調用過Observer
的兩個原型方法,一個是:
ob.updateIndices()
另外一個是:
ob.propagate('set', 'length', this.length)
首先看看updateIndices
函數,當時的函數的做用是更新子元素在parents的key,來看看具體實現:
p.updateIndices = function () { var arr = this.value var i = arr.length var ob while (i--) { ob = arr[i] && arr[i].$observer if (ob) { ob.parents[this.id].key = i } } }
接着看函數propagate
:
p.propagate = function (event, path, val, mutation) { this.emit(event, path, val, mutation) if (!this.parents) return for (var id in this.parents) { var parent = this.parents[id] if (!parent) continue var key = parent.key var parentPath = path ? key + Observer.pathDelimiter + path : key parent.ob.propagate(event, parentPath, val, mutation) } }
咱們以前說過propagate
函數的做用的就是觸發自身及其遞歸觸發父級的事件,首先調用emit
函數對外觸發時間,其參數分別是:事件名、路徑、值、mutatin
對象。而後接着遞歸調用父級的事件,而且對應改變觸發的path
參數。parentPath
等於parents[id].key
+ Observer.pathDelimiter
+ path
到此爲止咱們已經學習完了Vue是如何處理數組的響應化的,如今須要來看看是如何處理對象的響應化的。
在Observer
的構造函數中關於對象處理的代碼是:
if (type === OBJECT) { if (options && options.doNotAlterProto) { _.deepMixin(value, objectAugmentations) } else { _.augment(value, objectAugmentations) } this.walk(value) }
和數組同樣,咱們首先要了解一下objectAugmentations
的內部實現:
var _ = require('../util') var objectAgumentations = Object.create(Object.prototype) _.define(objectAgumentations, '$add', function (key, val) { if (this.hasOwnProperty(key)) return _.define(this, key, val, true) var ob = this.$observer ob.observe(key, val) ob.convert(key, val) ob.emit('add:self', key, val) ob.propagate('add', key, val) }) _.define(objectAgumentations, '$delete', function (key) { if (!this.hasOwnProperty(key)) return delete this[key] var ob = this.$observer ob.emit('delete:self', key) ob.propagate('delete', key) })
相比於arrayAugmentations
,objectAgumentations
內部實現則簡單的多,objectAgumentations
添加了兩個方法: $add
與$delete
。
$add
用於給對象添加新的屬性,若是該對象以前就存在鍵值爲key
的屬性則不作任何操做,不然首先使用_.define
賦值該屬性,而後調用ob.observe
目的是遞歸調用使得val
值響應化。而convert
函數的做用是將該屬性轉換成訪問器屬性getter/setter
使得屬性被訪問或者被改變的時候咱們可以監聽到,具體我能夠看一下convert
函數的內部實現:
p.convert = function (key, val) { var ob = this Object.defineProperty(ob.value, key, { enumerable: true, configurable: true, get: function () { if (Observer.emitGet) { ob.propagate('get', key) } return val }, set: function (newVal) { if (newVal === val) return ob.unobserve(val) val = newVal ob.observe(key, newVal) ob.emit('set:self', key, newVal) ob.propagate('set', key, newVal) } }) }
convert
函數的內部實現也不復雜,在get
函數中,若是開啓了全局的Observer.emitGet
開關,在該屬性被訪問的時候,會對調用propagate
觸發自己以及父級的對應get
事件。在set
函數中,首先調用unobserve
對之間的值接觸響應化,接着調用ob.observe
使得新賦值的數據響應化。最後首先觸發自己的set:self
事件,接着調用propagate
觸發自己以及父級的對應set
事件。
$delete
用於給刪除對象的屬性,若是不存在該屬性則直接退出,不然先用delete
操做符刪除對象的屬性,而後對外觸發自己的delete:self
事件,接着調用delete
觸發自己以及父級對應的delete
事件。
看完了objectAgumentations
以後,咱們在Observer
構造函數中知道,若是傳入的參數中存在op.doNotAlterProto
意味着不要改變對象的原型,則採用deepMixin
函數將$add
和$delete
函數添加到對象中,不然採用函數arguments
函數將$add
和$delete
添加到對象的原型中。最後調用了walk
函數,讓咱們看看walk
是內部是怎麼實現的:
p.walk = function (obj) { var key, val, descriptor, prefix for (key in obj) { prefix = key.charCodeAt(0) if ( prefix === 0x24 || // $ prefix === 0x5F // _ ) { continue } descriptor = Object.getOwnPropertyDescriptor(obj, key) // only process own non-accessor properties if (descriptor && !descriptor.get) { val = obj[key] this.observe(key, val) this.convert(key, val) } } }
首先遍歷obj
中的各個屬性,若是是以$
或者_
開頭的屬性名,則不作處理。接着獲取該屬性的描述符,若是不存在get
函數,則對該屬性值調用observe
函數,使得數據響應化,而後調用convert
函數將該屬性轉換成訪問器屬性getter/setter
使得屬性被訪問或者被改變的時候能被夠監聽。
到此爲止,咱們已經看完了整個Observer
模塊的全部代碼,其實基本原理和咱們以前設想都是差很少的,只不過Vue代碼中各個函數分解粒度很是小,使得代碼邏輯很是清晰。看到這裏,我推薦你也clone一份Vue源碼,checkout到對應的版本號,本身閱讀一遍,跑跑測試用例,打個斷點試着調試一下,應該會對你理解這個模塊有所幫助。
最後若是對這個系列的文章感興趣歡迎你們關注個人Github博客算是對我鼓勵,感謝你們的支持!