vue2响应式原理

结合源码记录 vue2 完整的响应式更新页面的周期

概览

先给出整体流程图, 大概有个印象

流程图

初始化阶段

在Vue初始化阶段, _init方法执行时, 会调用initState(vm)方法

1
2
3
4
5
6
7
8
9
// src/core/instance/init.js
Vue.prototype._init = function (options) {
var vm = this;
/* ... **/
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
/* ... **/
};

initState方法主要是对props, methods, data, computed 和 wathcer等属性做了初始化操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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)
}
}

可以看到, 当opts.data存在时就调用了initData(vm)方法, initData方法主要干了一件事. 监听对象, 调用observe方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/core/instance/state.js
function initData (vm: Component) {
/* ... **/
// proxy data on instance
const keys = Object.keys(data)
/* ... **/
let i = keys.length
while (i--) {
const key = keys[i]
/* ... **/
if (props && hasOwn(props, key)) {
/* ... **/
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}

这边顺便提一句, 为什么当我们在data中定义了属性后, 可以通过this.xxx的形式访问到这些属性. 因为在初始化data时, 会调用proxy(vm, '_data', key), 再看proxy方法的实现.

1
2
3
4
5
6
7
8
9
10
// src/core/instance/state.js
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

target就是vm实例, 实际也是拦截vm的get方法, 代理到vm._data下获取属性. 也就是说当调用this.xxx时, 实际获取的值是this._data.xxx

言归正传. 当调用observe方法时, 数据响应式的一大主角就登场了.

Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): 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
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

每个data在初始化后都会插入一个属性__ob__, 用来访问对应的Observer对象, 当data.__ob__不存在时就会去创建对象new Observer(value)

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
// src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
/* ... **/
} else {
this.walk(value)
}
}

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

Observer的constructor方法中调用了this.walk(value), walk方法中又遍历调用了defineReactive方法.

我们都知道vue2的响应式的原理是利用了API 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
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
/* ... **/
const getter = property && property.get
const setter = property && property.set
/* ... **/
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()
/* ... **/
}
return value
},
set: function reactiveSetter (newVal) {
/* ... **/
dep.notify()
}
})
}

去除其余逻辑, 发现defineReactive方法其实干了三件事.

1
const dep = new Dep()
1
2
3
4
5
6
7
8
Object.defineProperty(obj, key, {
get: ()=>{
if (Dep.target) {
dep.depend()
}
return value
}
})
1
2
3
4
5
Object.defineProperty(obj, key, {
set: ()=>{
dep.notify()
}
})

Dep

Dep实际上就是管理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
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = []
}

addSub (sub: Watcher) {
this.subs.push(sub)
}

removeSub (sub: Watcher) {
remove(this.subs, sub)
}

depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

Dep.target是Dep的静态属性, 也是将Watcher与Dep连接起来的关键.

当通过getter方法获取数据时, 调用dep.depend()方法, 实际就是调用Dep.target.addDep(this), 将dep添加给Watcher对象.而Watcher.addDep方法中, 又会调用dep.addSub(this)将Watcher添加给dep.subs

1
2
3
4
5
6
7
8
9
10
11
// src/score/observer/watcher.js
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);
}
}
};

当通过setter方法修改数据时, 调用dep.notify()方法, 实际就是遍历dep.subs也就是与其绑定的所有Watcher并调用update()方法

结合上面的defineReactive方法可以理解成

1
2
3
4
5
6
Object.defineProperty(obj, key, {
get: ()=>{
// 将依赖dep添加给对应的watcher(Dep.target)
// 将watcher添加到dep维护的数组subs中, 以备更新视图时用
}
})
1
2
3
4
5
Object.defineProperty(obj, key, {
set: ()=>{
// 获取跟当前依赖dep相关的watcher(保存在dep.subs中) 更新视图
}
})

另外说明下, 一个Dep实例可以添加给多个watcher, 同时一个Watcher实例也可以添加多个Dep.

1
2
3
4
5
6
export default class Watcher{
deps: Array<Dep>
}
export default class Dep{
subs: Array<Watcher>;
}

收集依赖

现在问题的焦点来到Dep.target上. Dep.target到底是怎么维护的, 我们知道是个Watcher, 并且是全局只维护的唯一一个对象, 它是什么时候被赋值的, 需要更新视图时, 又是如何拿到并更新视图的?

先看Watcher对象是何时创建的.对Watcher的理解根据其用途其实可以分几种, 比如用户watcher, computed watcher及data watcher, 我们主要看的是data watcher.

回到上面说的Vue初始化时会调用this._init方法, _init方法会调用vm.$mount开始创建并渲染组件

1
2
3
4
5
6
7
8
9
// src/core/instance/init.js
Vue.prototype._init = function (options) {
var vm = this;
/* ... **/
callHook(vm, 'created');
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
1
2
3
4
5
6
7
8
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

实际就是调用了mountComponent方法

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
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
/* ... **/
callHook(vm, 'beforeMount')

let updateComponent
/* ... **/
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
/* ... **/
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}

找到了, mountComponent中新建了Watcher对象!

也就是说, 当组件创建过程中, 会新建一个Watcher对象. 那又合适将watcher赋值给Dep.target呢?

Watcher类的constructor方法中会调用this.get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default class Watcher{
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
/* ... **/
this.value = this.lazy
? undefined
: this.get()
}
}

watcher.get方法中就是我们需要的答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/core/observer/watcher.js
get () {
pushTarget(this)
/* ... **/
try {
value = this.getter.call(vm, vm)
} catch (e) {
/* ... **/
} finally {
/* ... **/
popTarget()
/* ... **/
}
return value
}

先调用pushTarget(this), 接着调用this.getter.call(vm, vm), 最后调用popTarget()

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/core/observer/dep.js
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}

export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

this.getter就是初始化时传入的第二个参数

1
2
3
4
5
// src/core/instance/lifecycle.js
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {/* ... **/}, true)

而vm._update就是组件的渲染过程, 期间自然会访问属性的getter方法. 自然也就会触发dep.depend()来收集依赖. 所以watcher.get方法也可以简化为

pushTarget(this)
vm._update(vm._render())
popTarget()

触发依赖

以上是收集依赖的过程, 也就是getter方法的流程. 那么当更新数据时, 又是如何触发依赖使页面更新的呢?

上面说过了, 当修改数据时会触发属性的setter方法并调用dep.notify方法.

1
2
3
4
5
Object.defineProperty(obj, key, {
set: ()=>{
dep.notify()
}
})

而dep.notify又会遍历属性subs去遍历跟当前dep绑定上的所有watcher去调用watcher.update()方法.

1
2
3
4
5
6
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}

update方法接着调用了queueWatcher方法

1
2
3
4
5
6
7
8
9
10
11
12
// src/core/observer/watcher.js
export default class Watcher {
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}

queueWatcher方法中将watcher添加给一个任务队列数组queue, 并在下一个nextTick时遍历queue并调用watcher.run方法

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
// src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
/* ... **/
nextTick(flushSchedulerQueue)
}
}
}

function flushSchedulerQueue () {
/* ... **/
// 这边的排序涉及到watcher的执行顺序
queue.sort((a, b) => a.id - b.id)
/* ... **/
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
/* ... **/
watcher.run()
/* ... **/
}
}

而watcher.run方法, 又会再次调用this.get方法, 回到上面的流程.

1
2
3
4
5
6
// src/core/observer/watcher.js
run () {
/* ... **/
const value = this.get()
/* ... **/
}

最终再次设置dep.target, 最终更新视图

1
2
3
pushTarget(this)
vm._update(vm._render())
popTarget()