前言
VUE是當(dāng)下比較熱門的一個(gè)前端框架大溜,其顯著特點(diǎn)就是雙向數(shù)據(jù)綁定差油,即data更新view,view更新data,但其具體實(shí)現(xiàn)一直模模糊糊泳炉,這次就來搞個(gè)明明白白憾筏。
核心原理
其核心原理就是數(shù)據(jù)劫持
,數(shù)據(jù)劫持是用Object.definePorperty()
來實(shí)現(xiàn)的,看代碼
let data = {}
let num = 0;
Object.defineProperty(data, 'num', {
enumerable: true,
configurable: true,
get() {
return num;
},
set(v) {
num = v;
}
});
每當(dāng)獲取data['num']
的值時(shí)就會(huì)觸發(fā)它的get方法
花鹅,設(shè)置data['num']
的值時(shí)就會(huì)觸發(fā)它的set方法
踩叭,這樣就實(shí)現(xiàn)了數(shù)據(jù)劫持,一旦數(shù)據(jù)發(fā)生一些改變翠胰,就可以監(jiān)聽到容贝,然后去實(shí)現(xiàn)一些自定義的功能,例如:更新view
第一步之景,確定初始化方式
就模仿VUE的初始化好了斤富,首先我們寫HTML
<div id="app">
<h1>{{info}}</h1>
<input type="text" v-model="info">
<button v-on:click="btnClick">點(diǎn)擊</button>
</div>
接著,對它進(jìn)行初始化
let ymvue = new YMVue({
el: '#app',
data: {
info: 'hello world'
},
methods: {
mounted() {
console.log(this)
},
btnClick() {
this.hello = 'Hello Vue'
}
}
});
第二步锻狗,生成觀察者
VUE用的是觀察者模式满力,觀察者的主要功能就是數(shù)據(jù)劫持焕参,那我們定義一個(gè)觀察者Guard
,當(dāng)我們初始化一個(gè)YMVue
對象之后油额,就把它交給觀察者Guard
叠纷,然后遍歷它的data
集合,并對里面的數(shù)據(jù)進(jìn)行劫持潦嘶。
function Guard(obj) {
this.obj = obj; // YMVue對象
this.start(obj.data);
}
Guard.prototype = {
start(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach((key) => {
this.addGuard(data, key, data[key]);
});
}
},
addGuard(data, key, val) {
let self = this;
this.start(data[key]);
// let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
//if (Dep.target) {
// dep.addSub(Dep.target);
//}
return val;
},
set(v) {
if (val === v) {
return;
}
val = v;
//dep.update();
}
})
}
};
上面代碼中注釋代碼僅作暫時(shí)注釋涩嚣,待下面進(jìn)行說明
第三步,生成訂閱者
如果給觀察者傳入一個(gè)回調(diào)函數(shù)callback
掂僵,那么在觸發(fā)set
方法后執(zhí)行回調(diào)航厚,好像是實(shí)現(xiàn)了所有功能。
但是锰蓬,but,一個(gè)數(shù)據(jù)的更新可能會(huì)觸發(fā)無數(shù)個(gè)方法幔睬,如果通過給觀察者傳入回調(diào),那就必須對同一個(gè)數(shù)據(jù)進(jìn)行多次劫持芹扭,那自然是不得行了麻顶,所以需要個(gè)訂閱者,把數(shù)據(jù)更新后帶來的連鎖反應(yīng)訂閱到這個(gè)數(shù)據(jù)上去舱卡。
function BindCallbackToGuard(obj, name, callback) {
this.obj = obj; // YMVue對象
this.callback = callback; // 回調(diào)函數(shù)
this.name = name; // 訂閱的數(shù)據(jù)
this.value = this.get();
}
BindCallbackToGuard.prototype = {
excute() {
let val = this.obj.data[this.name];
let oldVal = this.value;
if (val !== oldVal) {
this.value = val;
this.callback.call(this.obj, val, oldVal);
}
},
get() {
Dep.target = this; // 把自己設(shè)為訂閱對象
let value = this.obj.data[this.name]; // 觸發(fā)觀察者的get函數(shù)
Dep.target = null;
return value;
}
};
第四步辅肾,創(chuàng)建一個(gè)訂閱者容器
第二步和第三步的代碼中都有一個(gè)Dep
,那Dep
到底是什么呢
Dep
其實(shí)是一個(gè)訂閱者容器灼狰,每當(dāng)有一個(gè)訂閱者訂閱了自己宛瞄,觀察者就把它放進(jìn)訂閱者容器里面,然后當(dāng)觀察者監(jiān)聽到數(shù)據(jù)有變的時(shí)候交胚,就去遍歷訂閱者容器份汗,然后執(zhí)行每個(gè)訂閱者訂閱的方法。
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
},
update() {
this.subs.forEach((sub) => { // sub是一個(gè)訂閱者
sub.excute();
})
}
};
這時(shí)候蝴簇,就可以取消第二步代碼中的注釋了
第五步杯活,生成DOM解析器
上面我們已經(jīng)把功能都搞定了,現(xiàn)在就需要把那些特殊的HTML跟這些代碼聯(lián)系到一起熬词,這時(shí)候就需要一個(gè)DOM解析器旁钧,其中對DOM元素的操作我們用到了文檔碎片Fragment
function Analysis(containerId, obj) {
this.obj = obj; // YMVue對象
this.dom = document.querySelector(containerId);
this.fragment = null;
this.init();
}
Analysis.prototype = {
init() {
if (this.dom) {
this.fragment = this.switchToFragment(this.dom); // 將NODE節(jié)點(diǎn)轉(zhuǎn)換為文檔碎片
this.analysisElement(this.fragment); // 開始解析
this.dom.appendChild(this.fragment); // 用文檔碎片替換node節(jié)點(diǎn)
} else {
console.log('Dom 元素不存在');
}
},
switchToFragment() {
let fragment = document.createDocumentFragment();
let child = this.dom.firstChild;
while (child) {
fragment.append(child);
child = this.dom.firstChild;
}
return fragment;
},
analysisElement(dom) {
let childNodes = dom.childNodes;
[].slice.call(childNodes).forEach((node) => {
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if (this.isElementNode(node)) { // 元素節(jié)點(diǎn)
this.analysisAttr(node);
} else if (this.isTextNode(node) && reg.test(text)) { // 文本節(jié)點(diǎn)
this.bindText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
this.analysisElement(node);
}
});
},
analysisAttr(node) {
let nodeAttrs = node.attributes;
Array.prototype.forEach.call(nodeAttrs, (attr) => {
let name = attr.name; // 屬性名稱
if (this.isCommand(name)) { // 屬性名以'v-'開頭
let value = attr.value; // 屬性值
let command = name.substring(2);
if (this.isEventCommand(command)) { // 事件指令,比如v-on:click
this.bindEvent(node, value, command);
} else { // v-model 指令
this.bindModel(node, value)
}
node.removeAttribute(name); // 移除后互拾,頁面上不顯示
}
})
},
bindText(node, name) {
let text = this.obj[name];
node.textContent = text || '';
// 添加一個(gè)訂閱者
new BindCallbackToGuard(this.obj, name, function (v) {
node.textContent = v;
});
},
bindEvent(node, name, command) {
let eventType = command.split(':')[1];
let method = this.obj.methods && this.obj.methods[name];
if (eventType && method) {
node.addEventListener(eventType, method.bind(this.obj), false);
}
},
bindModel(node, name) {
let self = this;
let text = this.obj[name];
node.value = text || '';
// 添加一個(gè)訂閱者
new BindCallbackToGuard(this.obj, name, function (v) {
node.value = v;
});
// 監(jiān)聽input事件歪今,當(dāng)它的value改變時(shí),同時(shí)更新其綁定值
node.addEventListener('input', function (e) {
let newVal = e.target.value;
if (text === newVal) {
return;
}
self.obj[name] = newVal;
text = newVal;
}, false);
},
isCommand(attr) {
return attr.indexOf('v-') === 0;
},
isEventCommand(dir) {
return dir.indexOf('on:') === 0;
},
isElementNode(node) {
return node.nodeType === 1;
},
isTextNode(node) {
return node.nodeType === 3;
}
};
第六步颜矿,定義初始化類
現(xiàn)在所有的功能基本完成寄猩,就差一個(gè)初始類YMVue
了
function YMVue(opts) {
this.data = opts.data;
this.methods = opts.methods || {};
Object.keys(this.data).forEach((key) => {
this.proxy(key); // 設(shè)置代理
});
new Guard(this); // 初始化觀察者
new Analysis(opts.el, this); // dom解析
this.methods.mounted && this.methods.mounted.call(this); // 初始化完成后,執(zhí)行mounted方法
}
YMVue.prototype = {
proxy(key) {
let self = this;
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return self.data[key];
},
set(v) {
self.data[key] = v;
}
})
}
};
初始化類有一個(gè)代理proxy
骑疆,它有什么用呢田篇?
因?yàn)閂UE改變的數(shù)據(jù)的操作是this.info='xxxx'
替废,但是this
指向的是VUE
對象,info
又在data
集合里泊柬,所以應(yīng)該是this.data.info='xxx'
椎镣,那取消中間的data
就需要用到代理。
結(jié)束
OK兽赁,大功告成状答,雖然功能距離真正的VUE還差的遠(yuǎn),但是簡單的數(shù)據(jù)雙向綁定就這么實(shí)現(xiàn)了闸氮,寫成一個(gè)插件剪况,隨便在哪都可以用起來教沾。
完整代碼在這