第二章、MVVM模式原理
一、MVVM和MVC模式的区别
讲到MVVM模式和MVC模式的区别,网上一大堆讲解的,我只简单讲解一下,MVC模式(Model-View-Controller)就是单向数据流。View ==> Controller ==> Model ==> View 形成闭环,视图层和数据层没有完全分离,View里面包含了Model的一些信息,数据直接驱动视图层变更,但是视图必须通过Controller来改变数据状态。
二、MVVM
Model <==> ViewModel <==> View
演变史:MVC ==> MVP ==> MVVM
- 低耦合高内聚
- 可重用性
- 独立开发
- 可测试
目前前端三大框架Vue、React、angular都是运用MVVM模式
三、双向数据绑定
,双向数据绑定原理:单向绑定 + 事件监听
例如:
// vue中
<input type="text" :value="meg" @input="meg=$event.target.value"> // 单向+监听事件
所以单向绑定也可以转化为双向数据绑定,只不过数据不会自动更新,需要监听事件(如oninput或onchange等)后主动改变数据。
四、双向数据绑定的原理介绍
实现双向数据绑定大致有如下几种
- 发布-订阅者模式(backbone.js)
- 脏值检查(angular.js)
- 数据劫持(vue.js)
Vue的数据劫持
Vue双向数据绑定的核心是运用了Object.defineProperty(),Vue会遍历当前实例的data里面的数据属性,通过Object.defineProperty()设置为访问器属性,然后在该属性的get函数中将其设置为watcher,在set函数中向其他watcher发布改变的消息。
配合发布/订阅者模式,改变其中的某个值,所有的watcher会更新自己,这些watcher也就是绑定dom中的显示信息。
watcher.js
import { pushTarget } from './dep'
export default class Watcher {
getter
cb
constructor (expOrFn, depFn) {
this.cb = depFn
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = () => {
if (/\./.test(expOrFn)) {
let helpData = data
expOrFn.split('.').forEach((path) => {
helpData = helpData[path]
})
return helpData
}
return data[expOrFn]
}
}
this.get()
}
get () {
pushTarget(this)
this.getter()
}
addDep (dep) {
dep.addSub(this)
}
update () {
this.getter()
this.cb && this.cb()
}
}
dep.js
export default class Dep {
subs = []
constructor () {
}
depend () {
Dep.target.addDep(this)
}
addSub (sub) {
this.subs.push(sub)
}
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
Dep.target = null
export function pushTarget (_target) {
Dep.target = _target
}
observer.js(理解)
以下是简写,当然是存在一些问题,但是有助于理解。
import Dep from './dep'
// obj是data方法里面的属性,vm是vue实例
function observer(obj, vm){
Object.keys(obj).forEach(key => {
defineReactive(vm, key, obj[key])
})
}
// 设置访问器属性
function defineReactive(obj, key, val){
let dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// Dep.target 是全局变量指向当前正在解析指令Complie生成的watcher实例
// 将wtacher添加到每个dep实例中,即每个dep实例都有watcher观察者。
if(Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set(newVal) {
if(newVal === val) return
val = newVal
// 发送通知给订阅者列表
dep.notify()
}
})
}
observer.js(源码)
// ......
import Dep from './dep'
// ......
export function defineReactive (obj, key, val) {
const dep = new Dep();
// ......
Object.defineProperty(obj, key, {
get () {
// ......
// 替代原来收集依赖的方式
dep.depend()
// ......
},
set (newVal) {
// ......
// 替代原来触发依赖执行的方式
dep.notify()
}
})
}
双向绑定流程分析
注意
- Dep.target就是Watcher的一个实例(watcher)
- Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数。
理解以上两点对下面讲解流程有很大帮助,这两点暂不在本文章中详解。
依赖收集流程
==>
(这部分没有附上代码,当data数据类型不是对象或者数组的时候就会执行这个walk方法) ==>
(这个方法容易理解,就是将所有数据属性变成访问器属性)==>
(Watcher构造函数中也有个get方法,这个get方法在初始Watcher构造函数(data初始化)时会执行一次的,watcher.getter(),执行首次页面渲染) ==>
==>
(watcher.addDep(new Dep()))==>
(这个newDeps是watcher的方法,起到缓存作用,具体还待研究) ==>
==>
视图更新流程
(数据发生变化时触发) ==>
==>
(当前数据的所有watcher都会调用watcher.update()) ==>
==>
(触发Compile中绑定的回调) ==>
(初始化也会调用这个方法,依赖收集流程有介绍) ==>
(Vue实例更新) ==>
(DOM更新完成)
总结:
通过Object.defineProperty()将data中的每个数据属性转变为访问器属性,在依赖收集时watcher的newDeps数组存了dep,同时dep的subs数组也存了watcher,这就做到了数据变化,遍历dep的subs数组中的每个watcher,因为watcher中也存了dep,能检测到对应的那个数据变化,从而触发compile的callback更新页面。