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
function toRefs(target) {
//// 其实就是遍历这个传入对象的所有属性并调用toRef函数
const res = isArray(target) ? (new Array(target.length)) : {}
for(const k in target) {
res[key] = toRef(target, key)
}
return res
}
在上一节我们通过实现一个简易的
reactive
函数,大致了解了Vue3响应式处理的操作,尽管对源码的还原度没有那么高,比如在源码中对数组的方法包裹等。但是对于我们了解Vue的处理和响应式操作来说是没有问题的,更何况这就是对源码的核心逻辑的抽离,让我们读起来更加的通俗易懂。那么这一小节,我们将继续补全上节没有说到的另外两个响应式API:
ref
函数和computed
函数。1. 复盘
在此之前,让我们先对上一节讨论的内容做一个小小的回忆:
要实现响应式操作就要用到我们常用的响应式API,如
reactive
函数reactive
函数要接收复杂类型,对于普通类型则会直接返回。reactive
函数会返回一个proxy
实例对象,与此同时会对这个proxy
进行缓存。proxy
的时候会对所代理的源对象创建getter
和setter
的拦截器,在进行获取或者修改的操作的时候就会触发拦截器。getter
拦截器中要收集状态的依赖(副作用函数),通过track
函数将其存储起来,存储依赖的数据结构要理解并记住setter
中通过trigger
函数触发依赖的重新执行,更新视图(后续我们还要知道这一步会进行复杂的处理,不定期还会再写关于这方面的内容)依赖就是一个函数,被称为副作用函数,简单理解一下就是:对数据进行依赖,数据变化就会产生副作用这是符合我们的思维模式的。就像:
我饿了,要吃饭。饿了就是状态变更,要吃饭就是我所体现出的副作用行为
。getter
,getter
又会调用track
函数存储函数和对象间的关系,因此在执行effect
函数的时候要对当前的effect
函数用变量保存起来,因为这样才能在track
函数中访问到,才能维护依赖和状态间的关系。setter
,在setter
中会通过调用trigger
函数触发源对象的依赖集合的执行,这样就能更新视图,实现响应式操作。那么事不宜迟开启我们今天的内容
2. Ref函数
ref
函数也是用于创建响应式数据的API,与reactive
不同的是ref
函数可以接收普通类型的数据,尽管ref
函数也仍然可以处理复杂类型的数据,不过这并不是一个好的选择。1. 区别
ref
函数既可以创建原始基础类型的响应式数据也可以创建复杂类型的响应式数据reactive
函数中对响应式的操作是重写proxy
拦截器,而在ref
中实现拦截操作的却与之不同ref
函数创建的响应式的数据被处理为一个有着value
属性的值,因此访问值需要state.value
这样才能获取到值ref
函数实际上返回的是一个新的对象,而reactive
函数返回的其实是这个源对象的代理记不住或者看蒙了不要紧,我们在后面的实现中,会一步步的对上面的内容进行解释
2. 理论准备
我们看到
ref
函数是可以对复杂类型做响应式的,这个原理在官网-ref响应式核心中 写出如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象
其实就是说:如果处理的是个复杂类型的数据,那响应式的处理还是交给
reactive
函数实现一个
ref
函数和reactive
函数的思路并不相同,实际上ref
函数的实现是和vue2相似的。对你没看错,在
ref
函数中的响应式是依靠于class
关键字定义的实例对象的get
和set
拦截器去拦截操作,那也不是像vue2那样使用的defineProperty
啊?class
关键字是ES6推出的,在我们的项目中会babel转义为es5代码的,那对象的get和set不就是在defineProperty
中的操作吗所以
ref
的实现你可以说是借助了defineProperty
,这点要区分一下不能混。That's all 那我们来动手实现吧
3. 实现
构造
RefImpl
类,我们响应式的核心就在这一模块基础的逻辑就是这样的
_value
用来做我们传入值的代理;_rawValue
用来存储原始值,在检查值是否修改的时候会用到在继续补全之前先来了解两个函数:
toRaw
和toReactive
toRaw
:如果看过Vue的官网,那应该知道这个API,响应式 API:进阶 | Vue.js 。简言之就是返回响应式对象的原始对象toReactive
:这并不是一个导出的函数,不过你翻看源码 vuejs/core/packages/reactive 差不多第400行的位置就能看到这个函数,先来实现这两个函数,再继续实现
RefImpl
类1.
toRaw
函数作用:返回一个数据的原始数据。
举个例子:
'__v_raw'
属性)raw
变量就一定能拿到值,这个值就是响应式对象的原始值!你可能会好奇这个
'__v_raw'
标识是什么,这是Vue3中用来标记响应式对象的标识,所以如果这个传入的数据拥有这个属性那么这不就意味着这是一个响应式的对象我们仍然需要递归拿到它的原始数据嘛!OK!get it!!
ReactiveFlags
,别担心,这并不影响你理解2.
toReactive
这个函数更简单!理论准备部分已经讲了:
ref
函数既可以处理普通类型也可以处理复杂类型,这个原理就是要判断传入的数据是否是一个对象,如果是对象那就使用reactive
函数进行包裹,如果不是那就返回这个值即可。所以这个toReactive
函数的目的就是判断是否是一个对象并进行转化的操作。好了!了解完上面的两个函数之后就可以继续进行下一步了。不过你肯定好奇为啥一定要实现这两个函数,别急这里就用上了。
存储_rawValue:
toRaw
在
RefImpl
的构造函数中传入的这个value
数据可能是个普通类型也可能是个复杂类型,也可能是个已经被代理的对象。但不论是什么类型,toRaw
函数都能找到它的原始数据,然后将这个原始数据赋给_rawValue
,这样在后续的比对数据是否更改的时候就能使用这个_rawValue
进行判别了存储_value:
toReactive
在构造函数中调用
toReactive
函数,toReactive
函数会返回一个proxy
或者是原始的数据(如果没看懂,请回看一下toReactive
函数实现的解释),然后将其赋给_value
用作数据的代理,后续的操作就会直接使用_value
3. 依赖收集
和先前的一样,在
get
拦截器中调用track
函数收集依赖函数实际上这里这样写是和最新版的源码是有出入的,在源码中函数功能的粒度要更细一些,比如收集依赖的时候会分为
track
函数和trackEffects
函数。调用的的收集函数也是有出入的,在
ref
源码中收集依赖调用的是trackEffects
函数并传递的是一个通过调用createDep
函数创建的dep
集合,在这个createEffects
函数中会对dep
这个集合添加activeEffect
也就是当前正在执行的effect
,其实原理是一样的。最核心的区别不过就是源码是依靠ReactiveEffect
这个类做的副作用依赖而我们是采用函数因为会更简单一些!这里只是顺带提一嘴,不用纠结和源码的出入,咱们这么写就是最简就是最容易理解的!
4. 变更执行
显然的,状态变更触发依赖的重新执行要调用
trigger
函数,在哪里调呢?就是在set
拦截器中。在构造函数中存储的原始值
_rawValue
在这里派上了用处,我们要用它来比对是否更改不过在这里还是有些小问题:
toReactive
函数进行转换,以及存储_rawValue
是否需要调用toRaw
进行转换,在这里还需要额外的处理。useDirectValue
直接翻译过来就是:使用直接的值吗,或者说成是否直接使用值,这个值就是你修改传入的值。然后基于这个useDirectValue
进行判定为什么需要转换的判定呢?
因为有些时候传入的值就是一个普通的类型那你进行
toReactive
转换或者toRaw
转换就没有意义了还有的时候你传入的是浅层或者只读的响应式对象,那么也仍然是不需要处理(为什么呢?在下面的判定条件部分解释了)
还有的时候你传入的是一个普通的响应式对象,那就需要处理了,需要对响应式对象进行转化。
好了那优化一下上面的代码
4. 其他的Ref API
说完了
ref
函数的基本构造,我们来补充和Ref
相关的API,分别是1. 理论准备
shallowRef
像前一章写过的shallowReactive
一样,shallowRef
也是一个浅层的代理,只对.value
做出响应式处理toRef
官网的解释:基于响应式对象上的一个属性,创建一个对应的ref
响应式对象。这样创建的ref
与其源属性保持同步:改变源属性的值将更新ref
的值,反之亦然。toRefs
官网的解释:将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的ref
。每个单独的ref
都是使用toRef()
创建的。我们用代码来解释要更为简单一些
2. 实现本体
1.
shallowRef
shallowRef
其实就是浅层代理,并不会对数据进行深层观测代理,看到这句话想没想到上面说的那个useDirectValue
判定条件?那个不就是判断是否是一个浅层代理或者一个只读的属性的吗,如果它是一个true
那就不会对值进行深度检测,那自然就是一个shallowRef
了在编写
shallowRef
的同时我也来重构一下上面的ref
创建的代码,使其更像源码,顺便就把shallowRef
的创建解释一下有了
__v_isShallow
属性就可以用它去判断useDirectValue
了,改动点在构造函数和set拦截器部分的注释部分2.
toRef
toRef
函数的实现就略有不同了,但是明确toRef
的作用:基于响应式对象上的属性创建ref
并将更改同步映射到源对象上直接引出实现:
ObjectRefImpl
,就不过多解释,代码解释就足矣了3.
toRefs
toRefs
就简单多了,就是利用了toRef
函数,如果你理解了toRef
,那么toRefs
其实就是一个循环的事情这样就是
toRefs
函数的实现,其实就是循环调用toRef
函数返回的都是一个ref
对象它的值就是这个响应式对象的对应的属性值,因此这个res结果的每个属性都是一个ref
对象,因此你解构取值也依然会存在响应式所以如果再有人问你为什么
toRefs
函数解构出属性的时候仍然会保留响应式,你就告诉他因为toRefs
函数对源对象的每个属性都创建了一个ref
对象,因此你解构出来的是这个ref
对象并不是源响应式对象的属性!(如果他还听不懂让他来看这篇🤣🤣🤣)3. computed函数
先简单演示一下
computed
的使用可以看出
computed
的值也是需要通过value
属性获取的,所以computed
返回的也是一个ref
对象getter
,那么这就是一个只读的computed
get
和set
属性的对象那么这就允许你做出更改1. 特点
computed
重新执行computed
的变量,computed
不会重复执行返回值来源于缓存2. 理论准备
对于
computed
函数的创建和正常运行是会涉及到effect
部分,如果有些遗忘或者还没看过上一篇的内容建议先看一看 通过实现最简reactive,学习Vue3响应式核心 - 掘金 (juejin.cn)computed
要实现的点v3和v2中的
computed
函数的实现有很大的出入,这点知道就好并不需要去翻源码,因为我刚看了一遍源码中2和3的写法,我认为3比2要容易懂得多。如果感兴趣也可以去看看vue2/computed & vue3/computed3. 实现
1.
ComputedRefImpl
看起来不难,所以就直接边写边解释吧
你可能觉得这么多个属性都有个啥子用啊!像
__v_isRef
,__v_isReadonly
啥的感觉都没啥用啊!其实不然,
ReactiveFlags
标识能够帮助Vue
更好的管理响应式状态。举个例子在ref
函数中我们在确定是否要对setter
中的新值进行转换的时候构造出来了useDirectValue
这个变量,它是根据对象本身是否是一个浅代理或者修改的值是否是一个shallow
或者readonly
来判断是否需要进行转换的。那么shallow
和readonly
怎么判断?不就是去看看这个新值上有没有__v_isShallow
或__v_isReadonly
属性吗!现在明白了吗?2.
Scheduler
在构造函数创建
effect
副作用函数的时候我们有一个scheduler
属性,我们挖了一个坑说如果没看懂等下一起说,那现在来填坑!为什么要这样写?为什么要写在这里面?
明确
computed
函数如果调用的时候是一个函数传参那他就是一个不可更改的computed
。那么对于不可修改的computed
来说还能在setter
中触发trigger
吗?显然不行其次
computed
的官方名称叫做计算属性,也就是说往往是去依赖某些状态,在那些状态变更的时候重新执行对应的依赖,那状态变更的时候就会执行effect
,也就是ComputedRefImpl
的effect
属性,那你说trigger
触发写在别的地方能行吗?显然不行,现在理解了不?好了那继续进行下一步为什么要把
scheduler
单独拿出来当作一个小节?首先上面的代码是可以作为
computed
正常运行的,但是你一试就会发现:不是说好在状态不变的时候读取缓存的值不重新执行effect
的嘛!这就是我们当前的不足,如何改进呢?
我们需要一个标志,标识所依赖的状态没有改变的时候就取出缓存值,在依赖改变的时候再将标识改为“改变” 然后重新执行依赖获取新值,这个标志就是
ComputedRefImpl
的一个属性叫做_dirty
因此我们重新写一下
ComputedRefImpl
好极了!这样以后无论数据变不变都不会再重新执行了!?🤔🤔
因为你需要在数据变化的时候重新将
_dirty
变为脏的,上面说过了就在Scheduler
中去操作!好了再来一遍!!这样在
getter
中所依赖的响应式状态变更的时候就能通过执行scheduler
来修改阈值并且触发依赖的重新执行不过还是有点问题!在依赖变更执行
effect
的时候要执行scheduler
,咱们之前实现的trigger
重执行函数的时候好像没处理这个吧!所以要简单的修改一下
3.
Trigger
函数修改我就废话不多说直接修改
先来看看原先的
trigger
函数是什么样子的改动版本:
不过就是新增了一个执行
effect.options.scheduler
的事情,又写triggerEffects
又写triggerEffect
的不会很麻烦吗?这其实是fix
的一个bug
我们能看到在
triggerEffects
中是让computed
的依赖先执行的。等所有的computed
依赖执行完毕之后再执行普通的副作用依赖,这就是fix
的bug
。我看了一下Evan You的提交记录是这样写的附上与之相关联的
issue
:github.com 看完你就明白了,我就不多赘述了4. 完整代码
Ref
、shallowRef
toRef
、toRefs
Computed
写在后面
如果对你有帮助,欢迎点赞、讨论、收藏、勘误!!
The text was updated successfully, but these errors were encountered: