Vue3 computed和watch怎么实现
computed
computed和watch在面试中经常被问到他们的区别,那么我们就从源码的实现来看看他们的具体实现
//packages/reactivity/src/computed.ts
exportfunctioncomputedT>
(
getterOrOptions:ComputedGetterT>
|WritableComputedOptionsT>
,
debugOptions?:DebuggerOptions,
isSSR=false
){
letgetter:ComputedGetterT>
letsetter:ComputedSetterT>
constonlyGetter=isFunction(getterOrOptions)
if(onlyGetter){
getter=getterOrOptions
setter=__DEV__
?()=>
{
console.warn('Writeoperationfailed:computedvalueisreadonly')
}
:NOOP
}
else{
getter=getterOrOptions.get
setter=getterOrOptions.set
}
//newComputedRefImpl
constcRef=newComputedRefImpl(getter,setter,onlyGetter||!setter,isSSR)
if(__DEV__&
&
debugOptions&
&
!isSSR){
cRef.effect.onTrack=debugOptions.onTrack
cRef.effect.onTrigger=debugOptions.onTrigger
}
//返回ComputedRefImpl实例
returncRefasany
}
可以看到computed内部只是先处理getter和setter,然后new一个ComputedRefImpl返回,如果你知道ref API的实现,可以发现他们的实现有很多相同之处
ComputedRefImpl
//packages/reactivity/src/computed.ts
exportclassComputedRefImplT>
{
publicdep?:Dep=undefined//存储effect的集合
private_value!:T
publicreadonlyeffect:ReactiveEffectT>
publicreadonly__v_isRef=true
publicreadonly[ReactiveFlags.IS_READONLY]:boolean=false
public_dirty=true//是否需要重新更新value
public_cacheable:boolean
constructor(
getter:ComputedGetterT>
,
privatereadonly_setter:ComputedSetterT>
,
isReadonly:boolean,
isSSR:boolean
){
//创建effect
this.effect=newReactiveEffect(getter,()=>
{
//调度器执行重新赋值_dirty为true
if(!this._dirty){
this._dirty=true
//触发effect
triggerRefValue(this)
}
}
)
//用于区分effect是否是computed
this.effect.computed=this
this.effect.active=this._cacheable=!isSSR
this[ReactiveFlags.IS_READONLY]=isReadonly
}
getvalue(){
//thecomputedrefmaygetwrappedbyotherproxiese.g.readonly()#3376
//computedref可能被其他代理包装,例如readonly()#3376
//通过toRaw()获取原始值
constself=toRaw(this)
//收集effect
trackRefValue(self)
//如果是脏的,重新执行effect.run(),并且将_dirty设置为false
if(self._dirty||!self._cacheable){
self._dirty=false
//run()方法会执行getter方法值会被缓存到self._value
self._value=self.effect.run()!
}
returnself._value
}
setvalue(newValue:T){
this._setter(newValue)
}
}
可以看到ComputedRefImplget的get实现基本和ref的get相同(不熟悉ref实现的请看上一章),唯一的区别就是_dirty值的判断,这也是我们常说的computed会缓存value,那么computed是如何知道value需要更新呢?
可以看到在computed构造函数中,会建立一个getter与其内部响应式数据的关系,这跟我们组件更新函数跟响应式数据建立关系是一样的,所以与getter相关的响应式数据发生修改的时候,就会触发getter effect 对应的scheduler,这里会将_dirty设置为true并去执行收集到的effect(这里通常是执行get里收集到的函数更新的effect),然后就会去执行函数更新函数,里面会再次触发computed的get,此时dirty已经被置为true,就会重新执行getter获取新的值返回,并将该值缓存到_vlaue。
小结:
所以computed是有两层的响应式处理的,一层是computed.value和函数的effect之间的关系(与ref的实现相似),一层是computed的getter和响应式数据的关系。
注意:如果你足够细心就会发现函数更新函数的effect触发和computed getter的effect的触发之间可能存在顺序的问题。假如有一个响应式数据a不仅存在于getter中,还在函数render中早于getter被访问,此时a对应的dep中更新函数的effect就会早于getter的effect被收集,如果此时a被改变,就会先执行更新函数的effect,那么此时render函数访问到computed.value的时候就会发现_dirty依然是false,因为getter的effect还没有被执行,那么此时依然会是旧值。vue3中对此的处理是执行effects的时候会优先执行computed对应的effect(此前章节也有提到):
//packages/reactivity/src/effect.ts
exportfunctiontriggerEffects(
dep:Dep|ReactiveEffect[],
debuggerEventExtraInfo?:DebuggerEventExtraInfo
){
//spreadintoarrayforstabilization
consteffects=isArray(dep)?dep:[...dep]
//computed的effect会先执行
//防止render获取computed值得时候_dirty还没有置为true
for(consteffectofeffects){
if(effect.computed){
triggerEffect(effect,debuggerEventExtraInfo)
}
}
for(consteffectofeffects){
if(!effect.computed){
triggerEffect(effect,debuggerEventExtraInfo)
}
}
}
watch
watch相对于computed要更简单一些,因为他只用建立getter与响应式数据之间的关系,在响应式数据变化时调用用户传过来的回调并将新旧值传入即可
//packages/runtime-core/src/apiWatch.ts
exportfunctionwatchT=any,ImmediateextendsReadonlyboolean>
=false>
(
source:T|WatchSourceT>
,
cb:any,
options?:WatchOptionsImmediate>
):WatchStopHandle{
if(__DEV__&
&
!isFunction(cb)){
warn(...)
}
//watch具体实现
returndoWatch(sourceasany,cb,options)
}
functiondoWatch(
source:WatchSource|WatchSource[]|WatchEffect|object,
cb:WatchCallback|null,
{
immediate,deep,flush,onTrack,onTrigger}
:WatchOptions=EMPTY_OBJ
):WatchStopHandle{
if(__DEV__&
&
!cb){
...
}
constwarnInvalidSource=(s:unknown)=>
{
warn(...)
}
constinstance=
getCurrentScope()===currentInstance?.scope?currentInstance:null
//constinstance=currentInstance
letgetter:()=>
any
letforceTrigger=false
letisMultiSource=false
//根据不同source创建不同的getter函数
//getter函数与computed的getter函数作用类似
if(isRef(source)){
getter=()=>
source.value
forceTrigger=isShallow(source)
}
elseif(isReactive(source)){
//source是reactive对象时自动开启deep=true
getter=()=>
source
deep=true
}
elseif(isArray(source)){
isMultiSource=true
forceTrigger=source.some(s=>
isReactive(s)||isShallow(s))
getter=()=>
source.map(s=>
{
if(isRef(s)){
returns.value
}
elseif(isReactive(s)){
returntraverse(s)
}
elseif(isFunction(s)){
returncallWithErrorHandling(s,instance,ErrorCodes.WATCH_GETTER)
}
else{
__DEV__&
&
warnInvalidSource(s)
}
}
)
}
elseif(isFunction(source)){
if(cb){
//getterwithcb
getter=()=>
callWithErrorHandling(source,instance,ErrorCodes.WATCH_GETTER)
}
else{
//nocb->
simpleeffect
getter=()=>
{
if(instance&
&
instance.isUnmounted){
return
}
if(cleanup){
cleanup()
}
returncallWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
}
else{
getter=NOOP
__DEV__&
&
warnInvalidSource(source)
}
//2.xarraymutationwatchcompat
//兼容vue2
if(__COMPAT__&
&
cb&
&
!deep){
constbaseGetter=getter
getter=()=>
{
constval=baseGetter()
if(
isArray(val)&
&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY,instance)
){
traverse(val)
}
returnval
}
}
//深度监听
if(cb&
&
deep){
constbaseGetter=getter
//traverse会递归遍历对象的所有属性以达到深度监听的目的
getter=()=>
traverse(baseGetter())
}
letcleanup:()=>
void
//watch回调的第三个参数可以用此注册一个cleanup函数会在下一次watchcb调用前执行
//常用于竞态问题的处理
letonCleanup:OnCleanup=(fn:()=>
void)=>
{
cleanup=effect.onStop=()=>
{
callWithErrorHandling(fn,instance,ErrorCodes.WATCH_CLEANUP)
}
}
//inSSRthereisnoneedtosetupanactualeffect,anditshouldbenoop
//unlessit'seagerorsyncflush
letssrCleanup:(()=>
void)[]|undefined
if(__SSR__&
&
isInSSRComponentSetup){
//ssr处理...
}
//oldValue声明多个source监听则初始化为数组
letoldValue:any=isMultiSource
?newArray((sourceas[]).length).fill(INITIAL_WATCHER_VALUE)
:INITIAL_WATCHER_VALUE
//调度器调用时执行
constjob:SchedulerJob=()=>
{
if(!effect.active){
return
}
if(cb){
//watch(source,cb)
//获取newValue
constnewValue=effect.run()
if(
deep||
forceTrigger||
(isMultiSource
?(newValueasany[]).some((v,i)=>
hasChanged(v,(oldValueasany[])[i])
)
:hasChanged(newValue,oldValue))||
(__COMPAT__&
&
isArray(newValue)&
&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY,instance))
){
//cleanupbeforerunningcbagain
if(cleanup){
//执行onCleanup传过来的函数
cleanup()
}
//调用cb参数为newValue、oldValue、onCleanup
callWithAsyncErrorHandling(cb,instance,ErrorCodes.WATCH_CALLBACK,[
newValue,
//passundefinedastheoldvaluewhenit'schangedforthefirsttime
oldValue===INITIAL_WATCHER_VALUE
?undefined
:isMultiSource&
&
oldValue[0]===INITIAL_WATCHER_VALUE
?[]
:oldValue,
onCleanup
])
//更新oldValue
oldValue=newValue
}
}
else{
//watchEffect
effect.run()
}
}
//important:markthejobasawatchercallbacksothatschedulerknows
//itisallowedtoself-trigger(#1727)
job.allowRecurse=!!cb
letscheduler:EffectScheduler
if(flush==='sync'){
//同步更新即每次响应式数据改变都会回调一次cb通常不使用
scheduler=jobasany//theschedulerfunctiongetscalleddirectly
}
elseif(flush==='post'){
//job放入pendingPostFlushCbs队列中
//pendingPostFlushCbs队列会在queue队列执行完毕后执行函数更新effect通常会放在queue队列中
//所以pendingPostFlushCbs队列执行时组件已经更新完毕
scheduler=()=>
queuePostRenderEffect(job,instance&
&
instance.suspense)
}
else{
//default:'pre'
job.pre=true
if(instance)job.id=instance.uid
//默认异步更新关于异步更新会和nextTick放在一起详细讲解
scheduler=()=>
queueJob(job)
}
//创建effecteffect.run的时候建立effect与getter内响应式数据的关系
consteffect=newReactiveEffect(getter,scheduler)
if(__DEV__){
effect.onTrack=onTrack
effect.onTrigger=onTrigger
}
//initialrun
if(cb){
if(immediate){
//立马执行一次job
job()
}
else{
//否则执行effect.run()会执行getter获取oldValue
oldValue=effect.run()
}
}
elseif(flush==='post'){
queuePostRenderEffect(
effect.run.bind(effect),
instance&
&
instance.suspense
)
}
else{
effect.run()
}
//返回一个取消监听的函数
constunwatch=()=>
{
effect.stop()
if(instance&
&
instance.scope){
remove(instance.scope.effects!,effect)
}
}
if(__SSR__&
&
ssrCleanup)ssrCleanup.push(unwatch)
returnunwatch
}
到此,关于“Vue3 computed和watch怎么实现”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: Vue3 computed和watch怎么实现
本文地址: https://pptw.com/jishu/301632.html
