当前位置: 首页 > Web前端 > HTML5

Immer.js浅析

时间:2023-04-05 18:27:01 HTML5

在函数式编程中,Immutable的特性非常重要,但在Javascript中显然无法从语言层面提供支持,但还有其他库(例如:Immutable.js)它可以为开发者提供这样的功能,所以我一直很好奇这些库是如何实现Immutable的。这一次,我将从Immer.js(小巧玲珑)入手,看看它的内部是如何工作的。CopyOnWrite(写时复制)第一次了解到这样的技术是在学习Java的时候。当然,这句话也很好理解:准备修改时,先复制一份再修改;这样可以避免直接修改本体数据,也可以将性能影响降到最低(不修改就不需要复制);Immer.js中也使用了这项技术,而Immer.js的基本思想是这样的:基本思想是你将所有的更改应用到一个临时的draftState,它是currentState的代理。完成所有突变后,Immer将根据对草案状态的突变生成nextState。这意味着您可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。个人简单翻译:主要思路是先在currentState的基础上生成一个代理draftState,之后所有的修改都在draftState上进行,避免直接修改currentState,当修改完成后,再根据draftState生成nextState.所以整个过程只涉及三个状态:currentState(输入状态)、draftState(中间状态)、nextState(输出状态);关键是draftState怎么生成,怎么应用修改,怎么生成最终的nextState。分析源码因为Immer.js确实很小,直接从核心API开始:constnextState=produce(baseState,draftState=>{draftState.push({todo:"Tweetaboutit"})draftState[1].done=true})上面的produce方法包含了刚才提到的currentState->draftState->nextState的整个过程,接下来深入produce方法:exportdefaultfunctionproduce(baseState,producer){...returngetUseProxies()?produceProxy(baseState,producer):produceEs5(baseState,producer)}Immer.js会判断是否可以使用ES6Proxy。如果不行,就只能用ES5来实现proxy了(当然会麻烦一点)。这里先从ES6Proxy的实现方法说起。后面回过头来分析ES5的实现。exportfunctionproduceProxy(baseState,producer){constpreviousProxies=proxies//1.备份当前代理对象proxies=[]try{constrootProxy=createProxy(undefined,baseState)//2.创建代理constreturnValue=producer.call(rootProxy,rootProxy)//3.应用修改letresultif(returnValue!==undefined&&returnValue!==rootProxy){if(rootProxy[PROXY_STATE].modified)thrownewError(RETURNED_AND_MODIFIED_ERROR)result=finalize(returnValue)//4.Generateobject}else{result=finalize(rootProxy)//5.Generateobject}each(proxies,(_,p)=>p.revoke())//6.注销所有当前代理returnresult}finally{proxies=previousProxies//7.恢复之前的代理对象}}这里,对关键步骤进行注释。步骤1与步骤6、7相关,主要是处理嵌套场景:constnextStateA=produce(baseStateA,draftStateA=>{draftStateA[1].done=true;constnextStateB=produce(baseStateB,draftStateB=>{draftStateB[1].done=true});})因为每个produce方法最后都要注销所有proxy,防止produce之后不能修改proxy对象(因为对produce对象的修改最终会映射到generatedobject),所以每次都需要备份代理,方便以后登出第二步,创建代理对象(核心)functioncreateProxy(parentState,base){if(isProxy(base))thrownewError("Immerbug.Plzreport.")conststate=createState(parentState,base)constproxy=Array.isArray(基数)?Proxy.revocable([state],arrayTraps):Proxy.revocable(state,objectTraps)proxies.push(proxy)returnproxy.proxy}这里Immer.js会使用crateState方法来封装我们传入的Data:{修改:false,//是否修改finalized:false,//是否finalizedparent,//parentstatebase,//selfstatecopy:undefined,//copiedstateproxies:{}//storeandgenerateproxyobject}然后生成根据数据是对象还是数组对应的代理。以下是代理拦截的操作:constobjectTraps={get,has(target,prop){returnpropinsource(target)},ownKeys(target){returnReflect.ownKeys(source(target))},set,deleteProperty,getOwnPropertyDescriptor,defineProperty,setPrototypeOf(){thrownewError("Immerdoesnotsupport`setPrototypeOf()`.")}}我们关注get和set方法就好了,因为这是最常用的方法,而且也很容易理解这两种方法的基本原理要理解Immer.js的核心,首先要看get方法:if(value===state.base[prop]&&isProxyable(value))return(state.copy[prop]=createProxy(state,value))返回值}else{if(has(state.proxies,prop))返回state.proxies[prop]constvalue=state.base[prop]if(!isProxy(value)&&isProxyable(value))return(state.proxies[prop]=createProxy(state,value))returnvalue}}at开头如果access属性等于PROXY_STATE的特殊值,会直接返回封装状态本身。如果是另一个属性,它将返回原始对象或其副本上的相应值。所以这里会有分店。如果state没有被修改,则访问state.base(初始对象),否则访问state.copy(因为不会对state.base进行修改,一旦修改,只有state.copy是最新的);在这里你还会看到其他代理对象只会在访问相应属性时才尝试创建,属于“惰性”模式。再看一下set方法:(state.proxies,prop)&&state.proxies[prop]===value))returntruemarkChanged(state)}state.copy[prop]=valuereturntrue}如果对象是第一次修改,markChanged方法会直接被触发,将自己的修改标记为true,然后向上冒泡到根对象调用markChange方法:shallowCopy(state.base)//在基本副本上复制代理Object.assign(state.copy,state.proxies)//是的,它也适用于数组if(state.parent)markChanged(state.parent)}}除了标记已修改外,还要做其他事情一个是从基础生成一个副本。当然,这里的浅拷贝是尽可能的使用已有的数据来减少内存消耗,同时也将之前创建的代理对象复制到proxies上。所以最终的state.copy既可以包含代理对象也可以包含普通对象,之后的访问修改直接在state.copy上进行。至此,currentState->draftState的初始转换完成,接着完成draftState->nextState的转换,也就是前面注释的第四步:result=finalize(returnValue)再看finalize方法:exportfunctionfinalize(base){if(isProxy(base)){conststate=base[PROXY_STATE]if(state.modified===true){if(state.finalized===true)返回state.copy状态。finalized=truereturnfinalizeObject(useProxies?state.copy:(state.copy=shallowCopy(base)),state)}else{returnstate.base}}finalizeNonProxiedObject(base)returnbase}这个方法主要是生成一个普通对象来自state.copy对象,刚才说了state.copy很可能既包含代理对象也包含普通对象,所以必须将代理对象转换为普通对象,state.finalized是标记是否转换完成.直接进入finalizeObject方法:(value)})returnfreeze(copy)}这里也是深度遍历,如果state.copy上的值不等于state.base上的值,肯定是被修改了,所以直接跳转到finalize进行转换,最后转换成state.copy后,冻结它,一个新的Immutable数据就诞生了。而另一个finalizeNonProxiedObject方法,目标是在普通对象中找到代理对象并进行转换,所以代码就不贴了。至此,Immer.js上的Proxy模式分析基本结束。在ES5上,因为没有ES6Proxy,所以只能模仿:functioncreateProxy(parent,base){constproxy=shallowCopy(base)each(base,i=>{Object.defineProperty(proxy,""+i,createPropertyProxy(""+i))})conststate=createState(parent,proxy,base)createHiddenProperty(proxy,PROXY_STATE,state)states.push(state)returnproxy}创建代理时,它首先从base,然后使用defineProperty对象的getter和setter拦截并映射到state.base或state.copy。事实上,现在我注意到ES5只能拦截getter和setter。如果我们在代理对象上删除一个属性或者添加一个属性,我们后面怎么知道,所以Immer.js最终会使用代理上的属性keys和base上的keys进行比较,来判断是增加还是减少attribute:functionhasObjectChanges(state){constbaseKeys=Object.keys(state.base)constkeys=Object.keys(state.proxy)return!shallowEqual(baseKeys,keys)}其他过程和ES6Proxy基本一致。结束Immter.js的实现还是比较巧妙的,以后可以用在状态管理上。