介绍今年对于从事前端开发的同学来说,最期待的一件事就是Vue3.0的发布。不过,Vue3.0距离发布还有一段时间,正式发布并不代表我们马上就可以用它来进行业务开发。还需要完善相应的生态工具。不过官方用是一回事,自己玩玩又是另外一回事了(hh)。Vue3.0专门准备了一个尝鲜项目,让大家体验一些Vue3.0中会出现的API,比如setup、reactive、toRefs、readonly等,顺便附上CompositionAPI文档的地址,还没有看过的同学赶紧Get吧,不要等到发布了才知道(笨鸟总得先飞,聪明鸟总得先飞吧?)。同样的,我也克隆玩了一段时间,对这个reactiveAPI颇感兴趣。那么,今天我们就来看看什么是响应式API(定义)?它是如何实现的(源代码实现)?1.定义及优点1.1定义反应式API的定义是传入一个对象,并在原有对象的基础上返回一个响应式代理,即返回一个Proxy,相当于Vue2x版本中的Vue.observer。首先我们要知道,原来的OptionsAPI在Vue3.0中被彻底废除了,取而代之的是CompositionAPI。CompositionAPI的简单版本如下所示:setup(){conststate=reactive({count:0,double:computed(()=>state.count*2)})functionincrement(){state.count++}return{state,increment}}可以看到,里面没有熟悉的data、computed、methods等,看起来有点React的风格。这个提案当时确实在社区引发了很多讨论,说Vue越来越像React。很多人不是很接受。具体可以看RFC的介绍。1.2优势回到本文的重点,很明显reactiveAPI是一个标准的数据选项,那么与数据选项相比有哪些优势呢?首先,Vue2x中对数据的响应式处理是基于Object.defineProperty()的,但它只监听对象的属性,不监听对象本身。因此,在添加对象属性时,通常需要这样做://vue2x添加属性Vue.$set(object,'name',wjc)反应式API是基于ES2015Proxy来实现对数据对象的响应式处理,即,在Vue3中。0可以给对象添加属性,这个属性也会有响应式的效果,例如://添加属性object.name='wjc'1.3invue3.0注意使用reactiveAPI应该是注意,当你在setup中返回时,需要以对象的形式返回,例如:exportdefault{setup(){constpos=reactive({x:0,y:0})return{pos:使用MousePosition()}}}或者,用toRefsAPI包装导出,在这种情况下我们可以使用扩展运算符或解构,例如:exportdefault{setup(){letstate=reactive({x:0,y:0})state=toRefs(state)return{...state}}}toRefs()具体是干什么的,后面会和reactive一起讲解2.源码实现首先相信大家都听说过Vue3.0使用TypeScript重构。所以,你可能希望这次看到一堆TypeScript类型和东西。出于各方面的考虑,这次只讲解编译转JS后的源码实现(没有门槛,大家放心hh)。2.1反应1。让我们先看看反应函数的实现:functionreactive(target){//如果试图观察一个只读代理,返回只读版本。如果(readonlyToRaw.has(目标)){返回目标;}//target被用户显式标记为只读if(readonlyValues.has(target)){returnreadonly(target);}if(isRef(target)){返回目标;}返回createReactiveObject(目标,rawToReactive,reactiveToRaw,mutableHandlers,mutableCollectionHandlers);}是的,可以看出首先是三个逻辑判断,分别对readonly、readonlyValues、isRef进行判断。我们先不看这些逻辑。通常我们在定义reactive的时候,会直接传入一个对象。所以它会命中最后一个逻辑createReactiveObject()。2.那我们转到createReactiveObject()的定义:functioncreateReactiveObject(target,toProxy,toRaw,baseHandlers,collectionHandlers){if(!isObject(target)){if((process.env.NODE_ENV!=='production')){console.warn(`值不能被响应:${String(target)}`);}返回目标;}//target已经有对应的Proxyletobserved=toProxy.get(target);如果(观察到!==void0){返回观察到;}//target已经是Proxyif(toRaw.has(target)){returntarget;}//只能观察值类型的白名单。如果(!canObserve(目标)){返回目标;consthandlers=collectionTypes.has(target.constructor)?集合处理程序:基础处理程序;observed=newProxy(target,handlers);toProxy.set(目标,观察);toRaw.set(观察,目标);returnobserved;}createReactiveObject()传入了四个参数,它们分别扮演的角色:target是我们定义的reactive对象toProxy是一个空的WeakSet,toProxy是一个空的WeakSet。baseHandlers是一个已经定义了get和set的对象,它看起来像这样:constbaseHandlers={get(target,key,receiver){},set(target,key,value,receiver){},deleteProxy:(key){},has:(target,key){},ownKey:(target){}};collectionHandlers是一个只包含get的对象。然后,输入createReactiveObject()。同样,我们这次也不去分析一些分支逻辑。在看源码的时候,我们需要保持一个平常心,先看主要逻辑,所以我们会打到最后的逻辑,即:consthandlers=collectionTypes.has(target.constructor)?集合处理程序:基础处理程序;observed=newProxy(target,handlers);toProxy.set(目标,观察);toRaw.set(观察,目标);它首先判断collectionTypes是否会包含我们传入的target的构造函数,collectionTypes是一个Set集合,主要包括Set、Map、WeakMap、WeakSet等四个集合构造函数。如果collectionTypes包含其构造函数,则为handlers分配只能获取的collectionHandlers对象,否则为baseHandlers对象。两者的区别在于前者只有get。显然,这是为不需要更新的变量定义保留的。比如大家熟悉的props只实现了get。然后,将目标和处理程序传递给代理,并实例化一个代理对象作为参数。这也是我们在一些文章中看到的。Vue3.0将Object.defineProperty替换为ES2015Proxy。最后两个逻辑也很重要。toProxy()将已经定义的Proxy对象的target和对应的observed作为key-valuepair放入toProxy的WeakMap中,下次如果有相同引用的target需要响应时会用到.打到前面的分支逻辑,返回定义之前定义的observed,即://targetalreadyhascorrespondingProxytargetisalreadyrelatedtotheProxyobjectletobserved=toProxy.get(target);如果(观察到!==void0){返回观察到;}而toRaw()是一个与toProxy相反的键值对,如果传入的target已经是Proxy对象,则用于下次返回target,即://targetisalreadyaProxytargetisalreadyaProxyobject如果(toRaw.has(目标)){返回目标;}2.2toRefs前面提到了使用reactive需要注意的点。提到toRefs可以让我们方便的使用解构和扩展操作符。其实最近Vue3.0的issue也有大神在这方面讲解。有兴趣的同学可以移步Whenit'sreallyneedtousetoRefsordertoretainreactivityofreactivevalue。我当时也玩了一把,如下图所示:可以看到,toRefs是在原来的Proxy对象的基础上,通过get和set返回了一个普通的对象。这样就解决了Proxy对象在遇到解构和扩展操作符后失去引用的问题。结束语好了,reactiveAPI的定义和通用源码实现如上文所述。至于分支逻辑,大家可以通过不同的case自己去阅读。当然,需要说明的是,这次的源码只是尝鲜版。不排除正式版会有很多优化,但是主体肯定是不变的。推荐看下一篇《4k+ 字分析 Vue 3.0 响应式原理(依赖收集和派发更新)》写作不易,觉得有收获的话,帅气地打出三连击吧!!!
