MVVM 是 Model-View-ViewModel 縮寫,也就是把 MVC 中的 Controller 演變成 ViewModel。Model 層代表資料模型,View 代表 UI 元件,ViewModel 是 View 和 Model 層的橋樑,資料會繫結到 viewModel 層並自動將資料渲染到頁面中,檢視變化的時候會通知 viewModel 層更新資料。
- Model: 代表資料模型,也可以在 Model 中定義資料修改和操作的業務邏輯。我們可以把 Model 稱為資料層,因為它僅僅關注資料本身,不關心任何行為
- View: 使用者操作介面。當 ViewModel 對 Model 進行更新的時候,會透過資料繫結更新到 View
- ViewModel: 業務邏輯層,View 需要什麼資料,ViewModel 要提供這個資料;View 有某些操作,ViewModel 就要響應這些操作,所以可以說它是 Model for View.
總結: MVVM 模式簡化了介面與業務的依賴,解決了資料頻繁更新。MVVM 在使用當中,利用雙向繫結技術,使得 Model 變化時,ViewModel 會自動更新,而 ViewModel 變化時,View 也會自動變化。
實現一個簡易的 MVVM 分為這麼幾步來
1.類 Vue:這個類接收的是一個 options。
el屬性:根元素的id
data屬性:雙向繫結的資料。
2.Dep 類:
subNode陣列:存放所依賴這個屬性 的依賴,
addSub方法:新增依賴的
removeSub方法:刪除,
updata方法:遍歷更新它subs中的所有依賴
target靜態屬性:用來表示 '當前的觀察者' ,依賴收集的時候可以將它新增到dep. subNode中
靜態的是指向類自身,而不是指向例項物件,主要是歸屬不同,這是靜態屬性的核心
3.observe 方法:監聽資料變化(引數:data,也就是 options.data)
遍歷data:使用Object.defineProperty()來重寫它的get和set,
使用new Dep():例項化一個dep物件,
get時:addSub 新增 '當前的觀察者Dep.target' 完成依賴收集,
set時:dep.updata 通知每一個依賴它的觀察者進行更新
4.compile 方法:來將 HTML 模版和資料結合起來(引數:node 節點)。
遍歷它的所有子級,判斷是否有firstElmentChild
有:進行遞迴呼叫compile方法,
沒有:child.innderHTML用判斷是否有(/\{\{(.*)\}\}/)需要雙向繫結的資料,
有:new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替換msg變數。
5.將 Dep.target 指向當前的這個 child,
呼叫this.opt.data[key]:觸發這個資料的get來對當前的child進行依賴收集,
目的:下次資料變化通知child進行檢視更新,
將Dep.target指為null(其實在Vue中是有一個targetStack棧用來存放target的指向的)
6.監聽 document 的 DOMContentLoaded
回撥函式中例項化這個Vue物件就可以了
需要注意的點:
childNodes 會獲取到所有的子節點以及文位元組點(包括元素標籤中的空白節點)
firstElementChild 表示獲取元素的第一個字元素節點,以此來區分是不是元素節點,如果是的話則呼叫 compile 進行遞迴呼叫,否則用正則匹配
<!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>MVVM</title>
</head>
<body>
<div id="app">
<h3>姓名</h3>
<p>{{name}}</p>
<h3>年齡</h3>
<p>{{age}}</p>
</div>
</body>
</html>
<script>
document.addEventListener(
"DOMContentLoaded",
function () {
let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };
let vm = new Vue(opt);
setTimeout(() => {
opt.data.name = "jing";
}, 2000);
},
false
);
class Vue {
constructor(opt) {
this.opt = opt;
this.observer(opt.data);
let root = document.querySelector(opt.el);
this.compile(root);
}
observer(data) {
Object.keys(data).forEach((key) => {
let obv = new Dep();
data["_" + key] = data[key];
Object.defineProperty(data, key, {
get() {
Dep.target && obv.addSubNode(Dep.target);
return data["_" + key];
},
set(newVal) {
obv.update(newVal);
data["_" + key] = newVal;
},
});
});
}
compile(node) {
[].forEach.call(node.childNodes, (child) => {
if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
let key = RegExp.$1.trim();
child.innerHTML = child.innerHTML.replace(
new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),
this.opt.data[key]
);
Dep.target = child;
this.opt.data[key];
Dep.target = null;
} else if (child.firstElementChild) this.compile(child);
});
}
}
class Dep {
constructor() {
this.subNode = [];
}
addSubNode(node) {
this.subNode.push(node);
}
update(newVal) {
this.subNode.forEach((node) => {
node.innerHTML = newVal;
});
}
}
</script>