解析Vue2.0雙向綁定實現(xiàn)原理
一、實現(xiàn)雙向綁定的做法
前端MVVM最令人激動的就是雙向綁定機制了,實現(xiàn)雙向數(shù)據(jù)綁定的做法大致有如下三種:
1.發(fā)布者-訂閱者模式(backbone.js)
思路:使用自定義的data屬性在HTML代碼中指明綁定。所有綁定起來的JavaScript對象以及DOM元素都將“訂閱”一個發(fā)布者對象。任何時候如果JavaScript對象或者一個HTML輸入字段被偵測到發(fā)生了變化,我們將代理事件到發(fā)布者-訂閱者模式,這會反過來將變化廣播并傳播到所有綁定的對象和元素。
2.臟值檢查(angular.js)
思路:angular.js 是通過臟值檢測的方式比對數(shù)據(jù)是否有變更,來決定是否更新視圖,最簡單的方式就是通過 setInterval() 定時輪詢檢測數(shù)據(jù)變動,angular只有在指定的事件觸發(fā)時進入臟值檢測,大致如下:
- DOM事件,譬如用戶輸入文本,點擊按鈕等。( ng-click )
- XHR響應事件 ( $http )
- 瀏覽器Location變更事件 ( $location )
- Timer事件( $timeout , $interval )
- 執(zhí)行 $digest() 或 $apply()
3.數(shù)據(jù)劫持(Vue.js)
思路: vue.js 則是采用數(shù)據(jù)劫持結合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數(shù)據(jù)變動時發(fā)布消息給訂閱者,觸發(fā)相應的監(jiān)聽回調(diào)。
由此可見,Object.defineProperty() 這個API是Vue實現(xiàn)雙向數(shù)據(jù)綁定的關鍵,我們先簡單了解下這個API,了解更多戳這里
二、Object.defineProperty()
簡單例子:
var obj = {};
Object.defineProperty(obj, 'hello', {
get: function() {
console.log('get val:'+ val);
return val;
},
set: function(newVal) {
val = newVal;
console.log('set val:'+ val);
}
});
obj.hello;
obj.hello='111';
結果:

如果去掉 obj.hello=‘111' 這行代碼,則get的返回值val會報錯val is not defined??梢奜bject.defineProperty() 監(jiān)控對數(shù)據(jù)的操作,可以自動觸發(fā)數(shù)據(jù)同步。下面我們先用Object.defineProperty()來實現(xiàn)一個非常簡單的雙向綁定。
三、實現(xiàn)簡單的雙向綁定
最簡單例子:
<!DOCTYPE html>
<head></head>
<body>
<div id="app">
<input type="text" id="a">
<span id="b"></span>
</div>
<script type="text/javascript">
var obj = {};
Object.defineProperty(obj, 'hello', {
get: function() {
console.log('get val:'+ val);
return val;
},
set: function(newVal) {
val = newVal;
console.log('set val:'+ val);
document.getElementById('a').value = val;
document.getElementById('b').innerHTML = val;
}
});
document.addEventListener('keyup', function(e) {
obj.hello = e.target.value;
});
</script>
</body>
</html>
實現(xiàn)效果如下:

上面例子直接用了dom操作改變了文本節(jié)點的值,而且是在我們知道是哪個id的情況下,通過document.getElementById 獲取到相應的文本節(jié)點,然后直接修改文本節(jié)點的值,這種做法是最簡單粗暴的。
封裝成一個框架,肯定不能是這種做法,所以我們需要一個解析dom,并能修改dom中相應的變量的模塊。
四、實現(xiàn)簡單Compile
首先我們需要獲取文本中真實的dom節(jié)點,然后再分析節(jié)點的類型,根據(jù)節(jié)點類型做相應的處理。
在上面例子我們多次操作了dom節(jié)點,為提高性能和效率,會先將所有的節(jié)點轉換城文檔碎片fragment進行編譯操作,解析操作完成后,再將fragment添加到原來的真實dom節(jié)點中。
<!DOCTYPE html>
<head></head>
<body>
<div id="app">
<input type="text" id="a" v-model="text">
{{text}}
</div>
<script type="text/javascript">
function Compile(node, vm) {
if(node) {this.$frag = this.nodeToFragment(node, vm);
return this.$frag;
}
}
Compile.prototype = {
nodeToFragment: function(node, vm) {
var self = this;
var frag = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
self.compileElement(child, vm);
frag.append(child); // 將所有子節(jié)點添加到fragment中,child是指向元素首個子節(jié)點的引用。將child引用指向的對象append到父對象的末尾,原來child引用的對象就跳到了frag對象的末尾,而child就指向了本來是排在第二個的元素對象。如此循環(huán)下去,鏈接就逐個往后跳了
}
return frag;
},
compileElement: function(node, vm) {
var reg = /\{\{(.*)\}\}/;
//節(jié)點類型為元素
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屬性賦值,進而觸發(fā)該屬性的set方法
vm.data[name]= e.target.value;
});
node.value = vm.data[name]; // 將data的值賦給該node
node.removeAttribute('v-model');
}
};
}
//節(jié)點類型為textif(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; // 獲取匹配到的字符串
name = name.trim();
node.nodeValue = vm.data[name]; // 將data的值賦給該node
}
}
},
}
function Vue(options) {
this.data = options.data;
var data = this.data;
var id = options.el;
var dom =new Compile(document.getElementById(id),this);
// 編譯完成后,將dom返回到app中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>
</html>
結果:

到這,我們做到了獲取文本中真實的dom節(jié)點,然后分析節(jié)點的類型,并能處理節(jié)點中相應的變量如上面代碼中的{{text}},最后渲染到頁面中。接著我們需要和雙向綁定聯(lián)系起來,實現(xiàn){{text}}響應式的數(shù)據(jù)綁定。
五、實現(xiàn)簡單observe
簡單的observe定義如下:

需要監(jiān)控data的屬性值,這個對象的某個值賦值,就會觸發(fā)setter,這樣就能監(jiān)聽到數(shù)據(jù)變化。然后注意vm.data[name]屬性將改為vm[name]

完整代碼如下:
<!DOCTYPE html>
<head></head>
<body>
<div id="app">
<input type="text" id="a" v-model="text">
{{text}}
</div>
<script type="text/javascript">
function Compile(node, vm) {
if(node) {
this.$frag = this.nodeToFragment(node, vm);
return this.$frag;
}
}
Compile.prototype = {
nodeToFragment: function(node, vm) {
var self = this;
var frag = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
self.compileElement(child, vm);
frag.append(child); // 將所有子節(jié)點添加到fragment中
}
return frag;
},
compileElement: function(node, vm) {
var reg = /\{\{(.*)\}\}/;
//節(jié)點類型為元素
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屬性賦值,進而觸發(fā)該屬性的set方法
vm[name]= e.target.value;
});
node.value = vm[name]; // 將data的值賦給該node
node.removeAttribute('v-model');
}
};
}
//節(jié)點類型為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 defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get: function() {
return val;
},
set: function (newVal) {
if(newVal === val) return;
val = newVal;
console.log(val);
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
}
function Vue(options) {
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom =new Compile(document.getElementById(id),this);
// 編譯完成后,將dom返回到app中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>
</html>
結果:

到這,雖然set方法觸發(fā)了,但是文本節(jié)點{{text}}的內(nèi)容沒有變化,要讓綁定的文本節(jié)點同步變化,我們需要引入訂閱發(fā)布模式。
六、訂閱發(fā)布模式
訂閱發(fā)布模式(又稱觀察者模式)定義了一種一對多的關系,讓多個觀察者同時監(jiān)聽某一個主題對象,這個主題對象的狀態(tài)發(fā)生改變時就會通知所有觀察者對象。
發(fā)布者發(fā)出通知 => 主題對象收到通知并推送給訂閱者 => 訂閱者執(zhí)行相應操作
首先我們要一個收集訂閱者的容器,定義一個Dep作為主題對象

然后定義訂閱者Watcher

添加訂閱者Watcher到主題對象Dep,發(fā)布者發(fā)出通知放到屬性監(jiān)聽里面

最后需要訂閱的地方

至此,比較簡單地實現(xiàn)了我們第三步用dom操作實現(xiàn)的雙向綁定效果,代碼:
<!DOCTYPE html>
<head></head>
<body>
<div id="app">
<input type="text" id="a" v-model="text">
{{text}}
</div>
<script type="text/javascript">
function Compile(node, vm) {
if(node) {
this.$frag = this.nodeToFragment(node, vm);
return this.$frag;
}
}
Compile.prototype = {
nodeToFragment: function(node, vm) {
var self = this;
var frag = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
self.compileElement(child, vm);
frag.append(child); // 將所有子節(jié)點添加到fragment中
}
return frag;
},
compileElement: function(node, vm) {
var reg = /\{\{(.*)\}\}/;
//節(jié)點類型為元素
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屬性賦值,進而觸發(fā)該屬性的set方法
vm[name]= e.target.value;
});
// node.value = vm[name]; // 將data的值賦給該node
new Watcher(vm, node, name, 'value');
}
};
}
//節(jié)點類型為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, 'nodeValue');
}
}
},
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
function Watcher(vm, node, name, type) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.type = type;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function() {
this.get();
this.node[this.type] = this.value; // 訂閱者執(zhí)行相應操作
},
// 獲取data的屬性值
get: function() {
this.value = this.vm[this.name]; //觸發(fā)相應屬性的get
}
}
function defineReactive (obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
//添加訂閱者watcher到主題對象Dep
if(Dep.target) {
// JS的瀏覽器單線程特性,保證這個全局變量在同一時間內(nèi),只會有同一個監(jiān)聽器使用
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if(newVal === val) return;
val = newVal;
console.log(val);
// 作為發(fā)布者發(fā)出通知
dep.notify();
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
}
function Vue(options) {
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom =new Compile(document.getElementById(id),this);
// 編譯完成后,將dom返回到app中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>
</html>
七、總結
關于雙向綁定的實現(xiàn),看了網(wǎng)上很多資料,開始看到是對Vue源碼的解析,看的過程似懂非懂。后來找到參考資料1,然后自己跟著實現(xiàn)一遍,才理解許多。感謝這篇文章的作者,寫的由淺入深,比較好理解。為了加深自己的理解,于是自己順著這個思路寫下這個筆記。本文主要了解了幾種雙向綁定的做法,然后先用原生JS,dom操作實現(xiàn)一個最簡單雙向綁定,在這個基礎上進行改裝,為減少dom操作,實現(xiàn)簡單的Compile(編譯HTML);接著為了實現(xiàn)數(shù)據(jù)監(jiān)聽,實現(xiàn)observe;最后為了實現(xiàn)數(shù)據(jù)的雙向綁定實現(xiàn)訂閱發(fā)布模式。
雖然實現(xiàn)的比較簡單,有很多功能沒有考慮,不過這個過程還是可以理解到Vue實現(xiàn)雙向綁定的原理。過程中,有思考:
1. Vue的源代碼中,用了文檔碎片fragment作為真實節(jié)點的存儲嗎?
之前有聽說用VDOM,在Vue源代碼中,也找過是否有創(chuàng)建文檔碎片,結果沒找到??戳藚⒖假Y料4中,VDOM的介紹,好像是把節(jié)點用JS對象模擬。類似:
模板
<ul id='list'> <li class='item'>Item 1</li> <li class='item'>Item 2</li> <li class='item'>Item 3</li> </ul>
js對象
var element = {
tagName: 'ul', // 節(jié)點標簽名
props: { // DOM的屬性,用一個對象存儲鍵值對
id: 'list'
},
children: [ // 該節(jié)點的子節(jié)點
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}
恩,這就又牽扯出模板了。先收住,我先盡量把簡單的搞懂。
2.Compile模塊對v-model節(jié)點的解析,事件的綁定,我只實現(xiàn)簡單的,特定的v-model,還有其它事件綁定如v-on等沒有分析,看了別人的代碼,情況一多起來,看得就有些吃力,希望后面自己會再來完善,給自己定一個這樣的框架在這.
代碼:戳這里
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
vue從后臺渲染文章列表以及根據(jù)id跳轉文章詳情詳解
這篇文章主要給大家介紹了關于vue從后臺渲染文章列表以及根據(jù)id跳轉文章詳情的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-12-12
vue路由事件beforeRouteLeave及組件內(nèi)定時器的清除方法
今天小編就為大家分享一篇vue路由事件beforeRouteLeave及組件內(nèi)定時器的清除方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09
原生JS實現(xiàn)Vue transition fade過渡動畫效果示例
這篇文章主要為大家介紹了原生JS實現(xiàn)Vue transition fade過渡動畫效果示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06
Vue中使用v-print打印出現(xiàn)空白頁問題及解決
這篇文章主要介紹了Vue中使用v-print打印出現(xiàn)空白頁問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09
Vue Element-ui實現(xiàn)樹形控件節(jié)點添加圖標詳解
這篇文章主要為大家介紹了Element-ui實現(xiàn)樹形控件節(jié)點添加圖標,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2021-11-11
vue實現(xiàn)分環(huán)境打包步驟(給不同的環(huán)境配置相對應的打包命令)
這篇文章主要介紹了vue實現(xiàn)分環(huán)境打包步驟(給不同的環(huán)境配置相對應的打包命令),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-06-06

