在掘金上看到了Vue雙向綁定的簡易實現(xiàn)梗顺,由于之前個人看的迷迷糊糊蹲嚣,特地抽出時間完全理解匕荸,于是有了這個注釋版代碼爹谭,全都是個人理解,有不當(dāng)之處可以發(fā)出來提醒我改正榛搔,覺得好的話也可以評論鼓勵诺凡。
HTML
<!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>Document</title>
</head>
<body>
<div id='app'>
<input type="text" id="test">
<span>{{testVal}}</span>
</div>
<script src='./vue.js'></script>
</body>
</html>
JS代碼
// 觀察者生成器
class Observer {
constructor(data) {
// 對傳入的數(shù)據(jù)進(jìn)行存儲
this.data = data;
// 對數(shù)據(jù)中的屬性進(jìn)行遍歷
Object.keys(data).forEach(key => {
// 獲取當(dāng)前key對應(yīng)的值
let value = data[key];
// 對要被觀察的key對應(yīng)的value創(chuàng)建一個訂閱者合集
let dep = new Dep();
// 對每個屬性都進(jìn)行一次“劫持”或者“代理”
Object.defineProperty(data, key, {
get() {
// 當(dāng)有訂閱者使用該屬性的時候东揣,那么訂閱者合集中則添加該訂閱者
Dep.target && dep.add(Dep.target);
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
//當(dāng)數(shù)據(jù)更新時,通知當(dāng)前屬性訂閱者合集中的所有訂閱者
dep.notify(newVal);
}
}
});
});
}
}
// 訂閱者生成器
class Watcher {
constructor(data, key, cb) {
// 初始化訂閱者初始數(shù)據(jù)
this.data = data;
// 初始化訂閱者回調(diào)函數(shù)
this.cb = cb;
// 初始化訂閱者key
this.key = key;
// 在新建一個訂閱者時腹泌,默認(rèn)訂閱者合集生成器的作用主體為當(dāng)前訂閱者
// 更深層次理解為嘶卧,所有待雙向綁定的數(shù)據(jù)都需要依次進(jìn)行綁定,訂閱者合集生成器的作用主體就是Dep.target
// 在創(chuàng)建訂閱者的時候當(dāng)前訂閱者就是主體凉袱,作用完畢后則訂閱者合集生成器的作用主體Dep.target置空為null
Dep.target = this;
// 該賦值屬性隱含了前面觀察者生成器Observer中已經(jīng)“劫持”的get函數(shù)芥吟,通過對該data某一鍵對應(yīng)的值的讀取
// 觸發(fā)了其get函數(shù),而get函數(shù)已經(jīng)在觀察者生成器中被“劫持”专甩,訂閱者生成器正在新建一個訂閱者钟鸵,那么訂閱者合集生成器的作用主體就是當(dāng)前訂閱者
// 滿足Dep.target && dep.add(Dep.target);那么當(dāng)前屬性對應(yīng)的訂閱者合集中就添加了當(dāng)前訂閱者,至此數(shù)據(jù)與訂閱者完成綁定
this.value = data[key];
// 綁定完成配深,訂閱者合集生成器的作用主體置空null
Dep.target = null;
}
update(newVal) {
let oldVal = this.value;
if (newVal !== oldVal) {
this.value = newVal;
// 當(dāng)數(shù)據(jù)更新時携添,通知訂閱者的回調(diào)函數(shù),通知訂閱者數(shù)據(jù)更新
this.cb(newVal, oldVal);
}
}
}
// 訂閱者合集生成器
class Dep {
constructor() {
// 初始化訂閱者合集篓叶,初始化時暫未有訂閱者烈掠,所以空數(shù)組
this.subs = [];
}
add(sub) {
// 訂閱者合集數(shù)組中添加訂閱者 ps:此處是否應(yīng)該去重存疑
this.subs.push(sub);
}
notify(newVal) {
// 當(dāng)前訂閱者合集對應(yīng)的數(shù)據(jù)(特定鍵對應(yīng)的數(shù)據(jù))更新的時候,調(diào)用當(dāng)前訂閱者合集中所有訂閱者的update函數(shù)
this.subs.forEach(sub => {
sub.update(newVal);
});
}
}
// 這里是出于方便考慮缸托,直接將訂閱者合集生成器的作用主題默認(rèn)綁定在訂閱者合集生成器上左敌,其實等價于定義一個全局變量DepTarget
Dep.target = null;
// 片段解析器
class Compile {
constructor(vm) {
// 對數(shù)據(jù)模型進(jìn)行綁定
this.vm = vm;
// 查找容器元素
let el = document.querySelector(this.vm.$el);
// 創(chuàng)建一個空白文檔片段作為父容器
let fragment = document.createDocumentFragment();
if (el) {
// 如果元素存在則放入空白文檔片段中等待編譯
while (el.firstChild) {
fragment.appendChild(el.firstChild);
}
// 編譯片段
this.compileElement(fragment);
//將編譯好的模板掛在在容器上
el.appendChild(fragment);
} else {
console.log('掛載元素不存在!');
}
}
compileElement(el) {
for (let node of el.childNodes) {
/*
node.nodeType
1:元素節(jié)點
3:文本節(jié)點
*/
if (node.nodeType === 1) {
for (let attr of node.attributes) {
let {
name: attrName, // ES6的賦值方法俐镐,提取屬性名和屬性值
value: exp
} = attr;
// v- 代表存在指令
if (attrName.indexOf('v-') === 0) {
/*
<div v-xxx=""> 元素上矫限,可以用很多指令,這里僅做學(xué)習(xí)佩抹,所以不判斷太多了
on 事件綁定
model 表單綁定
*/
let [dir, value] = attrName.substring(2).split(':');
if (dir === 'on') {
// 取 vm.methods 相應(yīng)的方法叼风,進(jìn)行綁定
let fn = this.vm.$methods[exp];
fn && node.addEventListener(value, fn.bind(this.vm), false);
} else if (dir === 'model') {
// 取 vm.data 進(jìn)行 input 的賦值,并且在 input 的時候更新 vm.data 上的值
let value = this.vm.$data[exp];
node.value = typeof value === 'undefined' ? '' : value;
// 綁定元素數(shù)據(jù)與數(shù)據(jù)模型上的數(shù)據(jù)
node.addEventListener('input', e => {
if (e.target.value !== value) {
this.vm.$data[exp] = e.target.value;
}
});
// 生成一個對于某個key相關(guān)聯(lián)的觀察者
// 對數(shù)據(jù)模型上的數(shù)據(jù)進(jìn)行觀察棍苹,回調(diào)函數(shù)就是將元素的value和數(shù)據(jù)模型上的value進(jìn)行同步更新
new Watcher(this.vm.$data, exp, newVal => {
node.value = typeof newVal === 'undefined' ? '' : newVal;
});
}
}
}
} else if (node.nodeType === 3) {
let reg = /\{\{(.*)\}\}/;
if (reg.test(node.textContent)) {
// 這里文本里也許會有多個 {{}} 无宿,{{}} 內(nèi)或許會有表達(dá)式,這里簡單處理枢里,就取一個值
let exp = reg.exec(node.textContent)[1].trim();
let value = this.vm.$data[exp];
node.textContent = typeof value === 'undefined' ? '' : value;
// 生成一個觀察者孽鸡,對data上的exp進(jìn)行訂閱,當(dāng)vm.$data[exp]更新時栏豺,則會同步更新文本節(jié)點的內(nèi)容
new Watcher(this.vm.$data, exp, newVal => {
node.textContent = typeof newVal === 'undefined' ? '' : newVal;
});
}
}
// 遞歸編譯模板
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
}
}
}
class MyVue {
constructor({
// 初始化Vue的容器彬碱,數(shù)據(jù)和方法,可以自行添加鉤子函數(shù)
el,
data,
methods
}) {
// 這里相當(dāng)于init奥洼,對Vue模型進(jìn)行數(shù)據(jù)綁定
let obs = new Observer(data);
this.$el = el;
this.$data = obs.data;
this.$methods = methods;
// 對數(shù)據(jù)進(jìn)行隔離巷疼,防止用戶直接操作數(shù)據(jù),目的在于用戶直接更新數(shù)據(jù)可能造成無法渲染頁面等一系列問題
Object.keys(this.$data).forEach(i => {
this.proxyKeys(i);
});
new Compile(this);
}
proxyKeys(key) {
let _this = this;
Object.defineProperty(_this, key, {
enumerable: false,
configurable: true,
get() {
return _this.$data[key];
},
set(newVal) {
_this.$data[key] = newVal;
}
});
}
}
let app = new MyVue({
el: '#app',
data: {
test: 123,
testVal:'測試數(shù)據(jù)'
},
methods: {
test() {
console.log('test function');
}
}
})
/*作者:卡布奇諾_Me
鏈接:https://juejin.im/post/5d15cf9ef265da1bbc6fe901
來源:掘金
*/