前言

首先声明一下,本来计划是一月一篇,三月加了一整个月的班,9107的这种,真挺累的。所以四月份就没有写了,而且最近觉得有道云笔记挺好用,所以很多东西都先在有道云里写了。就看后面啥时候有空贴出来。好了,步入正题。其实在这之前呢,就陆陆续续的阅读过vue2的源码,但是看的都零零散散的,这次决定来系统的解读下vue2的源码,希望自己能对vue2掌握的更加深刻以及提升自己的水平。这篇文章主要解读一下vue2的响应式原理,why? 因为这个问题实在是太常见了。😜

过程图

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
有道云笔记的过程图实在太丑了,我之后用其他的软件画一个QWQ
<!--graph TD
A[data/props]
A --> B(defineReactive)
B --> |tracking| D[依赖收集]
B --> |trigger| E[触发]
D --> |depend| F[Dep]
E --> |notify| F[Dep]

G[computed]
G --> H(computedWatcher)
H --> |tracking| I[依赖收集]
H --> |trigger| J[依赖收集]
I --> |depend| F[Dep]
J --> |notify| F[Dep]

K[watch]
K --> L(Watcher)
L --> |update| F[Dep]
L --> |addDep| F[Dep]
L --> |run| M(callback)

N[$mount]
N --> O(render watcher)
N --> P(updateComponent)
N --> |update| F[Dep]
N --> |addDep| F[Dep]-->

Object.defineProperty

Object.defineProperty(),它会在一个对象上定义一个新属性,或者修改一个对象的现有属性并返回此对象

vue的响应式主要是通过Object.defineProperty()来完成的。但由于Object.defineProperty()ES5的内容,所以vue本身兼容不了ie8。这里大致说明一下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
31
32
33
34
35
36
37
38
39
let obj = {
name: 'zh',
age: '23',
}

// 上面的赋值等价于
let obj1 = {};
Object.defineProperty(obj1, 'name', {
enumerable: true,
configurable: true,
writable: true,
value: 'zh'
})
Object.defineProperty(obj1, 'age', {
enumerable: true, // 是否可枚举
configurable: true, // 是否可以被改变以及被删除
writable: true, // 是否可以被赋值
value: 23 // 赋值, 不写value 默认赋值undefined
})

// enumerable,configurable,writable,value被称为数据描述符,
// enumerable,configurable,get(),set()被称为存取描述符。
//enumerable,configurable同时是两种描述符,数据描述符和存取描述符不能同时存在,同时存在时会报错。

Object.keys(obj).forEach(key => {
Object.defineProperty(obj,key,{
// 是一个给key提供的getter方法,当我们 访问 该key值的时候会被触发
get(){
console.log(`get${key}`)
},
// 是一个给key提供的setter方法,当我们 改变 该key值的时候会被触发
set(newVal){
console.log(`set变化,${key}的newVal是${newVal}`)
return newVal;
}
})
})
console.log(obj.age);
obj.name = 'zh1';

当我们给obj设置好了getset之后,我们就可以简单的把这个对象称为是一个响应式对象。那么,vue中的响应式对象究竟是怎么样的呢?

响应式对象入口

想了解响应式,我们先看下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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// vue的最初入口
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方法
}
// 定义了_init方法
initMixin(Vue)
// 添加数据相关处理方法,$set,$delete,$watch
stateMixin(Vue)
// 添加事件方法,$on,$once,$off,$emit
eventsMixin(Vue)
// 添加生命周期方法, _update,$forceUpdate,$destory
lifecycleMixin(Vue)
// 添加渲染相关处理 $nextTick, _render
renderMixin(Vue)

// _init方法内容
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++

let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化生命周期相关
initEvents(vm) // 初始化事件中心相关
initRender(vm) // 初始化渲染相关
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 数据初始化,响应式原理 start
initState(vm) // 可以看到这里是我们的目标
// 数据初始化,响应式原理 end
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

// initState 数据初始化。根据你传入的options的内容决定需要跑哪些对应的数据初始化方法
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 在这里的初始化其实没有啥特别的东西,主要逻辑如下:
// 1. 数据是否重复声明
// 2. 通过proxy处理数据,使得我们可以通过this.xx来访问数据
// 3. 对传入的数据做响应式处理
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)
}
}

// 初始化传入的data数据。
function initData (vm: Component) {
let data = vm.$options.data
// data为什么是以方法传入的?
// vue实例可能是组件。而对于组件来说,每个组件的data应该是独立存在的,不能互相影响。
// 所以说data为什么推荐的是传入function
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
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') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
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
)
} else if (!isReserved(key)) {
// 代理方法
// 我们的this.xx属性实际是访问的this._data.xx
proxy(vm, `_data`, key)
}
}
// observe data
// 重点地方, 响应式处理的方法
observe(data, true /* asRootData */)
}

observe

observe定义在core/instance/observer.js中。我们看下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
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判断是否是对象以及是否为v-dom
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
) {
// 实例化一个Observer类
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

我们可以看到,observe()主要是对传入的value做了是否缓存过的判断,然后返回的仍然是一个Observer实例对象。接下来我们看一下Observer里做了什么处理

Observer

Observer的定义和observe方法在同一个文件中,我们可以看到Observer是一个类。我们看下这个类中做了哪些处理。

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
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

function protoAugment (target, src: Object) {
target.__proto__ = src
}

export class Observer {
value: any;
dep: Dep;
// number of vms that have this object as root $data
vmCount: number;

constructor (value: any) {
this.value = value // 传入值
this.dep = new Dep() // 声明Dep类
this.vmCount = 0
// 绑定Observer实例到value的__ob__属性上
def(value, '__ob__', this)
// todo 看代码注释说是拦截,然后加强对array的处理。具体需要看后面是怎么用的。
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}

/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
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
})
}

从上面可以知道,Observer类就是给传入的object/array设置gettersetter,并没有做什么太多的处理,就是声明了一个Dep类。然后把自身实例设置到value__ob__属性上,最后就是对对象和数组的差异化处理。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 这才是真正声明一个响应式对象的方法
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 判断是否可以改变obj的值
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 触发更新
dep.notify()
}
})
}

从上面我们可以知道,defineReactive()主要是通过Object.defineProperty()obj设置gettersetter。并且还对childObj进行了observe操作,这样就能保证不管obj的结构多复杂,都能够把子属性变成响应式对象并能在访问或修改时及时的触发gettersetter。最后,我们在getter中进行依赖收集的操作,在setter中触发更新的操作。

总结

vue的响应式核心就是通过Object.defineProperty()来对传入对象添加gettersetter。由于篇幅已经挺长的了,之后的依赖收集和触发更新放到下一篇。