摘要:響應(yīng)式數(shù)據(jù)是在模塊中實(shí)現(xiàn)的我們可以看看是如何實(shí)現(xiàn)的。早期代碼使用是進(jìn)行單元測試,是事件模型的單元測試文件。模塊實(shí)際上采用采用組合繼承借用構(gòu)造函數(shù)原型繼承方式繼承了其目的就是繼承的,等方法。
前言
首先歡迎大家關(guān)注我的Github博客,也算是對(duì)我的一點(diǎn)鼓勵(lì),畢竟寫東西沒法獲得變現(xiàn),能堅(jiān)持下去也是靠的是自己的熱情和大家的鼓勵(lì)。接下來的日子我應(yīng)該會(huì)著力寫一系列關(guān)于Vue與React內(nèi)部原理的文章,感興趣的同學(xué)點(diǎn)個(gè)關(guān)注或者Star。
之前的兩篇文章響應(yīng)式數(shù)據(jù)與數(shù)據(jù)依賴基本原理和從Vue數(shù)組響應(yīng)化所引發(fā)的思考我們介紹了響應(yīng)式數(shù)據(jù)相關(guān)的內(nèi)容,沒有看的同學(xué)可以點(diǎn)擊上面的鏈接了解一下。如果大家都閱讀過上面兩篇文章的話,肯定對(duì)這方面內(nèi)容有了足夠的知識(shí)儲(chǔ)備,想來是時(shí)候來看看Vue內(nèi)部是如何實(shí)現(xiàn)數(shù)據(jù)響應(yīng)化。目前Vue的代碼非常龐大,但其中包含了例如:服務(wù)器渲染等我們不關(guān)心的內(nèi)容,為了能集中于我們想學(xué)習(xí)的部分,我們這次閱讀的是Vue的早期代碼,大家可以checkout到這里查看對(duì)應(yīng)的代碼。
之前零零碎碎的看過React的部分源碼,當(dāng)我看到Vue的源碼,覺得真的是非常優(yōu)秀,各個(gè)模塊之間解耦的非常好,可讀性也很高。Vue響應(yīng)式數(shù)據(jù)是在Observer模塊中實(shí)現(xiàn)的,我們可以看看Observer是如何實(shí)現(xiàn)的。
如果看過上兩篇文章的同學(xué)應(yīng)該會(huì)發(fā)現(xiàn)一個(gè)問題:數(shù)據(jù)響應(yīng)化的代碼與其他的代碼耦合太強(qiáng)了,比如說:
//代碼來源于文章:響應(yīng)式數(shù)據(jù)與數(shù)據(jù)依賴基本原理 //定義對(duì)象的單個(gè)響應(yīng)式屬性 function defineReactive(obj, key, value){ observify(value); Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: function(newValue){ var oldValue = value; value = newValue; //可以在修改數(shù)據(jù)時(shí)觸發(fā)其他的操作 console.log("newValue: ", newValue, " oldValue: ", oldValue); }, get: function(){ return value; } }); }
比如上面的代碼,set內(nèi)部的處理的代碼就與整個(gè)數(shù)據(jù)響應(yīng)化相耦合,如果下次我們想要在set中做其他的操作,就必須要修改set函數(shù)內(nèi)部的內(nèi)容,這是非常不友好的,不符合開閉原則(OCP: Open Close Principle)。當(dāng)然Vue不會(huì)采用這種方式去設(shè)計(jì),為了解決這個(gè)問題,Vue引入了發(fā)布-訂閱模式。其實(shí)發(fā)布-訂閱模式是前端工程師非常熟悉的一種模式,又叫做觀察者模式,它是一種定義對(duì)象間一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變的時(shí)候,其他觀察它的對(duì)象都會(huì)得到通知。我們最常見的DOM事件就是一種發(fā)布-訂閱模式。比如:
document.body.addEventListener("click", function(){ console.log("click event"); });
在上面的代碼中我們監(jiān)聽了body的click事件,雖然我們不知道click事件什么時(shí)候會(huì)發(fā)生,但是我們一定能保證,如果發(fā)生了body的click事件,我們一定能得到通知,即回調(diào)函數(shù)被調(diào)用。在JavaScript中因?yàn)楹瘮?shù)是一等公民,我們很少使用傳統(tǒng)的發(fā)布-訂閱模式,多采用的是事件模型的方式實(shí)現(xiàn)。在Vue中也實(shí)現(xiàn)了一個(gè)事件模型,我們可以看一下。因?yàn)閂ue的模塊之間解耦的非常好,因此在看代碼之前,其實(shí)我們可以先來看看對(duì)應(yīng)的單元測試文件,你就知道這個(gè)模塊要實(shí)現(xiàn)什么功能,甚至如果你愿意的話,也可以自己實(shí)現(xiàn)一個(gè)類似的模塊放進(jìn)Vue的源碼中運(yùn)行。
Vue早期代碼使用是jasmine進(jìn)行單元測試,emitter_spec.js是事件模型的單元測試文件。首先簡單介紹一下jasmine用到的函數(shù),可以對(duì)照下面的代碼了解具體的功能:
describe是一個(gè)測試單元集合
it是一個(gè)測試用例
beforeEach會(huì)在每一個(gè)測試用例it執(zhí)行前執(zhí)行
expect期望函數(shù),用作對(duì)期望值和實(shí)際值之間執(zhí)行邏輯比較
createSpy用來創(chuàng)建spy,而spy的作用是監(jiān)測函數(shù)的調(diào)用相關(guān)信息和函數(shù)執(zhí)行參數(shù)
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對(duì)象實(shí)例對(duì)外提供以下接口:
on: 注冊監(jiān)聽接口,參數(shù)分別是事件名和監(jiān)聽函數(shù)
emit: 觸發(fā)事件函數(shù),參數(shù)是事件名
off: 取消對(duì)應(yīng)事件的注冊函數(shù),參數(shù)分別是事件名和監(jiān)聽函數(shù)
once: 與on類似,僅會(huì)在第一次時(shí)通知監(jiān)聽函數(shù),隨后監(jiān)聽函數(shù)會(huì)被移除。
看完了上面的單元測試代碼,我們現(xiàn)在已經(jīng)基本了解了這個(gè)模塊要干什么,現(xiàn)在讓我們看看對(duì)應(yīng)的代碼:
// 刪去了注釋并且對(duì)代碼順序有調(diào)整 // ctx是監(jiān)聽回調(diào)函數(shù)的執(zhí)行作用域(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 } // 三種模式 // 不傳參情況清空所有監(jiān)聽函數(shù) // 僅傳事件名則清除該事件的所有監(jiān)聽函數(shù) // 傳遞事件名和回調(diào)函數(shù),則對(duì)應(yīng)僅刪除對(duì)應(yīng)的監(jiān)聽事件 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] // 這邊的代碼之所以會(huì)有cb.fn === fn要結(jié)合once函數(shù)去看 // 給once傳遞的監(jiān)聽函數(shù)其實(shí)已經(jīng)被wrapped過 // 但是仍然可以通過原來的監(jiān)聽函數(shù)去off掉 if (cb === fn || cb.fn === fn) { callbacks.splice(i, 1) break } } return this } // 觸發(fā)對(duì)應(yīng)事件的所有監(jiān)聽函數(shù),注意最多只能用給監(jiān)聽函數(shù)傳遞三個(gè)參數(shù)(采用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 } // 觸發(fā)對(duì)應(yīng)事件的所有監(jiān)聽函數(shù),傳遞參數(shù)個(gè)數(shù)不受限制(采用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 } // 通過調(diào)用on與off事件事件,在第一次觸發(fā)之后就`off`對(duì)應(yīng)的監(jiān)聽事件 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 }
我們可以看到上面的代碼采用了原型模式創(chuàng)建了一個(gè)Emitter類。配合Karma跑一下這個(gè)模塊 ,測試用例全部通過,到現(xiàn)在我們已經(jīng)閱讀完Emitter了,這算是一個(gè)小小的熱身吧,接下來讓我們正式看一下Observer模塊。
按照上面的思路我們先看看Observer對(duì)應(yīng)的測試用例observer_spec.js,由于Observer的測試用例非常長,我會(huì)在代碼注釋中做解釋,并盡量精簡測試用例,能讓我們了解模塊對(duì)應(yīng)功能即可,希望你能有耐心閱讀下來。
//測試用例是精簡版,否則太冗長 var Observer = require("../../../src/observe/observer") var _ = require("../../../src/util") //Vue內(nèi)部使用工具方法 var u = undefined Observer.pathDelimiter = "." //配置Observer路徑分隔符 describe("Observer", function () { var spy beforeEach(function () { spy = jasmine.createSpy("observer") }) //我們可以看到我們通過Observer.create函數(shù)可以將數(shù)據(jù)變?yōu)榭身憫?yīng)化, //然后我們監(jiān)聽get事件可以在屬性被讀取時(shí)觸發(fā)對(duì)應(yīng)事件,注意對(duì)象嵌套的情況(例如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 }) //我們可以監(jiān)聽響應(yīng)式數(shù)據(jù)的set事件,當(dāng)響應(yīng)式數(shù)據(jù)修改的時(shí)候,會(huì)觸發(fā)對(duì)應(yīng)的時(shí)間 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) }) //帶有$與_開頭的屬性都不會(huì)被處理 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) }) //訪問器屬性也不會(huì)被處理 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) }) // 對(duì)數(shù)屬性的get監(jiān)聽,注意嵌套的情況 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 }) // 對(duì)數(shù)屬性的get監(jiān)聽,注意嵌套的情況 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) }) // 我們看到可以通過監(jiān)聽mutate事件,在push調(diào)用的時(shí)候?qū)?yīng)觸發(fā)事件 // 觸發(fā)事件第一個(gè)參數(shù)是"",代表的是路徑名,具體源碼可以看出,對(duì)于數(shù)組變異方法都是空字符串 // 觸發(fā)事件第二個(gè)參數(shù)是數(shù)組本身 // 觸發(fā)事件第三個(gè)參數(shù)比較復(fù)雜,其中: // method屬性: 代表觸發(fā)的方法名稱 // args屬性: 代表觸發(fā)方法傳遞參數(shù) // result屬性: 代表觸發(fā)變異方法之后數(shù)組的結(jié)果 // index屬性: 代表變異方法對(duì)數(shù)組發(fā)生變化的最開始元素 // inserted屬性: 代表數(shù)組新增的元素 // remove屬性: 代表數(shù)組刪除的元素 // 其他的變異方法: pop、shift、unshift、splice、sort、reverse內(nèi)容都是非常相似的 // 具體我們就不一一列舉的了,如果有疑問可以自己看到全部的單元測試代碼 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]) }) // 我們可以看到響應(yīng)式數(shù)據(jù)中存在$add方法,類似于Vue.set,可以監(jiān)聽add事件 // 可以向響應(yīng)式對(duì)象中添加新一個(gè)屬性,如果之前存在該屬性則操作會(huì)被忽略 // 并且新賦值的對(duì)象也必須被響應(yīng)化 // 我們省略了對(duì)象數(shù)據(jù)$delete方法的單元測試,功能類似于Vue.delete,與$add方法相反,可以用于刪除對(duì)象的屬性 // 我們省略了數(shù)組的$set方法的單元測試,功能也類似與Vue.set,可以用于設(shè)置數(shù)組對(duì)應(yīng)數(shù)字下標(biāo)的值 // 我們省略了數(shù)組的$remove方法的單元測試,功能用于移除數(shù)組給定下標(biāo)的值或者給定的值,例如: // var arr = [{a:1}, {a:2}] // var ob = Observer.create(arr) // arr.$remove(0) => 移除對(duì)應(yīng)下標(biāo)的值 或者 // 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) }) // 下面的測試用例用來表示如果兩個(gè)不同對(duì)象parentA、parentB的屬性指向同一個(gè)對(duì)象obj,那么該對(duì)象obj改變時(shí)會(huì)分別parentA與parentB的監(jiān)聽事件 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) }) })源碼實(shí)現(xiàn) 數(shù)組
能堅(jiān)持看到這里,我們的長征路就走過了一半了,我們已經(jīng)知道了Oberver對(duì)外提供的功能了,現(xiàn)在我們就來了解一下Oberver內(nèi)部的實(shí)現(xiàn)原理。
Oberver模塊實(shí)際上采用采用組合繼承(借用構(gòu)造函數(shù)+原型繼承)方式繼承了Emitter,其目的就是繼承Emitter的on, off,emit等方法。我們在上面的測試用例發(fā)現(xiàn),我們并沒有用new方法直接創(chuàng)建一個(gè)Oberver的對(duì)象實(shí)例,而是采用一個(gè)工廠方法Oberver.create方法來創(chuàng)建的,我們接下來看源碼,由于代碼比較多我會(huì)盡量去拆分成一個(gè)個(gè)小塊來講:
// 代碼出自于observe.js // 為了方便講解我對(duì)代碼順序做了改變,要了解詳細(xì)的情況可以查看具體的源碼 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 = "" 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值沒有響應(yīng)化過(通過是否含有$observer屬性去判斷),則使用new操作符創(chuàng)建Obsever實(shí)例(區(qū)分對(duì)象OBJECT與數(shù)組ARRAY)。接下來我們看Observer的構(gòu)造函數(shù)是怎么定義的,首先借用Emitter構(gòu)造函數(shù):
Emitter.call(this, options && options.callbackContext)
配合原型繼承
var p = Observer.prototype = Object.create(Emitter.prototype)
從而實(shí)現(xiàn)了組合繼承Emitter,因此Observer繼承了Emitter的屬性(ctx)和方法(on,emit等)。我們可以看到Observer有以下屬性:
id: 響應(yīng)式數(shù)據(jù)的唯一標(biāo)識(shí)
value: 原始數(shù)據(jù)
type: 標(biāo)識(shí)是數(shù)組還是對(duì)象
parents: 標(biāo)識(shí)響應(yīng)式數(shù)據(jù)的父級(jí),可能存在多個(gè),比如var obj = { a : { b: 1}},在處理{b: 1}的響應(yīng)化過程中parents中某個(gè)屬性指向的就是obj的$observer。
我們接著看首先給該數(shù)據(jù)賦值$observer屬性,指向的是實(shí)例對(duì)象本身。_.define內(nèi)部是通過defineProperty實(shí)現(xiàn)的:
define = function (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value : val, enumerable : !!enumerable, writable : true, configurable : true }) }
下面我們首先看看是怎么處理數(shù)組類型的數(shù)據(jù)的
if (type === ARRAY) { _.augment(value, arrayAugmentations) this.link(value) }
如果看過我前兩篇文章的同學(xué),其實(shí)還記得我們對(duì)數(shù)組響應(yīng)化當(dāng)時(shí)還做了一個(gè)著重的原理講解,大概原理就是我們通過給數(shù)組對(duì)象設(shè)置新的原型對(duì)象,從而遮蔽掉原生數(shù)組的變異方法,大概的原理可以是:
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 () { // 調(diào)用對(duì)應(yīng)的原生方法并返回結(jié)果 // do everything you what do ! return original.apply(this, arguments); }; }); array.__proto__ = arrayAugmentations; }
回到Vue的源碼,雖然我們知道基本原理肯定是相同的,但是我們?nèi)匀恍枰纯?b>arrayAugmentations是什么?下面arrayAugmentations代碼比較長。我們會(huì)在注釋里面解釋基本原理:
// 代碼來自于array-augmentations.js var _ = require("../util") var arrayAugmentations = Object.create(Array.prototype) // 這邊操作和我們之前的實(shí)現(xiàn)方式非常相似 // 創(chuàng)建arrayAugmentations原型繼承`Array.prototype`從而可以調(diào)用數(shù)組的原生方法 // 然后通過arrayAugmentations覆蓋數(shù)組的變異方法,基本邏輯大致相同 ["push","pop","shift","unshift","splice","sort","reverse"].forEach(function (method) { var original = Array.prototype[method] // 覆蓋arrayAugmentations中的變異方法 _.define(arrayAugmentations, method, function () { var args = _.toArray(arguments) // 這里調(diào)用了原生的數(shù)組變異方法,并獲得結(jié)果 var result = original.apply(this, args) var ob = this.$observer var inserted, removed, index // 下面switch這一部分代碼看起來很長,其實(shí)目的就是針對(duì)于不同的變異方法生成: // insert removed inserted 具體的含義對(duì)照之前的解釋,了解即可 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 } // 如果給數(shù)組中插入新的數(shù)據(jù),則需要調(diào)用ob.link // link函數(shù)其實(shí)在上面的_.augment(value, arrayAugmentations)之后也被調(diào)用了 // 具體的實(shí)現(xiàn)我們可以先不管 // 我們只要知道其目的就是分別對(duì)插入的數(shù)據(jù)執(zhí)行響應(yīng)化 if (inserted) ob.link(inserted, index) // 其實(shí)從link我們就可以猜出unlink是干什么的 // 主要就是對(duì)刪除的數(shù)據(jù)解除響應(yīng)化,具體實(shí)現(xiàn)邏輯后面解釋 if (removed) ob.unlink(removed) // updateIndices我們也先不講是怎么實(shí)現(xiàn)的, // 目的就是更新子元素在parents的key // 因?yàn)閜ush和pop是不會(huì)改變現(xiàn)有元素的位置,因此不需要調(diào)用 // 而諸如splce shift unshift等變異方法會(huì)改變對(duì)應(yīng)下標(biāo)值,因此需要調(diào)用 if (method !== "push" && method !== "pop") { ob.updateIndices() } // 同樣我們先不考慮propagate內(nèi)部實(shí)現(xiàn),我們只要propagate函數(shù)的目的就是 // 觸發(fā)自身及其遞歸觸發(fā)父級(jí)的事件 // 如果數(shù)組中的數(shù)據(jù)有插入或者刪除,則需要對(duì)外觸發(fā)"length"被改變 if (inserted || removed) { ob.propagate("set", "length", this.length) } // 對(duì)外觸發(fā)mutate事件 // 可以對(duì)照我們之前講的測試用例"array push",就是在這里觸發(fā)的,回頭看看吧 ob.propagate("mutate", "", this, { method : method, args : args, result : result, index : index, inserted : inserted || [], removed : removed || [] }) return result }) }) // 可以回看一下測試用例 array set,目的就是設(shè)置對(duì)應(yīng)下標(biāo)的值 // 其實(shí)就是調(diào)用了splice變異方法, 其實(shí)我們在Vue中國想要改變某個(gè)下標(biāo)的值的時(shí)候 // 官網(wǎng)給出的建議無非是Vue.set或者就是splice,都是相同的原理 // 注意這里的代碼忽略了超出下標(biāo)范圍的值 _.define(arrayAugmentations, "$set", function (index, val) { if (index >= this.length) { this.length = index + 1 } return this.splice(index, 1, val)[0] }) // $remove與$add都是一個(gè)道理,都是調(diào)用的是`splice`函數(shù) _.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
上面的代碼相對(duì)比較長,具體的解釋我們在代碼中已經(jīng)注釋。到這里我們已經(jīng)了解完arrayAugmentations了,我們接著看看_.augment做了什么。我們在文章從Vue數(shù)組響應(yīng)化所引發(fā)的思考中講過Vue是通過__proto__來實(shí)現(xiàn)數(shù)組響應(yīng)化的,但是由于__proto__是個(gè)非標(biāo)準(zhǔn)屬性,雖然廣泛的瀏覽器廠商基本都實(shí)現(xiàn)了這個(gè)屬性,但是還是存在部分的安卓版本并不支持該屬性,Vue必須對(duì)此做相關(guān)的處理,_.augment就負(fù)責(zé)這個(gè)部分:
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__話調(diào)用deepMixin函數(shù)。而deepMixin的實(shí)現(xiàn)也是非常的簡單,就是使用Object.defineProperty將原對(duì)象的屬性描述符賦值給目標(biāo)對(duì)象。接著調(diào)用了函數(shù):
this.link(value)
關(guān)于link函數(shù)在上面的備注中我們已經(jīng)見過了:
if (inserted) ob.link(inserted, index)
當(dāng)時(shí)我們的解釋是將新插入的數(shù)據(jù)響應(yīng)化,知道了功能我們看看代碼的實(shí)現(xiàn):
// 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 } } }
其實(shí)代碼邏輯非常簡單,link函數(shù)會(huì)對(duì)給定數(shù)組index(默認(rèn)為0)之后的元素調(diào)用this.observe, 而observe其實(shí)也就是對(duì)給定的val值遞歸調(diào)用Observer.create,將數(shù)據(jù)響應(yīng)化,并建立父級(jí)的Observer與當(dāng)前實(shí)例的對(duì)應(yīng)關(guān)系。前面其實(shí)我們發(fā)現(xiàn)Vue不僅僅會(huì)對(duì)插入的數(shù)據(jù)響應(yīng)化,并且也會(huì)對(duì)刪除的元素調(diào)用unlink,具體的調(diào)用代碼是:
if (removed) ob.unlink(removed)
之前我們大致講過其用作就是對(duì)刪除的數(shù)據(jù)解除響應(yīng)化,我們來看看具體的實(shí)現(xiàn):
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 } }
代碼非常簡單,就是對(duì)數(shù)據(jù)調(diào)用unobserve,而unobserve函數(shù)的主要目的就是解除父級(jí)observer與當(dāng)前數(shù)據(jù)的關(guān)系并且不再保留引用,讓瀏覽器內(nèi)核必要的時(shí)候能夠回收內(nèi)存空間。
在arrayAugmentations中其實(shí)還調(diào)用過Observer的兩個(gè)原型方法,一個(gè)是:
ob.updateIndices()
另一個(gè)是:
ob.propagate("set", "length", this.length)
首先看看updateIndices函數(shù),當(dāng)時(shí)的函數(shù)的作用是更新子元素在parents的key,來看看具體實(shí)現(xiàn):
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 } } }
接著看函數(shù)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函數(shù)的作用的就是觸發(fā)自身及其遞歸觸發(fā)父級(jí)的事件,首先調(diào)用emit函數(shù)對(duì)外觸發(fā)時(shí)間,其參數(shù)分別是:事件名、路徑、值、mutatin對(duì)象。然后接著遞歸調(diào)用父級(jí)的事件,并且對(duì)應(yīng)改變觸發(fā)的path參數(shù)。parentPath等于parents[id].key + Observer.pathDelimiter + path
到此為止我們已經(jīng)學(xué)習(xí)完了Vue是如何處理數(shù)組的響應(yīng)化的,現(xiàn)在需要來看看是如何處理對(duì)象的響應(yīng)化的。
在Observer的構(gòu)造函數(shù)中關(guān)于對(duì)象處理的代碼是:
if (type === OBJECT) { if (options && options.doNotAlterProto) { _.deepMixin(value, objectAugmentations) } else { _.augment(value, objectAugmentations) } this.walk(value) }
和數(shù)組一樣,我們首先要了解一下objectAugmentations的內(nèi)部實(shí)現(xiàn):
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內(nèi)部實(shí)現(xiàn)則簡單的多,objectAgumentations添加了兩個(gè)方法: $add與$delete。
$add用于給對(duì)象添加新的屬性,如果該對(duì)象之前就存在鍵值為key的屬性則不做任何操作,否則首先使用_.define賦值該屬性,然后調(diào)用ob.observe目的是遞歸調(diào)用使得val值響應(yīng)化。而convert函數(shù)的作用是將該屬性轉(zhuǎn)換成訪問器屬性getter/setter使得屬性被訪問或者被改變的時(shí)候我們能夠監(jiān)聽到,具體我可以看一下convert函數(shù)的內(nèi)部實(shí)現(xiàn):
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函數(shù)的內(nèi)部實(shí)現(xiàn)也不復(fù)雜,在get函數(shù)中,如果開啟了全局的Observer.emitGet開關(guān),在該屬性被訪問的時(shí)候,會(huì)對(duì)調(diào)用propagate觸發(fā)本身以及父級(jí)的對(duì)應(yīng)get事件。在set函數(shù)中,首先調(diào)用unobserve對(duì)之間的值接觸響應(yīng)化,接著調(diào)用ob.observe使得新賦值的數(shù)據(jù)響應(yīng)化。最后首先觸發(fā)本身的set:self事件,接著調(diào)用propagate觸發(fā)本身以及父級(jí)的對(duì)應(yīng)set事件。
$delete用于給刪除對(duì)象的屬性,如果不存在該屬性則直接退出,否則先用delete操作符刪除對(duì)象的屬性,然后對(duì)外觸發(fā)本身的delete:self事件,接著調(diào)用delete觸發(fā)本身以及父級(jí)對(duì)應(yīng)的delete事件。
看完了objectAgumentations之后,我們在Observer構(gòu)造函數(shù)中知道,如果傳入的參數(shù)中存在op.doNotAlterProto意味著不要改變對(duì)象的原型,則采用deepMixin函數(shù)將$add和$delete函數(shù)添加到對(duì)象中,否則采用函數(shù)arguments函數(shù)將$add和$delete添加到對(duì)象的原型中。最后調(diào)用了walk函數(shù),讓我們看看walk是內(nèi)部是怎么實(shí)現(xiàn)的:
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中的各個(gè)屬性,如果是以$或者_開頭的屬性名,則不做處理。接著獲取該屬性的描述符,如果不存在get函數(shù),則對(duì)該屬性值調(diào)用observe函數(shù),使得數(shù)據(jù)響應(yīng)化,然后調(diào)用convert函數(shù)將該屬性轉(zhuǎn)換成訪問器屬性getter/setter使得屬性被訪問或者被改變的時(shí)候能被夠監(jiān)聽。
到此為止,我們已經(jīng)看完了整個(gè)Observer模塊的所有代碼,其實(shí)基本原理和我們之前設(shè)想都是差不多的,只不過Vue代碼中各個(gè)函數(shù)分解粒度非常小,使得代碼邏輯非常清晰。看到這里,我推薦你也clone一份Vue源碼,checkout到對(duì)應(yīng)的版本號(hào),自己閱讀一遍,跑跑測試用例,打個(gè)斷點(diǎn)試著調(diào)試一下,應(yīng)該會(huì)對(duì)你理解這個(gè)模塊有所幫助。
最后如果對(duì)這個(gè)系列的文章感興趣歡迎大家關(guān)注我的Github博客算是對(duì)我鼓勵(lì),感謝大家的支持!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/95842.html
摘要:當(dāng)數(shù)據(jù)改變時(shí),我們不需要直接觸發(fā)所有的回調(diào)函數(shù),而是去通知對(duì)應(yīng)的數(shù)據(jù)的,然后由去執(zhí)行相應(yīng)的邏輯。對(duì)于其邏輯可能是一個(gè)指令用于連接與響應(yīng)式數(shù)據(jù)或者是一個(gè)偵聽器的回調(diào)函數(shù),這樣就能符合單一職責(zé)原則,解除模塊之間的耦合度,讓程序更易維護(hù)。 前言 首先歡迎大家關(guān)注我的Github博客,也算是對(duì)我的一點(diǎn)鼓勵(lì),畢竟寫東西沒法獲得變現(xiàn),能堅(jiān)持下去也是靠的是自己的熱情和大家的鼓勵(lì)。接下來的日子我應(yīng)...
摘要:響應(yīng)式原理之之前簡單介紹了和類的代碼和作用,現(xiàn)在來介紹一下類和。對(duì)于數(shù)組,響應(yīng)式的實(shí)現(xiàn)稍有不同。不存在時(shí),說明不是響應(yīng)式數(shù)據(jù),直接更新。如果對(duì)象是響應(yīng)式的,確保刪除能觸發(fā)更新視圖。 Vue響應(yīng)式原理之Observer 之前簡單介紹了Dep和Watcher類的代碼和作用,現(xiàn)在來介紹一下Observer類和set/get。在Vue實(shí)例后再添加響應(yīng)式數(shù)據(jù)時(shí)需要借助Vue.set/vm.$se...
摘要:模板語法的將保持不變。基于的觀察者機(jī)制目前,的反應(yīng)系統(tǒng)是使用的和。為了繼續(xù)支持,將發(fā)布一個(gè)支持舊觀察者機(jī)制和新版本的構(gòu)建。 showImg(https://segmentfault.com/img/remote/1460000017862774?w=1898&h=796); 還有幾個(gè)月距離vue2的首次發(fā)布就滿3年了,而vue的作者尤雨溪也在去年年末發(fā)布了關(guān)于vue3.0的計(jì)劃,如果不...
摘要:接下來,我們就一起深入了解的數(shù)據(jù)響應(yīng)式原理,搞清楚響應(yīng)式的實(shí)現(xiàn)機(jī)制。回調(diào)函數(shù)只是打印出新的得到的新的值,由執(zhí)行后生成。及異步更新相信讀過前文,你應(yīng)該對(duì)響應(yīng)式原理有基本的認(rèn)識(shí)。 前言 Vue.js 的核心包括一套響應(yīng)式系統(tǒng)。 響應(yīng)式,是指當(dāng)數(shù)據(jù)改變后,Vue 會(huì)通知到使用該數(shù)據(jù)的代碼。例如,視圖渲染中使用了數(shù)據(jù),數(shù)據(jù)改變后,視圖也會(huì)自動(dòng)更新。 舉個(gè)簡單的例子,對(duì)于模板: {{ name ...
閱讀 964·2021-11-24 10:42
閱讀 3475·2021-11-19 11:34
閱讀 2605·2021-09-29 09:35
閱讀 2525·2021-09-09 09:33
閱讀 641·2021-07-26 23:38
閱讀 2515·2019-08-30 10:48
閱讀 1385·2019-08-28 18:07
閱讀 422·2019-08-26 13:44