本文主要以 vue 的 2.2.4 版本来进行分析。
题外话
本文适合有一定的vue.js使用基础的阅读,再来YY一下本文的主题,何为响应式,响应式就是“我响你应”,☺ 个人想法,结合vue模式来看:
前言
vue是一个轻量的MVVM的前端框架,它专注于view层;一个核心特色,当修改模型数据就能反应在view视图上,开发者不需要关心视图展示,只需关注我们熟悉的javascript对象,这会让状态管理非常的简单且直观,那么vue这种响应式原理如何实现的了?我们先看一个简单的demo:
1 | // 页面html |
执行这个代码,界面上展示出“hello world”,当我们点击以后,会改变为“change world”,我们会发现只修改了message为“change world”,并没有修改视图的Dom操作,但是界面自动更改。相当于我们修改数据层(model),就会自动响应到界面(view),这就是M-V模式,下面也会介绍V-M模式。
我们先简单说一下原理,首先通过Object.defineProperty将data的message属性添加get和set方法,这样当click时,执行修改message值,会触发set方法,进而执行Dom修改操作。这些都是vue框架帮我们实现,我们只需要关注数据层的修改。
接下来我们从三个方面详细介绍实现过程:
- Observer-监听数据变化
- 观察者模式-Dep订阅容器、Watcher订阅者
- vue中input、textarea等的双向绑定实现
Observer-监听数据变化
如上所说,首先要处理data的属性,Observer类主要作用就是通过Object.defineProperty方法将data的每一个属性都赋予 get 和 set 方法,这样一旦属性被访问或者更新,我们就可以追踪到这些变化。这就是所说的数据劫持。我们来看一下核心代码(关于Dep相关方法,可以先不用理睬,下面会详细介绍):
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象
1 | function defineReactive$$1( |
结合我们上面的Demo来说:
- 当我们取message值时,会执行get方法
- 当我们修改message值时,会执行set方法
我们会看到get和set方法中有其他的代码,那这些代码是做什么用?其实我们Observer类主要是给每一个data属性添加get和set方法,那怎么就体现到页面了?这个Dep类就起了关键作用。下面我们具体看一下:
观察者模式
vue中通过一个简单的观察者模式实现数据层和视图层的关联关系。核心方法是Dep和Watcher,其中Dep函数实现了一个管理Watcher订阅者的队列,负责添加、移除wacher监听对象;Watcher订阅者实现了model数据变化到视图的添加以及更新。
在vue2.0中,每一个模板都会对应生成一个页面视图的渲染方法,看下面的代码块:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 模板
<div id="main" @click="cl">
{{message}}
</div>
// render function
(function() {
with(this) {
return createElement('div', {
attrs: {
"id": "main"
},
on: {
"click": cl
}
}, [createTextVNode(_toString(message))])
}
})
结合demo来看,在vue的实例化过程中,通过对模板的分析生成render方法后,接下来会创建一个watcher实例,会将render方法作为Watcher的参数传;并在Watcher执行过程,首先将当前Watcher赋给Dep.target,然后触发render方法。当执行render方法时,会获取message值,此时会触发message的get方法。在get方法中,会将当前的Watcher对象存入dep的subs数组中。当修改message值时,会触发set方法,然后调用dep.notify,然后执行Watcher的update方法。进而修改视图。
来看一下实现流程图:
Dep代码实现如下(主要代码):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
29
30
31/**
* Dep消息订阅器
*/
var uid$1 = 0;
var Dep = function Dep() {
this.id = uid$1++;
this.subs = [];
};
// sub-watcher订阅者
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};
// 移除订阅者-watcher
Dep.prototype.removeSub = function removeSub(sub) {
remove(this.subs, sub);
};
// 将当前Watcher添加到subs中
Dep.prototype.depend = function depend() {
if (Dep.target) {
// 调用watcher的addDep方法
Dep.target.addDep(this);
}
};
// 遍历当前数据dep的subs数据,通知watcher更新视图
Dep.prototype.notify = function notify() {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
// 调用watcher的update方法
subs[i].update();
}
};
watcher代码实现如下(主要代码):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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43var Watcher = function Watcher(
vm,
expOrFn,
cb,
options
) {
this.vm = vm;
this.newDeps = [];
this.depIds = {};
this.newDepIds = {};
// 更新视图的方法赋给getter函数
this.getter = expOrFn;
this.value = this.get();
};
// 添加数据依赖,并执行视图更新方法
Watcher.prototype.get = function get() {
// 将当前Watcher赋给Dep.target
pushTarget(this);
var value;
var vm = this.vm;
value = this.getter.call(vm, vm);
return value
};
// 将watcher加入队列中然后一一执行, 执行Watcher.run
Watcher.prototype.update = function update() {
queueWatcher(this);
};
// 内部执行get方法
Watcher.prototype.run = function run() {
var value = this.get();
};
// 将watcher添加到对于的dep中
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
总结一下,结合上面的get和set方法代码,可以发现:
- get方法中通过dep.depend将当前的订阅者watcher添加到自己的dep(订阅容器)中。
- 改变值的时候,会调用set方法,通过dep.notify方法。遍历自己的dep容器中的各个watcher。并以此调用watcher.run方法,从而执行get方法。然后调用更新视图方法
另外
由于Object.defineProperty对数组的兼容性问题,当进行数组操作时候,不会触发setter函数,因此vue对数组进行特殊处理,对数组的修改方法进行监听
1 | var arrayProto = Array.prototype; |
vue中input、textarea等的双向绑定实现
上面的内容,我们看了M-V的实现,接下来看一下V-M的实现,我们结合input标签看一下,再解析这个标签过程中,会动态给它添加input事件,因此当用户修改数据时,input事件将监测到数据变化,然后value值赋给message,这样就会触发message的set方法,接下来的过程在上面已经说过了,双向绑定是不是很巧妙、简单。下面代码是它的抽象语法树结构: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// 标签
<input type="text" v-model="message" />
// render function
(function() {
with(this) {
return _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (message),
expression: "message"
}],
attrs: {
"type": "text"
},
domProps: {
"value": (message)
},
on: {
"input": function($event) {
if ($event.target.composing) return;
message = $event.target.value
}
}
})
}
})
最后
我们来一下整体流程图,是不是对vue的响应式原理有一定的认知?当然由于篇幅有限,这里还有很多细节的地方没有细细分析,后续会根据模块来进行具体的分析