摘要:當數據改變時,我們不需要直接觸發所有的回調函數,而是去通知對應的數據的,然后由去執行相應的邏輯。對于其邏輯可能是一個指令用于連接與響應式數據或者是一個偵聽器的回調函數,這樣就能符合單一職責原則,解除模塊之間的耦合度,讓程序更易維護。
前言
首先歡迎大家關注我的Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。接下來的日子我應該會著力寫一系列關于Vue與React內部原理的文章,感興趣的同學點個關注或者Star。
回顧上一篇文章Vue響應式數據: Observer模塊實現我們介紹Vue早期代碼中的Observer模塊,Observer模塊實現了數據的響應化并且用監聽者模式對外提供功能。任何模塊想要感知到數據變化,只要監聽Observer模塊對應的事件,從而將整個數據響應化的過程與相應的處理邏輯相獨立。
其實我們可以思考一下,在Vue中一個響應式數據發生改變可能會觸發那些邏輯呢?可能是一個對應的DOM改變,也可能是一個watch偵聽器的回調函數的調用,或者是導致一個computed計算屬性函數的調用。其實在之前的文章響應式數據與數據依賴基本原理我們就引入了一個Dep和Watcher的概念。同時還附上了一個概念圖:
我們當時為了解耦響應式數據和對應的數據改變后處理邏輯,我們增加了Dep和Watcher的概念,每一個響應式數據都有一個Dep用于集中管理和維護該數據改變時對應執行回調函數。當數據改變時,我們不需要直接觸發所有的回調函數,而是去通知對應的數據的Dep,然后由Dep去執行相應的邏輯。
將這個概念抽象出現出來,其基本邏輯就是上圖所示。了解設計模式的同學,應該很快就能意識到這是一個代理模式。引入Dep的目的其實也就是代理模式的優點,分離調用者和被調用者的邏輯,降低耦合性。可見設計模式在軟件開發中作用是非常廣泛的,甚至有時候你都沒有意識到它的存在。
我們前面說過,響應式數據改變后可能對應的是DOM修改的處理邏輯或者是watch函數對應的處理邏輯。為了弱化不同類型的處理邏輯,我們引入了Watcher類。Dep并不會關心每一個不同的注冊者的邏輯,Dep只知道每一個注冊者都是一個Watcher的實例,每次發生改變時只需要調用對應的update方法,具體的邏輯被隱藏在update函數之后。
Vue的內部實現邏輯基本上和我們的邏輯是一樣的。由bindings模塊負責上面所講的Dep的功能。
在Vue組件的初始化函數_init中調用了:
this._initBindings()
目的就是創建組件對應的binding Tree,在研究_initBindings函數之前,我們先看看Binding:
function Binding () { this._subs = [] } var p = Binding.prototype p._addChild = function (key, child) { child = child || new Binding() this[key] = child return child } p._addSub = function (sub) { this._subs.push(sub) } p._removeSub = function (sub) { this._subs.splice(this._subs.indexOf(sub), 1) } p._notify = function () { for (var i = 0, l = this._subs.length; i < l; i++) { this._subs[i].update() } }
Binding類的定義非常簡單,內部屬性_subs數組用來存儲對應的訂閱者(subscription),也就是我們后面將要說的Watcher,原型方法分別是:
_addSub: 用來增加對應的訂閱者
_removeSub: 用來刪除對應的訂閱者
_notify: 通知所有的訂閱者,其實就是遍歷整個訂閱者數據,并調用對應的update方法。
_addChild: 用來增加一個屬性名為key值的子Binding,其實也就用來構建Binding Tree。
看完Binding類我們接著看_initBindings函數的定義:
var Binding = require("../binding") var Path = require("../parse/path") var Observer = require("../observe/observer") exports._initBindings = function () { var root = this._rootBinding = new Binding() root._addChild("$data", root) if (this.$parent) { root._addChild("$parent", this.$parent._rootBinding) root._addChild("$root", this.$root._rootBinding) } this._observer // simple updates .on("set", this._updateBindingAt) .on("mutate", this._updateBindingAt) .on("delete", this._updateBindingAt) .on("add", this._updateAdd) .on("get", this._collectDep) }
_initBindings是在初始化Vue組件實例中調用的,因此this也就是指向的是當前的Vue實例對象。
首先我們看到給Vue的實例對象中創建了私有屬性_rootBinding,作為Bindings Tree的根節點,并且_rootBinding的$data屬性指向就是根節點本身。如果當前的Vue實例中存在父節點($parent),則通過給給_rootBinding添加$parent屬性來構建起與父級Bindings Tree的關聯。我們知道Bindings的主要作用就是在響應式數據改變時觸發對應的邏輯,因此_initBindings函數監聽了實例屬性_observer的各個事件。
set: 監聽響應式數據對象屬性值修改
mutate: 監聽響應式數據數組修改
delete: 監聽響應式數據對象屬性刪除
add: 監聽響應式數據對象屬性增加
get: 監聽響應式數據某個屬性被調用
我們看到對于set、mutate、delete事件我們都調用了內部的_updateBindingAt函數,接著看
_updateBindingAt函數定義:
exports._updateBindingAt = function (path) { // root binding updates on any change this._rootBinding._notify() var binding = this._getBindingAt(path, true) if (binding) { binding._notify() } } exports._getBindingAt = function (path) { return Path.getFromObserver(this._rootBinding, path) }
假如說數據是下面格式:
var vm = new Vue({ data: { a: { b: 1 } } })
當設置vm.a.b = 2時,我們調用_updateBindingAt的path為ab。_updateBindingAt函數首先會任何數據變化的時候都通知調用根級_rootBinding中的所有訂閱者,然后調用_getBindingAt函數去獲得當前路徑ab的binding,如果存在,則通知調用所有的訂閱者(下面箭頭指向的就是對應路徑ab的Binding)。關于Path模塊我們這里不做過多的介紹,我們只要知道Path.getFromObserver函數能遍歷Binding Tree找到對應路徑的Binding。
接下來我們當響應式數據觸發add事件時就會觸發_updateAdd函數:
exports._updateAdd = function (path) { var index = path.lastIndexOf(Observer.pathDelimiter) if (index > -1) path = path.slice(0, index) this._updateBindingAt(path) }
假設是下列的數據格式:
var vm = new Vue({ data: { a: {} } })
當我們執行vm.a.$add("b", 1)時,此時函數_updateAdd的參數path為ab,但是對應的binding還未創建,因此對應的Watcher還沒有依賴到該Binding。對于這種不存在Binding的Watcher,會暫時依賴于父級的Binding,因此函數_updateAdd也就是找到了對應父級的Binding,然后通知調用所有的訂閱者。
接下來觸發響應式數據的get事件時,對應調用函數:
exports._collectDep = function (path) { var watcher = this._activeWatcher if (watcher) { watcher.addDep(path) } }
函數_collectDep的主要目的就是收集依賴,當get事件觸發的時候,會將_activeWatcher添加到路徑path的Binding中。關于_activeWatcher與addDep函數,馬上我們會在Watcher模塊中介紹到。
我們前面已經講過,Binding中的訂閱者都是Watcher實例,Binding并不關心數據更改后的操作,對于Binding而言只需要調用訂閱者的update方法,具體的處理邏輯都隱藏在Watcher的背后。對于Watcher,其邏輯可能是一個指令directive(用于連接DOM與響應式數據)或者是一個watch偵聽器的回調函數,這樣就能符合單一職責原則,解除模塊之間的耦合度,讓程序更易維護。
在介紹Watcher之前,我們先介紹一下Batcher模塊,顧名思義,主要就是批處理任務,看過React源碼的同學應該也在其中看到過相似的概念。在這些框架中,有可能是因為某個操作過于昂貴(比如DOM操作),我們如果數據一改變就觸發相應的操作其實是不合適的,比如:
//修改前vm.a === 1 vm.a = 2; vm.a = 1;
其實兩次操作下來,我們的完全可以不需要進行操作,因為前后數據并沒有發生改變,這時如果我們進行批量處理,將兩次操作統一起來,就能在一定程度提升效率。
var _ = require("./util") function Batcher () { this._preFlush = null this.reset() } var p = Batcher.prototype p.push = function (job) { if (!job.id || !this.has[job.id]) { this.queue.push(job) this.has[job.id] = job if (!this.waiting) { this.waiting = true _.nextTick(this.flush, this) } } else if (job.override) { var oldJob = this.has[job.id] oldJob.cancelled = true this.queue.push(job) this.has[job.id] = job } } p.flush = function () { // before flush hook if (this._preFlush) { this._preFlush() } // do not cache length because more jobs might be pushed // as we run existing jobs for (var i = 0; i < this.queue.length; i++) { var job = this.queue[i] if (!job.cancelled) { job.run() } } this.reset() } p.reset = function () { this.has = {} this.queue = [] this.waiting = false } module.exports = Batcher
Batcher內部有四個屬性并對外提供三個方法:
屬性:
has: 用來記錄某個任務(job)是否已經在隊列中
queue: 用來存儲當前的任務隊列
waiting: 用來表示當前的任務隊列處于等待執行狀態
_preFlush: 用來在執行任務隊列前調用的鉤子函數
方法:
reset:重置參數屬性
push: 將任務放入批處理隊列
flush: 執行批處理隊列中的所有任務
上面的代碼邏輯非常簡單,不用逐一介紹,值得注意的是,每一個任務job都含有id屬性,用來唯一標識任務,如果當前任務隊列中已經存在并且任務的override屬性為false就不會重復放入。override屬性就是用來表示是否需要覆蓋已經存在的任務。任務的cancelled屬性用來表示該任務是否需要被取消執行。所有的任務job中的run方法就是任務所需要執行的內容。關于Vue.nextTick之后的文章我們會介紹,現在你可以就可以簡單理解成setTimeOut。
接下來我們來看一下Watcher模塊的實現:
var _ = require("./util") var Observer = require("./observe/observer") var expParser = require("./parse/expression") var Batcher = require("./batcher") var batcher = new Batcher() var uid = 0 function Watcher (vm, expression, cb, ctx, filters, needSet) { this.vm = vm this.expression = expression this.cb = cb // change callback this.ctx = ctx || vm // change callback context this.id = ++uid // uid for batching this.value = undefined this.active = true this.deps = Object.create(null) this.newDeps = Object.create(null) var res = _.resolveFilters(vm, filters, this) this.readFilters = res && res.read this.writeFilters = res && res.write // parse expression for getter/setter res = expParser.parse(expression, needSet) this.getter = res.get this.setter = res.set this.initDeps(res.paths) }
Watcher模塊主要做的就是解析表達式,從中收集依賴并且在數據改變的時候調用注冊的回調函數。
vm: 就是對應的響應式數據所在的Vue實例
expression: 待解析的表達式
cb: 注冊的回調函數,在數據改變時會調用
ctx: 回調函數執行的上下文
id: Watcher的標識,用在Batcher對應的job.id,每一個Watcher其實就是一個job
value: 表達式expression對應的計算值
active: 該watcher是否是激活的
deps: 用來存儲當前Watcher依賴的數據路徑
在整個Watcher構造函數中我們需要注意的是兩個部分:
var res = _.resolveFilters(vm, filters, this) this.readFilters = res && res.read this.writeFilters = res && res.write
和
res = expParser.parse(expression, needSet) this.getter = res.get this.setter = res.set this.initDeps(res.paths)
第一部分對應的就是過濾器的處理,比如存在:
var vm = new Vue({ data: { a: 1, b: -2 }, filters: { abs: function(v){ return Math.abs(v); } } })
那么Watcher在解析表達式a+b|abs,得到對應的結果值就是1。_.resolveFilters函數將filters解析成readFilters和writeFilters,其實本人也是從Vue2.0才開始入手的,之前并沒有聽過還存在什么writeFilter,于是翻看了Vue1.0的文檔,找了已經被廢棄的Vue1.0的雙向過濾器。比如:
Vue.filter("currency", { read: function (value) { return "$" + value.toFixed(2) }, write: function (value) { var number = +value.replace(/[^d.]/g, "") return isNaN(number) ? 0 : number } }) var vm = new Vue({ el: "#app", data: { price: 0 } })
currency過濾器就是雙邊過濾器,當在輸入框中輸入例如: $12的時候,我們發現vm.price已經被賦值為12。這就是write過濾器生效的結果。
我們來看一下工具類utils中filter模塊所提供的兩個方法resolveFilters與applyFilters:
exports.resolveFilters = function (vm, filters, target) { if (!filters) { return } var res = target || {} var registry = vm.$options.filters filters.forEach(function (f) { var def = registry[f.name] var args = f.args var reader, writer if (!def) { _.warn("Failed to resolve filter: " + f.name) } else if (typeof def === "function") { reader = def } else { reader = def.read writer = def.write } if (reader) { if (!res.read) { res.read = [] } res.read.push(function (value) { return args ? reader.apply(vm, [value].concat(args)) : reader.call(vm, value) }) } // only watchers needs write filters if (target && writer) { if (!res.write) { res.write = [] } res.write.push(function (value) { return args ? writer.apply(vm, [value, res.value].concat(args)) : writer.call(vm, value, res.value) }) } }) return res }
resolveFilters在被Watcher調用的時候,vm參數對應的就是Vue的實例,而target則是Watcher實例本身,傳入的filters就比較特殊了,比如我們上面的例子:a+b|abs,對應的filters就是
[{ name: "abs" args: null }]
我們看到filters是一個數組,其實每個元素的name對應的就是應用的過濾器函數名,而args則是傳入的預定的其他參數。代碼的邏輯非常的簡單,遍歷filters數組,將其中的每一個使用到的過濾器從vm.$options.filters取出,將對應的read和write包裝成新的函數,并分別放入res.read與res.write,并將res返回。然后配合下面的模塊提供的applyFilters函數,我們就可以一個值經過給定的一系列過濾器處理,得到最終的數值了。
exports.applyFilters = function (value, filters) { if (!filters) { return value } for (var i = 0, l = filters.length; i < l; i++) { value = filters[i](value) } return value }
第二部分代碼:
res = expParser.parse(expression, needSet) this.getter = res.get this.setter = res.set this.initDeps(res.paths)
涉及到的就是表達式的處理,我們之前講過,每個Watcher其實都是從一個表達式中收集依賴,并且在相應的數據發生改變的時候調用對應的回調函數,expParser模塊不是我們本次文章的重點內容,我們不需要知道它是怎么實現的,我們只要只要它是做什么的,可以看下面的代碼:
describe("Expression Parser", function () { it("parse getter", function () { var res = expParser.parse("a - b * 2 + 45"); expect(res.get({ a: 100, b: 23 })).toEqual(100 - 23 * 2 + 45) expect(res.paths[0]).toEqual("a"); expect(res.paths[b]).toEqual("b"); expect(res.paths.length).toEqual(2); }) it("parse setter", function () { var res = expParser.parse("a.b.d"); var scope = {}; scope.a = {b:{c:0}} res.set(scope, 123) expect(scope.a.b.c).toBe(123) expect(res.paths[0]).toEqual("a"); expect(res.paths.length).toEqual(1); }) });
其實從上面兩個測試用例中我們已經能看出expParser.parse的功能了,expParser.parse能轉化一個表達式,返回值res中的paths表示表達式依賴數據的根路徑,get函數用于從一個值域scope中取得表達式對應的計算值。而set函數用于給值域scope中設置表達式的值。
我們接著看this.initDeps(res.paths)
var p = Watcher.prototype p.initDeps = function (paths) { var i = paths.length while (i--) { this.addDep(paths[i]) } this.value = this.get() } p.addDep = function (path) { var vm = this.vm var newDeps = this.newDeps var oldDeps = this.deps if (!newDeps[path]) { newDeps[path] = true if (!oldDeps[path]) { var binding = vm._getBindingAt(path) || vm._createBindingAt(path) binding._addSub(this) } } }
initDeps函數的首先就是將表達式依賴根路徑中的每一個值調用函數addDep,將Watcher實例添加進入對應的Binding中,addDep內部實現也是非常的簡潔,調用_getBindingAt函數(已經存在對應的Binding)或者_createBindingAt(創建新的Binding)獲取到對應Binding并將自身添加進入。newDeps用來記錄此次addDep過程中之前不存在的依賴項。之后initDeps函數調用了this.get()獲取當前表達式對應的值。
p.get = function () { this.beforeGet() var value = this.getter.call(this.vm, this.vm.$scope) value = _.applyFilters(value, this.readFilters) this.afterGet() return value } p.beforeGet = function () { Observer.emitGet = true this.vm._activeWatcher = this this.newDeps = Object.create(null) } p.afterGet = function () { this.vm._activeWatcher = null Observer.emitGet = false _.extend(this.newDeps, this.deps) this.deps = this.newDeps }
get函數內部實質就是調用表達式對應的get函數獲取表達式當前對應的結果,然后通過applyFilters得到當前表達式對應過濾器處理后的值。值得注意的是,在調用之前執行了鉤子函數beforeGet,其目的就是開啟Observer的emitGet使得我們可以接受響應式數據的get事件,然后將當前Vue實例的_activeWatcher賦值成當前的Watcher并置空newDeps準備存儲本次新增的依賴數據項。我們在Binding提到過:
this._observer.on("get", this._collectDep) exports._collectDep = function (path) { var watcher = this._activeWatcher if (watcher) { watcher.addDep(path) } }
beforeGet所作的就是為了收集依賴所做的準備。afterGet所做的就是清除為依賴收集所做準備,邏輯和beforeGet正好相反。
我們知道Watcher會在相應的響應式數據改變的時候被對應Binding所調用,因此Watcher一定包含方法update:
p.update = function () { batcher.push(this) } p.run = function () { if (this.active) { var value = this.get() if ( (typeof value === "object" && value !== null) || value !== this.value ) { var oldValue = this.value this.value = value this.cb.call(this.ctx, value, oldValue) } } }
update并沒有理解調用對應回調函數,而且將Watcher放入Batcher隊列,Batcher會在恰當的時間調用Watcher的run函數。run內部會調用this.get()得到表達式當前的計算值,并且觸發回調函數。
Watcher還有一個函數用于從所有的依賴的Binding中移除自身:
p.teardown = function () { if (this.active) { this.active = false var vm = this.vm for (var path in this.deps) { vm._getBindingAt(path)._removeSub(this) } } }
teardown內部邏輯非常簡單,不再贅述。
講到這里大家可能被我粗糙的文筆搞的混亂了,我們舉個例子來看看,理順一下思路,假設存在下面的例子:
new Vue({ el: "#app", data: { a: { b: { c: 100 } }, d: { e: { f: 200 } } } })
{{a.b.c + d.e.f}}
對應于上面的數據,相應的構造好的Binding Tree如下的:
我們在調用this.get去收集表達式a.b.c+d.c.e的對應值時,我們會被Observer模塊的get事件觸發六次,分別對應的值為:
a
a.b
a.b.c
d
d.e
d.e.f
因此此時的Watcher中的dep存儲的就是對應的依賴路徑:
而此時的Watcher實例在Binding Tree的注冊情況如下:
到此為止,我們已經了解響應式數據是如何與Watcher對應的和響應式數據改變時觸發相應的操作。邏輯雖說不算特別難,但是還是有一定的復雜度的,建議可以對應看看源碼,調試一下疑惑的地方,相信會有更多的收獲。
如果文章有不正確的地方歡迎指正。最后還是希望大家能給我的Github博客點個Star。愿共同學習,一同進步。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108103.html
摘要:以上引用內容來自阮一峰的教程的章節原文地址請戳這里。最后本文最終實現代碼已經放在上,想要直接看效果的同學,可以上去直接,運行。 前言 如果你有讀過Vue的源碼,或者有了解過Vue的響應原理,那么你一定知道Object.defineProperty(),那么你也應該知道,Vue 2.x里,是通過 遞歸 + 遍歷 data對象來實現對數據的監控的,你可能還會知道,我們使用的時候,直接通過數...
摘要:一起來實現一個框架最近手癢,當然也是為了近階段的跳槽做準備,利用周五時光,仿照用法,實現一下的雙向綁定數據代理大胡子模板指令,等。 一起來實現一個mvvm框架 最近手癢,當然也是為了近階段的跳槽做準備,利用周五時光,仿照vue用法,實現一下mvvm的雙向綁定、數據代理、大胡子{{}}模板、指令v-on,v-bind等。當然由于時間緊迫,里面的編碼細節沒有做優化,還請各位看官多多包涵!看...
摘要:要實現最小化刷新,我們要將模板中的每個綁定都收集起來。思考題在最后的實現下,我們把模板改為下面這樣雖然很少會有人這樣寫,就會出現重復的實例,該如何解決這個問題,參考早期源碼學習系列之四如何實現動態數據綁定 上一篇文章我們了解了怎樣實現一個簡單模板引擎。但這個模板引擎只適合靜態模板,因為它是將模板整體編譯成字符串進行全量替換。如果每次數據改變都進行一次替換,會有兩個最主要的問題: 性能...
摘要:引言最近做的項目已經接近尾聲剛剛發到線上回顧和總結一下這段時間遇到的問題和個人的一些想法。通過在指令中比較前后值以及設置避免不必要更新導致的彈窗渲染。 引言 最近做的項目已經接近尾聲,剛剛發到線上,回顧和總結一下這段時間遇到的問題和個人的一些想法。 select下拉修改和復原 //部分下拉選項 {{o...
摘要:關于雙向數據綁定當我們在前端開發中采用的模式時,,指的是模型,也就是數據,,指的是視圖,也就是頁面展現的部分。參考沉思錄一數據綁定雙向數據綁定實現數據與視圖的綁定與同步,最終體現在對數據的讀寫處理過程中,也就是定義的數據函數中。 關于雙向數據綁定 當我們在前端開發中采用MV*的模式時,M - model,指的是模型,也就是數據,V - view,指的是視圖,也就是頁面展現的部分。通常,...
閱讀 5256·2021-09-22 15:50
閱讀 1862·2021-09-02 15:15
閱讀 1164·2019-08-29 12:49
閱讀 2543·2019-08-26 13:31
閱讀 3458·2019-08-26 12:09
閱讀 1209·2019-08-23 18:17
閱讀 2735·2019-08-23 17:56
閱讀 2929·2019-08-23 16:02