# 前言
MVVM模型下,VM(ViewModel)控制层核心功能就是现实了数据双向绑定。
- 单向绑定 => 把Model绑定到View,使用JavaScript代码更新数据Model时,View就会自动更新。
- 双向绑定 => 在单向绑定的基础上,用户更新View,数据Model也自动更新。常见的表单输入数据绑定。
# 双向绑定
MVVM:
- Model => 数据、业务逻辑
- View => UI视图
- ViewModel => 关联数据和视图层(框架的核心)
# ViewModel
职责:
- 数据变化 => 更新视图
- 视图变化 => 更新数据
Vue中主要的构成:
- Observer:对数据进行观察监听(Model层处理)
- Compiler:扫描解析元素上的指令(v-model...),根据指令替换模板数据、绑定更新函数(View层指令系统处理)
# 实现流程
new Vue()进行初始化操作,合并配置、初始化生命周期、初始化事件中心、初始化渲染、初始化data、props、computed、watcher...等等,Vue把这些不同的功能逻辑拆成单独的函数进行执行,让整个主线初始化逻辑清晰。
function Vue (options) {
this._init(options)
}
Vue.prototype._init = function (options) {
const vm = this
// merge options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
vm.$mount(vm.$options.el)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
重点关注 => 初始化data(initState)、初始化渲染(initRender)。
- new Vue()执行初始化,对data执行响应化处理,并且对data的每个key创建一个Dep实例,发生Observe中。
- 对模板执行编译,编译动态绑定的数据,从data中获取并初始化渲染,每读取到一个key创建一个Watcher添加到对应Dep中,编译指令同时Watcher绑定更新函数,发生在Compiler中。
- 定义的Watcher => 将来data发生变化时,都是Watcher来调用更新函数并且更新视图。
- 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher,将来data⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数。
# 初始化data
data的初始化主要做两件事:
- 遍历data函数返回对象的,通过proxy把每一个值vm._data.xxx都代理到vm.xxx上,实现通过vm._data.xxx访问data返回函数中对应的属性;
- 调用observe方法监测整个data的变化,变成响应式;
function initState (vm) {
const opts = vm.$options
initProps(vm, opts.props)
initMethods(vm, opts.methods)
initData(vm)
initComputed(vm, opts.computed)
initWatch(vm, opts.watch)
}
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
// 给数据添加一个Observer实例
new Observer(obj);
}
// 给对象的属性添加getter和setter,用于依赖收集和派发更新;
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
// 响应式处理
defineReactive(obj, key, obj[key]);
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 依赖收集
视图中会用到data中某个key,称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集。多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。
function defineReactive(obj, key, val) {
const dep = new Dep();
// Vue2使用的Object.defineProperty劫持数据的变化。
Object.defineProperty(obj, key, {
// 依赖收集Watcher
get() {
// Dep.target也就是Watcher实例
Dep.target && dep.addDep(Dep.target);
return val;
},
// 派发更新通知Watcher
set(newVal) {
if (newVal === val) return;
// 通知dep执行更新方法
dep.notify();
},
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Watcher维护更新视图
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Dep管理Watcher,Dep脱离Watcher单独存在是没有意义的。
class Dep {
constructor() {
// 依赖管理
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 初始化渲染
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数。
# Vue2、Vue3数据绑定机制差异
主要的差异还是在数据的劫持方式。
- Vue2使用的是Object.defineProperty。
- Vue3使用的是Proxy => Proxy可以创建一个对象的代理,从而实现对object基本操作的拦截和自定义。
# Object.defineProperty限制
- 无法劫持新创建的属性 => Vue.set以创建新属性解决。
- 无法劫持数组的变化 => Vue自身对数组原生方法(push、pop、shift、unshift、splice、sort、reverse)进行了劫持,使其支持响应式。
- 无法劫持直接索引修改数组 => 这个问题同样可以用Vue.set解决。
- 每次只能劫持对象一个属性,Vue2需要递归遍历data对象的所有属性依依进行劫持绑定。
# Vue3 -- Proxy
- Vue3使用Proxy实现数据劫持,Proxy可以拦截整个对象,无需递归遍历属性。
- 由于Vue3中改用Proxy实现数据劫持,Vue2中的Vue.set在Vue3中被移除。
# 总结
- 页面上数据发生变化,先找到对应Dep数据依赖,Dep通知对应所有Watcher进行更新。
- 使用发布订阅模式,定义对象之间一对多的依赖关系。data数据发生变化,所有依赖的Watcher都会被通知然后进行更新。
- 数据劫持方式:Vue2-Object.defineProperty、Vue3-Proxy。