Vue是現(xiàn)在前端非常流行的一個(gè)前端框架了矛辕,了解它的實(shí)現(xiàn)原理現(xiàn)在基本已經(jīng)快成為前端開發(fā)一個(gè)必備的基本功了呢铆,這篇文章將嘗試寫一個(gè)簡(jiǎn)單的Vue框架稚伍。
Vue數(shù)據(jù)監(jiān)聽(tīng)架構(gòu)
Vue主要架構(gòu)分為三個(gè)部分Compile
间校、Observer
和Watcher
結(jié)構(gòu)圖如下:
Obserer負(fù)責(zé)監(jiān)聽(tīng)Vue中的數(shù)據(jù)枯途,Compile負(fù)責(zé)Vue中涉及dom節(jié)點(diǎn)的渲染碌宴,Compile和Observer通過(guò)Watcher關(guān)聯(lián)杀狡,當(dāng)Observer監(jiān)聽(tīng)到數(shù)據(jù)變化會(huì)通過(guò)watcher使Compile更新頁(yè)面,反之亦然贰镣。
下邊就一部分一部分拆解Vue數(shù)據(jù)監(jiān)聽(tīng)架構(gòu)呜象。
Vue函數(shù)
這里簡(jiǎn)單模擬Vue函數(shù),el為Vue作用的dom節(jié)點(diǎn)鉤子碑隆,data為Vue主要監(jiān)聽(tīng)的數(shù)據(jù)恭陡,option為Vue中dom交互事件函數(shù)放置的地方。
class Vue {
constructor(el, data, option) {
this.$el = el;
this.$data = data;
this.$option = option; // 綁定方法放在這里
if (this.$el) {
new Observer(this.$data)
new Compile(this.$el, this);
}
}
}
Compile
構(gòu)造函數(shù)
compile負(fù)責(zé)Vue數(shù)據(jù)在頁(yè)面上的渲染上煤,首先看構(gòu)造函數(shù):
constructor(el, vm) {
this.vm = vm;
if (el && el.nodeType === 1) {
this.$el = el;
} else {
this.$el = document.querySelector(el);
}
const fragment = this.createFragment(this.$el);
this.compile(fragment);
this.$el.appendChild(fragment);
}
createFragment(el) {
const fragment = document.createDocumentFragment();
while (el.firstChild) {
fragment.appendChild(el.firstChild);
}
return fragment;
}
都是比較簡(jiǎn)單的功能休玩,首先在Vue構(gòu)造函數(shù)中將el與vue實(shí)例通過(guò)構(gòu)造函數(shù)傳遞進(jìn)來(lái),其他值得一說(shuō)的就是為了減少dom結(jié)構(gòu)變化造成的重排,使用了fragment拴疤,先將el子節(jié)點(diǎn)緩存在fragment中永部,然后compile后一次性插入el子節(jié)點(diǎn)中。
compile
compile(fragment) {
fragment.childNodes.forEach((childNode) => {
if (childNode && childNode.nodeType === 1) {
this.compileElement(childNode)
} else {
this.compileText(childNode)
}
if (childNode && childNode.childNodes.length > 0) {
this.compile(childNode);
}
})
}
遍歷子節(jié)點(diǎn)呐矾,發(fā)現(xiàn)如果是element節(jié)點(diǎn)進(jìn)行子節(jié)點(diǎn)的遞歸調(diào)用苔埋,這里簡(jiǎn)單處理為子節(jié)點(diǎn)只有element與text類型節(jié)點(diǎn)。分別針對(duì)element與text節(jié)點(diǎn)做編譯處理蜒犯。
編譯text與element類型子節(jié)點(diǎn)
compileElement(node) {
const attributes = Array.from(node.attributes);
attributes.forEach((attribute) => {
const {name, value} = attribute;
if (this.isDirective(name)) {
const [, directive] = name.split('-');
const [directiveName, eventName] = directive.split(':');
CompileUtil[directiveName](node, value, this.vm, eventName);
}
})
}
compileText(node) {
if (node.textContent && node.textContent.includes('{{')) {
CompileUtil['text'](node, node.textContent, this.vm)
}
}
isDirective(name) {
if (typeof name !== 'string') {
return false;
}
return name.startsWith('v-');
}
編譯element節(jié)點(diǎn)
編譯element節(jié)點(diǎn)首先遍歷節(jié)點(diǎn)屬性组橄,找出v-
開頭的屬性,簡(jiǎn)單假定這些就是vue框架渲染節(jié)點(diǎn)的鉤子屬性愧薛。
然后拆分鉤子屬性獲取到expr(獲取data值的屬性表達(dá)式
),綁定的事件名稱晨炕,然后開始渲染頁(yè)面。
渲染頁(yè)面部分是個(gè)很獨(dú)立的一塊工作毫炉,所以這里封裝了一個(gè)工具對(duì)象瓮栗。
編譯text節(jié)點(diǎn)
compileText(node) {
if (node.textContent && node.textContent.includes('{{')) {
CompileUtil['text'](node, node.textContent, this.vm)
}
}
文本類型節(jié)點(diǎn)主要判斷出是否是{{template }}
類型的節(jié)點(diǎn),然后將textConten傳遞給CompileUtil渲染到頁(yè)面瞄勾。
CompileUtil
結(jié)構(gòu)圖
首先針對(duì)vue的幾個(gè)常用指令
v-text费奸、v-html、v-modal與v-on
對(duì)應(yīng)了幾個(gè)操作方法进陡,update是對(duì)應(yīng)渲染到頁(yè)面方法的工具對(duì)象愿阐。首先從text方法來(lái)開始看:
text(node, expr, vm) {
let value = null;
if (expr.includes('{{')) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watch(args[1], vm, (newValue) => {
this.update.textUpdate(node, newValue);
});
return this.getValue(args[1], vm);
})
} else {
value = this.getValue(expr, vm);
new Watch(expr, vm, (newValue) => {
this.update.textUpdate(node, newValue);
});
}
this.update.textUpdate(node, value);
},
首先通過(guò)expr區(qū)分出是模版渲染還是v-text渲染,如果是模版渲染就用replace抽取出表達(dá)式趾疚,然后通過(guò)公用的表達(dá)式獲取值方法
拿到值渲染到頁(yè)面缨历。
watch類通過(guò)表達(dá)式關(guān)聯(lián)vm中的對(duì)象變化,然后通過(guò)回調(diào)函數(shù)重新渲染頁(yè)面糙麦。
getValue方法很簡(jiǎn)單,表達(dá)式通過(guò)‘.’拆分為數(shù)組辛孵,進(jìn)行reduce操作,然后將vue實(shí)例中的data作為起始值赡磅。
getValue(expr, vm) {
return expr.split('.').reduce((data, attr) => {
return data[attr];
}, vm.$data)
},
Watch與Dep
Dep
Dep類非常簡(jiǎn)單
class Dep {
constructor() {
this.subs = [];
}
add(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
Dep對(duì)象中負(fù)責(zé)添加watcher魄缚,在需要的時(shí)候發(fā)起通知,讓watcher更新頁(yè)面
watch
class Watch {
constructor(expr, vm, callBack) {
this.expr = expr;
this.vm = vm;
this.callBack = callBack;
this.oldValue = this.getOldValue();
}
update() {
const newValue = CompileUtil.getValue(this.expr, this.vm);
if (this.oldValue !== newValue) {
this.callBack(newValue);
this.oldValue = newValue;
}
}
getOldValue() {
Dep.target = this; // 用這種方式就不能Dep類與Watch類分在兩個(gè)文件焚廊,webpack打包target值會(huì)丟掉
const oldValue = CompileUtil.getValue(this.expr, this.vm); // 獲取data中的值冶匹,在get中添加Watch入Dep
Dep.target = null;
return oldValue;
}
}
watch類中在構(gòu)造函數(shù)中傳遞expr vm 與跟新的回調(diào)函數(shù),最重要的是getOldValue函數(shù)咆瘟,在這里邊在Dep類中添加了target屬性嚼隘,屬性值存了Watch實(shí)例對(duì)象,這里的關(guān)鍵思想是在這里通過(guò)CompileUtil.getValue獲取Vue中data值袒餐,并在Dep中上存了一個(gè)watch飞蛹,獲取data屬性值的時(shí)候會(huì)調(diào)用這個(gè)屬性的get方法须肆,如果Dep對(duì)象上target有值,就在Dep對(duì)象上添加一個(gè)watch桩皿。
update方法通過(guò)CompileUtils.getValue獲取watch中表達(dá)式值如果新值不等于老值就調(diào)用callback跟新頁(yè)面
Observer
Observer類是核心對(duì)象,這里通過(guò)構(gòu)造函數(shù)傳遞Vue需要監(jiān)聽(tīng)的對(duì)象
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === 'object') {
for (const key of Object.keys(data)) {
this.defineReactive(data, key, data[key]);
}
}
}
defineReactive(data, key, value) {
this.observe(value);
const dep = new Dep();
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get: () => {
Dep.target && dep.add(Dep.target)
return value;
},
set: (v) => {
this.observe(v);
if (v !== value) {
value = v;
dep.notify();
}
}
})
}
}
在observe方法中遍歷data對(duì)象幢炸,然后調(diào)用核心方法defineReactive
泄隔,這里注意的是在方法中首先回調(diào)了observe方法,因?yàn)閷?duì)象的屬性值可能也是個(gè)對(duì)象宛徊,所以回調(diào)了一下observe方法進(jìn)行深度監(jiān)聽(tīng)佛嬉,這里遍歷對(duì)象的每個(gè)屬性值,然后添加get 與set方法闸天,get方法中與watch對(duì)象中的getOldValue進(jìn)行聯(lián)動(dòng)暖呕,在set方法中因?yàn)樾略O(shè)置的值可能也是一個(gè)對(duì)象,所以也要回調(diào)一此observe方法苞氮,如果屬性設(shè)置的值與老值不同就調(diào)用dep進(jìn)行廣播所有watch進(jìn)行頁(yè)面更新湾揽。
這里set方法有個(gè)小技巧,set方法構(gòu)成一個(gè)閉包笼吟,v關(guān)聯(lián)了data的屬性值所以每次更新值都可以和data中的屬性值進(jìn)行比較库物。
測(cè)試
下邊簡(jiǎn)單測(cè)試一下功能
html部分的代碼
<input type="text" id="input">
<p v-text="text.value">
</p>
{{text.value}}
js部分的代碼
var vue = new Vue(
'#box',
{
text: {
value: '文本'
},
html: '<h1>html</h1>',
inputValue: 'input'
},
{
clickButton() {
alert(this.$data.text.value);
}
}
)
const input = document.getElementById('input');
input.addEventListener('input', (e) => {
vue.$data.text.value = e.target.value;
})
為了測(cè)試效果給input綁定時(shí)間修改input值修改文本綁定的變量
測(cè)試結(jié)果
改變input值后效果
v-html效果
v-html比較簡(jiǎn)單,首先看CompileUtil部分代碼:
html(node, expr, vm) {
const value = this.getValue(expr, vm);
this.update.htmlUpdate(node, value);
new Watch(expr, vm, (newValue) => {
this.update.htmlUpdate(node, newValue);
})
},
...
htmlUpdate(node, value) {
node.innerHTML = value;
},
...
思路很簡(jiǎn)單通過(guò)expr獲取變量值然后渲染到頁(yè)面,watch監(jiān)聽(tīng)到變化后重新調(diào)用update
測(cè)試
html部分代碼:
<button id="changeHtmlBtn">修改html</button>
<div v-html="html">
html
</div>
js部分代碼
const htmlBtn = document.getElementById('changeHtmlBtn');
htmlBtn.addEventListener('click', (e) => {
vue.$data.html = '<h2>changeHtml</h2>'
})
當(dāng)點(diǎn)擊button后修改div下的html
v-modal
v-modal就是我們常說(shuō)的雙向綁定
一樣我們先看CompileUtil部分代碼
...
setValue(expr, vm, inputValue) {
expr.split('.').reduce((data, currentValue, currentIndex, array) => {
if (currentIndex === array.length - 1) {
// 最后一個(gè)屬性值賦值input輸入的值
data[currentValue] = inputValue;
}
return data[currentValue];
}, vm.$data)
},
...
modal(node, expr, vm) {
node.addEventListener('input', (e) => {
const value = e.target.value;
this.setValue(expr, vm, value);
}, false);
new Watch(expr, vm, (newValue) => {
this.update.modalUpdate(node, newValue);
});
this.update.modalUpdate(node, this.getValue(expr, vm));
},
update: {
...
modalUpdate(node, value) {
node.value = value;
}
...
}
其實(shí)也很簡(jiǎn)單給節(jié)點(diǎn)綁定一個(gè)input事件贷帮,事件回調(diào)函數(shù)給vue中的data賦值戚揭,watch監(jiān)聽(tīng)框架中的變量變化后更新節(jié)點(diǎn)的value值,賦值操作封裝一個(gè)setValue方法撵枢,setValue方法和getValue方法一樣使用reduce方法民晒,在最后一個(gè)屬性賦值inputValue
測(cè)試
html代碼
<input type="text" v-modal = 'inputValue'>
<div>{{inputValue}}</div>
inputValue初始值賦值為input
效果
input初始值賦值為input
修改input輸入框值后,頁(yè)面動(dòng)態(tài)發(fā)生變化
結(jié)語(yǔ)
這里只是簡(jiǎn)單模擬vue框架锄禽,有很多地方存在缺陷潜必,大家有選擇的閱讀思考就好,感謝閱讀沟绪。