<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
vue.js是採用資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥來渲染檢視。
什麼是資料雙向繫結?
vue是一個mvvm框架,即資料雙向繫結,即當資料發生變化的時候,檢視也就發生變化,當檢視發生變化的時候,資料也會跟著同步變化。這也算是vue的精髓之處了。值得注意的是, 我們所說的資料雙向繫結,一定是對於UI控制元件來說的,非UI控制元件不會涉及到資料雙向繫結。 單向資料繫結是使用狀態管理工具(如redux)的前提。如果我們使用vuex,那麼資料流也是單項的,這時就會和雙向資料繫結有衝突,我們可以這麼解決。
為什麼要實現資料的雙向繫結?
在vue中,如果使用vuex,實際上資料還是單向的,之所以說是資料雙向繫結,這是用的UI控制元件來說,對於我們處理表單,vue的雙向資料繫結用起來就特別舒服了。
即兩者並不互斥, 在全域性性資料流使用單項,方便跟蹤; 區域性性資料流使用雙向,簡單易操作。
語法:
Object.defineProperty(obj, prop, descriptor)
引數說明:
返回值:
傳入函數的物件。即第一個引數obj;
針對屬性,我們可以給這個屬性設定一些特性,比如是否唯讀不可以寫;是否可以被for…in或Object.keys()遍歷。
給物件的屬性新增特性描述,目前提供兩種形式:資料描述和存取器描述。
當修改或定義物件的某個屬性的時候,給這個屬性新增一些特性:
Object.defineProperty()函數可以定義物件的屬性相關描述符, 其中的set和get函數對於完成資料雙向繫結起到了至關重要的作用,下面,我們看看這個函數的基本使用方式。
var obj = { foo: 'foo' } Object.defineProperty(obj, 'foo', { get: function () { console.log('將要讀取obj.foo屬性'); }, set: function (newVal) { console.log('當前值為', newVal); } }); obj.foo; // 將要讀取obj.foo屬性 obj.foo = 'name'; // 當前值為 name
可以看到,get即為我們存取屬性時呼叫,set為我們設定屬性值時呼叫。
<!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>
最終效果圖:
可以看到,實現一個簡單的資料雙向繫結還是不難的: 使用Object.defineProperty()來定義屬性的set函數,屬性被賦值的時候,修改Input的value值以及span中的innerHTML;然後監聽input的keyup事件,修改物件的屬性值,即可實現這樣的一個簡單的資料雙向繫結。
雙向繫結指令為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: '雙向資料繫結的例子' } }); </script> </body> </html>
最終的結果就是:你改變 input 文字方塊的內容的時候,p 標籤中的內容會跟著進行改變。
上面我們只是實現了一個最簡單的資料雙向繫結,而我們真正希望實現的時下面這種方式:
<div id="app"> <input type="text" v-model="text"> {{ text }} </div> <script> var vm = new Vue({ el: '#app', data: { text: 'hello world' } }); </script>
即和vue一樣的方式來實現資料的雙向繫結。那麼,我們可以把整個實現過程分為下面幾步:
如果希望實現任務一,我們還需要使用到 DocumentFragment 檔案片段,可以把它看做一個容器,如下所示:
<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樹:
使用檔案片段的好處在於:在檔案片段上進行操作DOM,而不會影響到真實的DOM,操作完成之後,我們就可以新增到真實DOM上,這樣的效率比直接在正式DOM上修改要高很多 。
vue進行編譯時,就是將掛載目標的所有子節點劫持到DocumentFragment中,經過一番處理之後,再將DocumentFragment整體返回插入掛載目標。
如下所示 :
<!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劫持,接著再把這個檔案片段新增到div上去。
<!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 = /{{(.*)}}/; // 節點型別為元素 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'); } } } // 節點型別為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); // 將子節點劫持到檔案片段中 } 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>
以上的程式碼實現而立任務一,我們可以看到,hello world 已經呈現在了輸入框和文位元組點中了。
我們再來看看任務二的實現思路: 當我們在輸入框輸入資料的時候,首先觸發的時input事件(或者keyup、change事件),在相應的事件處理程式中,我們獲取輸入框的value並賦值給vm範例的text屬性。 我們會利用defineProperty將data中的text設定為vm的存取器屬性,因此給vm.text賦值,就會觸發set方法。 在set方法中主要做兩件事情,第一是更新屬性的值,第二留在任務三種說。
<!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 = /{{(.*)}}/; // 節點型別為元素 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) { // 給相應的data屬性賦值,進而觸發屬性的set方法 vm[name] = e.target.value; }) node.value = vm[name]; // 將data的值賦值給該node node.removeAttribute('v-model'); } } } // 節點型別為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); // 將子節點劫持到檔案片段中 } 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) { // 響應式的資料繫結 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>
以上,任務二也就完成了,text屬性值會和輸入框的內容同步變化。
text屬性變化了,set方法觸發了,但是文位元組點的內容沒有變化。 如何才能讓同樣繫結到text的文位元組點也同步變化呢? 這裡又有一個知識點: 訂閱釋出模式。
訂閱釋出模式又稱為觀察者模式,定義了一種一對多的關係,讓多個觀察者同時監聽某一個主題物件,這個主題物件的狀態發生改變時就會通知所有的觀察者物件。
釋出者發出通知 =>主題物件收到通知並推播給訂閱者 => 訂閱者執行相應的操作。
// 一個釋出者 publisher,功能就是負責釋出訊息 - publish var pub = { publish: function () { dep.notify(); } } // 多個訂閱者 subscribers, 在釋出者釋出訊息之後執行函數 var sub1 = { update: function () { console.log(1); } } var sub2 = { update: function () { console.log(2); } } var sub3 = { update: function () { console.log(3); } } // 一個主題物件 function Dep() { this.subs = [sub1, sub2, sub3]; } Dep.prototype.notify = function () { this.subs.forEach(function (sub) { sub.update(); }); } // 釋出者釋出訊息, 主題物件執行notify方法,進而觸發訂閱者執行Update方法 var dep = new Dep(); pub.publish();
不難看出,這裡的思路還是很簡單的: 釋出者負責釋出訊息、 訂閱者負責接收接收訊息,而最重要的是主題物件,他需要記錄所有的訂閱這特訊息的人,然後負責吧釋出的訊息通知給哪些訂閱了訊息的人。
所以,當set方法觸發後做的第二件事情就是作為釋出者發出通知: “我是屬性text,我變了”。 文位元組點作為訂閱者,在接收到訊息之後執行相應的更新動作。
回顧一下,每當new一個Vue,主要做了兩件事情:第一是監聽資料:observe(data),第二是編譯HTML:nodeToFragment(id)
在監聽資料的過程中,會為data中的每一個屬性生成一個主題物件dep。
在編譯HTML的過程中,會為每一個與資料繫結相關的節點生成一個訂閱者 watcher,watcher會將自己新增到相應屬性的dep中。
我們已經實現了: 修改輸入框內容 => 在事件回撥函數中修改屬性值 => 觸發屬性的set方法。
接下來我們要實現的是: 發出通知 dep.notify() => 觸發訂閱者update方法 => 更新檢視。
這裡的關鍵邏輯是: 如何將watcher新增到關聯屬性的dep中。
function compile(node, vm) { var reg = /{{(.*)}}/; // 節點型別為元素 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) { // 給相應的data屬性賦值,進而觸發屬性的set方法 vm[name] = e.target.value; }) node.value = vm[name]; // 將data的值賦值給該node node.removeAttribute('v-model'); } } } // 節點型別為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的過程中,為每個和data關聯的節點生成一個Watcher。那麼Watcher函數中發生了什麼呢?
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]; // 觸發相應屬性的get } }
首先,將自己賦值給了一個全域性變數 Dep.target;
其次,執行了update方法,進而執行了 get 方法,get方法讀取了vm的存取器屬性, 從而觸發了存取器屬性的get方法,get方法將該watcher新增到對應存取器屬性的dep中;
再次,獲取順序性的值, 然後更新檢視。
最後將Dep.target設定為空。 因為他是全域性變數,也是watcher和dep關聯的唯一橋樑,任何時候,都必須保證Dep.target只有一個值。
最終如下:
<!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(); // 響應式的資料繫結 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; // 作為釋出者發出通知 dep.notify() } } }); } function nodeToFragment(node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { compile(child, vm); flag.appendChild(child); // 將子節點劫持到檔案片段中 } return flag; } function compile(node, vm) { var reg = /{{(.*)}}/; // 節點型別為元素 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) { // 給相應的data屬性賦值,進而觸發屬性的set方法 vm[name] = e.target.value; }) node.value = vm[name]; // 將data的值賦值給該node node.removeAttribute('v-model'); } } } // 節點型別為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]; // 觸發相應屬性的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>
到此這篇關於 面試問題Vue雙向資料繫結原理的文章就介紹到這了,更多相關Vue雙向資料繫結內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45