Vue源码分析之计算属性与侦听函数
上一节我们从头到尾分析了vue的响应式原理,接下来趁热打铁分析下computed、watch的实现原理。如果你对vue的响应式原理还很模糊,建议先阅读上一篇搞明白响应式原理再来看本篇文章,当然computed、watch是对响应式的扩展,响应式懂了这里才不会迷茫
还是一样如果你对于某些原理不是很明白时,学会自己打断点调试,用最简单的例子多分析几遍有助于理解
计算属性
计算属性顾名思义就是由计算得来的值,在vue中通常用来使用需要通过简单计算后的属性,引用vue官方描述:模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的,在模板中放入太多的逻辑会让模板过重且难以维护
简单使用
<template>
<div>{{ name }}</div>
<div>{{ age }}</div>
<div>{{ `我的名字:${this.name},今年 ${this.age} 岁` }}</div>
<div>{{ intro }}</div>
</template>
<script>
export default {
data: () => ({
name: "Jack",
age: 10
}),
computed: {
intro() {
return `我的名字:${this.name},今年 ${this.age} 岁`
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面便是一个最简单的计算属性使用例子,通过计算得到的一个值,使用计算属性模板更易维护也方便后续的扩展
计算属性缓存
应该都听说过计算属性有缓存吧,也就是当计算属性依赖的data值没有发生变化时,是不会重新执行计算属性的函数的,那在页面渲染时也就只会执行一次,我们使用上面的例子简单来验证下:
<template>
<!-- 模板中使用3个计算属性 -->
<div>{{ intro }}</div>
<div>{{ intro }}</div>
<div>{{ intro }}</div>
</template>
<script>
export default {
// 省略...
computed: {
intro() {
console.log("执行了intro函数");// 看这里会打印几次
return `我的名字:${this.name},今年 ${this.age} 岁`;
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
从以上结果可以看到intro
函数只打印了一次,也就是说在模板渲染中对intro函数的执行只触发了一次,这也就是所谓的计算属性缓存
,如果同样的函数定义在方法体中就会执行多遍,严重影响性能
看上去还挺神奇的,接下来就一探究竟👇
计算属性本质
我们知道vue中存在3种watcher分别是:render watcher
、computed watcher
、user watcher
,watcher主要用来执行对应的表达式,而watcher又是被dep控制的,dep大家应该都知道在对data进行getter/setter拦截时所附加的一个对象,用来收集相关联的watcher对象,当触发setter时便通知watcher进行更新,watcher也就会执行对应的表达式进行更新,这就是响应式的过程
那么计算属性其实本质就是一个watcher,由于定义在computed中又被称为计算wathcer
。其实通过响应式过程你大概已经知道了计算属性缓存的原因了,大概就是watcher的更新只被dep控制,只要dep所属的属性值没变就不会通知watcher进行更新,所以在模板渲染时并不会触发计算属性的多次执行,因为computed对应的data值没变,这就是原理
vue内部会统一把computed变成对象形式包含get/set两个函数,如上面的写法其实内部会变成:
intro = {
get() {
return `我的名字:${this.name},今年 ${this.age} 岁`;
},
set: noop // 空函数
}
2
3
4
5
6
通常计算属性的set使用也不多也不推荐使用set,一般一个计算属性一个函数就行了
下面通过一张图来加强理解计算属性的工作过程:
上图清晰的展示了只有data改变时便会通过dep通知计算watcher进行更新也就会执行计算属性的get函数,模板渲染时并不会触发计算属性的多次执行。上图也反映了一个问题:当data改变时只会通知计算watcher进行更新,而计算watcher并不会通知渲染watcher更新(只有dep会收集watcher,watcher不会收集watcher),那么渲染watcher又是如何知道需要重新渲染的呢?你是否也有一样的疑问呢❓
通过这个疑问并根据响应式原理应该能推出dep肯定和渲染watcher也会有关系的,就是这个关系是怎么建立起来的❓从响应式原理中我们知道dep会通过Dep.watcher
这个属性值和watcher产生关系也就是依赖收集,而当计算watcher表达式执行时,当前的Dep.watcher
也只会是计算watcher的get函数,并不会是渲染watcher,dep也就不会收集到渲染watcher,那么dep到底是如何收集到渲染watcher的,接下来带着这个疑问进一步了解下
原理实现
- 组件初始化时对computed属性进行初始化:
// src/core/instance/state 58行
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
// 初始化computed
if (opts.computed) initComputed(vm, opts.computed);
}
2
3
4
5
6
7
- 遍历computed为每一个属性生成对应的watcher(计算watcher):
// src/core/instance/state 137行
function initComputed(vm: Component, computed: Object) {
// 组件实例上定义 _computedWatchers 属性用来存放所有的computedwatcher
const watchers = (vm._computedWatchers = Object.create(null));
// 遍历 computed,生成对应的watcher
for (const key in computed) {
const userDef = computed[key];
// 如果是function就直接取值,反之对象取 get 属性
const getter = typeof userDef === "function" ? userDef : userDef.get;
// 定义当前 key 的watcher, 也就是计算属性watcher
watchers[key] = new Watcher(
vm,
getter || noop, // 这里看到 表达式就是 我们定义的函数或者get函数
noop,
{ lazy: true } // 注意这个选项很重要,用来标识这个是计算属性watcher(记住这个,后面会用到)
);
// 代理每个computed属性到实例上,这就是为什么 this可以获取到计算属性的原因
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 来看看计算watcher实例化时会怎么样,初始化时由于opts传入lazy,所以不会立即执行函数:
// src/core/observer/watcher
export default class Watcher {
vm: Component;
lazy: boolean;
value: any;
// 这里的expOrFn就是计算属性的getter
// cb为空,opts 为 {lazy: true}
constructor(vm: Component, expOrFn: string | Function, cb: Function, opts) {
this.vm = vm;
if (options) {
// lazy: true
this.lazy = !!options.lazy;
}
// 注意这个 dirty 很重要,来缓存计算属性的值,首先进来为 true
this.dirty = this.lazy;
if (typeof expOrFn === "function") {
// getter 变成了 计算属性的get函数
this.getter = expOrFn;
}
// 由于lazy为true,外面初始化时也就不会执行函数
this.value = this.lazy ? undefined : this.get();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 接着第2步骤最后代码,代理computed中的每个key到this实例上,这个步骤很关键也是重点。上一步骤我们知道了computed在初始化watcher时并不会立马执行,那么当组件执行render函数的过程中就会访问computed属性(假设模板中用到了computed属性),这时就会触发下方的getter函数
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
// 值为函数时
if (typeof userDef === "function") {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = noop;
} else { // 值为对象时
sharedPropertyDefinition.get = userDef.get
? userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
// 如果有set函数,没有设置noop 空函数
sharedPropertyDefinition.set = userDef.set || noop;
}
// 设置描述对象,对key进行getter/setter拦截,也就是访问 target[key] 时,就会访问下方的getter函数
Object.defineProperty(target, key, sharedPropertyDefinition);
}
// this实例上获取 computed时,就会触发这里的get
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
};
}
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
首先通过this拿到当前computed属性的watcher,刚开始dirty
的值时true,那么就会执行watcher.evaluate()
,内部会执行watcher的get方法,get中又会执行getter方法,从第3步应该知道this.getter
就是计算属性的函数。在执行计算属性函数过程中就会对data进行访问触发响应式对象对应的dep的depend对当前watcher进行收集,也就是会收集当前计算属性的watcher,最后计算的值赋值给当前watcher的value,这时将dirty设置为false。然后下一步判断Dep.target
为true时执行watcher.depend
,当前计算属性函数执行完后就会将Dep.target变成上一个watcher,这里就假设为渲染watcher,那么来看看watcher的depend干了啥
export default class Watcher {
vm: Component;
lazy: boolean;
value: any;
// 省略部分代码...
get() {
// 设置Dep.target为当前计算属性的watcher
pushTarget(this);
let value;
const vm = this.vm;
// 执行计算属性 函数,此过程会访问响应式对应,就会触发get进行dep进行depend当前watcher(依赖收集)
value = this.getter.call(vm, vm);
popTarget(); // 出栈
return value;
}
// dirty为true时执行
evaluate() {
this.value = this.get();
this.dirty = false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
depend中会遍历所有的deps(也就是收集了当前计算watcher的所有dep,也就是某些对象等key的dep),执行每个dep的depend方法,主动让这些响应式对象对当前的Dep.target
进行收集,上面我们说了当前的target为渲染watcher,也就是这个操作会让响应式对象和渲染watcher产生关系。
export default class Watcher {
vm: Component;
lazy: boolean;
value: any;
// 遍历所有收集了当前计算watcher的dep,执行dep的depend方法主动进行依赖收集
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
再来想想如果render函数中有很多地方用到了同一个计算属性,那么就会再次访问当前实例的计算属性getter,首先还是获取到当前计算属性的watcher,这时dirty值为false,所以就不会再执行watcher.evaluate
也就不会再执行计算属性的函数了,直接返回上一次计算后的结果watcher.value
,这也就是为什么计算属性会缓存的真实面目
// 省略部分代码...
// dirtry为false时,不会执行
if (watcher.dirty) {
watcher.evaluate();
}
return watcher.value;
2
3
4
5
6
- 当对应的响应式对象更新时就会触发计算属性的watcher更新,执行update函数,这里lazy为true顾将dirty设置为true,然后啥都不用做。接着dep会通知渲染watcher进行更新(上面讲了dep会收集渲染watcher),这样就会重新执行render函数,再次会访问计算属性的getter,那么就和第一次页面渲染访问计算属性一样的逻辑,执行计算属性的函数得到最新的值,这里就不再重复了
export default class Watcher {
dirty: boolean;
// 省略部分代码...
update() {
if (this.lazy) {
this.dirty = true;
}
}
}
2
3
4
5
6
7
8
9
到这里计算属性的原理就讲的差不多了,其实很简单。计算属性的执行的重新执行是由对应的data属性的dep控制的,并且dep也会收集渲染watcher。只有data的值改变时计算属性才会执行,否则都会使用上次的值,这样的逻辑也合情合理
小提示
computed可以通过cache:false
来取消缓存,通过它可以设置成不同的getter,这样在每次访问计算属性时都会执行它的函数,当然一般也用不到这个选项,官方文档也没有给出这个选项,这里可以从源码中定义初始化计算属性的getter时会进行判断,感兴趣的可以看看
computed: {
intro: {
get() {
console.log("执行了intro函数");
return `我的名字:${this.name},今年 ${this.age} 岁`;
},
cache: false
}
}
2
3
4
5
6
7
8
9
这样在每次访问intro都会执行,那么就会打印多次
代码调试
这里我们简单调试下上面的代码,来证明下以上的想法,首先是data的dep会收集渲染watcher,另一个就是多次使用同一个计算属性不会再执行
Vue3的computed
Vue3中的computed其实可vue2差不多,唯一一处不一样的就是响应式对象不会直接收集render函数,他只会收集对应的computed函数,而computed函数会收集render函数,因此当响应式对象的值发生变化时更新遵循以下规则:data => computed => render
,其实这样设计能更好的反映出每个对象的真正effect
以下是一个最简单的vue的computed使用例子:
<template>
<button @click="number++">改变number</button>
{{ total }}
</template>
<script>
import { ref, computed } from "vue"
export default {
setup() {
const number = ref(1);
const total = computed(() => number % 5);
return { total }
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个例子定义了一个number
响应式对象,然后定义了一个计算属性total,它是通过number的值并进行计算得到的。因此在页面render时会访问total,total会收集当前的activeEffect
也就是render函数,然后computed初次执行会访问number属性,触发number的get这样number就会收集当前的activeEffect
也就是total中的函数。这样当在页面中点击按钮改变number的值时,number首先会通知computed重新计算,computed在通知render函数重新渲染
当然computed也是有缓存的功能,也是通过dirty
来控制的,只要对应的响应式对象的值不变,dirty就为false不会执行,直接返回上次的值(首次dirty为true);接下来简单分析下整个流程👇:
- computed的内部实现过程,首先但我们用
computed(() => number.value % 5)
这样的代码时先执行computed函数,内部会对传入的参数做统一处理,你可以传函数也可以传对象({get(),set()}
),最后实例化ComputedRefImpl
并返回。ComputedRefImpl实例化首先会创建ReactiveEffect
,并标识当前computed属性,来表示是计算属性
// packages/reactivity/src/computed 112行
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
const onlyGetter = isFunction(getterOrOptions)
// 函数形式
if (onlyGetter) {
getter = getterOrOptions
setter = NOOP
} else {
// 对象形式和vue2一样
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
// 返回当前 ComputedRefImpl 实例
return cRef as any
}
// computed真正的对象,computed.value 就是这个类中的属性
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public _dirty = true
public _cacheable: boolean
constructor(getter, _setter) {
// 创建自己的ReactiveEffect对象
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
}
// 省略...
}
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
- 然后当组件render函数执行时会获取computed的value属性,首先会执行
trackRefValue
函数内部会让当前计算属性的dep
对render函数进行收集,然后由于初次_dirty
为true,就会执行effect.run
也就是ReactiveEffect的run,此过程会设置activeEffect
为当前computed,然后执行computed中我们定义函数。内部会访问number
触发它的get然后对computed的effect进行收集,最后执行完后_dirty
变成false,这样就完成了初次的依赖回收过程
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public _dirty = true
public _cacheable: boolean
// 省略...
// render获取计算属性值时触发
get value() {
const self = toRaw(this)
trackRefValue(self) // 此过程会让computed对render进行收集
if (self._dirty || !self._cacheable) {
self._dirty = false
// 第一次 dirty 为true,会执行run也就是执行computed回调函数,内部会访问响应式对象使computed被对应的data收集
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
// packages/reactivity/src/effect 53行
export class ReactiveEffect<T = any> {
run() {
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
// 改变当前activeEffect为 computed的Effect
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
// 执行computed 回调函数,内部访问number值,触发number对computed的收集
return this.fn()
}
}
}
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
- 当改变number的值时,会派发更新通知number的dep中的所有effect进行更新,这里就会通知computed effect进行更新,但这里不会执行
ReactiveEffect
的run函数,而执行scheduler
函数,scheduler仅仅会将_dirty
的值改为false,然后通知computed自己的dep中的所有effect进行更新,这样render函数重新执行时就和第一次执行一致了:
// number触发自己的依赖进行更新
export function triggerEffects(dep, debuggerEventExtraInfo) {
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
// 执行计算属性
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
// 省略...
}
function triggerEffect(effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo) {
if (effect !== activeEffect || effect.allowRecurse) {
// 注意这个 scheduler 属性很重要,这个在 ComputedRefImpl 实例化时会传入
// 所以它会执行 scheduler,而不是run函数,也就是不会立马执行computed回调,等到render时才会执行
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
export class ComputedRefImpl<T> {
// 部分省略...
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => { // 这个就是 scheduler
if (!this._dirty) {
this._dirty = true
// 上面执行这里时,computed通知自己dep中收集的所有依赖进行更新,这时就会重新执行render
triggerRefValue(this)
}
})
}
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
以上就是vue3的computed整体过程,整体来说vue3的实现更加清晰。vue3中通过track、trigger
effect机制完成依赖收集和派发更新,因此所有对象都可能是effect,而没有像vue2中那样死板。再者就是vue3的computed依赖收集能真正反映出依赖的收集关系,而不像vue2中data还需要额外对render收集一次!
侦听函数
侦听函数在vue中就是我们常用的watch又称user watcher
,之所以这么称呼是因为它也是一个watcher对象并且user的值为true。watch通常用来监听响应式对象,当响应式对象setter执行时会通过dep通知user watcher执行,就会执行我们watch中定义的回调函数,然后再做点什么👀
简单使用
<template>
<button @click="count++">改变</button>
</template>
<script>
export default {
data: () => ({
count: 0,
}),
watch: {
count: "watchCount"
},
methods: {
watchCount(n, o) {
console.log(n, o);
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
你可以能没见过以上watch形式,count
的值仅仅是一个字符串,并不是我们常用的函数或者对象,但是他确实能监听到count的变化,执行的是methods中对应的方法,这种写法后面你就会明白
初始化过程
其实user watch很简单,这里就简单的啃下源码:
// src/core/instance/state 58行
export function initState(vm: Component) {
const opts = vm.$options;
// 如果定义了watch,就进行初始化
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
// 初始化
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key];
// watch属性值为数组的情况
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
// watch 属性的值为普通对象
createWatcher(vm, key, handler);
}
}
}
// 每个watch 创建对应的watch
function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 这里会对handler做统一处理,handler也就是回调函数,可能是函数、对象、字符串
// 如果当前的属性的值(handler)为对象,那么回调函数就为 对象的 handler 属性(这种方式应该用过)
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
// 当前属性的值(handler)为字符串,回调就从当前实例上取
if (typeof handler === "string") {
handler = vm[handler];
}
// 最后执行执行 $watch 方法
return vm.$watch(expOrFn, handler, options);
}
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
这里列举下以上watch的不同的定义形式:
watch: {
// 函数
count() {},
// 数组和字符串,循环时就是字符串;字符串时回调函数会从this上拿,也就是vm['cb1']
// 这就是为什么我们的例子可以写成字符串的形式,其实通过vm['watchCount']会取到methods上的方法
count: ["cb1", "cb2"],
count: "cb1",
// 对象
count: {
handler() {}
}
}
2
3
4
5
6
7
8
9
10
11
12
$watch
我们定义的watch最终内部会调用原型$watch
方法,所以我们还可以使用$watch进行函数侦听:
const unwatch = this.$watch(key, cb, ops);
- $watch是实现侦听函数的核心,那么就来看看它内部具体实现:
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options?: Object): Function {
const vm: Component = this;
options = options || {};
// 设置user为true,这个很重要用来表示当前Watcher为user watcher
options.user = true;
// 创建user Watcher
const watcher = new Watcher(vm, expOrFn, cb, options);
// 如果设置 immediate,会立即执行一次 cb
if (options.immediate) {
cb.call(vm, watcher.value);
}
// $watch 函数会返回一个函数,执行它 会执行 watcher.teardown
return function unwatchFn() {
watcher.teardown();
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 创建userWatcher时会标识当前watcher为user watcher(user = true),并且有cb也就是watch的回调函数。这里最重要的就是
parsePath
对expOrFn
的解析,通常watch用来检测响应式对象,一般都会写对应的key也可以嵌套(如:foo、foo.bar
)。parsePath返回一个函数并赋值给watcher.getter
,然后立即执行getter,也就会执行parsePath返回的函数,执行时会将当前vm传入,然后根据watch监听的key循环获取到对应的响应式属性的值,然后返回。此过程就会触发检测的key对当前userWatcher的收集
export default class Watcher {
vm: Component;
cb: Function;
user: boolean;
value: any;
constructor(vm: Component, expOrFn: string | Function, cb: Function, options?: Object) {
this.vm = vm;
// this.user 为true
if (options) { this.user = !!options.user;}
// cb 就是 watch 的回调函数
this.cb = cb;
// 这里需要注意 expOrFn 通常为 字符串 如:this.$watch("a.b.c", cb, {});
if (typeof expOrFn === "function") { this.getter = expOrFn; }
// 那么就会走这里,这里会通过parsePath将 用户监听的 key 传递进去,最后返回
else { this.getter = parsePath(expOrFn); }
// 最后执行得到值
this.value = this.lazy ? undefined : this.get();
}
}
// parsePath 会返回一个函数
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) { return }
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
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
- 当watch监听的key值被修改后就会让dep通知user watcher进行更新,user watcher就会执行update方法,然后通过异步更新队列策略,最后执行watcher的run方法:
export default class Watcher {
// dep通知watch更新
update() {
if (this.lazy) { this.dirty = true; }
else if (this.sync) { this.run(); }
// 这里user watcheer 会进入更新队列
else { queueWatcher(this); }
}
// 更新队列执行时会执行watcher.run
run() {
if (this.active) {
// 拿到最新的值
const value = this.get();
if (value !== this.value || isObject(value) || this.deep) {
const oldValue = this.value;
this.value = value;
// 如果是user watcher,执行cb
if (this.user) {
this.cb.call(this.vm, value, oldValue);
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
}
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
immediate
immediate会让watch立即执行一次,来看它的实现:
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options?: Object): Function {
// 省略...
const watcher = new Watcher(vm, expOrFn, cb, options);
// 如果设置 immediate,会立即执行一次 cb
if (options.immediate) {
cb.call(vm, watcher.value);
}
//...
};
2
3
4
5
6
7
8
9
事件卸载
watch可以主动进行监听的卸载,watch会返回一个函数,执行它就会卸载监听。原理就是通过watcher的teardown方法遍历当前watcher的所有dep执行移除当前watcher,这样data更新时就不会再被通知更新了:
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options?: Object): Function {
// 省略...
const watcher = new Watcher(vm, expOrFn, cb, options);
// 省略...
// $watch 函数会返回一个函数,执行它 会执行 watcher.teardown
return function unwatchFn() {
watcher.teardown();
};
};
// 来看下teardown的实现
export default class Watcher {
// 遍历当前user watcher所有被收集的dep,执行removeSub这样就会让dep移除对user watcher的收集
teardown() {
if (this.active) {
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
deep
deep选项主要用来深层监听某个属性,比如下面深层监听foo,那么当foo或者bar改变时都会触发user watcher的回调函数执行:
const data = {
foo: {
bar: 1
}
}
this.$watch("foo", cb, { deep: true });
2
3
4
5
6
那么内部是如何实现deep的呢?其实道理很简单只要让深层嵌套的属性的dep也都收集当前的user watcher,那么深层嵌套的属性值改变时也会通知user watcher进行更新。主要就是通过traverse
方法深度遍历嵌套对象访问对应的key就会触发dep收集当前watcher,这样深层嵌套的属性值改变时也会通知watcher进行更新:
export default class Watcher {
// 省略
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}"`);
}
} finally {
// 当执行当前get方法获取到响应式对应的value时,如果用户设置了deep
// 那么就对当前值执行 traverse 方法,内部会循环遍历key,触发它们的get
if (this.deep) {
traverse(value);
}
}
return value;
}
}
// src/core/observer/traverse
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
// 不合法的值直接return
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
// 值是数组或者对象时,深层遍历就会触发每个key的get
// 然后触发每个key的dep执行depend,这样深层嵌套的key就会收集当前的user watcher
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
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
sync
这个属性顾名思义同步执行,通常情况下user watcher会被push到异步更新队列中去,然后按顺序进行更新。当设置了这个属性后,每次监听对象值的更新,都会立即执行user watcher的cb,而不会被推到异步更新队列中去,执行也就会被提前
export default class Watcher {
// 省略...
update() {
if (this.lazy) {
this.dirty = true;
}
// dep通知更新时,由于sync位true,会立即执行run得到最新的值然后执行cb
else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
注意事项
watch的侦听函数中不能再对所侦听的对象进行值的修改,这样就会造成死循环不断执行回调函数:
<template>
<button @click="count++">改变</button>
</template>
<script>
export default {
data: () => ({
count: 0,
}),
watch: {
count: "watchCount"
},
methods: {
watchCount(n, o) {
this.count ++;
console.log(n, o);
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vue3的watch
vue3中的watch其实和vue2的没啥区别,vue3的选项多了一个flush: 'pre' | 'post' | 'sync'
参数,其用来控制回调函数的执行时机,默认值为pre
会推送到queueJob
队列中,在页面渲染前执行;如果值为post
那么会将回调推送到postFlushCbs
中,这会在queue
执行完后再执行postFlushCbs
中的回调,在页面渲染后执行;最后一个值sync
和vue2一样,当watch的对象值改变时会立马执行不需要进入队列异步更新
// packages/runtime-core/src/apiWatch 158行
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
return doWatch(source as any, cb, options)
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
const instance =
getCurrentScope() === currentInstance?.scope ? currentInstance : null
// const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
// 省略代码...
// 1. 先对watch对象的值进行规范化,值可能是 对象、数组、函数等等
// deep时深度遍历
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
// 对回调做一次包装
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// 省略...
} else {
// watchEffect
effect.run()
}
}
// 处理flush 参数,用来区别不同的执行时机
let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
// 创建 ReactiveEffect
const effect = new ReactiveEffect(getter, scheduler)
if (cb) {
// 处理 immediate 情况,立即执行一次
if (immediate) {
job()
} else {
// 否则计算一次旧的值
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
effect.run()
}
// 卸载事件
const unwatch = () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
return unwatch
}
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
watchEffect
watchEffect本质还是watch,只不过参数调换了位置而已,这里更像React的useEffect,内部的回调会主动执行,并且当内部的依赖发生改变时,再次触发回调执行:
watchEffect(effect: (onCleanup: OnCleanup) => void, options?: { flush?: 'pre' | 'post' | 'sync' })
watchEffect原理很简单内部主动会执行一次回调函数,这个过程中就会访问到对应的响应式对象,然后触发对当前回调函数的收集,同理data更新时也会通知回调再次执行。回调函数在内部会被封装一次,将onCleanup
作为参数传入:
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
// source 为function
if (isFunction(source)) {
// 没有cb,不会走这里
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// watchEffect 回调会被封装一次
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
// 这里就会把 onCleanup 作为参数传入
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
}
// 清除副作用
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
const job: SchedulerJob = () => {
if (!effect.active) return
if (cb) { /* 省略... */ } else {
// watchEffect
effect.run()
}
}
const effect = new ReactiveEffect(getter, scheduler)
// initial run
effect.run()
const unwatch = () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
return unwatch
}
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
watchEffect还是很好理解的,剩下的还有watchPostEffect
、watchSyncEffect
都和这个一样,自己看看就行了
总结
到这里就把vue的响应式原理全部讲解完了,其中包括vue3的。响应式遵循发布/订阅模式,通过拦截对象的getter/setter实现对当前的effect进行收集和通知,这样就可以精确的进行更新,这也是为什么vue的更新粒度可以做到组件的原因。如果你对它的响应式还是比较模糊,建议你结合这两篇响应式原理文章尝试写几个例子并进行断点调试,可以帮助你解决自己的疑惑,你也可以在下方留言我会及时回复。接下来一起来看看vue的模板编译过程吧