上一篇簡析vue實現(xiàn)原理殃恒,我們知道在 Vue 的 MVVM 設計中,我們主要針對 Observer(數(shù)據(jù)劫持)、Dep(發(fā)布訂閱)、Watcher(數(shù)據(jù)監(jiān)聽)和 Compile(模板編譯)幾個部分來實現(xiàn)唧龄,下面來實現(xiàn)它們。
// MVVM.js稼病,入口文件选侨,整合上面的幾部分
class MVVM {
constructor(options) {
// 先把 el 和 data 掛在 MVVM 實例上
this.$el = options.el;
this.$data = options.data;
if (this.$el) {
// $data數(shù)據(jù)劫持
new Observer(this.$data);
// 將數(shù)據(jù)代理到實例上 vm.message = "hello"
this.proxyData(this.$data);
// 用數(shù)據(jù)和元素進行編譯
new Compile(this.$el, this);
}
}
proxyData(data) { // 代理數(shù)據(jù)的方法
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
});
});
}
}
// Compile.js
在 Vue 中的模板編譯的主要就是兩部分,元素節(jié)點中的指令和文本節(jié)點中的 Mustache 語法(雙大括號)然走,這是瀏覽器無法解析的部分。
class Compile {
constructor(el, vm) {
//dom
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 如過傳入的根元素存在戏挡,才開始編譯
if (this.el) {
// 1芍瑞、把這些真實的 Dom 移動到內(nèi)存中,即 fragment(文檔碎片)
let fragment = this.node2fragment(this.el);
// ********** 以下為新增代碼 **********
// 2褐墅、將模板中的指令中的變量和 {{}} 中的變量替換成真實的數(shù)據(jù)
this.compile(fragment);
// 3拆檬、把編譯好的 fragment 再塞回頁面中
this.el.appendChild(fragment);
// ********** 以上為新增代碼 **********
}
}
/* 輔助方法 */
// 判斷是否是元素節(jié)點
isElementNode(_node) {
return _node.nodeType === 1;
}
// ********** 以下為新增代碼 **********
// 判斷屬性是否為指令
isDirective(name) {
return name.includes("v-");
}
// ********** 以上為新增代碼 **********
/* 核心方法 */
// 將根節(jié)點轉移至文檔碎片
node2fragment(el) {
// 創(chuàng)建文檔碎片
let fragment = document.createDocumentFragment();
// 第一個子節(jié)點
let firstChild;
// 循環(huán)取出根節(jié)點中的節(jié)點并放入文檔碎片中
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
// ********** 以下為新增代碼 **********
// 解析文檔碎片
compile(fragment) {
// 當前父節(jié)點節(jié)點的子節(jié)點,包含文本節(jié)點妥凳,類數(shù)組對象
let childNodes = fragment.childNodes;
// 轉換成數(shù)組并循環(huán)判斷每一個節(jié)點的類型
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) { // 是元素節(jié)點
// 遞歸編譯子節(jié)點
this.compile(node);
// 編譯元素節(jié)點的方法
this.compileElement(node);
} else { // 是文本節(jié)點
// 編譯文本節(jié)點的方法
this.compileText(node);
}
});
}
// 編譯元素
compileElement(node) {
// 取出當前節(jié)點的屬性竟贯,類數(shù)組
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
// 獲取屬性名,判斷屬性是否為指令逝钥,即含 v-
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 如果是指令屑那,取到該屬性值得變量在 data 中對應得值,替換到節(jié)點中
let exp = attr.value;
// 取出方法名
let [, type] = attrName.split("-");
// 調(diào)用指令對應得方法
CompileUtil[type](node, this.vm, exp);
}
});
}
// 編譯文本
compileText(node) {
// 獲取文本節(jié)點的內(nèi)容
let exp = node.textContent;
// 創(chuàng)建匹配 {{}} 的正則表達式
//let reg = /\{\{([^}+])\}\}/g;
//“.”表示任意字符艘款〕旨剩“+”表示前面表達式一次乃至多次』┡兀“?”表示匹配模式是非貪婪的蜘欲。
let reg = /\{\{(.+?)\}\}/g;
// 如果存在 {{}} 則使用 text 指令的方法
if (reg.test(exp)) {
CompileUtil["text"](node, this.vm, exp);
}
}
// ********** 以上為新增代碼 **********
}
// CompileUtil.js
CompileUtil 中存儲著所有的指令方法及指令對應的更新方法,由于 Vue 的指令很多晌柬,我們這里只實現(xiàn)典型的 v-model 和 “{{ }}” 對應的方法姥份,考慮到后續(xù)更新的情況,我們統(tǒng)一把設置值到 Dom 中的邏輯抽取出對應上面兩種情況的方法年碘,存放到 CompileUtil 的 updater 對象中澈歉。
CompileUtil = {};
// 更新Dom節(jié)點方法
CompileUtil.updater = {
// 文本更新
textUpdater(node, value) {
node.textContent = value;
},
// 輸入框更新
modelUpdater(node, value) {
node.value = value;
}
};
// 獲取 data 值的方法
CompileUtil.getVal = function (vm, exp) {
// 將匹配的值用 . 分割開,如 vm.data.a.b
exp = exp.split(".");
// 歸并取值
return exp.reduce((prev, next) => {
return prev[next];
}, vm.$data);
};
// 獲取文本 {{}} 中變量在 data 對應的值
CompileUtil.getTextVal = function (vm, exp) {
// 使用正則匹配出 {{ }} 間的變量名盛泡,再調(diào)用 getVal 獲取值
return exp.replace(/\{\{([^}]+)\}\}/g, (...args) => {
return this.getVal(vm, args[1]);
});
};
// 設置 data 值的方法
CompileUtil.setVal = function (vm, exp, newVal) {
exp = exp.split(".");
return exp.reduce((prev, next, currentIndex) => {
// 如果當前歸并的為數(shù)組的最后一項闷祥,則將新值設置到該屬性
if(currentIndex === exp.length - 1) {
return prev[next] = newVal
}
// 繼續(xù)歸并
return prev[next];
}, vm.$data);
}
// 處理 v-model 指令的方法
CompileUtil.model = function (node, vm, exp) {
// 獲取賦值的方法
let updateFn = this.updater["modelUpdater"];
// 獲取 data 中對應的變量的值
let value = this.getVal(vm, exp);
// 添加觀察者,作用與 text 方法相同
new Watcher(vm, exp, newValue => {
updateFn && updateFn(node, newValue);
});
// v-model 雙向數(shù)據(jù)綁定,對 input 添加事件監(jiān)聽
node.addEventListener('input', e => {
// 獲取輸入的新值
let newValue = e.target.value;
// 更新到節(jié)點
this.setVal(vm, exp, newValue);
});
// 第一次設置值
updateFn && updateFn(node, value);
};
// 處理文本節(jié)點 {{}} 的方法
CompileUtil.text = function (node, vm, exp) {
// 獲取賦值的方法
let updateFn = this.updater["textUpdater"];
// 獲取 data 中對應的變量的值
let value = this.getTextVal(vm, exp);
// 通過正則替換凯砍,將取到數(shù)據(jù)中的值替換掉 {{ }}
exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 解析時遇到了模板中需要替換為數(shù)據(jù)值的變量時箱硕,應該添加一個觀察者
// 當變量重新賦值時,調(diào)用更新值節(jié)點到 Dom 的方法
new Watcher(vm, args[1], newValue => {
// 如果數(shù)據(jù)發(fā)生變化悟衩,重新獲取新值
updateFn && updateFn(node, newValue);
});
});
// 第一次設置值
updateFn && updateFn(node, value);
};
// Watcher.js
Watcher 類內(nèi)部要做的事情剧罩,獲取更改前的值存儲起來,并創(chuàng)建一個 update 實例方法座泳,在值被更改時去執(zhí)行實例的 callback 以達到視圖的更新惠昔。
class Watcher {
constructor(vm, exp, callback) {
this.vm = vm; // MVVM 的實例
this.exp = exp; // 模板綁定數(shù)據(jù)的變量名 exp
this.callback = callback;
// 更改前的值
this.value = this.get();
}
get() {
// 將當前的 watcher 添加到 Dep 類的靜態(tài)屬性上
Dep.target = this;
// 獲取值觸發(fā)數(shù)據(jù)劫持
let value = CompileUtil.getVal(this.vm, this.exp);
// 清空 Dep 上的 Watcher,防止重復添加
Dep.target = null;
return value;
}
update() {
// 獲取新值
let newValue = CompileUtil.getVal(this.vm, this.exp);
// 獲取舊值
let oldValue = this.value;
// 如果新值和舊值不相等挑势,就執(zhí)行 callback 對 dom 進行更新
if(newValue !== oldValue) {
this.callback(newValue);
}
}
}
// Dep.js
發(fā)布訂閱將要執(zhí)行的函數(shù)統(tǒng)一存儲在一個數(shù)組中管理镇防,當達到某個執(zhí)行條件時潮饱,循環(huán)這個數(shù)組并執(zhí)行每一個成員来氧。
class Dep {
constructor() {
this.subs = [];
}
// 添加訂閱
addSub(watcher) {
this.subs.push(watcher);
}
// 通知
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// Observer.js 數(shù)據(jù)劫持 Observer 類
class Observer {
constructor (data) {
this.observe(data);
}
// 添加數(shù)據(jù)監(jiān)聽
observe(data) {
if(!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
// 劫持(實現(xiàn)數(shù)據(jù)響應式)
this.defineReactive(data, key, data[key]);
this.observe(data[key]); // 深度劫持
});
}
// 數(shù)據(jù)響應式
defineReactive (object, key, value) {
let _this = this;
// 每個變化的數(shù)據(jù)都會對應一個數(shù)組,這個數(shù)組是存放所有更新的操作
let dep = new Dep();
// 獲取某個值被監(jiān)聽到
Object.defineProperty(object, key, {
enumerable: true,
configurable: true,
get () { // 當取值時調(diào)用的方法
Dep.target && dep.addSub(Dep.target);
return value;
},
set (newValue) { // 當給 data 屬性中設置的值適合啦扬,更改獲取的屬性的值
if(newValue !== value) {
_this.observe(newValue); // 重新賦值如果是對象進行深度劫持
value = newValue;
dep.notify(); // 通知所有人數(shù)據(jù)更新了
}
}
});
}
}
來驗證下我們寫的MVVM扑毡。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MVVM</title>
</head>
<body>
<div id="app">
<!-- 雙向數(shù)據(jù)綁定 靠的是表單 -->
<input type="text" v-model="message">
<div>{{message}}</div>
<ul>
<li>{{message}}</li>
</ul>
{{message}}
</div>
<!-- 引入依賴的 js 文件 -->
<script src="./Watcher.js"></script>
<script src="./Observer.js"></script>
<script src="./Compile.js"></script>
<script src="./CompileUtil.js"></script>
<script src="./Dep.js"></script>
<script src="./MVVM.js"></script>
<script>
let vm = new MVVM({
el: '#app',
data: {
message: 'hello world!'
}
})
</script>
</body>
</html>