You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functioncreateSetter(shallow=false){returnfunctionset(target: object,key: string|symbol,value: unknown,receiver: object): boolean{letoldValue=(targetasany)[key]if(!shallow){value=toRaw(value)oldValue=toRaw(oldValue)// 如果旧值是ref的情况时,ref内部也有set拦截,if(!isArray(target)&&isRef(oldValue)&&!isRef(value)){oldValue.value=valuereturntrue}}else{// in shallow mode, objects are set as-is regardless of reactive or not}// 数组判断判断索引是否存在,对象判断是否有keyconsthadKey=isArray(target)&&isIntegerKey(key)
? Number(key)<target.length
: hasOwn(target,key)constresult=Reflect.set(target,key,value,receiver)// don't trigger if target is something up in the prototype chain of originalif(target===toRaw(receiver)){if(!hadKey){// 无key ADDtrigger(target,TriggerOpTypes.ADD,key,value)}elseif(hasChanged(value,oldValue)){// 有key SETtrigger(target,TriggerOpTypes.SET,key,value,oldValue)}}returnresult}}
在正文开始之前,先简述一下响应式
Proxy
的原理。原理简述
Proxy
是ES6
新增的对象,可以对指定对象做一层代理。我们通过
Proxy
创建的对象,无非会经过2个步骤,达成我们的响应式需求。watch
函数中的依赖。watch
函数。如何收集依赖
在对象通过
Proxy
代理后,我们就可以在读取对象的属性时加上一层拦截,通常形式为:在
get
拦截方法中,我们即可以拿到对象本身target
,读取的属性key
,和调用者receiver
(就是p
对象),在这里我们就能够获取当前访问的属性key
。通常我们会在方法里访问代理后的对象:
当我们执行了
fn
函数后,我们就会触发我们的get
拦截,只需要在get
拦截中记录下当前执行的函数,就可以建立一个key => fn
的映射,后续可以在属性值发生改变后再次调用fn
函数。所以比较疑惑的点是如何在执行我们的
get
拦截的同时,还能获取到是哪一个函数调用了这个代理对象。在
Vue3
的实现中,是使用了一个effect
函数来包装我们自己的函数。为的就是能将调用了代理对象的函数保存下来。
在
get
拦截中还有一个需要注意的点,如果我们需要代理的对象是数组,那么在调用如push
、pop
、includes
等大部分数组方法时,其实都会触发get
拦截,这些方法都会访问数组的length
属性。触发Watch函数
我们会在值修改后触发保存下来的
key => fn
映射的函数。set
拦截会在设置属性值的时触发。其他的拦截方式
除去我们读取属性时的
get
拦截,还需要在其他操作中收集依赖,完善响应式的功能。has
,in
操作符拦截。ownKeys
Object.getOwnPropertyNames()
。Object.getOwnPropertySymbols()
。Object.keys()
。Reflect.ownKeys()
。除去设置属性的
set
拦截来触发依赖函数,还需要在删除属性时也触发。deleteProperty
,删除属性时拦截。除去普通对象和数组的代理,还有一个难点是
Map
和Set
对象的代理。详细的原理实现可以我之前的链接,本文中就不再实现了。
接下来进入正文部分。
源码浅析
Vue3是
Monorepo
,响应式的包reacitvity
是单独的一个包。reactivity
受了以上3个包的启发,刚好我也拜读过observer-util
的源码,reactivity
相对“前辈”做了很多巧妙的改进和功能的增强。shallow
模式,只有第一层值为响应式。readonly
模式,不会收集依赖,不能修改。ref
对象。文件结构
baseHandlers
和collectionHandlers
为功能的主要实现文件,也就是Proxy
对象对应的拦截器函数,effect
为观察者函数文件。本文主要分析的也是这3部分。
对象数据结构
Target
类型为需要Proxy
的原始对象,上面定义了4个内部属性。targetMap
为内部保存收集的依赖函数的一个WeakMap
。它的键名是未经过
Proxy
响应式操作的原始对象,值为key => Set<依赖函数>
的Map
。我们会通过
targetMap
获取当前对象对应的key => Set<依赖函数>
的Map
,从中取出key
对应的所有依赖函数,然后在值发生改变后调用它们。以下4个
Map
是内部记录原始对象Target
到reactive
或readonly
后对象的映射关系。baseHandlers
baseHandlers
这个文件里主要是创建了针对普通对象,数组的Proxy
拦截器函数。先看收集依赖的
get
拦截器。get
在
get
拦截器中,首先是一个巧妙的处理返回ReactiveFlags
对应的值,不需要将它对应的值真正的赋值在对象上,接着会对数组做特殊的处理,收集依赖的函数为track
,它定义在effect.ts
中,在后文会分析这一模块。如果返回的值是对象,则会延迟转换对象。set
set
拦截器里主要是判断设置的key
是否存在,然后分2种参数去trigger
,trigger
函数为触发收集effect
函数的方法,同样定义在effect.ts
中,这里先暂且不提。ownKeys
ownKeys
拦截器同样是收集依赖,需要注意的是传入的key
参数,在target
为数组的时候key
为length
,对象的时候key
为ITERATE_KEY
,ITERATE_KEY
仅为一个symbol
值的标识符,后续会通过这个值来取到对应的effect
函数,实际是不存在这个key
的。effect
本文的原理简述中提到,如果我们想要知道对象被哪一个函数调用了,需要将函数放入我们自己的运行函数中来调用。实际代码中我们是将传入
effect
方法的函数做了一层新的包装,它的类型为ReactiveEffect
。数据结构
其中比较重要的字段为
deps
,如果我们在执行该effectFn
函数收集依赖时,得到了如下的依赖结构:那么我们的
ReactiveEffect
方法effectFn
的deps
属性保存的值就是这2个key
所对应的Set
。effect
函数中通过createReactiveEffect
创建了ReactiveEffect
。在执行
effect
函数前,先将函数保存到全局变量activeEffect
中,这样在函数执行的同时,对应的拦截器在收集依赖的时候就能知道当前是哪一个函数在执行。cleanup
cleanup
方法清除依赖关系。上文提到了
deps
属性的结构,保存的是依赖了effectFn
的Set
,遍历它们,将effectFn
从所有的Set
中删除。track
track
方法收集依赖,功能非常简单,将activeEffect
添加进Dep
。Trigger
trigger
方法执行ReactiveEffect
,内部会做一些类型判断,比如TriggerOpTypes.CLEAR
只存在于Map
和Set
。数组的特殊处理
虽然使用了
Proxy
,但是数组方法还是需要特殊处理,避免一些边界情况,它们并没有重写数组方法。includes, indexOf,lastIndexOf
这3个方法的特殊处理是为了同时能够判断是否存在响应式数据。
为了确保响应式的值和非响应式的值都可以被判断,所以可能会遍历两次。
避免循环依赖
数组的方法基本都会隐式的依赖
lengh
属性,在某些情况可能会出现循环依赖(#2137)。总结
以上为Vue 3响应式的对象和数组拦截的源码浅析,本文只简单分析了
baseHandlers
中重要的拦截器,后续会带来collectionHandlers
的分析。The text was updated successfully, but these errors were encountered: