好多人看完我的這個文章對它的理解還是只是知道了大概原理,但是對具體的Vue雙向綁定的實現(xiàn)很模糊,因此就出了這篇文章,供大家參考希望可以得到收獲亦歉,以下是主要代碼邏輯,先陳述一下這一過程都需要什么:
需要有一個接收Vue實例配置項的構造函數(shù)SimpleVue畅哑,給他加兩個原型方法分別是observe()和compile()肴楷,再構造出一個訂閱器watcher,給他加一個更新視圖方法
- observe():用來劫持并監(jiān)聽數(shù)據(jù)變化的數(shù)據(jù)監(jiān)聽器荠呐,有變化就會通知下文中的訂閱器watcher
- compile():節(jié)點DOM解析器赛蔫,用來獲取和解析每一個節(jié)點及其指令,根據(jù)初始化的模板數(shù)據(jù)來創(chuàng)建訂閱器watcher
- watcher():訂閱器watcher泥张,用來接收屬性值的相關數(shù)據(jù)的變化通知濒募,調(diào)用自身原型方法update從而更新視圖
由于Vue就是一個MVVM的框架理念,所以就要通過Object.defineProperty()
方法來劫持并監(jiān)聽所有屬性值相關的數(shù)據(jù)圾结,看看它是否變化,如有變化則通知訂閱器watcher看是否需要視圖更新齿诉,這一過程就是我們的數(shù)據(jù)監(jiān)聽器observe
的工作任務筝野,由于數(shù)據(jù)和訂閱器是一對多的關系,所以通知訂閱器的時候需要把數(shù)據(jù)對應的訂閱器的集合都放在一個oWatcherObj
對象中粤剧,接下來需要一個節(jié)點DOM解析器compile
歇竟,主要用來迭代遞歸獲取和解析每一個節(jié)點及其指令,根據(jù)初始化的模板數(shù)據(jù)來創(chuàng)建訂閱器watcher
抵恋,實例化watcher就會接到數(shù)據(jù)變化的通知焕议,進而實現(xiàn)VM更新視圖
template:
<div id="simpleVue">
<button yf-on:click="copy">戳我</button>
<div>
<textarea yf-model="name"></textarea>
<div yf-text="name"></div>
</div>
<hr>
<button yf-on:click="show">顯示/隱藏</button>
<div yf-if="isShow">
<input type="text" yf-model="webSite">
<div yf-text="webSite"></div>
</div>
</div>
SimpleVue構造:
class SimpleVue { // 簡化版Vue實例的構造 用來接收實例的配置項
constructor(options) {
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.oWatcherObj = {}; // 所有屬性值相關的數(shù)據(jù)對應的訂閱器的集合都放在該對象中
this.observe(); // 調(diào)用數(shù)據(jù)監(jiān)聽器對屬性值相關的數(shù)據(jù)進行劫持并監(jiān)聽
this.compile(this.$el); // 對該DOM節(jié)點進行解析
}
observe() { // 數(shù)據(jù)監(jiān)聽器 用來劫持并監(jiān)聽屬性值相關數(shù)據(jù)的變化 如有變化則通知訂閱器watcher
for (let key in this.$data) {
let value = this.$data[key];
this.oWatcherObj[key] = []; // 初始化該數(shù)據(jù)的訂閱器 數(shù)據(jù)和訂閱器的關系是一對多
let oWatcherObj = this.oWatcherObj[key];
Object.defineProperty(this.$data, key, { // 關鍵方法 可以修改對象身上的默認屬性值的ES5方法 下面用到的是ES中兩大屬性中的訪問器屬性,有以下四種描述符對象
configurable: false, // 該狀態(tài)下的屬性描述符不能被修改和刪除
enumerable: false, // 該狀態(tài)下的屬性描述符中的屬性不可被枚舉
get() { // 屬性值相關的數(shù)據(jù)讀取函數(shù)
return value;
},
set(newVal) { // 屬性值相關的數(shù)據(jù)寫入函數(shù)
if (newVal !== value) {
value = newVal;
oWatcherObj.forEach((obj) => {
obj.update(); // 通知和該數(shù)據(jù)相關的所有訂閱器
});
}
}
});
}
}
compile(el) { // 節(jié)點DOM解析器 用來獲取和解析每一個節(jié)點及其指令 根據(jù)初始化的模板數(shù)據(jù)來創(chuàng)建訂閱器watcher
let nodes = el.children;
for (let i = 0; i < nodes.length; i++) { // 迭代同級所有節(jié)點
let node = nodes[i];
if (node.children.length > 0) {
this.compile(node); // 遞歸所有子節(jié)點
}
if (node.hasAttribute('yf-on:click')) { // 節(jié)點中如存在該指令則執(zhí)行以下操作
let eventAttrVal = node.getAttribute('yf-on:click');
node.addEventListener('click', this.$methods[eventAttrVal].bind(this.$data)); // 綁定獲取到的指令對應的數(shù)據(jù)所觸發(fā)的方法
}
if (node.hasAttribute('yf-if')) {
let ifAttrVal = node.getAttribute('yf-if');
this.oWatcherObj[ifAttrVal].push(new Watcher(this, node, "", ifAttrVal)); // 給該指令對應的數(shù)據(jù)創(chuàng)建訂閱器放在該數(shù)據(jù)對應的訂閱器數(shù)組里
}
if (node.hasAttribute('yf-model')) {
let modelAttrVal = node.getAttribute('yf-model');
node.addEventListener('input', ((i) => { // 前方高能:此處有閉包請繞行!!! i的問題
this.oWatcherObj[modelAttrVal].push(new Watcher(this, node, "value", modelAttrVal));
return () => {
this.$data[modelAttrVal] = nodes[i].value; // 將該指令所在節(jié)點的值扔給該指令的數(shù)據(jù)
}
})(i));
}
if (node.hasAttribute('yf-text')) {
let textAttrVal = node.getAttribute('yf-text');
this.oWatcherObj[textAttrVal].push(new Watcher(this, node, "innerText", textAttrVal));
}
}
}
}
訂閱器構造:
class Watcher { // 訂閱器構造 用來接收屬性值的相關數(shù)據(jù)的變化通知 從而更新視圖
constructor(...arg) {
this.vm = arg[0];
this.el = arg[1];
this.attr = arg[2];
this.val = arg[3];
this.update(); // 初始化訂閱器時更新一下視圖
}
update() { // 將收到的新的數(shù)據(jù)更新在視圖中從而實現(xiàn)真正的VM
if (this.vm.$data[this.val] === true) {
this.el.style.display = 'block';
} else if (this.vm.$data[this.val] === false) {
this.el.style.display = 'none';
} else {
this.el[this.attr] = this.vm.$data[this.val];
}
}
}
Shortcuts
希望大家閱讀完本文可以有所收獲,因為能力有限弧关,掌握的知識也是不夠全面盅安,歡迎大家提出來一起分享唤锉!謝謝O(∩_∩)O~
歡迎訪問我的GitHub,喜歡的可以star别瞭,項目隨意fork窿祥,支持轉(zhuǎn)載但要下標注,同時恭候:個人博客