0%

Vue响应式分析

示例HTML

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
<!DOCTYPE html>
<html>

<head>
<title>Vue.js</title>
<!-- Delete ".min" for console warnings in development -->
<script src="../../dist/vue.js"></script>
</head>

<body>
<div id="app">
{{message}
</div>

<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
created() {
this.message = '你好'
}
})
</script>
</body>

</html>

接下来我们就要分析在created钩子中改变this.message是如何影响视图变化的

响应式数据的核心defineReactive

在vue中所有对于响应式数据的定义最终都要进入这个方法,这个方法通过Object.defineProperty这个方法定义属性的getter和setter,通过在getter收集依赖,在setter触发依赖来实现响应式数据更新的过程。

我们来看看初次渲染时有哪些位置进入了这个方法。

  1. initRender
    • defineReactive(vm, '$attrs')
    • defineReactive(vm, '$listeners')
  2. initState -> initData -> observe -> new Observer -> observer.walk -> defineReactive
    • 这个过程是对data中的数据进行响应化处理

initState完毕之后,我们再次进入这个方法是通过callHook(vm, 'created'),因为

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
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

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()
}
})
}

当对data中的数据set值时,会调用reactiveSetter方法,如果该值是对象,还会递归响应化子对象,这部分通过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
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
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
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

通过调用dep实例的notify方法通知所有订阅这个数据的watcher,调用watcher的update方法,从而实现数据的响应式更新。而getter是通过在render function中获取data中的数据来实现依赖收集的。

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
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

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

我们在实例化render watcher的时候,调用了this.get方法,dep.target就是这个渲染watcher,然后在调用render function时就可以找到这个watcher,并且调用defineReactive作用域中的dep.depend,调用watcher的addDep,更新watcher的newDepIds属性和newDeps属性,如果这个dep没出现过,那么调用dep的addSub,将watcher作为dep管理的subs(watcher数组)被添加。

1
2
3
4
5
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
1
2
3
4
5
6
7
8
9
10
addDep (dep: Dep) {
const 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方法遍历subs,并调用watcher中的update方法:

1
2
3
4
5
6
7
8
9
10
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

会把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
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
// 避免watcher重复入队
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
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

if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

会在nextTick调用flushSchedulerQueue:

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
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
// flushSchedulerQueue
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

timerfunc定义如下,调度了一个微任务,微任务回调执行flushCallbacks

1
2
3
4
5
6
7
8
9
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}

函数调用栈为空时,执行微任务flushCallbacks:

1
2
3
4
5
6
7
8
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

callbacks只包含一个函数就是我们之前push的这个函数

1
2
3
4
5
6
7
8
9
10
11
12
() => {
if (cb) {
try {
// flushSchedulerQueue
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
}

所以我们执行flushSchedulerQueue方法:

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
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)

// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// 对于renderwatcher有
// 调用before就是触发beforeUpdate的hook
//new Watcher(vm, updateComponent, noop, {
//before () {
//if (vm._isMounted && !vm._isDestroyed) {
//callHook(vm, 'beforeUpdate')
//}
//}
//}, true /* isRenderWatcher */)
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}

// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()

resetSchedulerState()

// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)

// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}

进入watcher.run方法,run方法首先调用get方法,于是又重新触发一波渲染,即执行update(vm.render())方法,修改过后的值便重新显示在页面上。

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
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

总结

响应式数据更新的核心时defineReactive,通过在数据更改时触发setter,调用dep.notify方法,dep管理很多个观测这个数据的watcher,通过触发watcher的update方法,实际上queueWatcher,并在nextTick方法中将回调函数保存在回调队列中,同时利用primise调度一个微任务flushCallbacks,当函数调用栈为空时,执行微任务flushCallbacks,从而执行原来保存的所有callback,即flushSchedulerQueue,这个queue中的元素是watcher实例,调用watcher的run方法,进而调用get方法,触发重新渲染update(vm.render())。