什么是 MVVM 数据双向绑定
MVC | MVP | MVVM
MVC(Model-View-Controller)
图中简单描述了 MVC 中 Model(数据)、View(视图)、Controller(控制器)三者的关系:
- Model:数据模型,用来存储数据
- View:视图界面,用来展示 UI 界面和响应用户交互
- Controller:控制器,监听模型数据的改变和控制器视图行为,处理用户交互
注意:各部分之间的通信都是单向的。Controller 层触发 View 层时,并不会更新 View 层中的数据,View 层中的数据是通过监听 Model 层数据变化而自动更新的,与 Controller 层无关
MVC 框架主要有两个缺点:
- 大部分逻辑都集中在 Controller 层,给 Controller 层带来很大的压力,而已经有独立处理事件能力的 View 层却没有用到
- Controller 层与 View 层之间是一一对应的,断绝了 View 层复用的可能
MVP(Model-View-Presenter)
- Model:数据模型,数据库接口或远程服务器 api
- View:显示数据并且与用户交互
- Presenter:从 Model 中获取数据并提供给 View,还负责处理后台任务
MVP 模式将 Controller 改为 Presenter,并且各部分之间的通信变成双向的。
在 MVC 中,View 层可以通过访问 Model 层来更新,但在 MVP 框架中,View 层必须通过 Presenter 层提供的接口,然后 Presenter 再去访问 Model
MVP 框架的缺点:
- View 层和 Model 层都需要经过 Presenter 层,导致 Presenter 层比较复杂,维护起来容易出现问题
- 因为没有绑定数据,所有数据都需要 Presenter 层“手动同步”
MVVM(Model-View-ViewModel)
- Model:数据层的域模型,它主要做域模型的同步。
- View:视图模板,View 层不负责处理状态,做的是数据绑定的声明、指令的声明、事件绑定的声明
- ViewModel:ViewModel 层把 View 层需要的数据暴露,并对 View 层的几种声明负责,也就是处理 View 层的具体业务逻辑。
MVVM 的双向数据绑定
可以这样理解:双向数据绑定是一个模板引擎,它会根据数据的变化实时渲染。
如图所示,View 层和 Model 层之间的修改都会同步到对方:
MVVM 模型中数据绑定方法一般有三种:
- 数据劫持
- 发布-订阅模式
- 脏值检查
Vue2.x 中的双向数据绑定
首先了解几个概念:
- Observer:数据监听器,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者
- Compile:指定解析器,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化
- Watcher:订阅者,可以收到属性的变化通知并执行相应的方法,从而更新视图
- Dep:订阅器,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理
Observer
Observer 的实质就是使用 defineProperty
重写对象属性的 getter/setter
方法。但由于 defineProperty
无法监测到对象和数组内部的变化,所以遇到子属性为对象时,会递归观察该属性直至子属性为简单数据类型。
在 vue2.x 版本中对数组的处理方法是重写 push、pop、shift、unshift、splice、sort、reverse
方法。
源码目录 src/core/observer
,首先看下入口函数 observe
:
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 检测数据是否为 'object',如果不是则退出
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe()
方法尝试为 value 创建观察者实例,观察成功则返回新的观察者或已有的观察者,对象被观察后会有 __ob__
属性,用于存储观察者实例。
再来看看 Observer
类:
// can we use __proto__?
// const hasProto = '__proto__' in {} // '../util/env.js'
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 给 value 对象通过 defineProperty 追加 __ob__ 属性
def(value, '__ob__', this)
// 对数组进行特殊处理
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
}
function protoAugment (target, src: Object) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
Observer
类除了属性的定义,就是对数组的特殊处理了。处理的方法就是通过原型链去修改数组的方法(push、pop、shift、unshift、splice、sort、reverse
),并且对数组的每个元素进行 observe()
,因为数组元素也可能是对象,需要递归劫持,直到为基本类型。
至此,defineProperty
对数组内部变化的监听问题就解决了。(关于数组方法的重写,可以看目录 src/core/observer/array.js
)
接着继续看 walk()
函数:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
walk()
函数遍历对象的每个属性,并侵占它们的 getter/setter
,接下来就是数据劫持的重点函数 defineReactive()
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 获取对象的对象描述
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 获取原来的get、set
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 递归:继续监听该属性值(只有val为对象时才有childOb)
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ...,
set: ...
})
}
defineProperty()
先定义了依赖中心 dep
,再获取对象的原生 get/set
方法,然后递归监听该属性,因为当前属性可能是对象,最后通过 defineProperty
劫持 getter/setter
函数
看一下 get/set
方法:
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 添加依赖
dep.depend()
// 如果有子观察者,也给它添加依赖
if (childOb) {
childOb.dep.depend()
// 如果该属性是数组,查看每项是否含有观察者,有则添加依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 判断新旧值是否相等
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (getter && !setter) return
// 设置新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 劫持新值
childOb = !shallow && observe(newVal)
// 发送变更通知
dep.notify()
}
到这,数据劫持就完成了。总的来看,observe()
对数组对象进行了递归遍历,将每个属性的 setter/getter
进行了改造,在赋值操作时会触发订阅中心的通知事件。
Dep
Dep(订阅中心),它要做的事很简单,维护一个容器存储订阅者,源码目录 src/core/observer/dep.js
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加订阅者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移出订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 将自己作为依赖传给目标订阅者
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有订阅者
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
Watcher
Watcher 作为 Observer 和 Compile 之间通信的桥梁,主要做的事是:
- 在自身实例化时往订阅中心添加自己
- 自身必须有一个 update 方法
- 属性变动
dep.notice()
通知时,能调用自身的 update 方法,并触发 Compile 中绑定的回调
源码目录 src/core/observer/watcher.js
let uid = 0
export default class Watcher {
constructor (vm,expOrFn,cb,options,isRenderWatcher) {
this.vm = vm
vm._watchers.push(this)
this.cb = cb
this.id = ++uid // uid for batching
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 指向渲染函数,会触发getter
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
// 关闭
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
// 防止重复添加
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
实例化 Watcher 的时候,调用 get()
方法,通过 pushTarget()
方法(Dep.target = target
)标记订阅者是当前 watcher 实例,强行触发属性定义的 getter 方法,getter 方法执行的时候,就会在属性的 dep
添加当前 watcher 实例,从而在属性变化时,能接收到更新通知。
Watcher 的 addDep()
方法内为了防止重复添加订阅者到订阅中心,维护了一个 Set
用于存储订阅中心的 id。
Compile
待更新
Vue3.x 中的双向数据绑定
Proxy 取代 Object.defineProperty
理由:
- 在 Vue2.x 中使用
Object.defineProperty
无法监测到数组下标的变化,导致 vue 对数组的一些方法进行了重写 Object.defineProperty
只能劫持对象的属性,因此需要对每个对象的每个属性进行遍历,Vue2.x 中通过递归遍历 data 对象对数据监控,对性能有很大的影响
Proxy 实现一个数据绑定和监听:
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value)
return Reflect.set(target, property, value)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`get '${property}' = ${target[property]}`)
})
p.a = 2 // bind 'value' to '2'
p.a // get 'a' = 2
reactive
源码目录 packages/reactivity/src/reactive.ts