前言

不知道写什么,所以没有前言
我们从入口开始讲起

从入口开始

1
2
3
4
5
6
7
8
9
// vue/src/core/instance/index.js
function Vue(options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}

可以看到,vue调用了一个_init方法,并传入了options,那我们转到_init方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// vue/src/core/instance/init.js
export function initMixin(Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {

vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm); // 对数据进行处理
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');


// 如果传入了el, 进行挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}

然后对数据进行处理的是initState函数,那我们转到initState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// vue/src/core/instance/state.js
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

我们可以从执行的函数名看出,这里完成了props,methods,data,computed,watch的初始化,我们来看看data的初始化,我们转到initData

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
45
46
47
48
49
50
51
52
53
54
function initData(vm: Component) {
let data = vm.$options.data;
// 如果data是一个函数,就执行函数
// 并且把data保留到vm._data上
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
// 判断是否是平面对象
// let obj = {} 这种就是平面对象
if (!isPlainObject(data)) {
data = {};
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
// 这个循环是检测是否有重名和占据保留字的
while (i--) {
const key = keys[i];
if (process.env.NODE_ENV !== 'production') {
// 检测是否和methods传入的字段重名
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 检测是否和methods传入的字段重名
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
}
// 这是检测是否占据了保留字
// vue把_和$开头的识别为保留字
else if (!isReserved(key)) {
// 把vm_data上的数据代理到vm上
// 以后访问vm.xxx等同于访问vm._data.xxx
proxy(vm, `_data`, key)
}
}
// observe data
// 这里开始正式观测数据
observe(data, true /* asRootData */)
}

我们可以看到,在经过前面的一堆处理和检测后,observe函数被调用了,并且把data作为参数传了进去,从函数名我们也可以看出,这就是实现数据响应式的函数。
然后我们看看observe函数的实现

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
export function observe(value: any, asRootData: ?boolean): Observer | void {
// 判断是不是对象
if (!isObject(value) || value instanceof VNode) {
return
}
// Observe
let ob: Observer | void;
// 判断value是否已经被观测
// 如果已经观测,value.__ob__就是Observer对象
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
) {
// 如果判断通过就进行观测
// 观测是通过new Observer进行的
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob;
}

在经过了一系列判断后,如果这个对象没有被观测过,就会通过new Observer的方式进行观测,那我们来看看Observe做了什么。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
// 这里的value就是你传入的data对象
constructor(value : any) {
this.value = value;
// 这个dep是个数组用的
this.dep = new Dep();
this.vmCount = 0;
// 定义__ob__
// 这里把定义了访问器属性value.__ob__,值是this,也就是observer
def(value, '__ob__', this);
// 判断value是否是数组
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment;
// 这是把value的原型改成arrayMethods
augment(value, arrayMethods, arrayKeys);
// 调用这个方法来观测数组
this.observeArray(value)
} else {
// 如果value不是数组,就调用walk方法观测value
this.walk(value)
}
}

/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
// 进行普通对象的观测
walk(obj: Object) {
// 获取对象的keys
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
// 调用defineReactive进行某个值的观测
defineReactive(obj, keys[i])
}
}

/**
* Observe a list of Array items.
*/
// 观测数组
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

// 把某个数据弄成访问器属性
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}

在上面我已经把源码放了出来,可以看出,vue会把数组和对象作为两种情况讨论,所以下面我们把情况分为对对象和对数组来进行讨论。
不过我们要先花一点时间了解对一些流程做一个整体的了解。首先我们要知道,在vue里,一个组件对应一个渲染watcher,这个watcher上的update方法执行时,就会重新渲染这个组件,那么这个组件怎么知道什么时候要重新渲染呢,其实是这样的,在这个组件渲染时,这个组件会把它对应的渲染watcher推到Dep.target上,而在访问渲染需要的数据时,会触发定义的getter方法,这个时候,数据会从Dep.target上拿到这个watcher,然后把它保存到一个地方,这个地方就是dep实例,dep.subs会缓存对应的watcher对象,在这个数据的值刷新时,定义的setter方法就会被触发,setter方法会调用dep.notify,这个方法会遍历保存在dep里的watcher,调用watcherupdate方法,对渲染watcher来说,调用update方法就相当于调用了重新渲染组件的方法。这就是数据响应式的一个大致流程,接下来我们来看看源码的实现。

对象的观测

第一层

假设我们有以下的代码

1
2
3
4
5
6
7
8
9
let vm = new Vue({
el : "#app",
data() {
return {
name : "sena",
age : "16"
}
}
})

那么在执行的时候发生了什么呢,我们来看看,首先是调用了new Observer(),然后调用了walk方法,需要关注的代码就只有这些

1
2
3
4
constructor(value : any) {
this.value = value;
this.walk(value)
}

然后在walk方法里,遍历所有属性,用defineReactive定义响应式

1
2
3
4
5
6
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

然后我们看看defineReactive(PS:删除了一部分这种情况不会执行/不重要的代码,之后也会逐步根据不同情况添加代码)

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
export function defineReactive(
obj: Object,
key: string,
val: any
) {
const dep = new Dep();


Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// getter收集依赖
// 获取属性对应的值
const value = val;
// Dep.target会存放当前组件的渲染Watcher
if (Dep.target) {
// 这句话会把Dep.target添加到dep的中
// 也就是保存了watcher
dep.depend();
}
return value
},
set: function reactiveSetter(newVal) {
// setter派发更新
const value = val;
// 判断新旧值是否相等, 相等就不触发派发更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal;
// 这里通知渲染watcher执行update方法,进行组件更新
dep.notify()
}
})
}

通过执行defineReactive,vue的数据得以和watcher关联起来,由此完成了响应式。
显然,如果只完成第一层的观测,代码是很简单的,但是正常情况下要观测的数据比这复杂的多,那我们来看看多层的实现吧

多层实现

实际上,多层实现也没有太复杂,依旧是先调用walk方法,执行defineReactive,但是这里多了一些处理

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
export function defineReactive(
obj: Object,
key: string,
val: any
) {
const dep = new Dep();

+ // 新增代码1 : 递归调用进行观测
+ let childOb = observe(val);

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// getter收集依赖
// 获取属性对应的值
const value = val;
// Dep.target会存放当前组件的渲染Watcher
if (Dep.target) {
// 这句话会把Dep.target添加到dep的中
// 也就是保存了watcher
dep.depend();
}
return value
},
set: function reactiveSetter(newVal) {
// setter派发更新
const value = val;
// 判断新旧值是否相等, 相等就不触发派发更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal;
+ // 新增代码2 : 如果新的值也是对象,也会进行观测
+ childOb = observe(newVal);
// 这里通知渲染watcher执行update方法,进行组件更新
dep.notify()
}
})
}

要完成观测多重嵌套,只要添加两行代码就可以了,我接下来会用一个组件,然后去源码中分别注释这两行,然后观察效果

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
<template>
<div class="home">
<p>name : {{name}}</p>
<p>age : {{age}}</p>
<p>school.name : {{school.name}}</p>
<button @click="click">click me</button>
</div>
</template>

<script>
export default {
name: 'home',
data() {
return {
name : "sena",
age : "16",
school : {
name : "school name"
}
}
},
methods : {
click() {
this.school.name = "another school name";
console.log("click");
console.log(this);
}
}
}
</script>

首先我们注释的是新增代码1,也就是let childOb = observe(val)这一句,注释后组件可以正常渲染

然后我们点击一下按钮,可以看到,尽管school.name已经更新,但是组件没有刷新,这是因为school.name没有被观测

我们把注释去掉,然后点击按钮

这次school.name在改变的同时也触发了更新,我们也可以见到,school.name变成了一个访问器属性,这证明school.name已经通过defineReactive获得了收集依赖和派发更新的能力。
所以我们可以知道,这一行代码的用处是递归处理深层的数据,给深层数据定义响应式。

现在我们去源码里把第二行注释掉,也就是childOb = observe(newVal)这一句,刷新页面后点击按钮,修改了school.name后依旧可以正常更新组件

那是不是意味着没有问题了呢,当然不是,我们改一下代码
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
<template>
<div class="home">
<p>{{school.name}}</p>
<button @click="school = {
name : 'another school name'
}">button1</button>
<button @click="click">button2</button>
</div>
</template>

<script>

export default {
name: 'home',
data() {
return {
name : "sena",
age : "16",
school : {
name : "school name"
}
}
},
methods : {
click() {
this.school.name = "school name";
console.log("click");
console.log(this);
},
}
}
</script>

页面刷新后分别点击button1和button2,可以看到这样的现象
file

诶,为什么点击button2时没有更新页面呢,其实很简单,我们直接修改了school的值,那么school就是一个全新的对象,这个新的对象自然是没有被观测过的,我们从图中可以看到school.name已经不是一个访问器属性了,也就是说修改它时setter不会被触发,也就不能触发组件更新的逻辑。顺带一提,这个时候school仍然是一个访问器属性,因为getter和setter是对应在对象的键上的。

然后我们把注释去掉,就能正常更新了,school.name也在school被重新赋值后被观测
file

数据的观测

第一层

new Observer(value)时,如果value是一个数组,要处理的流程就和是对象的情况不一样了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constructor(value: any) {
this.value = value;
+ // 这个dep是数组专用的,具体使用后面会说
+ this.dep = new Dep();
+ // 定义__ob__, 这个属性会在之后用上
+ // 这里把定义了访问器属性value.__ob__,值是this,也就是observer
+ def(value, '__ob__', this);
+ if (Array.isArray(value)) {
+ // 修改原型
+ value.__proto__ = arrayMethods;
+ this.observeArray(value)
} else {
this.walk(value)
}
}

可以看到,如果要观测的对象是数组类型,会调用observeArray方法,那我们来看看这个方法干了什么。

1
2
3
4
5
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}

observeArray遍历了数组的每一项,并且调用了observe方法去观测他们,我们来看看observe方法

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
export function observe(value: any, asRootData: ?boolean): Observer | void {
// 判断是不是对象
if (!isObject(value) || value instanceof VNode) {
return
}
// Observe
let ob: Observer | void;
// 判断value是否已经被观测
// 如果已经观测,value.__ob__就是Observer对象
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
) {
// 如果判断通过就进行观测
// 观测是通过new Observer进行的
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob;
}

可以看到,observe只会对没有观测过的数据和对象类型的数据进行观测,所以observeArray实际上是观测数组上的对象,并不观测下标,我们可以验证一下。

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
<template>
<div class="home">
<p v-for="(game, index) in gameList" :key="index">{{game.gameName}}</p>
<button @click="click">button</button>
</div>
</template>

<script>

export default {
name: 'home',
data() {
return {
name : "sena",
age : "16",
school : {
name : "school name"
},
gameList : [{
gameName : "GTA6"
}, {
gameName : "MONSTER HUNTER : WORLD"
}]
}
},
methods : {
click() {
console.log(this);
}
},
mounted() {
window.vm = this;
},
beforeDestroy() {
window.vm = null;
}
}
</script>

file

很明显,数组里的[0], [1]都不是访问器属性的,但是数组里的对象被观测了,这就是为什么我们平时通过下标修改数组是不会触发更新的。

那么数组的变化侦测是怎么实现的呢,虽然你可能现在不清楚,但你一定知道我们平时想要在修改数组时触发更新,一般都是通过调用push,shift这样的函数,这是怎么实现的呢?
在new Observer时有这样几句话

1
2
3
4
5
if (Array.isArray(value)) {
+ // 修改原型
+ value.__proto__ = arrayMethods;
+ this.observeArray(value)
}

可以看到,数组的原型被修改成了arrayMethods,那我们来看看arrayMethods的实现

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
45
46
// 数组原来的原型
const arrayProto = Array.prototype;
// 响应式数组原型
export const arrayMethods = Object.create(arrayProto);
// 这些是会修改数组本身的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];

/**
* Intercept mutating methods and emit events
* 拦截数组方法
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 调用原来的方法
const result = original.apply(this, args);
// 这是保存在被观测对象上的Observer
const ob = this.__ob__;
// 这是新插入数组的值
let inserted;
switch (method) {
case 'push':
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对数组的方法做了拦截,如果调用这些会修改数组本身的方法,就会走到被拦截的方法里,然后在观测新添加的值后调用ob.dep.notify(), 通知组件watcher更新,顺带讲点细节问题,还记得Observr的构造函数吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constructor(value: any) {
this.value = value;
// 这个dep是数组专用的,在数组的拦截方法里调用ob.dep.notify();实际上就是调用了这个dep
this.dep = new Dep();
// 定义__ob__, 这个属性会在之后用上
// 这里把定义了访问器属性value.__ob__,值是this,也就是observer
def(value, '__ob__', this);
if (Array.isArray(value)) {
// 修改原型
value.__proto__ = arrayMethods;
this.observeArray(value)
} else {
this.walk(value)
}
}

this.depvalue.__ob__都是有目的的,前者是为了能在数组的拦截方法中访问到dep,用于触发组件更新,后者是为了能在数组的拦截方法中访问到Observer,当然也可以标识这个对象是否已经被观测。

除了上面的地方,我们还要知道,针对value是数组的情况,vue也在defineReactive这里做了一些处理,我们来看一下

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
export function defineReactive(
obj: Object,
key: string,
val: any
) {
const dep = new Dep();

// 递归调用进行观测
let childOb = observe(val);

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// getter收集依赖
// 获取属性对应的值
const value = val;
// Dep.target会存放当前组件的渲染Watcher
if (Dep.target) {
// 这句话会把Dep.target添加到dep的中
// 也就是保存了watcher
dep.depend();
+ if (childOb) {
+ childOb.dep.depend();
+ }
}
return value
},
set: function reactiveSetter(newVal) {
// setter派发更新
const value = val;
// 判断新旧值是否相等, 相等就不触发派发更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal;
// 果新的值也是对象,也会进行观测
childOb = observe(newVal);
// 这里通知渲染watcher执行update方法,进行组件更新
dep.notify()
}
})
}

可以看到在get里加了一个调用childOb.dep.depend的逻辑,那么为什么要这么做呢,我们可以在数组的拦截方法里调用dep.notify,但是问题是,怎么把渲染watcher添加到dep中,也许你会说,在get中使用dep.depend(),然而并不是这样的,在get中调用的dep是依赖这个属性的,在数组拦截的方法中能获得的dep,是observer.dep这个属性,那怎么办呢,vue的做法也很简单,首先递归调用observe,把返回值保存在childOb上,然后在get中添加上面的几行,childOb如果存在,value一定是数组或者对象类型,那么就会调用childOb.dep.depend把渲染watcher保存起来,childOb.dep就是在数组拦截的函数中能获取到的dep,这些方法也会用这个dep触发渲染watcher重新渲染组件。

我们来做个对比,如果,在源码中注释掉新增的这几行
然后就会变成这样

可以从图中看到,就算通过push方法修改了数组也没有触发组件更新,这就是因为observe.dep这个位置上没有保存渲染watcher

然后把注释去掉
file
这次在push后,组件成功刷新,原因就是我上面说的dep里保存了渲染watcher

你以为到这里就结束了?不不不,我们还有最后一个问题要解决,那就是数组套数组的问题

多层数组

我们修改下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="home">
<p>{{arr[0]}}</p>
</div>
</template>

<script>
export default {
name: 'home',
data() {
return {
arr : [[1,2,3],4]
}
},
mounted() {
window.vm = this;
},
beforeDestroy() {
window.vm = null;
}
}
</script>

然后我们测试一下
file
很明显,生产的数组没有保存渲染watcher,那要怎么做呢,我们还要对defineReactive稍加改进

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
45
46
export function defineReactive(
obj: Object,
key: string,
val: any
) {
const dep = new Dep();

// 递归调用进行观测
let childOb = observe(val);

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// getter收集依赖
// 获取属性对应的值
const value = val;
// Dep.target会存放当前组件的渲染Watcher
if (Dep.target) {
// 这句话会把Dep.target添加到dep的中
// 也就是保存了watcher
dep.depend();
if (childOb) {
childOb.dep.depend();
+ if (Array.isArray(value)) {
+ dependArray(value);
+ }
}
}
return value
},
set: function reactiveSetter(newVal) {
// setter派发更新
const value = val;
// 判断新旧值是否相等, 相等就不触发派发更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal;
// 果新的值也是对象,也会进行观测
childOb = observe(newVal);
// 这里通知渲染watcher执行update方法,进行组件更新
dep.notify()
}
})
}

可以看到,新增的代码在判断value是否为Array类型后,调用了一个叫做dependArray的方法,那么这个方法做了什么呢

1
2
3
4
5
6
7
8
9
10
11
12
13
function dependArray(value) {
// 遍历数组
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
// 看看数组这一项上有没有__ob__这个属性(就是说这一项是不是数组或者对象)
// 有就调用dep.depend()
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
// 如果这个项是Array类型就继续递归
dependArray(e);
}
}
}

可以看到,vue的处理是,触发深层所有数组保存的observer.dep.depend()方法,从而让所有的子数组都能保存渲染watcher。
我们去掉源码中的注释,然后再测试一下
file
现在就好了,深层的数组也能被观测了

后记

好啦这次的分享就到这里了,如果有错误的地方,欢迎各位大佬指正,那么下次见(咕咕咕)。顺带祝各位大佬们520快乐。