MVVM 框架前生 (MVC 框架)
在介紹 MVVM 框架 之前, 先讓我們一起了解一下 MVC 框架
MVC 框架 將整個前端頁面分成 View, Controller, Modal
當視圖發(fā)生變化, 通過 Controller(控件) 將響應(yīng)傳入到 Model(數(shù)據(jù)源) 中, 由數(shù)據(jù)源改變 View 上面的數(shù)據(jù)
整個過程看起來是行云流水, 業(yè)務(wù)邏輯放在 Model 中, 頁面渲染邏輯放在 View 中
什么是 “MVVM 框架”
可以看到 MVVM 分別指 View, Model, View-Model
View 通過 View-Model 的 DOM Listeners 將事件綁定到 Model上
而 Model 則通過 Data Bindings 來管理 View 中的數(shù)據(jù)
View-Model 從中起到一個連接橋的作用
View 層: 也叫視圖層, 在前端開發(fā)中, 通常就是 DOM 層, 主要的作用是給用戶展示各種信息
Model 層: 也叫數(shù)據(jù)層, 數(shù)據(jù)可能是我們固定的死數(shù)據(jù), 更多的是來自我們服務(wù)器, 從網(wǎng)絡(luò)上請求下來的數(shù)據(jù)
Vue-Model 層: 也叫視圖模型層, 視圖模型層是 View 和 Model 溝通的橋梁, 一方面實現(xiàn)了 Data Binding(數(shù)據(jù)綁定), 將 Model 的改變實時的反應(yīng)到 View 中, 另一方面它實現(xiàn)了 DOM Listener(DOM監(jiān)聽), 當 DOM 發(fā)生一些事件 (點擊昂秃、輸入、滾動杜窄、touch等) 時, 可以監(jiān)聽到, 并在需要的情況下改變對應(yīng)的 Data
MVVM 與 MVC 的區(qū)別
如果你看對 MVC 框架 有所了解的話, 你會發(fā)現(xiàn) MVC 框架 實際運用上卻存在一個問題: 那就是 MVC 框架 允許 View 和 Model 直接進行通信!!!
這會造成 View 和 Model 之間隨著業(yè)務(wù)量的不斷龐大, 會出現(xiàn)蜘蛛網(wǎng)一樣難以處理的依賴關(guān)系, 完全背離了開發(fā)所應(yīng)該遵循的 “開放封閉原則”
-
面對這個問題, MVVM 框架 就出現(xiàn)了, 它與 MVC 框架 的主要區(qū)別以下兩點:
- 實現(xiàn)數(shù)據(jù)與視圖的分離
- 通過數(shù)據(jù)來驅(qū)動視圖, 開發(fā)者只需要關(guān)心數(shù)據(jù)變化, DOM 操作被封裝了
MVVM 原理
MVVM 的實現(xiàn)主要是三個核心點:
- 響應(yīng)式: Vue 如何監(jiān)聽 data 的屬性變化
- 模板解析: Vue 的模板是如何被解析的
- 渲染: Vue 模板是如何被渲染成 HTML 的
響應(yīng)式
對于 MVVM 來說, data 一般是放在一個對象當中, 例如:
let obj = {
rp: 0
}
當我們訪問或修改 obj 的屬性的時候, 例如:
console.log(obj.rp) // 訪問
obj.rp = 10 // 修改
但是這樣的操作 Vue 本身是沒有辦法感知到的, 那么應(yīng)該如何讓 Vue 知道我們進行了訪問或是修改的操作呢?
那就要使用 Object.defineProperty
let VueModel = {}
let data = {
name: "zhangsan",
age: 20
}
for (let key in data) {
Object.defineProperty(VueModel, key, {
get: () => {
console.log("get", data[key]) // 監(jiān)聽
return data[key]
},
set: value => {
console.log("set", value) // 監(jiān)聽
data[key] = value
}
})
}
通過 Object.defineProperty 將 data 里的每一個屬性的訪問與修改都變成了一個函數(shù), 在函數(shù) get 和 set 中我們即可監(jiān)聽到 data 的屬性發(fā)生了改變
模版解析
首先模板是什么肠骆?
模板本質(zhì)上是一串字符串, 它看起來和 HTML 的格式很相像, 實際上有很大的區(qū)別, 因為模板本身還帶有邏輯運算, 比如 v-if, v-for 等等, 但它最后還是要轉(zhuǎn)換為 HTML 來顯示
<div id="app">
<div>
<input v-model="title">
<button @click="submit">submit</button>
</div>
<div>
<ul>
<li v-for="(item, index) in list" :key="index">{{item}}</li>
</ul>
</div>
</div>
模板在 Vue 中必須轉(zhuǎn)換為 JS 代碼
- 原因在于: 在前端環(huán)境下, 只有 JS 才是一個圖靈完備語言, 才能實現(xiàn)邏輯運算, 以及渲染為 HTML 頁面
這里就引出了 Vue 中一個特別重要的函數(shù) —— render
render 函數(shù)中的核心就是 with 函數(shù)
with 函數(shù)將某個對象添加到作用域鏈的頂部
如果在 statement 中有某個未使用命名空間的變量, 跟作用域鏈中的某個屬性同名, 則這個變量將指向這個屬性值
例如:
let obj = {
name: "feng",
age: 20,
getAddress: () => {
alert("shanghai")
}
}
function fnc() {
with(obj) {
alert(age)
alert(name)
getAddress()
}
}
fnc()
with 將 obj 這個對象放在了自己函數(shù)的作用域鏈的頂部, 當執(zhí)行下列函數(shù)時, 就會自動到 obj 這個對象去尋找同名的屬性
而在 render 函數(shù)中, with 的用法是這樣
<div id="app">
<div>
<input v-model="title">
<button @click="submit">submit</button>
</div>
<div>
<ul>
<li v-for="(item, index) in list" :key="index">{{item}}</li>
</ul>
</div>
</div>
<script>
let data = {
title: '',
list: []
}
let app = new Vue({
el: '#app',
data,
methods: {
submit() {
this.list.push(this.title)
this.title = ''
}
}
})
with(this) {
return _c(
'div',
{
attrs: { "id": "app" }
},
[
_c(
'div',
[
_c(
'input',
{
directives: [
{
name: "model",
rawName: "v-model",
value: (title),
expression: "title"
}
],
domProps: {
"value": (title)
},
on: {
"input": function ($event) {
if ($event.target.composing) return;
title = $event.target.value
}
}
}
),
_v(" "),
_c(
'button',
{
on: {
"click": submit
}
},
[_v("submit")]
)
]
),
_v(" "),
_c('div',
[
_c(
'ul',
_l((list), function (item) { return _c('li',[_v(_s(item))]) })
)
]
)
]
)
}
</script>
在一開始, 因為 new 操作符, 所以 this 指向了 app, 通過 with 我們將 app 這個對象放在作用域鏈的頂部, 因為在函數(shù)內(nèi)部我們會多次調(diào)用 app 內(nèi)部的屬性, 所以使用 with 可以縮短變量長度, 提供系統(tǒng)運行效率
其中的 _c 函數(shù)表示的是創(chuàng)建一個新的 HTML 元素, 其基本用法為:
_c(element, { attrs }, [ children... ])
element 表示所要創(chuàng)建的 HTML 元素類型
attrs 表示所要創(chuàng)建的元素的屬性
children 表示該 HTML 元素的子元素
_v 函數(shù)表示創(chuàng)建一個文本節(jié)點
_l 函數(shù)表示創(chuàng)建一個數(shù)組
最終 render 函數(shù)返回的是一個虛擬 DOM
如何將模板渲染為 HTML
模板渲染為 HTML 分為兩種情況
- 第一種是初次渲染的時候
- 第二種是渲染之后數(shù)據(jù)發(fā)生改變的時候, 它們都需要調(diào)用 updateComponent
其形式如下:
app._update(vnode) {
const prevVnode = app._vnode
app._vnode = vnode
if (!prevVnode) {
app.$el = app.__patch__(app.$el, vnode)
} else {
app.$el = app.__patch__(prevVnode, vnode)
}
}
function updateComponent() {
app._update(app._render())
}
首先讀取當前的虛擬 DOM——app._vnode, 判斷其是否為空
- 若為空, 則為初次渲染, 將虛擬 DOM 全部渲染到所對應(yīng)的容器當中(app.$el)
- 若不為空, 則是數(shù)據(jù)發(fā)生了修改, 通過響應(yīng)式我們可以監(jiān)聽到這一情況, 使用 diff 算法完成新舊對比并修改
參考文章: zhuanglog