摘要:對于的動態(tài)數(shù)據(jù)綁定,經過反復地看源碼和博客講解,總算能夠理解它的實現(xiàn)了,心累分享一下學習成果,同時也算是做個記錄。
對于vue.js的動態(tài)數(shù)據(jù)綁定,經過反復地看源碼和博客講解,總算能夠理解它的實現(xiàn)了,心累~ 分享一下學習成果,同時也算是做個記錄。完整代碼GitHub地址:https://github.com/hanrenguang/Dynamic-data-binding。也可以到倉庫的 README 閱讀本文。
整體思路不知道有沒有同學和我一樣,看著vue的源碼卻不知從何開始,真叫人頭大。硬生生地看了observer, watcher, compile這幾部分的源碼,只覺得一臉懵逼。最終,從這里得到啟發(fā),作者寫得很好,值得一讀。
關于動態(tài)數(shù)據(jù)綁定呢,需要搞定的是 Dep , Observer , Watcher , Compile 這幾個類,他們之間有著各種聯(lián)系,想要搞懂源碼,就得先了解他們之間的聯(lián)系。下面來理一理:
Observer 所做的就是劫持監(jiān)聽所有屬性,當有變動時通知 Dep
Watcher 向 Dep 添加訂閱,同時,屬性有變化時,Observer 通知 Dep,Dep 則通知 Watcher
Watcher 得到通知后,調用回調函數(shù)更新視圖
Compile 則是解析所綁定元素的 DOM 結構,對所有需要綁定的屬性添加 Watcher 訂閱
由此可以看出,當屬性發(fā)生變化時,是由Observer -> Dep -> Watcher -> update view,Compile 在最開始解析 DOM 并添加 Watcher 訂閱后就功成身退了。
從程序執(zhí)行的順序來看的話,即 new Vue({}) 之后,應該是這樣的:先通過 Observer 劫持所有屬性,然后 Compile 解析 DOM 結構,并添加 Watcher 訂閱,再之后就是屬性變化 -> Observer -> Dep -> Watcher -> update view,接下來就說說具體的實現(xiàn)。
從new一個實例開始談起網上的很多源碼解讀都是從 Observer 開始的,而我會從 new 一個MVVM實例開始,按照程序執(zhí)行順序去解釋或許更容易理解。先來看一個簡單的例子:
test {{user.name}}
{{user.age}}
接下來都將以其為例來分析。下面來看一個簡略的 MVVM 的實現(xiàn),在此將其命名為 hue。為了方便起見,為 data 屬性設置了一個代理,通過 vm._data 來訪問 data 的屬性顯得麻煩且冗余,通過代理,可以很好地解決這個問題,在注釋中也有說明。添加完屬性代理后,調用了一個 observe 函數(shù),這一步做的就是 Observer 的屬性劫持了,這一步具體怎么實現(xiàn),暫時先不展開。先記住他為 data 的屬性添加了 getter 和 setter。
function Hue(options) { this.$options = options || {}; let data = this._data = this.$options.data, self = this; Object.keys(data).forEach(function(key) { self._proxyData(key); }); observe(data); self.$compile = new Compile(self, options.el || document.body); } // 為 data 做了一個代理, // 訪問 vm.xxx 會觸發(fā) vm._data[xxx] 的getter,取得 vm._data[xxx] 的值, // 為 vm.xxx 賦值則會觸發(fā) vm._data[xxx] 的setter Hue.prototype._proxyData = function(key) { let self = this; Object.defineProperty(self, key, { configurable: false, enumerable: true, get: function proxyGetter() { return self._data[key]; }, set: function proxySetter(newVal) { self._data[key] = newVal; } }); };
再往下看,最后一步 new 了一個 Compile,下面我們就來講講 Compile。
Compilenew Compile(self, options.el || document.body) 這一行代碼中,第一個參數(shù)是當前 Hue 實例,第二個參數(shù)是綁定的元素,在上面的示例中為class為 .test 的div。
關于 Compile,這里只實現(xiàn)最簡單的 textContent 的綁定。而 Compile 的代碼沒什么難點,很輕易就能讀懂,所做的就是解析 DOM,并添加 Watcher 訂閱。關于 DOM 的解析,先將根節(jié)點 el 轉換成文檔碎片 fragment 進行解析編譯操作,解析完成后,再將 fragment 添加回原來的真實 DOM 節(jié)點中。來看看這部分的代碼:
function Compile(vm, el) { this.$vm = vm; this.$el = this.isElementNode(el) ? el : document.querySelector(el); if (this.$el) { this.$fragment = this.node2Fragment(this.$el); this.init(); this.$el.appendChild(this.$fragment); } } Compile.prototype.node2Fragment = function(el) { let fragment = document.createDocumentFragment(), child; // 也許有同學不太理解這一步,不妨動手寫個小例子觀察一下他的行為 while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }; Compile.prototype.init = function() { // 解析 fragment this.compileElement(this.$fragment); };
以上面示例為例,此時若打印出 fragment,可觀察到其包含兩個p元素:
{{user.name}}
{{user.age}}
下一步就是解析 fragment,直接看代碼及注釋吧:
Compile.prototype.compileElement = function(el) { let childNodes = Array.from(el.childNodes), self = this; childNodes.forEach(function(node) { let text = node.textContent, reg = /{{(.*)}}/; // 若為 textNode 元素,且匹配 reg 正則 // 在上例中會匹配 "{{user.name}}" 及 "{{user.age}}" if (self.isTextNode(node) && reg.test(text)) { // 解析 textContent,RegExp.$1 為匹配到的內容,在上例中為 "user.name" 及 "user.age" self.compileText(node, RegExp.$1); } // 遞歸 if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }); }; Compile.prototype.compileText = function(node, exp) { // this.$vm 即為 Hue 實例,exp 為正則匹配到的內容,即 "user.name" 或 "user.age" compileUtil.text(node, this.$vm, exp); }; let compileUtil = { text: function(node, vm, exp) { this.bind(node, vm, exp, "text"); }, bind: function(node, vm, exp, dir) { // 獲取更新視圖的回調函數(shù) let updaterFn = updater[dir + "Updater"]; // 先調用一次 updaterFn,更新視圖 updaterFn && updaterFn(node, this._getVMVal(vm, exp)); // 添加 Watcher 訂閱 new Watcher(vm, exp, function(value, oldValue) { updaterFn && updaterFn(node, value, oldValue); }); }, // 根據(jù) exp,獲得其值,在上例中即 "vm.user.name" 或 "vm.user.age" _getVMVal: function(vm, exp) { let val = vm; exp = exp.trim().split("."); exp.forEach(function(k) { val = val[k]; }); return val; } }; let updater = { // Watcher 訂閱的回調函數(shù) // 在此即更新 node.textContent,即 update view textUpdater: function(node, value) { node.textContent = typeof value === "undefined" ? "" : value; } };
正如代碼中所看到的,Compile 在解析到 {{xxx}} 后便添加了 xxx 屬性的訂閱,即 new Watcher(vm, exp, callback)。理解了這一步后,接下來就需要了解怎么實現(xiàn)相關屬性的訂閱了。先從 Observer 開始談起。
Observer從最簡單的情況來考慮,即不考慮數(shù)組元素的變化。暫時先不考慮 Dep 與 Observer 的聯(lián)系。先看看 Observer 構造函數(shù):
function Observer(data) { this.data = data; this.walk(data); } Observer.prototype.walk = function(data) { const keys = Object.keys(data); // 遍歷 data 的所有屬性 for (let i = 0; i < keys.length; i++) { // 調用 defineReactive 添加 getter 和 setter defineReactive(data, keys[i], data[keys[i]]); } };
接下來通過 Object.defineProperty 方法給所有屬性添加 getter 和 setter,就達到了我們的目的。屬性有可能也是對象,因此需要對屬性值進行遞歸調用。
function defineReactive(obj, key, val) { // 對屬性值遞歸,對應屬性值為對象的情況 let childObj = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { // 直接返回屬性值 return val; }, set: function(newVal) { if (newVal === val) { return; } // 值發(fā)生變化時修改閉包中的 val, // 保證在觸發(fā) getter 時返回正確的值 val = newVal; // 對新賦的值進行遞歸,防止賦的值為對象的情況 childObj = observe(newVal); } }); }
最后補充上 observe 函數(shù),也即 Hue 構造函數(shù)中調用的 observe 函數(shù):
function observe(val) { // 若 val 是對象且非數(shù)組,則 new 一個 Observer 實例,val 作為參數(shù) // 簡單點說:是對象就繼續(xù)。 if (!Array.isArray(val) && typeof val === "object") { return new Observer(val); } }
這樣一來就對 data 的所有子孫屬性(不知有沒有這種說法。。)都進行了“劫持”。顯然到目前為止,這并沒什么用,或者說如果只做到這里,那么和什么都不做沒差別。于是 Dep 上場了。我認為理解 Dep 與 Observer 和 Watcher 之間的聯(lián)系是最重要的,先來談談 Dep 在 Observer 里做了什么。
Observer & Dep在每一次 defineReactive 函數(shù)被調用之后,都會在閉包中新建一個 Dep 實例,即 let dep = new Dep()。Dep 提供了一些方法,先來說說 notify 這個方法,它做了什么事?就是在屬性值發(fā)生變化的時候通知 Dep,那么我們的代碼可以增加如下:
function defineReactive(obj, key, val) { let childObj = observe(val); const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; childObj = observe(newVal); // 發(fā)生變動 dep.notify(); } }); }
如果僅考慮 Observer 與 Dep 的聯(lián)系,即有變動時通知 Dep,那么這里就算完了,然而在 vue.js 的源碼中,我們還可以看到一段增加在 getter 中的代碼:
// ... get: function() { if (Dep.target) { dep.depend(); } return val; } // ...
這個 depend 方法呢,它又做了啥?答案是為閉包中的 Dep 實例添加了一個 Watcher 的訂閱,而 Dep.target 又是啥?他其實是一個 Watcher 實例,???一臉懵逼,先記住就好,先看一部份的 Dep 源碼:
// 標識符,在 Watcher 中有用到,先不用管 let uid = 0; function Dep() { this.id = uid++; this.subs = []; } Dep.prototype.depend = function() { // 這一步相當于做了這么一件事:this.subs.push(Dep.target) // 即添加了 Watcher 訂閱,addDep 是 Watcher 的方法 Dep.target.addDep(this); }; // 通知更新 Dep.prototype.notify = function() { // this.subs 的每一項都為一個 Watcher 實例 this.subs.forEach(function(sub) { // update 為 Watcher 的一個方法,更新視圖 // 沒錯,實際上這個方法最終會調用到 Compile 中的 updaterFn, // 也即 new Watcher(vm, exp, callback) 中的 callback sub.update(); }); }; // 在 Watcher 中調用 Dep.prototype.addSub = function(sub) { this.subs.push(sub); }; // 初始時引用為空 Dep.target = null;
也許看到這還是一臉懵逼,沒關系,接著往下。大概有同學會疑惑,為什么要把添加 Watcher 訂閱放在 getter 中,接下來我們來說說這 Watcher 和 Dep 的故事。
Watcher & Dep先讓我們回顧一下 Compile 做的事,解析 fragment,然后給相應屬性添加訂閱:new Watcher(vm, exp, cb)。new 了這個 Watcher 之后,Watcher 怎么辦呢,就有了下面這樣的對話:
Watcher:hey Dep,我需要訂閱 exp 屬性的變動。
Dep:這我可做不到,你得去找 exp 屬性中的 dep,他能做到這件事。
Watcher:可是他在閉包中啊,我無法和他聯(lián)系。
Dep:你拿到了整個 Hue 實例 vm,又知道屬性 exp,你可以觸發(fā)他的 getter 啊,你在 getter 里動些手腳不就行了。
Watcher:有道理,可是我得讓 dep 知道是我訂閱的啊,不然他通知不到我。
Dep:這個簡單,我?guī)湍悖忝看斡|發(fā) getter 前,把你的引用告訴 Dep.target 就行了。記得辦完事后給 Dep.target 置空。
于是就有了上面 getter 中的代碼:
// ... get: function() { // 是否是 Watcher 觸發(fā)的 if (Dep.target) { // 是就添加進來 dep.depend(); } return val; } // ...
現(xiàn)在再回頭看看 Dep 部分的代碼,是不是好理解些了。如此一來, Watcher 需要做的事情就簡單明了了:
function Watcher(vm, exp, cb) { this.$vm = vm; this.cb = cb; this.exp = exp; this.depIds = new Set(); // 返回一個用于獲取相應屬性值的函數(shù) this.getter = parseGetter(exp.trim()); // 調用 get 方法,觸發(fā) getter this.value = this.get(); } Watcher.prototype.get = function() { const vm = this.$vm; // 將 Dep.target 指向當前 Watcher 實例 Dep.target = this; // 觸發(fā) getter let value = this.getter.call(vm, vm); // Dep.target 置空 Dep.target = null; return value; }; Watcher.prototype.addDep = function(dep) { const id = dep.id; if (!this.depIds.has(id)) { // 添加訂閱,相當于 dep.subs.push(this) dep.addSub(this); this.depIds.add(id); } }; function parseGetter(exp) { if (/[^w.$]/.test(exp)) { return; } let exps = exp.split("."); return function(obj) { for (let i = 0; i < exps.length; i++) { if (!obj) return; obj = obj[exps[i]]; } return obj; }; }
最后還差一部分,即 Dep 通知變化后,Watcher 的處理,具體的函數(shù)調用流程是這樣的:dep.notify() -> sub.update(),直接上代碼:
Watcher.prototype.update = function() { this.run(); }; Watcher.prototype.run = function() { let value = this.get(); let oldVal = this.value; if (value !== oldVal) { this.value = value; // 調用回調函數(shù)更新視圖 this.cb.call(this.$vm, value, oldVal); } };結語
到這就算寫完了,本人水平有限,若有不足之處歡迎指出,一起探討。
參考資料https://github.com/DMQ/mvvm
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://www.ezyhdfw.cn/yun/83072.html
摘要:指令的職責是,當表達式的值改變時,將其產生的連帶影響,響應式地作用于。對象形式佐客湯姆咪口修飾符修飾符是以半角句號指明的特殊后綴,用于指出一個指令應該以特殊方式綁定。修飾符修飾符允許你控制由精確的系統(tǒng)修飾符組合觸發(fā)的事件。 指令 指令(Directives)是帶有v-前綴的特殊屬性。指令的職責是,當表達式的值改變時,將其產生的連帶影響,響應式地作用于DOM。 v-if條件判斷 T...
摘要:指令帶有前綴,以表示它們是提供的特殊屬性。最后,我們需要為賦值世界舞王尼古拉斯趙四世界舞王尼古拉斯趙四初學就到這里了,相信你已經在腦子里確定了的原理的概念也已經非常清楚了,希望你能夠在學習的道路上越走越遠,最后感謝你的瀏覽。 vue.js vue介紹 Vue.js(讀音 /vju?/,類似于 view) 是一套構建用戶界面的漸進式框架。與其他重量級框架不同的是,Vue 采用自底向上增量...
摘要:每一個計算屬性都包含一個和一個。使用計算屬性的原因在于它的依賴緩存。及與綁定的主要用法是動態(tài)更新元素上的屬性。測試文字當?shù)谋磉_式過長或邏輯復雜時,還可以綁定一個計算屬性。 學習筆記:前端開發(fā)文檔 計算屬性 所有的計算屬性都以函數(shù)的形式寫在Vue實例中的computed選項內,最終返回計算后的結果。 計算屬性的用法 在一個計算屬性中可以完成各種復雜的邏輯,包括運算、函數(shù)調用等,只要最終...
摘要:直接創(chuàng)建組件使用標簽名組件模板,根據(jù)組件構造器來創(chuàng)建組件。相應的,實例也被稱為父組件。而且不允許子組件直接修改父組件中的數(shù)據(jù),強制修改會報錯。 一、組件 (一)什么是組件 組件(Component)是 Vue.js最強大的功能之一。組件可以擴展 HTML元素,封裝可重用的代碼組件是自定義元素(對象)。 (二)創(chuàng)建組件的兩種方式 官方推薦組件標簽名是使用-連接的組合詞,例如:。 1、使用...
摘要:直接創(chuàng)建組件使用標簽名組件模板,根據(jù)組件構造器來創(chuàng)建組件。相應的,實例也被稱為父組件。而且不允許子組件直接修改父組件中的數(shù)據(jù),強制修改會報錯。 一、組件 (一)什么是組件 組件(Component)是 Vue.js最強大的功能之一。組件可以擴展 HTML元素,封裝可重用的代碼組件是自定義元素(對象)。 (二)創(chuàng)建組件的兩種方式 官方推薦組件標簽名是使用-連接的組合詞,例如:。 1、使用...
摘要:一介紹也稱為,讀音類似,錯誤讀音,由華人尤雨溪開源并維護。隱藏四事件之前說了一些關于事件的指令,這里詳細學習一下事件的相關知識。還有一些其他鍵盤事件,具體參考官方文檔。模板就是,用來進行數(shù)據(jù)綁定,顯示在頁面中,也稱為語法。 一、Vue.js介紹 Vue.js也稱為Vue,讀音類似view,錯誤讀音v-u-e,由華人尤雨溪開源并維護。 Vue有以下特點: 是一個構建用戶界面的框架 是一...
閱讀 960·2021-11-23 09:51
閱讀 1205·2021-11-15 17:57
閱讀 1718·2021-09-22 15:24
閱讀 866·2021-09-07 09:59
閱讀 2298·2019-08-29 15:10
閱讀 1907·2019-08-29 12:47
閱讀 818·2019-08-29 12:30
閱讀 3456·2019-08-26 13:51