不少人在面試過程當中都有問到Vue雙向綁定的原理和實現,這是一個老生常談的面試題了,雖然網上也有不少實現雙向綁定的文章,可是我看後以爲對於大多數前端小白來講,不是很容易理解,因此,這篇文章我就用最簡單的代碼教你們怎麼實現一個Vue的雙向綁定。javascript
用過Vue框架的都知道,頁面在初始化的時候,咱們能夠把data裏的屬性渲染到頁面上,改動頁面上的數據時,data裏的屬性也會相應的更新,這就是咱們所說的雙向綁定,因此,簡單來講,咱們要實現一個雙向綁定要實現如下3點操做:html
v-modle
指令和{{}}
指令,而後把data裏的屬性綁定到相應的指令上,因此咱們要實現一個解析器Compile,這是第一點;Object.defineProperty
中的getter
和 setter
方法對屬性進行劫持,這裏咱們要實現一個監視器Observer,這是二點;Object.defineProperty
數據劫持的時候接收屬性改變通知,更新視圖,因此咱們要實現一個訂閱者Watcher,這是第三點。首先,咱們從最基本的解析指令開始,話很少說,先上代碼:前端
v-model
和
{{}}
指令,可是頁面渲染的時候,咱們在瀏覽器看到的節點是這樣的。
v-model
和
{{}}
開始。 話很少說,上代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>MVVMdemo</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
<div>{{text}}</div>
</div>
</body>
<script type="text/javascript">
var vm = new Vue({
el: 'app',
data: {
text: 'hello world',
}
})
function Vue(options) {
this.data = options.data;
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this) //DocumentFragment(文檔片斷)
document.getElementById(id).appendChild(dom); //將處理好的DocumentFragment從新添加到Dom中
}
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.childNodes && node.childNodes.length) {
node.childNodes.forEach(function (node) {
compile(node, vm)
})
} else {
//解析v-model
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;
node.value = vm.data[name]; //將data裏的值賦給node
node.removeAttribute('v-model'); //移除v-model屬性
}
};
}
//解析{{}}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 獲取匹配到的字符串
name = name.trim();
node.nodeValue = vm[name]
}
}
}
}
</script>
</html>
複製代碼
上面這段代碼就是解析指令的簡單方法,我來簡單解釋一下:vue
document.createDocumentFragment()
document.createDocumentFragment()
至關於一個空的容器, 是用來建立一個虛擬的節點對象,在這裏咱們要作的就是:在遍歷節點的同時對相應指令進行解析,解析完一個指令將其添加到createDocumentFragment
中,解析完後再從新渲染頁面,這樣的好處就是減小頁面渲染dom的次數,詳細內容可參考文檔 createDocumentFragment()用法總結function compile (node, vm)
compile()
方法裏面咱們對每一個節點進行判斷,首先判斷節點是否包含有子節點,有的話繼續調用compile()方法進行解析。沒有的話就判斷節點類型,咱們主要是判斷element元素類型
和文本text元素類型
,而後分別對這兩種類型進行解析。完成了以上步驟後,咱們的代碼就能夠正常顯示在頁面上了, 可是,有一個問題,咱們頁面上綁定了data裏的屬性,可是在改變input框裏的數據的時候,相應的data裏面的數據沒有同步更新。因此,接下來咱們要對數據的更新進行劫持,經過Object.defineProperty()
劫持data裏的對應屬性變化。 java
要實現數據的雙向綁定,咱們須要經過Object.defineProperty()來實現數據劫持,監聽屬性的變化。 因此,接下來咱們先經過一個簡單的例子來了解Object.defineProperty()
的工做原理。node
var obj ={};
var name="hello";
Object.defineProperty(obj,'name',{
get:function(val) {//獲取屬性
console.log('get方法被調用了');
return name
},
set:function(val) { //設置屬性
console.log('set方法被調用了');
name=val
}
})
console.log(obj.name);
obj.name='hello world'
console.log(obj.name);
複製代碼
運行代碼,咱們能夠看到控制檯輸出:面試
Object.defineProperty( )
設置了對象obj的name屬性,對其get和set進行重寫操做,顧名思義,get就是在讀取name屬性這個值觸發的函數,set就是在設置name屬性這個值觸發的函數,關於
Object.defineProperty()
這裏就很少說了,具體能夠參考文檔
defineProperty()使用教程
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>MVVMdemo</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
<div>{{text}}</div>
</div>
</body>
<script type="text/javascript">
var vm = new Vue({
el: 'app',
data: {
text: 'hello world',
}
})
function Vue(options) {
this.data = options.data;
var id = options.el;
observe(this.data,this); //初始化的時候對data裏的全部屬性進行監聽
var dom = nodeToFragment(document.getElementById(id), this) //DocumentFragment(文檔片斷)
document.getElementById(id).appendChild(dom); //將處理好的DocumentFragment從新添加到Dom中
}
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.childNodes && node.childNodes.length) {
node.childNodes.forEach(function (node) {
compile(node, vm)
})
} else {
//解析v-model
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;
node.addEventListener('input',function(e){
vm[name]=e.target.value;
})
node.value= vm[name];//將data裏的值賦給node
node.removeAttribute('v-model'); //移除v-model屬性
}
};
}
//解析{{}}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 獲取匹配到的字符串
name = name.trim();
node.nodeValue = vm[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);//打印(監聽數據的修改)
}
})
}
//地遞歸遍歷全部data屬性
function observe(obj,vm) {
Object.keys(obj).forEach(function(key){
defineReactive(vm,key,obj[key])
})
}
</script>
</html>
複製代碼
咱們在頁面初始化的時候,經過遞歸遍歷data全部子屬性,給每一個屬性添加一個監視器,在監聽到數據變化時候,就會觸發defineProperty( )裏的set方法,咱們能夠在控制檯輸出看到set方法裏監聽到屬性的變化。數組
不少人看過網上的其餘實現MVVM實現的代碼,可是都說對Watcher訂閱者不是很瞭解,其實拋開代碼,Watcher實現的功能其實很簡單,就是當Vue實例化的時候,給每一個屬性注入一個訂閱者Watcher,方便在Object.defineProperty()
數據劫持中監聽屬性的獲取(get方法),在Object.defineProperty()
監聽到數據改變的時候(set方法),經過Watcher通知更新,因此簡單來講,Watcher就是起到一個橋樑的做用。咱們上面已經經過Object.defineProperty()
監聽到數據的改變,接下來咱們經過實現Watcher 來完成雙向綁定的最後一步。瀏覽器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>MVVMdemo</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
<div>{{text}}</div>
</div>
</body>
<script type="text/javascript">
function Vue(options) {
this.data = options.data;
var id = options.el;
observe(this.data, this); //初始化的時候對data裏的全部屬性進行監聽
var dom = nodeToFragment(document.getElementById(id), this) //DocumentFragment(文檔片斷)
document.getElementById(id).appendChild(dom); //將處理好的DocumentFragment從新添加到Dom中
}
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.childNodes && node.childNodes.length) {
node.childNodes.forEach(function (node) {
compile(node, vm)
})
} else {
//解析v-model
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;
node.addEventListener('input', function (e) {
vm[name] = e.target.value;
});
node.value = vm[name];//將data裏的值賦給node
node.removeAttribute('v-model'); //移除v-model屬性
}
};
new Watcher(vm, node, name, 'input');//生成一個新的Watcher,標記爲input
}
//解析{{}}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 獲取匹配到的字符串
name = name.trim();
new Watcher(vm, node, name, 'text');//生成一個新的Watcher,標記爲文本text
}
}
}
}
//地遞歸遍歷全部data屬性
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
val = newVal;
// 做爲發佈者發出通知
dep.notify();
}
});
}
//將全部初始化的生成的訂閱者都收集到一個數組中
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub)
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
})
}
}
//訂閱者Watcher
function Watcher(vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
//執行對應的更新函數
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
// 獲取 data 中的屬性值
get: function () {
this.value = this.vm[this.name]; // 觸發相應屬性的 get
}
}
</script>
<script type="text/javascript">
var vm = new Vue({
el: 'app',
data: {
text: 'hello world',
}
})
</script>
</html>
複製代碼
咱們在第二步的代碼基礎上,加了一個訂閱者Watcher和一個消息收集器Dep,接下來我就跟你們說說他們都作了什麼。 首先:bash
function Watcher(vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
//執行對應的更新函數
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
// 獲取 data 中的屬性值
get: function () {
this.value = this.vm[this.name]; // 觸發相應屬性的 get
}
}
複製代碼
Watcher()方法接收的參數爲vm實例,node節點對象,name傳入的節點類型的名稱,nodeType節點類型。
首先,將本身賦給了一個全局變量 Dep.target;
其次,執行了 update 方法,進而執行了 get 方法,get 的方法讀取了 vm 的訪問器屬性,從而觸發了訪問器屬性的 get 方法,get 方法中將該 watcher 添加到了對應訪問器屬性的 dep 中;
再次,獲取屬性的值,而後更新視圖。
最後,將 Dep.target 設爲空。由於它是全局變量,也是 watcher 與 dep 關聯的惟一橋樑,任什麼時候刻都必須保證 Dep.target 只有一個值。
在實例化的時候,咱們針對每一個屬性都添加一個Watcher()訂閱者,在observe()的監聽屬性賦值的時候,將每一個屬性綁定的訂閱者存儲在Dep數組中,在set方法觸發的時候,調用dep.notify()方法通知Watcher()更新數據,最後實現了視圖的更新。
以上就是Vue雙向綁定的基本實現原理及代碼,固然,這只是基本的實現代碼,簡單直觀的展示給你們看,若是你們想更深刻了解的話,推薦你們去閱讀這篇文章 vue的雙向綁定原理及實現 。
好啦,以上就是本次的分享,但願對你們理解Vue雙向綁定的理解有所幫助,也但願你們有什麼不懂或者建議,能夠留言互動。