起因
用vue這么久,也了解過它的雙向綁定原理,但是沒有實現過,所以還是實地手寫一個深入理解下。
它是什么
- 單向綁定:Model數據改變,引起View視圖的更新。
- 雙向綁定:Model數據改變,引起View視圖的更新;View視圖的改變,引起Model數據的更新。
雙向綁定,就是在單向綁定的基礎上給可輸入元素(input、textarea 等)添加了change( input )事件,來動態修改model。
總結: 雙向綁定 = 單向綁定 + 事件的監聽
原理分析
單向綁定:
1. 通過Object.defineProperty()來實現對屬性的劫持,達到監聽數據變動的目的。
2. 發布訂閱模式。維護一個數組,用來收集訂閱者,當數據變動時,發布消息給訂閱者。
3. 訂閱者收到通知,觸發相應的監聽回調,更新視圖。事件監聽
給可輸入元素(input、textarea等)添加change(input)事件,來動態修改model。
思考實現步驟
- 新建兩個文件( index.html 和 index.js ),并寫好初始化代碼。
- index.js 里實現雙向綁定:
1. 實現一個監聽器Observer,用來劫持監聽所有屬性,若有變動,就通知訂閱者。
2. 實現一個訂閱者Watcher,每一個Watcher都綁定一個更新函數,Watcher可以收到屬性的變化通知,并執行相應的更新函數,從而更新視圖。
3. 因為訂閱者是有很多個,所以我們需要有一個消息訂閱器Dep來專門收集這些訂閱者。
4. 實現一個解析器Compile,可以掃描解析每個節點的相關指令(v-model,v-on等)。如果節點存在這些指令,則初始化這些節點的模板數據,使之可以顯示在視圖上,然后初始化相應的訂閱者(Watcher)。
5. 事件監聽,改變model數據
開始代碼實現
- 步驟一:新建兩個文件( index.html 和 index.js ),并初始化代碼。
index.html
<!DOCTYPE html>
<html>
<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>實現-vue-雙向綁定</title>
</head>
<body>
<div id="practice">
<h2>實現-vue-雙向綁定</h2>
<div v-text="name"></div>
<div v-text="desc"></div>
<input type="text" v-model="desc" />
</div>
<script src="./index.js"></script>
<script>
new Vue({
el: '#practice',
data: {
name: 'lingzi',
desc: 'i am so cute'
}
})
</script>
</body>
</html>
index.js
class Vue {
constructor(options) {
}
}
- 步驟二:index.js里實現雙向綁定
- 實現一個監聽器Observer,用來劫持監聽所有屬性,若有變動,就通知訂閱者。
class Vue {
constructor(options) {
let data = options.data;
const el = document.querySelector(options.el);
this.Observer(data);
}
// 監聽器
Observer(obj) {
if (!data || typeof data !== 'object') return;
for (const key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get: () => {
return value;
},
set: (newValue) => {
value = newValue;
// TODO 通知訂閱者
}
})
}
}
}
- 實現一個訂閱者Watcher,每一個Watcher都綁定一個更新函數,Watcher可以收到屬性的變化通知,并執行相應的更新函數,從而更新視圖。
class Vue {
... //省略
}
// 訂閱者
class Watcher {
constructor(el, vm, exp, attr) {
this.el = el;
this.vm = vm;
this.exp = exp;
this.attr = attr;
this.update();
}
update() {
this.el[this.attr] = this.vm.data[this.exp]; //更新視圖
}
}
3.因為訂閱者是有很多個,所以我們需要有一個消息訂閱器Dep來專門收集這些訂閱者。
class Vue {
... //省略
Observer(obj) {
if (!obj || typeof obj !== 'object') return;
for (const key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get: () => {
return value;
},
set: (newValue) => {
value = newValue;
// TODO 通知訂閱者
this.dep.notify(); // ++++++++加上這句
}
})
}
}
}
// 訂閱者
class Watcher {
... //省略
}
// 收集訂閱者
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
- 實現一個解析器Compile,可以掃描解析每個節點的相關指令(v-model,v-on等)。如果節點存在這些指令,則初始化這些節點的模板數據,使之可以顯示在視圖上,然后初始化相應的訂閱者(Watcher)。
class Vue {
constructor(options) {
... //省略
this.Observer(this.data);
this.Compile(this.el); // ++++++++++加上這句
}
// 監聽器
Observer(obj) {
... //省略
}
Compile(el) {
const nodes = el.children;
[...nodes].forEach((node, index) => {
if (node.hasAttribute('v-text')) {
let attrVal = node.getAttribute('v-text');
this.dep.addSub(new Watcher(node, this, attrVal, 'innerHTML'));
}
if (node.hasAttribute('v-model')) {
let attrVal = node.getAttribute('v-model');
this.dep.addSub(new Watcher(node, this, attrVal, 'value'));
}
})
}
}
- 事件監聽,改變model數據
Compile(el) {
const nodes = el.children;
[...nodes].forEach((node, index) => {
... //省略
if (node.hasAttribute('v-model')) {
let attrVal = node.getAttribute('v-model');
this.dep.addSub(new Watcher(node, this, attrVal, 'value'));
// ++++++++++++ 加上下面兩句
node.addEventListener('input', () => {
this.data[attrVal] = node.value;
})
}
})
}
結尾發言
實現的簡陋型,供自己理解。
代碼倉庫地址:https://github.com/lingziyb/study-notes/tree/master/vue-two-way-bind