MVVM 設計模式,是由 MVC、MVP 等設計模式進化而來,M - 數據模型(Model),VM - 視圖模型(ViewModel),V - 視圖層(View)。MVVM 的核心是 ViewModel 層,它就像是一個中轉站(value converter),負責轉換 Model 中的數據對象來讓數據變得更容易管理和使用,該層向上與視圖層進行雙向數據綁定,向下與 Model 層通過接口請求進行數據交互,起呈上啟下作用。如下圖所示:

Vue中的MVVM思想
使用 MVVM 設計模式的前端框架很多,其中漸進式框架 Vue 是典型的代表,深得廣大前端開發(fā)者的青睞。

從上圖中可以看出MVVM主要分為這么幾個部分:
- 模板編譯(Compile)
- 數據劫持(Observer)
- 訂閱-發(fā)布(Dep)
- 觀察者(Watcher)
我們來看一個 vue 的實例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue</title>
</head>
<body>
<div id="app">
<p>{{ number }}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const VM = new Vue({
el: '#app',
data: {
number: 0
},
})
setInterval(() => {
VM.number++
}, 1000)
</script>
</body>
</html>
我們對原理進行分析一下:
- 首先
new Vue()執(zhí)行初始化,通過Observer對data上的屬性執(zhí)行響應式處理。也就是Object.defineProperty對數據屬性進行劫持。 - 通過
Compile進行模板編譯,對模板里動態(tài)綁定的數據,使用data數據進行初始化。 - 在模板初始化時,在觸發(fā)
Object.defineProperty內的getter時,創(chuàng)建更新函數和Watcher類。 - 同一屬性在模板中可能出現(xiàn)多次,就會創(chuàng)建多個
Watcher,就需要Dep來統(tǒng)一管理。 - 當數據發(fā)生變化時,找到屬性對應的
dep,通知所有Watcher執(zhí)行更新函數。
創(chuàng)建Vue類
// 創(chuàng)建 Vue 類
class Vue {
constructor(options) {
// 保存選項
this.$options = options;
this.$data = options.data;
// 響應式處理
observe(this.$data)
// 將數據代理到實例上
proxyData(this, '$data')
// 用數據和元素進行編譯
new Compiler(options.el, this)
}
}
// 代理數據的方法
function proxyData (vm, sourceKey) {
Object.keys(vm[sourceKey]).forEach(key => {
Object.defineProperty(vm, key, {
get () {
return vm[sourceKey][key]
},
set (newVal) {
vm[sourceKey][key] = newVal
}
})
})
}
上面代碼創(chuàng)建了一個 Vue 類和 proxyData 方法,Vue 類接收 options 參數,內部調用 observe 方法對傳入參數 options.data 數據,遞歸進行響應式處理。
使用 proxyData 方法把數據代理到實例上,讓我們獲取和修改數據的時候可以直接通過 this 或 this.$data, 如: this.number 或 this.$data.number 。
最后使用 Compiler 對模板進行編譯,初始化動態(tài)綁定的數據。
模板編譯(Compile)
// 模板編譯
class Compiler {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
// 執(zhí)行編譯
this.compile(this.$el)
}
}
compile (el) {
// 遍歷 el 樹
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
if (this.isElementNode(node)) { // 元素節(jié)點
// 編譯元素節(jié)點的方法
this.compileElement(node)
} else { // 文本節(jié)點
// 編譯文本節(jié)點的方法
this.compileText(node)
}
// 如果還有子節(jié)點,繼續(xù)遞歸
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
})
}
// 判斷是否是元素節(jié)點
isElementNode (node) {
return node.nodeType === 1
}
// 判斷屬性是否為指令
isDirective (attr) {
return attr.indexOf('v-') === 0
}
// 編譯元素
compileElement (node) {
// 遍歷節(jié)點屬性
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach((attr) => {
const attrName = attr.name // 屬性名
const exp = attr.value // 動態(tài)判定的變量名
//找到 v-xxx 的指令,如 v-text/v-html
if (this.isDirective(attrName)) {
let [, dir] = attrName.split("-") // 指令名 如 text/html
// 調用指令對應得方法
this[dir] && this[dir](node, exp)
}
})
}
// 編譯文本
compileText (node) {
let txt = node.textContent; // 獲取文本節(jié)點的內容
let reg = /\{\{(.*)\}\}/; // 創(chuàng)建匹配 {{}} 的正則表達式
// 如果存在 {{}} 則使用 text 指令的方法
if (node.nodeType === 3 && reg.test(txt)) {
this.update(node, RegExp.$1.trim(), 'text')
}
}
update (node, exp, dir) {
// 調用指令對應的更新函數
const callback = this[dir + 'Updater'];
callback && callback(node, this.$vm[exp])
// 更新處理,創(chuàng)建 Watcher,保存更新函數
new Watcher(this.$vm, exp, function (val) {
callback && callback(node, val)
})
}
// v-text
text (node, exp) {
this.update(node, exp, 'text')
}
textUpdater (node, value) {
node.textContent = value
}
// v-html
html (node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater (node, value) {
node.innerHTML = value
}
}
編譯過程中,以根元素開始,也就是實例化 Vue 時傳入的 options.el 進行遞歸編譯節(jié)點,使用 isElementNode 方法判斷是文本節(jié)點還是元素節(jié)點。
如果是文本節(jié)點,正則匹配(雙大括號){{ xxx }};使用 v-text 指令方式初始化讀取數據。
若為元素節(jié)點,遍歷屬性,找到 v-text 或 v-html,初始化動態(tài)綁定的數據。
在初始化數據時,創(chuàng)建 Watcher 和更新函數。
數據劫持(Observer)
function observe (obj) {
if (typeof obj !== 'object' || obj == null) {
return;
}
// 傳入境來的對象做響應式處理
new Observer(obj)
}
function defineReactive (obj, key, val) {
// 遞歸劫持數據
observe(val)
// 創(chuàng)建與 key 對應的 Dep 管理相關的 Watcher
const dep = new Dep();
//對數據進行劫持
Object.defineProperty(obj, key, {
get () {
// 依賴收集
Dep.target && dep.addDep(Dep.target)
return val;
},
set (newVal) {
if (newVal !== val) {
// 如果 newVal 為 Object ,就需要對其響應式處理
observe(newVal)
val = newVal;
// 通知更新
dep.notify()
}
}
})
}
class Observer {
constructor(value) {
this.value = value;
if (typeof value === 'object') {
this.walk(value)
}
}
// 對象數據響應化
walk (obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
上面代碼,創(chuàng)建了 observe 和 defineReactive 方法,還有 Observer 類 。
observe 方法用于類型判斷。
Observer 類收一個參數,若參數是 object 類型,調用 defineReactive 方法對其屬性進行劫持。
在 defineReactive 方法中,通過 Object.defineProperty 對屬性進行劫持。并對每個 key 創(chuàng)建 Dep 的實例,還記得模板編譯時,對動態(tài)綁定的值,進行初始化的時候會創(chuàng)建 Watcher 嗎?Watcher 內保存有對應的更新函數;defineReactive 中,數據被讀取的時候,就會觸發(fā) getter , getter 中就會把 Watcher push 到對應的 Dep 中,這個過程就叫做依賴收集。當值發(fā)生改變的時候,觸發(fā) setter 調用這個key 所對應的 Dep 內的 notify 方法,通知更新。
訂閱-發(fā)布(Dep)
class Dep {
constructor() {
this.deps = []
}
addDep (dep) {
this.deps.push(dep)
}
notify () {
this.deps.forEach(dep => dep.update())
}
}
每個 key 都會創(chuàng)建一個 Dep ,每個 Dep 內都會有一個 deps 數組,用來同一管理這個 key 所對應的 Watcher 實例。
addDep 方法用于添加訂閱。
notify 方法用于通知更新。
觀察者(Watcher)
// 觀察者,保存更新函數。
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm
this.key = key
this.updateFn = updateFn
Dep.target = this // 在靜態(tài)屬性上保存當前實例
this.vm[this.key] // 觸發(fā)數據劫持 get
Dep.target = null // 在讀取屬性 觸發(fā)get后,依賴收集完畢,現(xiàn)在置空
}
update () {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
Watcher 類接收三個參數,Vue 的實例、 綁定的變量名 key 和 updateFn 更新方法。Watcher 在模板編譯時被創(chuàng)建。我們用 Dep.target 靜態(tài)屬性來保存當前的實例。主動觸發(fā)一次響應式的 getter , 使其實例被添加到 Dep 中,完成依賴收集,完成后,將靜態(tài)屬性 Dep.target 置空。
內部創(chuàng)建一個 update 更新方法。在 Dep 的 notify 方法通知更新時被調用。
完整代碼
// 模板編譯
class Compiler {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
// 執(zhí)行編譯
this.compile(this.$el)
}
}
compile (el) {
// 遍歷 el 樹
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
if (this.isElementNode(node)) { // 元素節(jié)點
// 編譯元素節(jié)點的方法
this.compileElement(node)
} else { // 文本節(jié)點
// 編譯文本節(jié)點的方法
this.compileText(node)
}
// 如果還有子節(jié)點,繼續(xù)遞歸
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
})
}
// 判斷是否是元素節(jié)點
isElementNode (node) {
return node.nodeType === 1
}
// 判斷屬性是否為指令
isDirective (attr) {
return attr.indexOf('v-') === 0
}
// 編譯元素
compileElement (node) {
// 遍歷節(jié)點屬性
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach((attr) => {
const attrName = attr.name // 屬性名
const exp = attr.value // 動態(tài)判定的變量名
//找到 v-xxx 的指令,如 v-text/v-html
if (this.isDirective(attrName)) {
let [, dir] = attrName.split("-") // 指令名 如 text/html
// 調用指令對應得方法
this[dir] && this[dir](node, exp)
}
})
}
// 編譯文本
compileText (node) {
let txt = node.textContent; // 獲取文本節(jié)點的內容
let reg = /\{\{(.*)\}\}/; // 創(chuàng)建匹配 {{}} 的正則表達式
// 如果存在 {{}} 則使用 text 指令的方法
if (node.nodeType === 3 && reg.test(txt)) {
this.update(node, RegExp.$1.trim(), 'text')
}
}
update (node, exp, dir) {
// 調用指令對應的更新函數
const callback = this[dir + 'Updater'];
callback && callback(node, this.$vm[exp])
// 更新處理,創(chuàng)建 Watcher,保存更新函數
new Watcher(this.$vm, exp, function (val) {
callback && callback(node, val)
})
}
// v-text
text (node, exp) {
this.update(node, exp, 'text')
}
textUpdater (node, value) {
node.textContent = value
}
// v-html
html (node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater (node, value) {
node.innerHTML = value
}
}
// 創(chuàng)建 Vue 類
class Vue {
constructor(options) {
// 保存選項
this.$options = options;
this.$data = options.data;
// 響應式處理
observe(this.$data)
// 將數據代理到實例上
proxyData(this, '$data')
// 用數據和元素進行編譯
new Compiler(options.el, this)
}
}
// 代理數據的方法
function proxyData (vm, sourceKey) {
Object.keys(vm[sourceKey]).forEach(key => {
Object.defineProperty(vm, key, {
get () {
return vm[sourceKey][key]
},
set (newVal) {
vm[sourceKey][key] = newVal
}
})
})
}
function observe (obj) {
if (typeof obj !== 'object' || obj == null) {
return;
}
// 傳入境來的對象做響應式處理
new Observer(obj)
}
function defineReactive (obj, key, val) {
// 遞歸劫持數據
observe(val)
// 創(chuàng)建與 key 對應的 Dep 管理相關的 Watcher
const dep = new Dep();
//對數據進行劫持
Object.defineProperty(obj, key, {
get () {
// 依賴收集
Dep.target && dep.addDep(Dep.target)
return val;
},
set (newVal) {
if (newVal !== val) {
// 如果 newVal 為 Object ,就需要對其響應式處理
observe(newVal)
val = newVal;
// 通知更新
dep.notify()
}
}
})
}
class Observer {
constructor(value) {
this.value = value;
if (typeof value === 'object') {
this.walk(value)
}
}
// 對象數據響應化
walk (obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
// 觀察者,保存更新函數。
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm
this.key = key
this.updateFn = updateFn
Dep.target = this // 在靜態(tài)屬性上保存當前實例
this.vm[this.key] // 觸發(fā)數據劫持 get
Dep.target = null // 在讀取屬性 觸發(fā)get后,依賴收集完畢,現(xiàn)在置空
}
update () {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
// 訂閱-發(fā)布,管理某個key相關所有Watcher實例
class Dep {
constructor() {
this.deps = []
}
addDep (dep) {
this.deps.push(dep)
}
notify () {
this.deps.forEach(dep => dep.update())
}
}
相關鏈接
如果覺得還湊合的話,給個贊吧?。。∫部梢詠砦业膫€人博客逛逛 https://www.mingme.net/