当前位置: 首页 > Web前端 > vue.js

Vue3源码分析(八):ref和computed原理揭秘

时间:2023-03-31 20:51:55 vue.js

在Vue3新推出的响应式API中,Ref系列无疑是使用频率最高的API之一,而computed属性在之前的版本中是一个非常熟悉的option,但是Vue3也提供了独立的api,方便我们直接创建计算值。在今天的文章中,笔者将为大家讲解一下ref和computed的实现原理。让我们一起开始本章的学习。ref当我们有一个独立的原始值,比如一个字符串,当我们想让它响应式的时候,我们可以创建一个对象,把字符串以键值对的形式放到对象中,然后传给reactive。而Vue通过ref为我们提供了一种更简单的方式来完成。import{ref}from'vue'constcount=ref(0)console.log(count.value)//0count.value++console.log(count.value)//1ref将返回一个可变的反应对象,该对象将其内部值保持为反应性引用,这是引用名称的来源。该对象仅包含一个名为value的属性。ref是如何实现的?ref的源码位置在@vue/reactivity库中,路径为packages/reactivity/src/ref.ts。下面我们一起看看ref的实现。导出函数ref(value:T):ToRefexportfunctionref(value:T):Ref>exportfunctionref():Ref<吨|undefined>exportfunctionref(value?:unknown){returncreateRef(value)}从refapi的函数签名可以看出,ref函数接收一个任意类型的值作为它的value参数,并返回一个Ref类型的值。exportinterfaceRef{value:T[RefSymbol]:true_shallow?:boolean}从返回值Ref的类型定义来看,ref的返回值有一个value属性和一个privatesymbolkey,还有一个attribute类型为_shallow的布尔值,标识它是否是一个shallowRef。函数体直接返回createRef函数的返回值。createReffunctioncreateRef(rawValue:unknown,shallow=false){if(isRef(rawValue)){returnrawValue}returnnewRefImpl(rawValue,shallow)}c??reateRef的实现也很简单,入参为rawValue和shallow,以及rawValue的创建记录了ref的原始值,而shallow是一个shallowresponsiveapi,表示是否是shallowRef。函数的逻辑是先用isRef判断是否是rawValue,如果是则直接返回ref对象。否则,返回一个新创建的RefImpl类的实例对象。RefImplclassclassRefImpl{private_value:Tpublicreadonly__v_isRef=trueconstructor(private_rawValue:T,publicreadonly_shallow:boolean){//如果是浅响应,直接将_value设置为_rawValue,否则处理_rawValue通过转换this._value=_shallow?_rawValue:convert(_rawValue)}getvalue(){//读取值之前,先通过track采集值依赖track(toRaw(this),TrackOpTypes.GET,'value')returnthis._value}setvalue(newVal){//如果需要更新if(hasChanged(toRaw(newVal),this._rawValue)){//更新_rawValue和_valuethis._rawValue=newValthis._value=this._shallow?newVal:convert(newVal)//通过触发器分发值来更新trigger(toRaw(this),TriggerOpTypes.SET,'value',newVal)}}}在RefImpl类中,有一个私有变量_value用于存储最新的值参考;公共只读变量__v_isRef用于标识对象是一个ref反应对象标志,在谈论反应式api时与ReactiveFlag相同。在RefImpl的构造函数中,接受一个私有的_rawValue变量来存储ref的旧值;public_shallow变量用于区分是否是浅响应。在构造函数内部,首先判断_shallow是否为true,如果是shallowRef,则直接将原值赋值给_value,否则通过convert转换赋值。convert函数内部,其实就是判断传入的参数是否为对象,如果是对象,通过reactiveapi创建一个代理对象返回,否则直接返回原参数。当我们以ref.value的形式读取ref的值时,就会触发该值的getter方法。在getter中,会先通过track收集ref对象的值的依赖,收集完成后返回ref的值。当我们修改ref.value时,会触发value的setter方法来比较新旧值。如果值不同需要更新,会先更新新旧值,然后通过触发器更新ref对象的value属性。让依赖于此引用的副作用函数执行更新。如果有依赖曲目收藏,对触发分发更新感到困惑的朋友,建议先阅读我的上一篇文章。在上一篇文章中,作者详细解释了这个过程。至此,作者已经把ref的实现解释清楚了。Computed在文档中是这样描述computedapi的:接受一个getter函数,返回一个不可变的响应式ref对象,带有getter函数的返回值。或者它也可以使用具有get和set函数的对象来创建可写的ref对象。Computedfunction根据这个API的描述,很明显computed接受一个函数或者对象类型的参数,那么我们先从它的函数签名说起。导出函数computed(getter:ComputedGetter):ComputedRefexportfunctioncomputed(options:WritableComputedOptions):WritableComputedRefexportfunctioncomputed(getterOrOptions:ComputedGetter>|WritableComputedOptions)在计算函数的重载中,第一行代码接收一个getter类型的参数,返回一个ComputedRef类型的函数签名。的返回值返回一个不可变的反应式ref对象。在第二行代码中,计算函数接受一个选项对象并返回一个可写的ComputedRef类型,这是文档的第二种情况,创建一个可写的ref对象。第三行代码是重载这个函数的最广泛的情况,参数名已经提到了这个:getterOrOptions。我们看一下computedapi中的相关类型定义:T>}导出类型ComputedGetter=(ctx?:any)=>Texport类型ComputedSetter=(v:T)=>voidexport接口WritableComputedOptions{get:ComputedGetterset:ComputedSetter}从类型定义可知,WritableComputedRef和ComputedRef都是从Ref类型扩展而来的,这也解释了为什么文档中说computed返回一个ref类型的响应式对象。接下来看computedapi函数体的完整逻辑://ifTheparametergetterOrOptionsisafunctionif(isFunction(getterOrOptions)){//那么这个函数一定是getter,将函数赋值给gettergetter=getterOrOptions//这种场景下,如果在DEV中访问setter环境,会报警告setter=__DEV__?()=>{console.warn('Writeoperationfailed:computedvalueisreadonly')}:NOOP}else{//在这个判断中,表示参数是一个option,所以getter或者set可以赋值getter=getterOrOptions.getsetter=getterOrOptions.set}returnnewComputedRefImpl(getter,setter,isFunction(getterOrOptions)||!getterOrOptions.set)asany}在computedapi中,会先判断传入的参数是getter函数还是选项对象。如果是函数,这个函数只能是getter函数。此时getter被赋值,DEV环境下访问setter不会成功,同时会报warning。如果输入不是函数,computed会把它当作一个对象,有get和set属性,将对象中的get和set赋值给对应的getter和setter。最后,处理完成后,会返回一个ComputedRefImpl类的实例对象,对计算出的api进行处理。ComputedRefImplClass这个类和我们前面介绍的RefImplClass类似,只是构造函数中的逻辑有点不同。先看类中的成员变量:publicreadonly[ReactiveFlags.IS_READONLY]:boolean}和RefImpl类相比,增加了一个_dirty私有成员变量,为effect增加了一个只读副作用函数变量,增加了一个__v_isReadonly标志。然后看构造函数中的逻辑:>{if(!this._dirty){this._dirty=truetrigger(toRaw(this),TriggerOpTypes.SET,'value')}}})this[ReactiveFlags.IS_READONLY]=isReadonly}在构造函数中,它将是agetter创建一个sideeffectfunction,并在sideeffectoptions中设置为延迟执行,并添加一个scheduler。在scheduler中,会判断this._dirty标志是否为false,如果为false,则将this._dirty设置为true,并使用trigger调度更新。如果对这个sideeffect的执行时机和scheduler什么时候在sideeffect中执行这些问题感到困惑的同学,建议先阅读前面的文章了解effect的sideeffects,再去了解其他reactiveAPIs。一定是更有效的。getvalue(){//这个计算出的ref可能被其他代理对象const包装self=toRaw(this)if(self._dirty){//执行getter时执行副作用函数,并调度更新,从而更新依赖值self._value=this.effect()self._dirty=false}//调用track收集依赖track(self,TrackOpTypes.GET,'value')//返回最新的值returnself._value}setvalue(newValue:T){//执行setter函数this._setter(newValue)}在computed中,通过getter函数获取value时,会先执行sideeffectfunction,returnsideeffectfunction的value会被赋值给_value,_dirty的value会赋值给setfalse,保证如果computed中的依赖关系不发生变化,sideeffectfunction不会再次执行,那么得到的_dirty在getter始终为false,不需要再次执行副作用函数,节省高架。然后通过track收集依赖,返回_value的值。在setter中,它只是执行我们传入的setter逻辑,至此computedapi的实现已经解释完了。总结本文作者基于以上sideeffectfunctions和dependentcollectionanddispatchupdates的知识点,讲解了Vue3响应式中最常用的两个APIref和computed的实现。这两个API在创建返回类实例时,通过实例中的构造函数和value属性设置的get、set完成响应式跟踪。当我们学会使用这些,并且知道为什么,就一定能够帮助我们充分发挥这些api的作用,同时也能让你快速写出一些不符合你期望的代码。至于定位问题,可以判断是写错了还是api本身不支持某种调用方式。最后,如果这篇文章能帮助你理解响应式apiref和computed在Vue3中的实现原理,希望你能给这篇文章点个赞??。如果想继续关注后续文章,也可以关注我的账号或者关注我的github。再次感谢所有可爱的读者。