?面試問題Vue雙向數(shù)據(jù)綁定原理
前言
vue.js是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個(gè)屬性的setter,getter,在數(shù)據(jù)變動(dòng)時(shí)發(fā)布消息給訂閱者,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)來渲染視圖。
具體步驟
- 1、需要observer的數(shù)據(jù)對象進(jìn)行遞歸遍歷,包括子屬性對象的屬性,都加上 setter和getter 這樣的話,給這個(gè)對象的某個(gè)值賦值,就會(huì)觸發(fā)setter,那么就能監(jiān)聽到了數(shù)據(jù)變化
- 2、compile解析模板指令,將模板中的變量替換成數(shù)據(jù),然后初始化渲染頁面視圖,并將每個(gè)指令對應(yīng)的節(jié)點(diǎn)綁定更新函數(shù),添加監(jiān)聽數(shù)據(jù)的訂閱者,一旦數(shù)據(jù)有變動(dòng),收到通知,更新視圖
- 3、Watcher訂閱者是Observer和Compile之間通信的橋梁,主要做的事情是:
(1)在自身實(shí)例化時(shí)往屬性訂閱器(dep)里面添加自己
(2)自身必須有一個(gè)update()方法
(3)待屬性變動(dòng)dep.notice()通知時(shí),能調(diào)用自身的update()方法,并觸發(fā)Compile中綁定的回調(diào),則功成身退。 - 4、MVVM作為數(shù)據(jù)綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監(jiān)聽自己的model數(shù)據(jù)變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達(dá)到數(shù)據(jù)變化 -> 視圖更新;視圖交互變化(input) -> 數(shù)據(jù)model變更的雙向綁定效果。
什么是數(shù)據(jù)雙向綁定?
vue是一個(gè)mvvm框架,即數(shù)據(jù)雙向綁定,即當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候,視圖也就發(fā)生變化,當(dāng)視圖發(fā)生變化的時(shí)候,數(shù)據(jù)也會(huì)跟著同步變化。這也算是vue的精髓之處了。值得注意的是, 我們所說的數(shù)據(jù)雙向綁定,一定是對于UI控件來說的,非UI控件不會(huì)涉及到數(shù)據(jù)雙向綁定。 單向數(shù)據(jù)綁定是使用狀態(tài)管理工具(如redux)的前提。如果我們使用vuex,那么數(shù)據(jù)流也是單項(xiàng)的,這時(shí)就會(huì)和雙向數(shù)據(jù)綁定有沖突,我們可以這么解決。
為什么要實(shí)現(xiàn)數(shù)據(jù)的雙向綁定?
在vue中,如果使用vuex,實(shí)際上數(shù)據(jù)還是單向的,之所以說是數(shù)據(jù)雙向綁定,這是用的UI控件來說,對于我們處理表單,vue的雙向數(shù)據(jù)綁定用起來就特別舒服了。
即兩者并不互斥, 在全局性數(shù)據(jù)流使用單項(xiàng),方便跟蹤; 局部性數(shù)據(jù)流使用雙向,簡單易操作。
什么是Object.defineProperty?
語法:
Object.defineProperty(obj, prop, descriptor)
參數(shù)說明:
- obj:必需。目標(biāo)對象
- prop:必需。需定義或修改的屬性的名字
- descriptor:必需。目標(biāo)屬性所擁有的特性
返回值:
傳入函數(shù)的對象。即第一個(gè)參數(shù)obj;
針對屬性,我們可以給這個(gè)屬性設(shè)置一些特性,比如是否只讀不可以寫;是否可以被for…in或Object.keys()遍歷。
給對象的屬性添加特性描述,目前提供兩種形式:數(shù)據(jù)描述和存取器描述。
當(dāng)修改或定義對象的某個(gè)屬性的時(shí)候,給這個(gè)屬性添加一些特性:
一、訪問器屬性
Object.defineProperty()函數(shù)可以定義對象的屬性相關(guān)描述符, 其中的set和get函數(shù)對于完成數(shù)據(jù)雙向綁定起到了至關(guān)重要的作用,下面,我們看看這個(gè)函數(shù)的基本使用方式。
var obj = { foo: 'foo' } Object.defineProperty(obj, 'foo', { get: function () { console.log('將要讀取obj.foo屬性'); }, set: function (newVal) { console.log('當(dāng)前值為', newVal); } }); obj.foo; // 將要讀取obj.foo屬性 obj.foo = 'name'; // 當(dāng)前值為 name
可以看到,get即為我們訪問屬性時(shí)調(diào)用,set為我們設(shè)置屬性值時(shí)調(diào)用。
二、簡單的數(shù)據(jù)雙向綁定實(shí)現(xiàn)方法
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>forvue</title> </head> <body> <input type="text" id="textInput"> 輸入:<span id="textSpan"></span> <script> var obj = {}, textInput = document.querySelector('#textInput'), textSpan = document.querySelector('#textSpan'); Object.defineProperty(obj, 'foo', { set: function (newValue) { textInput.value = newValue; textSpan.innerHTML = newValue; } }); textInput.addEventListener('keyup', function (e) { obj.foo = e.target.value; }); </script> </body> </html>
最終效果圖:
可以看到,實(shí)現(xiàn)一個(gè)簡單的數(shù)據(jù)雙向綁定還是不難的: 使用Object.defineProperty()來定義屬性的set函數(shù),屬性被賦值的時(shí)候,修改Input的value值以及span中的innerHTML;然后監(jiān)聽input的keyup事件,修改對象的屬性值,即可實(shí)現(xiàn)這樣的一個(gè)簡單的數(shù)據(jù)雙向綁定。
雙向綁定指令為v-model:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Vue入門之htmlraw</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> </head> <body> <div id="app"> <!-- v-model可以直接指向data中的屬性,雙向綁定就建立了 --> <input type="text" name="txt" v-model="msg"> <p>您輸入的信息是:{{ msg }}</p> </div> <script> var app = new Vue({ el: '#app', data: { msg: '雙向數(shù)據(jù)綁定的例子' } }); </script> </body> </html>
最終的結(jié)果就是:你改變 input 文本框的內(nèi)容的時(shí)候,p 標(biāo)簽中的內(nèi)容會(huì)跟著進(jìn)行改變。
三、 實(shí)現(xiàn)任務(wù)的思路
上面我們只是實(shí)現(xiàn)了一個(gè)最簡單的數(shù)據(jù)雙向綁定,而我們真正希望實(shí)現(xiàn)的時(shí)下面這種方式:
<div id="app"> <input type="text" v-model="text"> {{ text }} </div> <script> var vm = new Vue({ el: '#app', data: { text: 'hello world' } }); </script>
即和vue一樣的方式來實(shí)現(xiàn)數(shù)據(jù)的雙向綁定。那么,我們可以把整個(gè)實(shí)現(xiàn)過程分為下面幾步:
- 輸入框以及文本節(jié)點(diǎn)與 data 中的數(shù)據(jù)綁定
- 輸入框內(nèi)容變化時(shí),data 中的數(shù)據(jù)同步變化。即 view => model 的變化。
- data 中的數(shù)據(jù)變化時(shí),文本節(jié)點(diǎn)的內(nèi)容同步變化。即 model => view 的變化。
四、DocumentFragment
如果希望實(shí)現(xiàn)任務(wù)一,我們還需要使用到 DocumentFragment 文檔片段,可以把它看做一個(gè)容器,如下所示:
<div id="app"> </div> <script> var flag = document.createDocumentFragment(), span = document.createElement('span'), textNode = document.createTextNode('hello world'); span.appendChild(textNode); flag.appendChild(span); document.querySelector('#app').appendChild(flag) </script>
這樣,我們就可以得到下面的DOM樹:
使用文檔片段的好處在于:在文檔片段上進(jìn)行操作DOM,而不會(huì)影響到真實(shí)的DOM,操作完成之后,我們就可以添加到真實(shí)DOM上,這樣的效率比直接在正式DOM上修改要高很多 。
vue進(jìn)行編譯時(shí),就是將掛載目標(biāo)的所有子節(jié)點(diǎn)劫持到DocumentFragment中,經(jīng)過一番處理之后,再將DocumentFragment整體返回插入掛載目標(biāo)。
如下所示 :
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>forvue</title> </head> <body> <div id="app"> <input type="text" id="a"> <span id="b"></span> </div> <script> var dom = nodeToFragment(document.getElementById('app')); console.log(dom); function nodeToFragment(node) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { flag.appendChild(child); } return flag; } document.getElementById('app').appendChild(dom); </script> </body> </html>
即首先獲取到div,然后通過documentFragment劫持,接著再把這個(gè)文檔片段添加到div上去。
五、初始化數(shù)據(jù)綁定
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>forvue</title> </head> <body> <div id="app"> <input type="text" v-model="text"> {{ text }} </div> <script> function compile(node, vm) { var reg = /{{(.*)}}/; // 節(jié)點(diǎn)類型為元素 if (node.nodeType === 1) { var attr = node.attributes; // 解析屬性 for (var i = 0; i < attr.length; i++) { if (attr[i].nodeName == 'v-model') { var name = attr[i].nodeValue; // 獲取v-model綁定的屬性名 node.value = vm.data[name]; // 將data的值賦值給該node node.removeAttribute('v-model'); } } } // 節(jié)點(diǎn)類型為text if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1; // 獲取匹配到的字符串 name = name.trim(); node.nodeValue = vm.data[name]; // 將data的值賦值給該node } } } function nodeToFragment(node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { compile(child, vm); flag.appendChild(child); // 將子節(jié)點(diǎn)劫持到文檔片段中 } return flag; } function Vue(options) { this.data = options.data; var id = options.el; var dom = nodeToFragment(document.getElementById(id), this); // 編譯完成后,將dom返回到app中。 document.getElementById(id).appendChild(dom); } var vm = new Vue({ el: 'app', data: { text: 'hello world' } }); </script> </body> </html>
以上的代碼實(shí)現(xiàn)而立任務(wù)一,我們可以看到,hello world 已經(jīng)呈現(xiàn)在了輸入框和文本節(jié)點(diǎn)中了。
六、響應(yīng)式的數(shù)據(jù)綁定
我們再來看看任務(wù)二的實(shí)現(xiàn)思路: 當(dāng)我們在輸入框輸入數(shù)據(jù)的時(shí)候,首先觸發(fā)的時(shí)input事件(或者keyup、change事件),在相應(yīng)的事件處理程序中,我們獲取輸入框的value并賦值給vm實(shí)例的text屬性。 我們會(huì)利用defineProperty將data中的text設(shè)置為vm的訪問器屬性,因此給vm.text賦值,就會(huì)觸發(fā)set方法。 在set方法中主要做兩件事情,第一是更新屬性的值,第二留在任務(wù)三種說。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>forvue</title> </head> <body> <div id="app"> <input type="text" v-model="text"> {{ text }} </div> <script> function compile(node, vm) { var reg = /{{(.*)}}/; // 節(jié)點(diǎn)類型為元素 if (node.nodeType === 1) { var attr = node.attributes; // 解析屬性 for (var i = 0; i < attr.length; i++) { if (attr[i].nodeName == 'v-model') { var name = attr[i].nodeValue; // 獲取v-model綁定的屬性名 node.addEventListener('input', function (e) { // 給相應(yīng)的data屬性賦值,進(jìn)而觸發(fā)屬性的set方法 vm[name] = e.target.value; }) node.value = vm[name]; // 將data的值賦值給該node node.removeAttribute('v-model'); } } } // 節(jié)點(diǎn)類型為text if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1; // 獲取匹配到的字符串 name = name.trim(); node.nodeValue = vm[name]; // 將data的值賦值給該node } } } function nodeToFragment(node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { compile(child, vm); flag.appendChild(child); // 將子節(jié)點(diǎn)劫持到文檔片段中 } return flag; } function Vue(options) { this.data = options.data; var data = this.data; observe(data, this); var id = options.el; var dom = nodeToFragment(document.getElementById(id), this); // 編譯完成后,將dom返回到app中。 document.getElementById(id).appendChild(dom); } var vm = new Vue({ el: 'app', data: { text: 'hello world' } }); function defineReactive(obj, key, val) { // 響應(yīng)式的數(shù)據(jù)綁定 Object.defineProperty(obj, key, { get: function () { return val; }, set: function (newVal) { if (newVal === val) { return; } else { val = newVal; console.log(val); // 方便看效果 } } }); } function observe (obj, vm) { Object.keys(obj).forEach(function (key) { defineReactive(vm, key, obj[key]); }); } </script> </body> </html>
以上,任務(wù)二也就完成了,text屬性值會(huì)和輸入框的內(nèi)容同步變化。
七、 訂閱/發(fā)布模式(subscribe & publish)
text屬性變化了,set方法觸發(fā)了,但是文本節(jié)點(diǎn)的內(nèi)容沒有變化。 如何才能讓同樣綁定到text的文本節(jié)點(diǎn)也同步變化呢? 這里又有一個(gè)知識點(diǎn): 訂閱發(fā)布模式。
訂閱發(fā)布模式又稱為觀察者模式,定義了一種一對多的關(guān)系,讓多個(gè)觀察者同時(shí)監(jiān)聽某一個(gè)主題對象,這個(gè)主題對象的狀態(tài)發(fā)生改變時(shí)就會(huì)通知所有的觀察者對象。
發(fā)布者發(fā)出通知 =>主題對象收到通知并推送給訂閱者 => 訂閱者執(zhí)行相應(yīng)的操作。
// 一個(gè)發(fā)布者 publisher,功能就是負(fù)責(zé)發(fā)布消息 - publish var pub = { publish: function () { dep.notify(); } } // 多個(gè)訂閱者 subscribers, 在發(fā)布者發(fā)布消息之后執(zhí)行函數(shù) var sub1 = { update: function () { console.log(1); } } var sub2 = { update: function () { console.log(2); } } var sub3 = { update: function () { console.log(3); } } // 一個(gè)主題對象 function Dep() { this.subs = [sub1, sub2, sub3]; } Dep.prototype.notify = function () { this.subs.forEach(function (sub) { sub.update(); }); } // 發(fā)布者發(fā)布消息, 主題對象執(zhí)行notify方法,進(jìn)而觸發(fā)訂閱者執(zhí)行Update方法 var dep = new Dep(); pub.publish();
不難看出,這里的思路還是很簡單的: 發(fā)布者負(fù)責(zé)發(fā)布消息、 訂閱者負(fù)責(zé)接收接收消息,而最重要的是主題對象,他需要記錄所有的訂閱這特消息的人,然后負(fù)責(zé)吧發(fā)布的消息通知給哪些訂閱了消息的人。
所以,當(dāng)set方法觸發(fā)后做的第二件事情就是作為發(fā)布者發(fā)出通知: “我是屬性text,我變了”。 文本節(jié)點(diǎn)作為訂閱者,在接收到消息之后執(zhí)行相應(yīng)的更新動(dòng)作。
八、 雙向綁定的實(shí)現(xiàn)
回顧一下,每當(dāng)new一個(gè)Vue,主要做了兩件事情:第一是監(jiān)聽數(shù)據(jù):observe(data),第二是編譯HTML:nodeToFragment(id)
在監(jiān)聽數(shù)據(jù)的過程中,會(huì)為data中的每一個(gè)屬性生成一個(gè)主題對象dep。
在編譯HTML的過程中,會(huì)為每一個(gè)與數(shù)據(jù)綁定相關(guān)的節(jié)點(diǎn)生成一個(gè)訂閱者 watcher,watcher會(huì)將自己添加到相應(yīng)屬性的dep中。
我們已經(jīng)實(shí)現(xiàn)了: 修改輸入框內(nèi)容 => 在事件回調(diào)函數(shù)中修改屬性值 => 觸發(fā)屬性的set方法。
接下來我們要實(shí)現(xiàn)的是: 發(fā)出通知 dep.notify() => 觸發(fā)訂閱者update方法 => 更新視圖。
這里的關(guān)鍵邏輯是: 如何將watcher添加到關(guān)聯(lián)屬性的dep中。
function compile(node, vm) { var reg = /{{(.*)}}/; // 節(jié)點(diǎn)類型為元素 if (node.nodeType === 1) { var attr = node.attributes; // 解析屬性 for (var i = 0; i < attr.length; i++) { if (attr[i].nodeName == 'v-model') { var name = attr[i].nodeValue; // 獲取v-model綁定的屬性名 node.addEventListener('input', function (e) { // 給相應(yīng)的data屬性賦值,進(jìn)而觸發(fā)屬性的set方法 vm[name] = e.target.value; }) node.value = vm[name]; // 將data的值賦值給該node node.removeAttribute('v-model'); } } } // 節(jié)點(diǎn)類型為text if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1; // 獲取匹配到的字符串 name = name.trim(); // node.nodeValue = vm[name]; // 將data的值賦值給該node new Watcher(vm, node, name); } } }
在編譯HTML的過程中,為每個(gè)和data關(guān)聯(lián)的節(jié)點(diǎn)生成一個(gè)Watcher。那么Watcher函數(shù)中發(fā)生了什么呢?
function Watcher(vm, node, name) { Dep.target = this; this.name = name; this.node = node; this.vm = vm; this.update(); Dep.target = null; } Watcher.prototype = { update: function () { this.get(); this.node.nodeValue = this.value; }, // 獲取data中的屬性值 get: function () { this.value = this.vm[this.name]; // 觸發(fā)相應(yīng)屬性的get } }
首先,將自己賦值給了一個(gè)全局變量 Dep.target;
其次,執(zhí)行了update方法,進(jìn)而執(zhí)行了 get 方法,get方法讀取了vm的訪問器屬性, 從而觸發(fā)了訪問器屬性的get方法,get方法將該watcher添加到對應(yīng)訪問器屬性的dep中;
再次,獲取順序性的值, 然后更新視圖。
最后將Dep.target設(shè)置為空。 因?yàn)樗侨肿兞?,也是watcher和dep關(guān)聯(lián)的唯一橋梁,任何時(shí)候,都必須保證Dep.target只有一個(gè)值。
最終如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>forvue</title> </head> <body> <div id="app"> <input type="text" v-model="text"> <br> {{ text }} <br> {{ text }} </div> <script> function observe(obj, vm) { Object.keys(obj).forEach(function (key) { defineReactive(vm, key, obj[key]); }); } function defineReactive(obj, key, val) { var dep = new Dep(); // 響應(yīng)式的數(shù)據(jù)綁定 Object.defineProperty(obj, key, { get: function () { // 添加訂閱者watcher到主題對象Dep if (Dep.target) { dep.addSub(Dep.target); } return val; }, set: function (newVal) { if (newVal === val) { return; } else { val = newVal; // 作為發(fā)布者發(fā)出通知 dep.notify() } } }); } function nodeToFragment(node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { compile(child, vm); flag.appendChild(child); // 將子節(jié)點(diǎn)劫持到文檔片段中 } return flag; } function compile(node, vm) { var reg = /{{(.*)}}/; // 節(jié)點(diǎn)類型為元素 if (node.nodeType === 1) { var attr = node.attributes; // 解析屬性 for (var i = 0; i < attr.length; i++) { if (attr[i].nodeName == 'v-model') { var name = attr[i].nodeValue; // 獲取v-model綁定的屬性名 node.addEventListener('input', function (e) { // 給相應(yīng)的data屬性賦值,進(jìn)而觸發(fā)屬性的set方法 vm[name] = e.target.value; }) node.value = vm[name]; // 將data的值賦值給該node node.removeAttribute('v-model'); } } } // 節(jié)點(diǎn)類型為text if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1; // 獲取匹配到的字符串 name = name.trim(); // node.nodeValue = vm[name]; // 將data的值賦值給該node new Watcher(vm, node, name); } } } function Watcher(vm, node, name) { Dep.target = this; this.name = name; this.node = node; this.vm = vm; this.update(); Dep.target = null; } Watcher.prototype = { update: function () { this.get(); this.node.nodeValue = this.value; }, // 獲取data中的屬性值 get: function () { this.value = this.vm[this.name]; // 觸發(fā)相應(yīng)屬性的get } } function Dep () { this.subs = []; } Dep.prototype = { addSub: function (sub) { this.subs.push(sub); }, notify: function () { this.subs.forEach(function (sub) { sub.update(); }); } } function Vue(options) { this.data = options.data; var data = this.data; observe(data, this); var id = options.el; var dom = nodeToFragment(document.getElementById(id), this); // 編譯完成后,將dom返回到app中。 document.getElementById(id).appendChild(dom); } var vm = new Vue({ el: 'app', data: { text: 'hello world' } }); </script> </body> </html>
到此這篇關(guān)于 面試問題Vue雙向數(shù)據(jù)綁定原理的文章就介紹到這了,更多相關(guān)Vue雙向數(shù)據(jù)綁定內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解如何在Vue3+TS的項(xiàng)目中使用NProgress進(jìn)度條
本文主要介紹了詳解如何在Vue3+TS的項(xiàng)目中使用NProgress進(jìn)度條,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06vue學(xué)習(xí)筆記之vue1.0和vue2.0的區(qū)別介紹
今天我們來說一說vue1.0和vue2.0的主要變化有哪些?對vue相關(guān)知識感興趣的朋友一起學(xué)習(xí)吧2017-05-05vue項(xiàng)目配置同一局域網(wǎng)可使用ip訪問的操作
這篇文章主要介紹了vue項(xiàng)目配置同一局域網(wǎng)可使用ip訪問的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10