本文主要以 vue 的 2.2.4 版本来进行分析。


题外话

本文适合有一定的vue.js使用基础的阅读,再来YY一下本文的主题,何为响应式,响应式就是“我响你应”,☺ 个人想法,结合vue模式来看:

image

前言

vue是一个轻量的MVVM的前端框架,它专注于view层;一个核心特色,当修改模型数据就能反应在view视图上,开发者不需要关心视图展示,只需关注我们熟悉的javascript对象,这会让状态管理非常的简单且直观,那么vue这种响应式原理如何实现的了?我们先看一个简单的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 页面html
<div id="main" @click="cl">
{{message}}
</div>

// vue代码
new Vue({
el: "#main",
data: {
message: "hello world"
},
methods: {
cl: function() {
this.message = "change worlds"
}
}
});

执行这个代码,界面上展示出“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
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
function defineReactive$$1(
obj,
key,
val,
customSetter
) {
// 新建一个Dep实例
var dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = val;
// Dep.target主要作用是能在此方法中调用watcher对象,并添加到的自己的dep队列中,以便在set值时候调用
if (Dep.target) {
dep.depend();
}
return value
},
set: function reactiveSetter(newVal) {
var value = val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
val = newVal;
childOb = observe(newVal);
dep.notify();
}
});
}

结合我们上面的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方法。进而修改视图。
来看一下实现流程图:
image
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
43
var 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
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
43
44
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function(method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator() {
var arguments$1 = arguments;

var i = arguments.length;
var args = new Array(i);
while (i--) {
args[i] = arguments$1[i];
}
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
inserted = args;
break
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) {
ob.observeArray(inserted);
}
// notify change
ob.dep.notify();
return result
});
});

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的响应式原理有一定的认知?当然由于篇幅有限,这里还有很多细节的地方没有细细分析,后续会根据模块来进行具体的分析
image