一個簡化版Vue助你理解Vue原理(vue 原理)
一個簡化版Vue助你理解Vue原理(vue 原理)
好多人看完我的這個文章對它的理解還是只是知道了大概原理,但是對具體的Vue雙向綁定的實現(xiàn)很模糊,因此就出了這篇文章,供大家參考希望可以得到收獲,以下是主要代碼邏輯,先陳述一下這一過程都需要什么:
需要有一個接收Vue實例配置項的構(gòu)造函數(shù)SimpleVue,給他加兩個原型方法分別是observe()和compile(),再構(gòu)造出一個訂閱器watcher,給他加一個更新視圖方法
- observe():用來劫持并監(jiān)聽數(shù)據(jù)變化的數(shù)據(jù)監(jiān)聽器,有變化就會通知下文中的訂閱器watcher
- compile():節(jié)點DOM解析器,用來獲取和解析每一個節(jié)點及其指令,根據(jù)初始化的模板數(shù)據(jù)來創(chuàng)建訂閱器watcher
- watcher():訂閱器watcher,用來接收屬性值的相關(guān)數(shù)據(jù)的變化通知,調(diào)用自身原型方法update從而更新視圖
由于Vue就是一個MVVM的框架理念,所以就要通過Object.defineProperty()方法來劫持并監(jiān)聽所有屬性值相關(guān)的數(shù)據(jù),看看它是否變化,如有變化則通知訂閱器watcher看是否需要視圖更新,這一過程就是我們的數(shù)據(jù)監(jiān)聽器observe的工作任務(wù),由于數(shù)據(jù)和訂閱器是一對多的關(guān)系,所以通知訂閱器的時候需要把數(shù)據(jù)對應(yīng)的訂閱器的集合都放在一個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構(gòu)造:
class SimpleVue { // 簡化版Vue實例的構(gòu)造 用來接收實例的配置項
constructor(options) {
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.oWatcherObj = {}; // 所有屬性值相關(guān)的數(shù)據(jù)對應(yīng)的訂閱器的集合都放在該對象中
this.observe(); // 調(diào)用數(shù)據(jù)監(jiān)聽器對屬性值相關(guān)的數(shù)據(jù)進行劫持并監(jiān)聽
this.compile(this.$el); // 對該DOM節(jié)點進行解析
}
observe() { // 數(shù)據(jù)監(jiān)聽器 用來劫持并監(jiān)聽屬性值相關(guān)數(shù)據(jù)的變化 如有變化則通知訂閱器watcher
for (let key in this.$data) {
let value = this.$data[key];
this.oWatcherObj[key] = []; // 初始化該數(shù)據(jù)的訂閱器 數(shù)據(jù)和訂閱器的關(guān)系是一對多
let oWatcherObj = this.oWatcherObj[key];
Object.defineProperty(this.$data, key, { // 關(guān)鍵方法 可以修改對象身上的默認屬性值的ES5方法 下面用到的是ES中兩大屬性中的訪問器屬性,有以下四種描述符對象
configurable: false, // 該狀態(tài)下的屬性描述符不能被修改和刪除
enumerable: false, // 該狀態(tài)下的屬性描述符中的屬性不可被枚舉
get() { // 屬性值相關(guān)的數(shù)據(jù)讀取函數(shù)
return value;
},
set(newVal) { // 屬性值相關(guān)的數(shù)據(jù)寫入函數(shù)
if (newVal !== value) {
value = newVal;
oWatcherObj.forEach((obj) => {
obj.update(); // 通知和該數(shù)據(jù)相關(guān)的所有訂閱器
});
}
}
});
}
}
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)); // 綁定獲取到的指令對應(yīng)的數(shù)據(jù)所觸發(fā)的方法
}
if (node.hasAttribute(\’yf-if\’)) {
let ifAttrVal = node.getAttribute(\’yf-if\’);
this.oWatcherObj[ifAttrVal].push(new Watcher(this, node, \”\”, ifAttrVal)); // 給該指令對應(yīng)的數(shù)據(jù)創(chuàng)建訂閱器放在該數(shù)據(jù)對應(yīng)的訂閱器數(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));
}
}
}
}
訂閱器構(gòu)造:
class Watcher { // 訂閱器構(gòu)造 用來接收屬性值的相關(guān)數(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
- 戳我運行代碼
- Edit on GitHub
- Try in JSFiddle
希望大家閱讀完本文可以有所收獲,因為能力有限,掌握的知識也是不夠全面,歡迎大家提出來一起分享!謝謝O(∩_∩)O~