Vue3响应式 源码解析(一) #122
zhangyu1818
announced in
zh-cn
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
在正文开始之前,先简述一下响应式
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操作符拦截。ownKeysObject.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的分析。Beta Was this translation helpful? Give feedback.
All reactions