Vue雙向綁定原理:Vue內(nèi)部通過 Object.defineProperty方法以屬性攔截的方式俺驶,把data對象的每個(gè)數(shù)據(jù)的讀寫轉(zhuǎn)化為getter / setter,當(dāng)數(shù)據(jù)變化時(shí)通知視圖更新。
一赞赖、MVVM數(shù)據(jù)雙向綁定
MVVM數(shù)據(jù)雙向綁定主要是指:數(shù)據(jù)變化更新視圖,視圖變化更新數(shù)據(jù)。
image.png
即:
- 輸入框內(nèi)容變化時(shí)界睁,Data 中的數(shù)據(jù)同步變化。View 導(dǎo)致 Data 的變化兵拢。(通過事件監(jiān)聽的方式來實(shí)現(xiàn))
- Data 中的數(shù)據(jù)變化時(shí)翻斟,文本節(jié)點(diǎn)的內(nèi)容同步變化。Data 導(dǎo)致 View 的變化说铃。(通過操作DOM實(shí)現(xiàn))
監(jiān)聽器 Observer 只要是讓對象變的 "可觀測"访惜,即每次讀寫數(shù)據(jù)時(shí),我們能感知到數(shù)據(jù)被讀取了或數(shù)據(jù)被改寫了腻扇。Vue2.0源碼中用到Object.defineProperty()來劫持各個(gè)數(shù)據(jù)屬性的setter / getter债热。關(guān)于Object.defineProperty 方法,在 MDN 上是這么定義的:
Object.defineProperty() 方法會直接在一個(gè)對象上定義一個(gè)新屬性幼苛,或者修改一個(gè)對象的現(xiàn)有屬性窒篱,并返回這個(gè)對象。
二、Object.defineProperty() 語法
Object.defineProperty(obj, prop, descriptor)
參數(shù):
- obj: 要在其上定義屬性的對象墙杯。
- prop:要定義或修改的屬性的名稱配并。
- descriptor:將被定義或修改的屬性描述符。
返回值:被傳遞給函數(shù)的對象高镐。
屬性描述符:
Object.defineProperty() 為對象定義屬性溉旋,分為數(shù)據(jù)描述符和存取描述符,兩種形式不能混用嫉髓。
數(shù)據(jù)描述符和存取描述符均具有以下可選鍵值:
- configurable:一個(gè)總開關(guān)观腊,一旦將它設(shè)為false,就不能刪除或重新設(shè)置defineProperty監(jiān)聽的屬性算行。為true時(shí)可以進(jìn)行刪除或重新使用defineProperty設(shè)置新值梧油。默認(rèn)為false。
- enumerable:當(dāng)屬性值為true時(shí)纱意,該屬性才能出現(xiàn)在對象枚舉的屬性中婶溯。默認(rèn)為false。
數(shù)據(jù)描述符具有以下可選鍵值:
- value:該屬性對應(yīng)的值偷霉,可以為任意有效的 JavaScript值(數(shù)值迄委、對象、函數(shù)等)类少。默認(rèn) undefined叙身。
- writable:設(shè)置屬性值是否允許被賦值運(yùn)算符改變。true為允許硫狞,false為不允許被重寫信轿。默認(rèn)false。
存取描述符具有以下可選鍵值:
- get:用于給屬性提供 getter 方法残吩,當(dāng)訪問該屬性時(shí)财忽,該方法會被執(zhí)行,執(zhí)行時(shí)不需要傳入?yún)?shù)泣侮,但可以拿到this對象即彪。默認(rèn)為undefined。
- set:用于給屬性提供 setter 方法活尊,當(dāng)屬性修改時(shí)隶校,該方法會被執(zhí)行。該方法接收唯一參數(shù)蛹锰,即該屬性新的參數(shù)值深胳。默認(rèn)為undefined。
通過 Object.defineProperty() 實(shí)現(xiàn)一個(gè)簡單的輸入框雙向綁定:
<!DOCTYPE html>
<html lang="zh">
<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>defineProperty實(shí)現(xiàn)綁定</title>
<script type="text/javascript" src="./example.js"></script>
</head>
<body>
<div id="myApp">
<input type="text" id="myInput" />
<div id="myDiv"></div>
</div>
</body>
<script type="text/javascript">
var myInput = document.getElementById('myInput');
var myDiv = document.getElementById('myDiv');
let obj = {
value: '監(jiān)聽數(shù)據(jù)'
}
// 將初始化數(shù)據(jù)賦值給元素
myInput.value = obj.value;
myDiv.innerHTML = obj.value;
Object.defineProperty(obj, 'value', {
set(newVal) {
// 監(jiān)聽對象屬性值改變,更新div元素innerHTML屬性
myDiv.innerHTML = newVal;
}
})
myInput.oninput = function(e) {
// 更新對象值,來觸發(fā)Object.defineProperty的set方法
obj.value = e.target.value;
}
</script>
</html>
要了解Vue雙向綁定原理铜犬,首先要明白三個(gè)概念:
1舞终、觀察者( observer ):數(shù)據(jù)監(jiān)聽器轻庆,負(fù)責(zé)對數(shù)據(jù)對象的所有屬性進(jìn)行監(jiān)聽劫持,并將消息發(fā)送給訂閱者進(jìn)行數(shù)據(jù)更新权埠。
2榨了、訂閱者( watcher ):負(fù)責(zé)接收數(shù)據(jù)的變化,并執(zhí)行更新視圖(view)攘蔽。數(shù)據(jù)與訂閱者是一對多的關(guān)系。
3呐粘、解析器( compile ):負(fù)責(zé)對你的每個(gè)節(jié)點(diǎn)元素指令進(jìn)行掃描和解析满俗,負(fù)責(zé)相關(guān)指令的數(shù)據(jù)綁定初始化及創(chuàng)造數(shù)據(jù)對應(yīng)的訂閱者(每個(gè)通過指令綁定該屬性數(shù)據(jù)的元素都是一個(gè)訂閱者)。
html:
<!DOCTYPE html>
<html lang="zh">
<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>雙向綁定</title>
<script type="text/javascript" src="./example.js"></script>
</head>
<body>
<div id="myApp">
<input type="button" value="加個(gè)!" z-on:click="fun1" />
<input type="button" value="加個(gè)作岖?" @click="fun2" />
<input type="text" style="width:400px" z-model="site">
<p z-html="site"></p>
<p z-text="site"></p>
</div>
</body>
<script type="text/javascript">
var vm = new Example({
el: '#myApp',
data: {
site: 'Vue雙向綁定原理',
age: 12,
sex: '男'
},
methods: {
fun1() {
this.site += '!'
},
fun2() {
this.site += '?'
}
}
})
</script>
</html>
example.js雙向綁定代碼:
function Example(options) { // 創(chuàng)建構(gòu)造函數(shù)Example,并接收對象結(jié)構(gòu)體options
this.$el = document.querySelector(options.el); // 獲取指定掛載的元素
this.$data = options.data; // 將數(shù)據(jù)掛載到實(shí)例
this.$methods = options.methods; // 存放對象的方法
this.binding = {}; // 所有與數(shù)據(jù)相關(guān)的訂閱者對象都存放于此,$data下每個(gè)數(shù)據(jù)對應(yīng)一個(gè)數(shù)組,用于對應(yīng)多個(gè)訂閱者
this.observer(); // 調(diào)用觀察者,對數(shù)據(jù)進(jìn)行劫持
this.compile(this.$el); // 對元素上綁定的指令如(v-model)進(jìn)行解析,并創(chuàng)建訂閱者.(所有綁定$data下該屬性的元素都將成為該屬性數(shù)據(jù)的訂閱者)
}
// 觀察者
Example.prototype.observer = function() {
if (!this.$data || typeof this.$data !== 'object') return;
var value = ''; // 記錄$data每個(gè)屬性的屬性值
for (var key in this.$data) { // 遍歷數(shù)據(jù)對象
value = this.$data[key]; // 對象屬性值
this.binding[key] = []; // 初始化數(shù)據(jù)訂閱者,一對多關(guān)系,為一個(gè)數(shù)組
var binding = this.binding[key]; // 存放當(dāng)前數(shù)據(jù)相關(guān)的所有訂閱者
// 開始監(jiān)聽劫持
this.defineReactive(this.$data, key, value, binding); // 通過創(chuàng)建方法實(shí)現(xiàn)數(shù)據(jù)分離,私有化,實(shí)現(xiàn)閉包
}
}
Example.prototype.defineReactive = function (data, key, value, binding) {
Object.defineProperty(data, key, {
get() {
return value; // 返回當(dāng)前值
},
set(newVal) { // newVal 為設(shè)置修改后的新值
if (newVal !== value) {
value = newVal; // 更新數(shù)據(jù)
// 以后該屬性數(shù)據(jù)值改變后都會執(zhí)行一次數(shù)據(jù)更新
binding.forEach(watcher => {
watcher.update(); // 通知與本數(shù)據(jù)相關(guān)的訂閱者們(即綁定該數(shù)據(jù)的DOM元素)進(jìn)行視圖更新
})
}
}
})
}
// 解析器 (解析指令并創(chuàng)建訂閱者)
Example.prototype.compile = function(el) {
var nodes = el.children; // 獲取所有子節(jié)點(diǎn)(元素節(jié)點(diǎn))
for (var i = 0; i < nodes.length; i ++) { // 遍歷子節(jié)點(diǎn)
var node = nodes[i]; // 具體節(jié)點(diǎn)
if (node.children.length > 0) { // 判斷是否具有子節(jié)點(diǎn)
this.compile(node); // 遞歸
}
if (node.hasAttribute("z-on:click")) { // 該節(jié)點(diǎn)是否擁有 z-on:click 指令
var attrVal = node.getAttribute('z-on:click'); // 獲取指令對應(yīng)的方法名
// 為元素綁定click事件,事件方法為$methods下的方法,并將this指向this.$data
node.addEventListener('click', this.$methods[attrVal].bind(this.$data));
}
if (node.hasAttribute("@click")) { // 該節(jié)點(diǎn)是否擁有@click指令
var attrVal = node.getAttribute('@click'); // 獲取指令對應(yīng)的方法名
// 為元素綁定click事件,事件方法為$methods下的方法,并將this指向this.$data
node.addEventListener('click', this.$methods[attrVal].bind(this.$data));
}
if (node.hasAttribute("z-model")) { // 該節(jié)點(diǎn)是否擁有z-model指令
var attrVal = node.getAttribute('z-model'); // 獲取指令對應(yīng)的數(shù)據(jù)屬性
node.addEventListener("input", ((i) => { // 為指令添加input事件
this.binding[attrVal].push(new Watcher(node, "value", this, attrVal)); // 將該元素添加為當(dāng)前數(shù)據(jù)的訂閱者唆垃,并將數(shù)據(jù)初始值作用與綁定指令的元素上
return () => { // input事件處理函數(shù)
this.$data[attrVal] = nodes[i].value; // 更新$data的屬性值,會在觀察者中劫持
}
})(i));
}
if (node.hasAttribute("z-html")) { // 該節(jié)點(diǎn)是否擁有z-html指令
var attrVal = node.getAttribute('z-html'); // 獲取指令對應(yīng)的數(shù)據(jù)屬性
this.binding[attrVal].push(new Watcher(node, 'innerHTML', this, attrVal));
}
if (node.hasAttribute('z-text')) { // 該節(jié)點(diǎn)是否用擁有z-text指令
var attrVal = node.getAttribute('z-text'); // 獲取指令對應(yīng)的數(shù)據(jù)屬性
this.binding[attrVal].push(new Watcher(node, 'innerText', this, attrVal));
}
}
}
// 訂閱者
function Watcher(el, attr, vm, val) {
this.el = el; // 指令對應(yīng)的元素
this.attr = attr; // 要更改的元素屬性
this.vm = vm; // 指令所在實(shí)例
this.val = val; // 指令綁定的值
this.update(); // 更新視圖view
}
// 數(shù)據(jù)變化,更新視圖痘儡。
Watcher.prototype.update = function() {
this.el[this.attr] = this.vm.$data[this.val];
}