MVVM 框架前生 (MVC 框架)
在介绍 MVVM 框架 之前, 先让我们一起了解一下 MVC 框架
MVC 框架 将整个前端页面分成 View, Controller, Modal
当视图发生变化, 通过 Controller(控件) 将响应传入到 Model(数据源) 中, 由数据源改变 View 上面的数据
整个过程看起来是行云流水, 业务逻辑放在 Model 中, 页面渲染逻辑放在 View 中
什么是 “MVVM 框架”
可以看到 MVVM 分别指 View, Model, View-Model
View 通过 View-Model 的 DOM Listeners 将事件绑定到 Model上
而 Model 则通过 Data Bindings 来管理 View 中的数据
View-Model 从中起到一个连接桥的作用
View 层: 也叫视图层, 在前端开发中, 通常就是 DOM 层, 主要的作用是给用户展示各种信息
Model 层: 也叫数据层, 数据可能是我们固定的死数据, 更多的是来自我们服务器, 从网络上请求下来的数据
Vue-Model 层: 也叫视图模型层, 视图模型层是 View 和 Model 沟通的桥梁, 一方面实现了 Data Binding(数据绑定), 将 Model 的改变实时的反应到 View 中, 另一方面它实现了 DOM Listener(DOM监听), 当 DOM 发生一些事件 (点击、输入、滚动、touch等) 时, 可以监听到, 并在需要的情况下改变对应的 Data
MVVM 与 MVC 的区别
如果你看对 MVC 框架 有所了解的话, 你会发现 MVC 框架 实际运用上却存在一个问题: 那就是 MVC 框架 允许 View 和 Model 直接进行通信!!!
这会造成 View 和 Model 之间随着业务量的不断庞大, 会出现蜘蛛网一样难以处理的依赖关系, 完全背离了开发所应该遵循的 “开放封闭原则”
-
面对这个问题, MVVM 框架 就出现了, 它与 MVC 框架 的主要区别以下两点:
- 实现数据与视图的分离
- 通过数据来驱动视图, 开发者只需要关心数据变化, DOM 操作被封装了
MVVM 原理
MVVM 的实现主要是三个核心点:
- 响应式: Vue 如何监听 data 的属性变化
- 模板解析: Vue 的模板是如何被解析的
- 渲染: Vue 模板是如何被渲染成 HTML 的
响应式
对于 MVVM 来说, data 一般是放在一个对象当中, 例如:
let obj = {
rp: 0
}
当我们访问或修改 obj 的属性的时候, 例如:
console.log(obj.rp) // 访问
obj.rp = 10 // 修改
但是这样的操作 Vue 本身是没有办法感知到的, 那么应该如何让 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]) // 监听
return data[key]
},
set: value => {
console.log("set", value) // 监听
data[key] = value
}
})
}
通过 Object.defineProperty 将 data 里的每一个属性的访问与修改都变成了一个函数, 在函数 get 和 set 中我们即可监听到 data 的属性发生了改变
模版解析
首先模板是什么?
模板本质上是一串字符串, 它看起来和 HTML 的格式很相像, 实际上有很大的区别, 因为模板本身还带有逻辑运算, 比如 v-if, v-for 等等, 但它最后还是要转换为 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 中必须转换为 JS 代码
- 原因在于: 在前端环境下, 只有 JS 才是一个图灵完备语言, 才能实现逻辑运算, 以及渲染为 HTML 页面
这里就引出了 Vue 中一个特别重要的函数 —— render
render 函数中的核心就是 with 函数
with 函数将某个对象添加到作用域链的顶部
如果在 statement 中有某个未使用命名空间的变量, 跟作用域链中的某个属性同名, 则这个变量将指向这个属性值
例如:
let obj = {
name: "feng",
age: 20,
getAddress: () => {
alert("shanghai")
}
}
function fnc() {
with(obj) {
alert(age)
alert(name)
getAddress()
}
}
fnc()
with 将 obj 这个对象放在了自己函数的作用域链的顶部, 当执行下列函数时, 就会自动到 obj 这个对象去寻找同名的属性
而在 render 函数中, 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 这个对象放在作用域链的顶部, 因为在函数内部我们会多次调用 app 内部的属性, 所以使用 with 可以缩短变量长度, 提供系统运行效率
其中的 _c 函数表示的是创建一个新的 HTML 元素, 其基本用法为:
_c(element, { attrs }, [ children... ])
element 表示所要创建的 HTML 元素类型
attrs 表示所要创建的元素的属性
children 表示该 HTML 元素的子元素
_v 函数表示创建一个文本节点
_l 函数表示创建一个数组
最终 render 函数返回的是一个虚拟 DOM
如何将模板渲染为 HTML
模板渲染为 HTML 分为两种情况
- 第一种是初次渲染的时候
- 第二种是渲染之后数据发生改变的时候, 它们都需要调用 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 全部渲染到所对应的容器当中(app.$el)
- 若不为空, 则是数据发生了修改, 通过响应式我们可以监听到这一情况, 使用 diff 算法完成新旧对比并修改
参考文章: zhuanglog