您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)如何實(shí)現(xiàn)vue雙向綁定,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
當(dāng)今前端天下以 Angular、React、vue 三足鼎立的局面,你不選擇一個(gè)陣營(yíng)基本上無法立足于前端,甚至是兩個(gè)或者三個(gè)陣營(yíng)都要選擇,大勢(shì)所趨。
所以我們要時(shí)刻保持好奇心,擁抱變化,只有在不斷的變化中你才能利于不敗之地,保守只能等死。
最近在學(xué)習(xí) Vue,一直以來對(duì)它的雙向綁定只能算了解并不深入,最近幾天打算深入學(xué)習(xí)下,通過幾天的學(xué)習(xí)查閱資料,算是對(duì)它的原理有所認(rèn)識(shí),所以自己動(dòng)手寫了一個(gè)雙向綁定的例子,下面我們一步步看如何實(shí)現(xiàn)的。
看完這篇文章之后我相信你會(huì)對(duì) Vue 的雙向綁定原理有一個(gè)清楚的認(rèn)識(shí)。也能幫助我們更好的認(rèn)識(shí) Vue。
先看效果圖
//代碼: <div id="app"> <input v-model="name" type="text"> <h2>{{name}}</h2> </div> <script src="./js/observer.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compile.js"></script> <script src="./js/index.js"></script> <script> const vm = new Mvue({ el: "#app", data: { name: "我是摩登" } }); </script>
在正式開始之前我們先來說說數(shù)據(jù)綁定的事情,數(shù)據(jù)綁定我的理解就是讓數(shù)據(jù)M(model)展示到 視圖V(view)上。我們常見的架構(gòu)模式有 MVC、MVP、MVVM模式,目前前端框架基本上都是采用 MVVM 模式實(shí)現(xiàn)雙向綁定,Vue 自然也不例外。但是各個(gè)框架實(shí)現(xiàn)雙向綁定的方法略有所不同,目前大概有三種實(shí)現(xiàn)方式。
發(fā)布訂閱模式
Angular 的臟查機(jī)制
數(shù)據(jù)劫持
而 Vue 則采用的是數(shù)據(jù)劫持與發(fā)布訂閱相結(jié)合的方式實(shí)現(xiàn)雙向綁定,數(shù)據(jù)劫持主要通過 Object.defineProperty
來實(shí)現(xiàn)。
這篇文章我們不詳細(xì)討論 Object.defineProperty 的用法,我們主要看看它的存儲(chǔ)屬性 get 與 set。我們來看看通過它設(shè)置的對(duì)象屬性之后有何變化。
var people = { name: "Modeng", age: 18 } people.age; //18 people.age = 20;
上述代碼就是普通的獲取/設(shè)置對(duì)象的屬性,看不到什么奇怪的變化。
var modeng = {} var age; Object.defineProperty(modeng, 'age', { get: function () { console.log("獲取年齡"); return age; }, set: function (newVal) { console.log("設(shè)置年齡"); age = newVal; } }); modeng.age = 18; console.log(modeng.age);
你會(huì)發(fā)現(xiàn)通過上述操作之后,我們?cè)L問 age 屬性時(shí)會(huì)自動(dòng)執(zhí)行 get 函數(shù),設(shè)置 age 屬性時(shí),會(huì)自動(dòng)執(zhí)行 set 函數(shù),這就給我們的雙向綁定提供了非常大的方便。
我們知道 MVVM 模式在于數(shù)據(jù)與視圖的保持同步,意思是說數(shù)據(jù)改變時(shí)會(huì)自動(dòng)更新視圖,視圖發(fā)生變化時(shí)會(huì)更新數(shù)據(jù)。
所以我們需要做的就是如何檢測(cè)到數(shù)據(jù)的變化然后通知我們?nèi)ジ乱晥D,如何檢測(cè)到視圖的變化然后去更新數(shù)據(jù)。檢測(cè)視圖這個(gè)比較簡(jiǎn)單,無非就是我們利用事件的監(jiān)聽即可。
那么如何才能知道數(shù)據(jù)屬性發(fā)生變化呢?這個(gè)就是利用我們上面說到的 Object.defineProperty 當(dāng)我們的屬性發(fā)生變化時(shí),它會(huì)自動(dòng)觸發(fā) set 函數(shù)從而能夠通知我們?nèi)ジ乱晥D。
通過上面的描述與分析我們知道 Vue 是通過數(shù)據(jù)劫持結(jié)合發(fā)布訂閱模式來實(shí)現(xiàn)雙向綁定的。我們也知道數(shù)據(jù)劫持是通過 Object.defineProperty 方法,當(dāng)我們知道這些之后,我們就需要一個(gè)監(jiān)聽器 Observer 來監(jiān)聽屬性的變化。得知屬性發(fā)生變化之后我們需要一個(gè) Watcher 訂閱者來更新視圖,我們還需要一個(gè) compile 指令解析器,用于解析我們的節(jié)點(diǎn)元素的指令與初始化視圖。所以我們需要如下:
Observer 監(jiān)聽器:用來監(jiān)聽屬性的變化通知訂閱者
Watcher 訂閱者:收到屬性的變化,然后更新視圖
Compile 解析器:解析指令,初始化模版,綁定訂閱者
順著這條思路我們一步一步去實(shí)現(xiàn)。
監(jiān)聽器的作用就是去監(jiān)聽數(shù)據(jù)的每一個(gè)屬性,我們上面也說了使用 Object.defineProperty
方法,當(dāng)我們監(jiān)聽到屬性發(fā)生變化之后我們需要通知 Watcher 訂閱者執(zhí)行更新函數(shù)去更新視圖,在這個(gè)過程中我們可能會(huì)有很多個(gè)訂閱者 Watcher 所以我們要?jiǎng)?chuàng)建一個(gè)容器 Dep 去做一個(gè)統(tǒng)一的管理。
function defineReactive(data, key, value) { //遞歸調(diào)用,監(jiān)聽所有屬性 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 執(zhí)行更新視圖函數(shù)'); this.subs.forEach(sub => { sub.update(); }) } Dep.target = null;
以上我們就創(chuàng)建了一個(gè)監(jiān)聽器 Observer,我們現(xiàn)在可以嘗試一下給一個(gè)對(duì)象添加監(jiān)聽然后改變屬性會(huì)有何變化。
var modeng = { age: 18 } observer(modeng); modeng.age = 20;
我們可以看到瀏覽器控制臺(tái)打印出 “屬性變化通知 Watcher 執(zhí)行更新視圖函數(shù)” 說明我們實(shí)現(xiàn)的監(jiān)聽器沒毛病,既然監(jiān)聽器有了,我們就可以通知屬性變化了,那肯定是需要 Watcher 的時(shí)候了。
Watcher 主要是接受屬性變化的通知,然后去執(zhí)行更新函數(shù)去更新視圖,所以我們做的主要是有兩步:
把 Watcher 添加到 Dep 容器中,這里我們用到了 監(jiān)聽器的 get 函數(shù)
接收到通知,執(zhí)行更新函數(shù)。
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; //儲(chǔ)存訂閱器 const value = this.vm.$data[this.prop]; //因?yàn)閷傩员槐O(jiān)聽,這一步會(huì)執(zhí)行監(jiān)聽器里的 get方法 Dep.target = null; return value; } }
這一步我們把 Watcher 也給弄了出來,到這一步我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的雙向綁定了,我們可以嘗試把兩者結(jié)合起來看下效果。
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; }); }
這里我們嘗試?yán)靡粋€(gè)實(shí)例來把數(shù)據(jù)與需要監(jiān)聽的屬性傳遞進(jìn)來,通過監(jiān)聽器監(jiān)聽數(shù)據(jù),然后添加屬性訂閱,綁定更新函數(shù)。
<p id="app">{{name}}</p> const vm = new Mvue({ el: "#app", data: { name: "我是摩登" } }, "name");
我們可以看到數(shù)據(jù)已經(jīng)正常的顯示在頁(yè)面上,那么我們?cè)谕ㄟ^控制臺(tái)去修改數(shù)據(jù),發(fā)生變化后視圖也會(huì)跟著修改。
到這一步我們我們基本上已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的雙向綁定,但是不難發(fā)現(xiàn)我們這里的屬性都是寫死的,也沒有指令模板的解析,所以下一步我們來實(shí)現(xiàn)一個(gè)模板解析器。
Compile 的主要作用一個(gè)是用來解析指令初始化模板,一個(gè)是用來添加添加訂閱者,綁定更新函數(shù)。
因?yàn)樵诮馕?DOM 節(jié)點(diǎn)的過程中我們會(huì)頻繁的操作 DOM, 所以我們利用文檔片段(DocumentFragment)來幫助我們?nèi)ソ馕?DOM 優(yōu)化性能。
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; //將子節(jié)點(diǎn),全部移動(dòng)文檔片段里 while (child) { fragment.appendChild(child); child = el.firstChild; } return fragment; } }
然后我們就需要對(duì)整個(gè)節(jié)點(diǎn)和指令進(jìn)行處理編譯,根據(jù)不同的節(jié)點(diǎn)去調(diào)用不同的渲染函數(shù),綁定更新函數(shù),編譯完成之后,再把 DOM 片段添加到頁(yè)面中。
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); //渲染{{}} 模板 } //遞歸編譯子節(jié)點(diǎn) 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); } }); }, //省略。。。 }
因?yàn)榇a比較長(zhǎng)如果全部貼出來會(huì)影響閱讀,我們主要是講整個(gè)過程實(shí)現(xiàn)的思路,文章結(jié)束我會(huì)把源碼發(fā)出來,有興趣的可以去查看全部代碼。
到這里我們的整個(gè)的模板編譯也已經(jīng)完成,不過這里我們并沒有實(shí)現(xiàn)過多的指令,我們只是簡(jiǎn)單的實(shí)現(xiàn)了 v-model
指令,本意是通過這篇文章讓大家熟悉與認(rèn)識(shí) Vue 的雙向綁定原理,并不是去創(chuàng)造一個(gè)新的 MVVM 實(shí)例。所以并沒有考慮很多細(xì)節(jié)與設(shè)計(jì)。
現(xiàn)在我們實(shí)現(xiàn)了 Observer、Watcher、Compile,接下來就是把三者給組織起來,成為一個(gè)完整的 MVVM。
這里我們創(chuàng)建一個(gè) Mvue 的類(構(gòu)造函數(shù))用來承載 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); }
然后我們就去測(cè)試一下結(jié)果,看看我們實(shí)現(xiàn)的 Mvue 是不是真的可以運(yùn)行。
<p id="app"> <h2>{{name}}</h2> </p> <script src="./js/observer.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compile.js"></script> <script src="./js/index.js"></script> <script> const vm = new Mvue({ el: "#app", data: { name: "完全沒問題,看起來是不是很酷!" } }); </script>
我們嘗試去修改數(shù)據(jù),也完全沒問題,但是有個(gè)問題就是我們修改數(shù)據(jù)時(shí)時(shí)通過 vm.$data.name 去修改數(shù)據(jù),而不是想 Vue 中直接用 vm.name 就可以去修改,那這個(gè)是怎么做到的呢?其實(shí)很簡(jiǎn)單,Vue 做了一步數(shù)據(jù)代理操作。
我們來改造下 Mvue 添加數(shù)據(jù)代理功能,我們也是利用 Object.defineProperty
方法進(jìn)行一步中間的轉(zhuǎn)換操作,間接的去訪問。
function Mvue(options) { this.$options = options; this.$data = options.data; this.$el = document.querySelector(options.el); //數(shù)據(jù)代理 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; } }); }
關(guān)于如何實(shí)現(xiàn)vue雙向綁定就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。