摘要:儲存訂閱器因為屬性被監聽,這一步會執行監聽器里的方法這一步我們把也給弄了出來,到這一步我們已經實現了一個簡單的雙向綁定了,我們可以嘗試把兩者結合起來看下效果。總結本文主要是對雙向綁定原理的學習與實現。
當今前端天下以 Angular、React、vue 三足鼎立的局面,你不選擇一個陣營基本上無法立足于前端,甚至是兩個或者三個陣營都要選擇,大勢所趨。
所以我們要時刻保持好奇心,擁抱變化,只有在不斷的變化中你才能利于不敗之地,保守只能等死。
最近在學習 Vue,一直以來對它的雙向綁定只能算了解并不深入,最近幾天打算深入學習下,通過幾天的學習查閱資料,算是對它的原理有所認識,所以自己動手寫了一個雙向綁定的例子,下面我們一步步看如何實現的。
看完這篇文章之后我相信你會對 Vue 的雙向綁定原理有一個清楚的認識。也能幫助我們更好的認識 Vue。
先看效果圖
//代碼:數據綁定{{name}}
在正式開始之前我們先來說說數據綁定的事情,數據綁定我的理解就是讓數據M(model)展示到 視圖V(view)上。我們常見的架構模式有 MVC、MVP、MVVM模式,目前前端框架基本上都是采用 MVVM 模式實現雙向綁定,Vue 自然也不例外。但是各個框架實現雙向綁定的方法略有所不同,目前大概有三種實現方式。
發布訂閱模式
Angular 的臟查機制
數據劫持
而 Vue 則采用的是數據劫持與發布訂閱相結合的方式實現雙向綁定,數據劫持主要通過 Object.defineProperty 來實現。
Object.defineProperty這篇文章我們不詳細討論 Object.defineProperty 的用法,我們主要看看它的存儲屬性 get 與 set。我們來看看通過它設置的對象屬性之后有何變化。
var people = { name: "Modeng", age: 18 } people.age; //18 people.age = 20;
上述代碼就是普通的獲取/設置對象的屬性,看不到什么奇怪的變化。
var modeng = {} var age; Object.defineProperty(modeng, "age", { get: function () { console.log("獲取年齡"); return age; }, set: function (newVal) { console.log("設置年齡"); age = newVal; } }); modeng.age = 18; console.log(modeng.age);
你會發現通過上述操作之后,我們訪問 age 屬性時會自動執行 get 函數,設置 age 屬性時,會自動執行 set 函數,這就給我們的雙向綁定提供了非常大的方便。
分析我們知道 MVVM 模式在于數據與視圖的保持同步,意思是說數據改變時會自動更新視圖,視圖發生變化時會更新數據。
所以我們需要做的就是如何檢測到數據的變化然后通知我們去更新視圖,如何檢測到視圖的變化然后去更新數據。檢測視圖這個比較簡單,無非就是我們利用事件的監聽即可。
那么如何才能知道數據屬性發生變化呢?這個就是利用我們上面說到的 Object.defineProperty 當我們的屬性發生變化時,它會自動觸發 set 函數從而能夠通知我們去更新視圖。
實現通過上面的描述與分析我們知道 Vue 是通過數據劫持結合發布訂閱模式來實現雙向綁定的。我們也知道數據劫持是通過 Object.defineProperty 方法,當我們知道這些之后,我們就需要一個監聽器 Observer 來監聽屬性的變化。得知屬性發生變化之后我們需要一個 Watcher 訂閱者來更新視圖,我們還需要一個 compile 指令解析器,用于解析我們的節點元素的指令與初始化視圖。所以我們需要如下:
Observer 監聽器:用來監聽屬性的變化通知訂閱者
Watcher 訂閱者:收到屬性的變化,然后更新視圖
Compile 解析器:解析指令,初始化模版,綁定訂閱者
順著這條思路我們一步一步去實現。
監聽器 Observer監聽器的作用就是去監聽數據的每一個屬性,我們上面也說了使用 Object.defineProperty 方法,當我們監聽到屬性發生變化之后我們需要通知 Watcher 訂閱者執行更新函數去更新視圖,在這個過程中我們可能會有很多個訂閱者 Watcher 所以我們要創建一個容器 Dep 去做一個統一的管理。
function defineReactive(data, key, value) { //遞歸調用,監聽所有屬性 observer(value); var dep = new Dep(); Object.defineProperty(data, key, { get: function () { if (Dep.target) { dep.addSub(Dep.target); } return value; }, set: function (newVal) { if (value !== newVal) { value = newVal; dep.notify(); //通知訂閱器 } } }); } function observer(data) { if (!data || typeof data !== "object") { return; } Object.keys(data).forEach(key => { defineReactive(data, key, data[key]); }); } function Dep() { this.subs = []; } Dep.prototype.addSub = function (sub) { this.subs.push(sub); } Dep.prototype.notify = function () { console.log("屬性變化通知 Watcher 執行更新視圖函數"); this.subs.forEach(sub => { sub.update(); }) } Dep.target = null;
以上我們就創建了一個監聽器 Observer,我們現在可以嘗試一下給一個對象添加監聽然后改變屬性會有何變化。
var modeng = { age: 18 } observer(modeng); modeng.age = 20;
我們可以看到瀏覽器控制臺打印出 “屬性變化通知 Watcher 執行更新視圖函數” 說明我們實現的監聽器沒毛病,既然監聽器有了,我們就可以通知屬性變化了,那肯定是需要 Watcher 的時候了。
訂閱者 WatcherWatcher 主要是接受屬性變化的通知,然后去執行更新函數去更新視圖,所以我們做的主要是有兩步:
把 Watcher 添加到 Dep 容器中,這里我們用到了 監聽器的 get 函數
接收到通知,執行更新函數。
function Watcher(vm, prop, callback) { this.vm = vm; this.prop = prop; this.callback = callback; this.value = this.get(); } Watcher.prototype = { update: function () { const value = this.vm.$data[this.prop]; const oldVal = this.value; if (value !== oldVal) { this.value = value; this.callback(value); } }, get: function () { Dep.target = this; //儲存訂閱器 const value = this.vm.$data[this.prop]; //因為屬性被監聽,這一步會執行監聽器里的 get方法 Dep.target = null; return value; } }
這一步我們把 Watcher 也給弄了出來,到這一步我們已經實現了一個簡單的雙向綁定了,我們可以嘗試把兩者結合起來看下效果。
function Mvue(options, prop) { this.$options = options; this.$data = options.data; this.$prop = prop; this.$el = document.querySelector(options.el); this.init(); } Mvue.prototype.init = function () { observer(this.$data); this.$el.textContent = this.$data[this.$prop]; new Watcher(this, this.$prop, value => { this.$el.textContent = value; }); }
這里我們嘗試利用一個實例來把數據與需要監聽的屬性傳遞進來,通過監聽器監聽數據,然后添加屬性訂閱,綁定更新函數。
{{name}}const vm = new Mvue({ el: "#app", data: { name: "我是摩登" } }, "name");
我們可以看到數據已經正常的顯示在頁面上,那么我們在通過控制臺去修改數據,發生變化后視圖也會跟著修改。
到這一步我們我們基本上已經實現了一個簡單的雙向綁定,但是不難發現我們這里的屬性都是寫死的,也沒有指令模板的解析,所以下一步我們來實現一個模板解析器。
Compile 解析器Compile 的主要作用一個是用來解析指令初始化模板,一個是用來添加添加訂閱者,綁定更新函數。
因為在解析 DOM 節點的過程中我們會頻繁的操作 DOM, 所以我們利用文檔片段(DocumentFragment)來幫助我們去解析 DOM 優化性能。
function Compile(vm) { this.vm = vm; this.el = vm.$el; this.fragment = null; this.init(); } Compile.prototype = { init: function () { this.fragment = this.nodeFragment(this.el); }, nodeFragment: function (el) { const fragment = document.createDocumentFragment(); let child = el.firstChild; //將子節點,全部移動文檔片段里 while (child) { fragment.appendChild(child); child = el.firstChild; } return fragment; } }
然后我們就需要對整個節點和指令進行處理編譯,根據不同的節點去調用不同的渲染函數,綁定更新函數,編譯完成之后,再把 DOM 片段添加到頁面中。
Compile.prototype = { compileNode: function (fragment) { let childNodes = fragment.childNodes; [...childNodes].forEach(node => { let reg = /{{(.*)}}/; let text = node.textContent; if (this.isElementNode(node)) { this.compile(node); //渲染指令模板 } else if (this.isTextNode(node) && reg.test(text)) { let prop = RegExp.$1; this.compileText(node, prop); //渲染{{}} 模板 } //遞歸編譯子節點 if (node.childNodes && node.childNodes.length) { this.compileNode(node); } }); }, compile: function (node) { let nodeAttrs = node.attributes; [...nodeAttrs].forEach(attr => { let name = attr.name; if (this.isDirective(name)) { let value = attr.value; if (name === "v-model") { this.compileModel(node, value); } node.removeAttribute(name); } }); }, //省略。。。 }
因為代碼比較長如果全部貼出來會影響閱讀,我們主要是講整個過程實現的思路,文章結束我會把源碼發出來,有興趣的可以去查看全部代碼。
到這里我們的整個的模板編譯也已經完成,不過這里我們并沒有實現過多的指令,我們只是簡單的實現了 v-model 指令,本意是通過這篇文章讓大家熟悉與認識 Vue 的雙向綁定原理,并不是去創造一個新的 MVVM 實例。所以并沒有考慮很多細節與設計。
現在我們實現了 Observer、Watcher、Compile,接下來就是把三者給組織起來,成為一個完整的 MVVM。
創建 Mvue這里我們創建一個 Mvue 的類(構造函數)用來承載 Observer、Watcher、Compile 三者。
function Mvue(options) { this.$options = options; this.$data = options.data; this.$el = document.querySelector(options.el); this.init(); } Mvue.prototype.init = function () { observer(this.$data); new Compile(this); }
然后我們就去測試一下結果,看看我們實現的 Mvue 是不是真的可以運行。
{{name}}
我們嘗試去修改數據,也完全沒問題,但是有個問題就是我們修改數據時時通過 vm.$data.name 去修改數據,而不是想 Vue 中直接用 vm.name 就可以去修改,那這個是怎么做到的呢?其實很簡單,Vue 做了一步數據代理操作。
數據代理我們來改造下 Mvue 添加數據代理功能,我們也是利用 Object.defineProperty 方法進行一步中間的轉換操作,間接的去訪問。
function Mvue(options) { this.$options = options; this.$data = options.data; this.$el = document.querySelector(options.el); //數據代理 Object.keys(this.$data).forEach(key => { this.proxyData(key); }); this.init(); } Mvue.prototype.init = function () { observer(this.$data); new Compile(this); } Mvue.prototype.proxyData = function (key) { Object.defineProperty(this, key, { get: function () { return this.$data[key] }, set: function (value) { this.$data[key] = value; } }); }
到這里我們就可以像 Vue 一樣去修改我們的屬性了,非常完美。完全自己動手實現,你也來試試把,體驗下自己動手寫代碼的樂趣。
總結本文主要是對 Vue 雙向綁定原理的學習與實現。
主要是對整個思路的學習,并沒有考慮到太多的實現與設計的細節,所以還存在很多問題,并不完美。
源碼地址,整個過程的全部代碼,希望對你有所幫助。
如果你覺得本文對你有幫助,歡迎轉發,點贊。
關注微信公眾號:六小登登。領取全套學習資源
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98674.html
摘要:雙向數據綁定可算是前端領域經久不衰的熱詞,不管是前端開發還是面試都會有所涉及。因此,中的挺身而出,拯救了中對數組數據處理的不足。有興趣的朋友請期待筆者的下一篇博客,討論下用實現雙向數據綁定。 雙向數據綁定可算是前端領域經久不衰的熱詞,不管是前端開發還是面試都會有所涉及。而且不同的框架也想盡一切辦法去實現這一特性,比如:Knockout / Backbone --- 發布-訂閱模式Ang...
摘要:模塊則負責維護,以及各個模塊間的調度思考題了解了的實現機制,你能否自己動手也試著用百來行代碼實現一個庫呢好了本教程第一部分設計篇就寫到這里,具體請移步下一篇教學向行代碼教你實現一個低配版的庫代碼篇我會用給出一版實現。 適讀人群 本文適合對MVVM有一定了解(如有主流框架ng,vue等使用經驗配合本文服用則效果更佳),雖然會用這類框架,但是對框架底層核心實現又不太清楚,或者能說出個所以然...
摘要:關于雙向數據綁定當我們在前端開發中采用的模式時,,指的是模型,也就是數據,,指的是視圖,也就是頁面展現的部分。參考沉思錄一數據綁定雙向數據綁定實現數據與視圖的綁定與同步,最終體現在對數據的讀寫處理過程中,也就是定義的數據函數中。 關于雙向數據綁定 當我們在前端開發中采用MV*的模式時,M - model,指的是模型,也就是數據,V - view,指的是視圖,也就是頁面展現的部分。通常,...
摘要:模塊化是隨著前端技術的發展,前端代碼爆炸式增長后,工程化所采取的必然措施。目前模塊化的思想分為和。特別指出,事件不等同于異步,回調也不等同于異步。將會討論安全的類型檢測惰性載入函數凍結對象定時器等話題。 Vue.js 前后端同構方案之準備篇——代碼優化 目前 Vue.js 的火爆不亞于當初的 React,本人對寫代碼有潔癖,代碼也是藝術。此篇是準備篇,工欲善其事,必先利其器。我們先在代...
閱讀 3172·2021-09-22 15:05
閱讀 2748·2019-08-30 15:56
閱讀 1054·2019-08-29 17:09
閱讀 792·2019-08-29 15:12
閱讀 2076·2019-08-26 11:55
閱讀 3037·2019-08-26 11:52
閱讀 3370·2019-08-26 10:29
閱讀 1374·2019-08-23 17:19