本博客为Vue.js设计与实现中第四章的学习笔记。详情请查阅原书。
响应式数据基本结构
响应式数据Proxy
vue3采用proxy代理进行数据的双向绑定,这是一个最基础的proxy:
1 2 3 4 5 6 7 8 9 10
| const data = { name: 'John Doe', age: 25 };
const a = new Proxy(data, { get(target, key, receiver) { }, set(target, key, newVal, receiver) { }, });
|
proxy对data进行代理,在访问属性时触发getter方法,设置属性时触发setter方法。
1 2 3 4 5 6 7 8 9 10 11
| const a = new Proxy(data,{ get(target,key){ console.log("get",target[key]) return target[key]; }, set(target,key,newVal){ target[key] = newVal console.log("set",target[key]) } })
|
响应式数据的副作用函数effect()
副作用函数指的是会产生副作用的函数,effect 函数的执行会直接或间接影响其他函数的执行,这时我们说 effect 函数产生了副作用。
1 2 3 4
| let val = 1; function effect(){ val = 2; }
|
我们希望在响应式数据被触发/修改的同时触发一些其他函数的效果(也就是副作用函数),就需要利用proxy,对get,set方法进行修改。
为了方便使用副作用函数,我们可以将effect存储到一个桶里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const bucket = new Set()
const data = { text: 'hello world' }
const obj = new Proxy(data, {
get(target, key) { bucket.add(effect) return target[key] }, set(target, key, newVal) { target[key] = newVal bucket.forEach(fn => fn()) return true } })
|
这样就实现了一个基本的副作用函数触发机制。
为了能够正常使用,我们再将effect修改为匿名函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| let activeEffect
function effect(fn) { activeEffect = fn fn() }
const obj = new Proxy(data, { get(target, key) { if (activeEffect) { bucket.add(activeEffect) } return target[key] }, set(target, key, newVal) { target[key] = newVal bucket.forEach(fn => fn()) return true } })
|
这样我们就可以在get方法正确向桶里添加副作用函数,并在set里批量执行他们。
存放effect()与target目标字段联系的WeakMap()
此时的桶里存放了所有的副作用函数,在实际操作中应该针对代理对象的不同属性值分别设置对应的effect()。我们应该修改这个数据结构。
如果用 target 来表示一个代理对象所代理的原始对象,用key来表示被操作的字段名,用 effectFn 来表示被注册的副作用函数, 那么可以为这三个角色建立如下关系:
1 2 3 4 5 6 7 8 9
| target1 └── key1 └── effectFn1 └── effectFn1 └── key2 └── effectFn1 target2 └── key3 └── effectFn2
|
我们尝试实现这个新的数据结构:

其中 WeakMap 的键是原始对象 target,WeakMap 的值是一个 Map 实例,而 Map 的键是原始对象 target 的 key,Map 的值是一个 由副作用函数组成的 Set。
用代码实现这个数据结构:
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
| const bucket = new WeakMap();
function track(target, key) { if (!activeEffect) return let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) } let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) }
function trigger(target, key) { const depsMap = bucket.get(target) if (!depsMap) return const effects = depsMap.get(key) effects && effects.forEach(fn => fn()) }
const obj = new Proxy(data, { get(target, key) { track(target, key); return target[key]; }, set(target, key, newVal) { target[key] = newVal trigger(target, key) } })
|
为什么使用WeakMap?
简单地说,WeakMap 对 key 是弱引用,不影响垃圾回收器的工 作。据这个特性可知,一旦 key被垃圾回收器回收,那么对应的键和值就访问不到了。所以 WeakMap 经常用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息,例如上面的场景中,如果 target 对象没有任何引用了,说明用户侧不再需要它了, 这时垃圾回收器会完成回收任务。但如果使用 Map 来代替 WeakMap, 那么即使用户侧的代码对 target 没有任何引用,这个 target 也不 会被回收,最终可能导致内存溢出
分支切换与遗留副作用函数的关系清理cleanup()
副作用函数的依赖关系可能是动态变化的。例这是一个分支切换的例子:
1 2 3
| effect(() => { text = a.ok ? a.name : 'default'; });
|
当a.ok
为true
时,副作用函数依赖a.ok
和a.name
。为false
时,副作用函数仅依赖a.ok
,不再依赖a.name
。
但分支切换可能会产生遗留的副作用函数。在这个例子里,如果没有清理旧依赖,修改a.name
时仍然会触发副作用函数,尽管此时它不再需要a.name
。
解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除,当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用函数。所以,如果我们能做到每次副作用函数执行前,将其从所有相关联的依赖集合中移除,再重新添加新的依赖集合,那么问题就迎刃而解了。
我们可以给副作用函数添加一个属性deps,一个存储所有包含当前副作用函数的依赖集合:
1 2 3 4 5 6 7 8 9 10 11 12 13
| let activeEffect function effect(fn) { const effectFn = () => { activeEffect = effectFn fn() } effectFn.deps = [] effectFn() }
|
并在track函数里进行deps数组依赖集合的收集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function track(target,key){ if(!activeEffect){ return } let depsMap = bucket.get(target) if(!depsMap){ bucket.set(target,(depsMap = new Map())) } let deps = depsMap.get(key) if(!deps){ depsMap.set(key,(deps = new Set())) } deps.add(activeEffect) activeEffect.deps.push(deps) }
|
如以上代码所示,在 track 函数中我们将当前执行的副作用函数 activeEffect 添加到依赖集合 deps 中,这说明 deps 就是一个与 当前副作用函数存在联系的依赖集合,于是我们也把它添加到 activeEffect.deps 数组中,这样就完成了对依赖集合的收集。
我们接着实现副作用函数执行后的依赖删除函数 cleanup():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function cleanup(effectFn){ for(let i =0;i<effectFn.deps.length;i++){ const deps = effectFn.deps[i] deps.delete(effectFn) } effectFn.deps.length = 0 }
function effect(fn){ const effectFn = () => { cleanup(effectFn); activeEffect = effectFn; fn(); } effectFn.deps = [] effectFn() }
|
cleanup 函数接收副作用函数作为参数,遍历副作用函数的 effectFn.deps 数组,该数组的每一项都是一个依赖集合,然后将 该副作用函数从依赖集合中移除,最后重置 effectFn.deps 数组。
至此,我们的响应系统已经可以避免副作用函数产生遗留了。但 如果你尝试运行代码,会发现目前的实现会导致无限循环执行,问题 出在 trigger 函数中:
1 2 3 4 5 6
| function trigger(target, key) { const depsMap = bucket.get(target) if (!depsMap) return const effects = depsMap.get(key) effects && effects.forEach(fn => fn()) }
|
当副作用函数执行时,会调用 cleanup 进行清除,实际上就是从 effects 集合中将当前执行的副作用函数剔除,但是cleanup()
完成后fn()
的执行会触发track导致其重新被收集到集合中,又添加到foreach遍历里,导致死循环。
也就是说只要不将其添加到原来的foreach就可以。我们再设置一个桶用来遍历:
1 2 3 4 5 6 7 8 9
| function trigger(target,key){ const depsMap = bucket.get(target) if(!depsMap) return const depsEffects = depsMap.get(key) const effectsToRun = new Set() effectsToRun&&effectsToRun.forEach(fn => { fn() }); }
|
effect嵌套与effect栈
在实际使用中effect()是可以发生嵌套的。比如vue.js的渲染函数就是在一个effect中执行的。effect嵌套如下所示:
1 2 3 4
| effect(function effectFn1() { effect(function effectFn2() { }) })
|
我们目前的代码是不支持嵌套的。所以我们要把effect()修改成可嵌套的形式,也就是递归。而为了正确处理递归(先执行内层函数再执行外层),我们要引入另一个先进后出的数据结构,也就是栈。
在代码里,我们依旧使用activeEffect作为标识不变,通过修改activeEffect的内容来正确指向当前的副作用函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let activeEffect = null; const effectStack = [];
function effect(fn){ const effectFn = () => { cleanup(effectFn); activeEffect = effectFn; effectStack.push(effectFn); fn(); effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } effectFn.deps = [] effectFn() }
|
这样栈顶指向的一定是当前执行的effect(),成功实现了effect()
嵌套的递归。
防止effect()无限递归
以下是一个简单的自增函数:
但这个函数会引起栈溢出。在这个语句中,既会读取 obj.foo 的值,又会设置 obj.foo的值,而这就是导致问题的根本原因。
此代码的执行流程:读取 obj.foo 的值,触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。于是此effect()再次被触发,重新开始此流程,导致无限递归调用自己。
在此问题里,读取和设置操作是在同一个副作用函数内进行的,因此导致了无限递归。此时读取时的track和设置时的trigger都是使用activeEffect进行标识的。因此,我们可以在 trigger 发生时增加守卫条件: 如果 trigger时的副作用函数与当前正在执行的副作用函数相同,则不触发执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function trigger(target,key){ const depsMap = bucket.get(target) if(!depsMap) return const depsEffects = depsMap.get(key) const effectsToRun = new Set() depsEffects && depsEffects.forEach(effectFn => { if(effectFn !== activeEffect){ effectsToRun.add(effectFn) } }) effectsToRun&&effectsToRun.forEach(fn => { fn() }); }
|
这样我们就能够避免无限递归调用,从而避免栈溢出。
调度配置
调度执行
可调度性是响应系统非常重要的特性。所谓可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。当我们需要在不调整代码的情况下实现以上需求时,就需要相应系统支持调度。
我们可以给effect()
设置一个选项参数options
,允许用户配置内容:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function effect(fn,options = {}){ const effectFn = () => { cleanup(effectFn) activeEffect = effectFn; effectStack.push(effectFn); fn(); effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } effectFn.options = options; effectFn.deps = []; effectFn ; }
|
用户在调用effect()
时可以指定一个调度器,包含一个scheduler 调度函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| effect( ()=>{text = a.name}, { scheduler(fn){ console.log("scheduler") setTimeout(() => { console.log("scheduler settimeout 1000") fn() }, 1000); }, })
|
有了调度函数,我们在 trigger
中触发副作用函数重新执行时,就可以直接调用用户传递的调度器函数,从而把控制权交给用户:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function trigger(target,key){ const depsMap = bucket.get(target) if(!depsMap) return const depsEffects = depsMap.get(key) const effectsToRun = new Set() depsEffects && depsEffects.forEach(effectFn => { if(effectFn !== activeEffect){ effectsToRun.add(effectFn) } }) effectsToRun&&effectsToRun.forEach(fn => { console.log("trigger",fn.options.scheduler) if(fn.options.scheduler){ fn.options.scheduler(fn) }else{ fn() } }); }
|
通过以上修改,我们就可以控制副作用函数的执行了。
懒执行 lazy effect
在有些场景下,我们并不希望立即执行副作用函数,而是希望它在需要的时候才执行,例如计算属性。这时我们可以通过在 options 中添加 lazy 属性来达到目的,当 options.lazy为 true 时,不立即执行副作用函数:
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
| const effectFn = effect( ()=>{text = a.name}, { lazy: true, } )
function effect(fn,options = {}){ const effectFn = () => { cleanup(effectFn) activeEffect = effectFn; effectStack.push(effectFn); fn(); effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } effectFn.options = options; effectFn.deps = [] if(!options.lazy){ effectFn() } return effectFn }
|
通过添加lazy:true
,当调用 effect 函数时,通过其返回值能够拿到对应的副作用函数,这样我们就能手动执行该副作用函数了:
1 2 3 4 5 6 7
| const effectFn = effect( ()=>{text = a.name}, { lazy: true, } ) effectFn();
|
如果仅仅能够手动执行副作用函数,其意义并不大。但如果我们把这个可以手动执行的函数看作一个可以返回任何值的getter函数,就可以取到其返回值:
1 2 3 4 5 6 7
| const effectFn = effect( () => obj.foo + obj.bar, { lazy: true } )
const value = effectFn()
|
为了实现这个目标,我们再对 effect 函数做一些修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function effect(fn,options = {}){ const effectFn = () => { cleanup(effectFn) activeEffect = effectFn; effectStack.push(effectFn); const res = fn(); effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; return res; } effectFn.options = options; effectFn.deps = [] if(!options.lazy){ effectFn() } return effectFn; }
|
我们将真正的副作用函数fn()的结果作为effectFn的返回值,可以拿到副作用函数的执行结果。在此基础上就可以实现计算属性了。
lazy effect实现 计算属性computed()
我们定义一个 computed函数,它接收一个 getter 函数作为参数,我们把 getter 函数作为副作用函数,用它创建一个 lazy 的 effect。computed 函数的执行会返回一个对象,该对象的 value 属性是一个访问器属性,只有当读取 value 的值时,才会执行 effectFn 并将其结果作为返回值返回:
1 2 3 4 5 6 7 8 9
| function computed(getter){ const effectFn = effect(getter,{lazy:true}) const obj = { get value(){ return effectFn() } } return obj }
|
利用computed进行属性计算:
1 2 3 4 5 6 7
| const a = { age:25 } const sum = computed(()=>{ return a.age + 1 }) console.log("computed:",sum.value)
|
它可以进行计算属性功能,但它还没有对值进行缓存。多次调用sum.value
,每次访问都会导致重复计算。因此我们需要添加一个对值进行缓存的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function computed(getter){ let value let dirty = true const effectFn = effect(getter,{lazy:true}) const obj = { get value(){ if(dirty){ value = effectFn() dirty = false } return value } } return obj }
|
通过添加dirty
标识属性是否需要重新计算,并利用value缓存上次计算的值。
我们接着实现computed依赖的响应式数据改变时,重置dirty为true的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function computed(getter){ let value let dirty = true const effectFn = effect(getter,{lazy:true,scheduler:()=>{ if(!dirty){ dirty = true } }}) const obj = { get value(){ if(dirty){ value = effectFn() dirty = false } return value } } return obj }
|
我们为 effect 添加了 scheduler 调度器函数,它会在 getter 函数中所依赖的响应式数据变化时执行,这样我们在 scheduler 函 数内将 dirty 重置为 true,当下一次访问 sumRes.value 时,就会 重新调用 effectFn 计算值,这样就能够得到预期的结果了。
我们理一下此时的函数触发顺序:
const obj = computed(()=>{...})
创建computed,注册副作用函数,由于lazy:true
不执行effectFn()
;
obj.value
首次访问,并通过调度函数scheduler
执行effectFn()
,进而执行getter函数;
- getter函数访问响应式数据,触发数据(如
obj.a
和 obj.b
)的track函数,将 effectFn
收集为它们的依赖;
- 响应式数据(如
obj.a
和 obj.b
)修改时,触发trigger函数,运行它的副作用函数,其中包括了effectFn
;
- 由于设置了调度函数
scheduler
,通过scheduler
重置dirty
的值;
- 下次访问
obj.value
,重新计算effectFn
,得到预期结果。
我们已经实现了computed()的绝大部分内容,但还有一个问题:computed依赖的响应式数据(如 obj.a
和 obj.b
)改变时,不会触发副作用函数的渲染。
分析问题的原因,我们发现,从本质上看这就是一个典型的 effect 嵌套。一个计算属性内部拥有自己的 effect,并且它是懒执 行的,只有当真正读取计算属性的值时才会执行。对于计算属性的 getter 函数来说,它里面访问的响应式数据只会把 computed 内部 的 effect 收集为依赖。而当把计算属性用于另外一个 effect 时, 就会发生 effect 嵌套,外层的 effect 不会被内层 effect 中的响应式数据收集。(人话:外层的obj.value
不会被内层的effectFn()
触发)
当读取计算属性的值时,我们可以手动调用 track 函数进行追踪;当计算属性依赖的响应式数据发生变化时,我 们可以手动调用 trigger 函数触发响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function computed(getter){ let value let dirty = true const effectFn = effect(getter,{lazy:true,scheduler:()=>{ if(!dirty){ dirty = true trigger(obj,'value') } }}) const obj = { get value(){ if(dirty){ value = effectFn() dirty = false } track(obj,'value') return value } } return obj }
|
再理一下这里的track和trigger:
- 访问
sum.value
时,触发track(obj, 'value')
,将当前激活的副作用函数(也就是外部 effect
)收集到 obj.value
的依赖集合中;
- 当计算属性的依赖(如
data.a
或 data.b
)变化时,触发 trigger(obj, 'value')
,obj.value
的副作用函数(如外部 effect
)重新执行。
这里的track到底是怎么添加依赖的?
注意在这里,activeEffect由于effect栈的使用,会自动指向从effectFn()
改成外部 effect
,如以下的例子:
1 2 3 4 5 6 7
| const data = reactive({ a: 1, b: 2 }); const sum = computed(() => data.a + data.b);
effect(() => { console.log(sum.value); });
|
- 外部
effect
的回调函数开始执行,此时 activeEffect
指向该回调函数。
- 回调函数中访问
sum.value
,触发计算属性的 get value()
。
- 在
get value()
中,首次访问会执行 effectFn()
(即计算属性的 getter
函数)。
effectFn()
执行时,activeEffect
暂时切换到 effectFn
。
effectFn()
执行完毕,activeEffect
恢复为外部的 effect
回调函数。
get value()
继续执行,调用 track(obj, 'value')
,此时 activeEffect
仍指向外部的回调函数(computed的渲染函数)。
在数据结构上,对于sum.value
来说,他会建立以下联系:
1 2 3
| computed(obj) └── value └── effectFn
|
如下图所示:

监听方法watch()
watch的实现原理
所谓 watch,其本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。实际上,watch 的实现本质上就是利用了 effect 以及 options.scheduler 选项,以下是一个简单的watch实现:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function watch(source,cb){ effect( ()=>source.name, { scheduler(){ cb() } } ) }
watch(a,()=>{console.log("数据变化")}) a.name = "newname"
|
我们再对source的读取进行修改,使其任意属性值变化都会触发回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function watch(source,cb){ effect( ()=>traverse(source),{ scheduler(){ cb() } }) }
function traverse(value,seen = new Set()){ if(typeof value !== 'object' || value === null || seen.has(value)){return} seen.add(value) for(const k in value){ traverse(value[k],seen) } return value }
|
实际使用中,我们有手动指定watch依赖的响应式数据的需求。我们对watch函数进行修改,不传入响应式数据,而是传入一个触发响应式数据的getter函数,当这些指定的数据变化时再触发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function watch(source,cb){ let getter if(typeof source === 'function'){ getter = source } else{ getter = ()=>traverse(source) } effect(()=>getter(),{ scheduler(){ cb() } }) }
|
watch还有一个重要功能,在回调函数中拿到旧值和新值,我们再次修改代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function watch(source,cb){ let getter if(typeof source === 'function'){ getter = source } else{ getter = ()=>traverse(source) } let oldValue, newValue const job = () => { newValue = effectFn() cb(newValue,oldValue) oldValue = newValue } const effectFn = effect(()=>getter(),{ lazy: true, scheduler:job, }) oldValue = effectFn() }
|
通过以上代码我们就能拿到新旧值了。
立即执行的watch 与回调执行时机
vue关于 watch 有两个特性:一个是立即执行的回调函数,另一个是回调函数的执行时机
首先来看立即执行的回调函数。在 Vue.js 中可以通过选项参数 immediate 来指定回调是否需要 立即执行:
1 2 3 4 5 6
| watch(obj, () => { console.log('变化了') }, { immediate: true })
|
当 immediate 选项存在并且为 true 时,回调函数会在该 watch 创建时立刻执行一次。回到watch的代码我们可以发现,在代码执行逻辑上就是直接触发了一次scheduler调度器。我们修改watch函数:
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
| function watch(source,cb,options = {}){ let getter if(typeof source === 'function'){ getter = source } else{ getter = ()=>traverse(source) } let oldValue, newValue const job = () => { newValue = effectFn() cb(newValue,oldValue) oldValue = newValue } const effectFn = effect(()=>getter(),{ lazy: true, scheduler:job }) if(options.immediate){ job() } else{ oldValue = effectFn() } }
|
这样就实现了回调函数的立即执行功能。由于回调函数是立即执行的,所以第一次回调执行时没有所谓的旧值,因此此时回调函数的 oldValue 值为 undefined,这也是符合预期的。
接下来我们通过其他选项参数来指定回调函数的执行时机。例如在 Vue3中使用 flush选项来指定:
1 2 3 4 5 6
| 1 watch(obj, () => { 02 console.log('变化了') 03 }, { 04 05 flush: 'pre' 06 })
|
flush 本质上是在指定调度函数的执行时机。 当 flush 的值为 ‘post’ 时,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行,我们可以用如下代码进行模拟:
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
| function watch(source,cb,options = {}){ let getter if(typeof source === 'function'){getter = source} else{getter = ()=>traverse(source)} let oldValue, newValue const job = () => { newValue = effectFn() cb(newValue,oldValue) oldValue = newValue } const effectFn = effect(()=>getter(),{ lazy: true, scheduler:()=>{ if(options.flush === 'post'){ Promise.resolve().then(job) } else if(options.flush === 'pre'){ job()} else{job()} } }) if(options.immediate){job()} else{oldValue = effectFn()} }
|
如以上代码所示,我们修改了调度器函数 scheduler的实现方式,在调度器函数内检测 options.flush 的值是否为 post,如果 是,则将 job 函数放到微任务队列中,从而实现异步延迟执行;否则直接执行 job 函数,这本质上相当于 ‘sync’ 的实现机制,即同步执行。对于 options.flush 的值为 ‘pre’ 的情况,我们暂时还没有办法模拟,因为这涉及组件的更新时机,其中 ‘pre’ 和 ‘post’ 原本的 语义指的就是组件更新前和更新后,不过这并不影响我们理解如何控 制回调函数的更新时机。
过期的副作用
竞态问题通常在多进程或多线程编程中被提及,举一个竞态问题的简单例子:
1 2 3 4 5
| let finalData watch(obj,async ()=>{ const res = await axios.get('/react666/') finalData = res.data })
|
如果我们发送了两次请求,且第二次请求比第一次请求先返回,就会导致data最终存储的是第一次请求的结果
,但我们实际上需要最新的数据,也就是第二次请求的结果。为了解决此问题,我们需要一个让第一次请求的结果过期的手段,我们来看看vue的watch是如何实现的。
在 Vue.js 中,watch 函数的回调函数接收第三个参数 onInvalidate,它是一个函数,通过 onInvalidate 函数注册一个回调,副作用函数过期时就会执行此回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| watch(a, async (newValue, oldValue, onInvalidate)=>{ let expired = false onInvalidate(()=>{ expired = true }) const res = await axios.get('/react666/') if(!expired){ finalData = res.data } })
|
接下来我们来看watch是如何处理过期的副作用函数的。在 watch 内部每次检测到变更后,在副作用函数重新执行之前,会先调用onInvalidate函数注册的过期回调函数:
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
| function watch(source,cb,options = {}){ let getter if(typeof source === 'function'){getter = source} else{getter = ()=>traverse(source)} let cleanup function onInvalidate(fn){ cleanup = fn } let oldValue, newValue const job = () => { newValue = effectFn() if(cleanup){ cleanup() } cb(newValue,oldValue,onInvalidate) oldValue = newValue }
const effectFn = effect(()=>getter(),{ lazy: true, scheduler:()=>{ if(options.flush === 'post'){Promise.resolve().then(job)} else if(options.flush === 'pre'){job()} else{job()} } }) if(options.immediate){ job() } else{ oldValue = effectFn() } }
|
onInvalidate
这一块有点绕,我们理一下:
- watch需要在副作用函数过期时执行用户自定义的过期回调函数,于是设置变量
cleanup
- watch设置一个
onInvalidate()
函数,获取用户的过期回调函数并赋值给cleanup
- 用户通过
onInvalidate()
注册自己的过期回调函数
注意用户在使用时,watch(obj, async (newValue, oldValue, onInvalidate) => {... }
,虽然第三个参数为onInvalidate
,但实际上我们真正的回调函数应该填进onInvalidate()
里,onInvalidate
这个参数是给watch方法获取回调用的。
整个流程是这样,假设watch监听的数据变化两次,会触发两次异步操作:
