当前位置: 首页 > 科技观察

UseMutableSource

时间:2023-03-12 03:57:19 科技观察

深度解读《React18新特性》前言大家好,我是??,接下来会发布一个新系列,Reactv18新特性的解读,主要是新特性的背景介绍,功能介绍,原理分析等几个方面,勇敢做第一个吃螃蟹的人。希望支持我的朋友可以点赞,转发,再看,关注一波公众号,继续分享前端技术硬文。useMutableSource最早的RFC提案始于2020年2月。它将作为React18中的新功能出现。用提案中的描述来总结useMutableSource。useMutableSource使React组件能够以ConcurrentMode模式安全高效地读取外部数据源,检测组件渲染过程中的变化,并在数据源发生变化时安排更新。说到外部数据源,就要从state和update说起。在React或Vue等传统UI框架中,虽然都使用了虚拟DOM,但是仍然无法将更新单元委托给虚拟DOM,所以更新的最小粒度还是在组件层面,组件统一管理数据状态并参与在安排更新。回到我们的主角React,既然组件控制了state状态。那么在v17及之前的版本中,React如果要更新视图,只能改变内部数据状态。纵观React的几种更新方法,都离不开自身的状态。我们来看看React的几种更新模式。组件本身改变状态。函数使用状态|useReducer类组件setState|强制性升级。props变化,组件更新带来的子组件的更新。上下文已更新,组件使用当前上下文。无论哪种方式,它本质上都是一种状态变化。道具变化来自父组件的状态变化。Context的变化来自于Provider中的value变化,value一般是state或者state的衍生物。从上面可以总结出:state和view的关系updateModel=>View。但是状态仅限于组件内部的数据,如果状态来自外部(组件级别之外)。那么如何完成外部数据源到内部状态的转换,数据源发生变化,组件重新渲染呢?普通模式下,先将外部数据externalData通过选择器映射,将组件需要的数据映射到state|道具。这是一个完整的步骤,接下来需要订阅订阅外部数据源的变化。如果有变化,需要自己强制更新forceUpdate。下面两张图代表数据注入和数据订阅更新。一个典型的外部数据源是redux中的store。redux如何安全地将store中的状态转换为组件的状态。也许我可以用一段代码来表示react-redux中状态变化到视图更新的流程。conststore=createStore(reducer,initState)functionApp({selector}){const[state,setReduxState]=React.useState({})constcontextValue=useMemo(()=>{/*订阅商店更改*/store.subscribe(()=>{/*使用选择器选择订阅状态*/constvalue=selector(data.getState())/*如果有变化*/if(ifHasChange(state,value)){setReduxState(value)}})},[store])return

...
}但示例中的代码没有实际意义,不是源代码。我在这里是为了让大家清楚地了解这个过程。这就是redux和react本质上的工作方式。通过store.subscribe来订阅状态的变化,但本质上比代码片段要复杂的多,通过选择器(selector)找到组件需要的状态。这里先解释一下selector,因为业务组件往往不需要整个store中的所有状态数据,而只需要下面的一些状态。这时候就需要从state中选择‘useful’,与props合并。细心的同学应该会发现selector需要和react-redux中connect的第一个参数mapStateToProps挂钩。至于细节,没关系,因为今天的重点是useMutableSource。以上是没有useMutableSource的情况,现在使用useMutableSource不再需要将订阅更新的过程交给组件。如下:/*创建store*/conststore=createStore(reducer,initState)/*创建外部数据源*/constexternalDataSource=createMutableSource(store,store.getState())/*订阅更新*/constsubscribe=(store,callback)=>store.subscribe(callback);functionApp({selector}){/*如果订阅状态发生变化,组件会更新*/conststate=useMutableSource(externalDataSource,selector,subscribe)}通过createMutableSource创建外部数据源,并使用MutableSource来使用外部数据源。当外部数据源发生变化时,组件会自动渲染。以上是通过useMutableSource实现的订阅更新,减少了APP内部的组件代码,提高了代码的健壮性,一定程度上降低了耦合度。接下来就让我们全面了解一下这款V18的新特性吧。2.功能介绍具体功能介绍过程还是参考最新的RFC。createMutableSource和useMutableSource在某种程度上有点像createContext和useContext。不同的是context需要Provider注入内部状态,而今天的主角是注入外部状态。所以首先你应该看看如何使用这两者。创建createMutableSource以创建数据源。它有两个参数:contexternalDataSource=createMutableSource(store,store.getState())第一个参数:是外部数据源,比如redux中的store,第二个参数:一个函数,函数的返回值作为数据源这里需要注意的是要保持数据源和数据版本号的一致性,即如果数据源发生变化,那么数据版本号也会发生变化,遵循不可变原则(immutability)在某种程度上。可以理解为数据版本号是证明数据源唯一性的标志。该api引入了useMutableSource以使用非传统数据源。它的功能类似于ContextAPI和useSubscription。(之前没用过useSubscription的同学可以多了解下)。下面看一下useMutableSource的基本使用:constvalue=useMutableSource(source,getSnapShot,subscribe)useMutableSource是一个hooks,它有三个参数:source:MutableSource可以理解为一个带内存的数据源对象。getSnapshot:(source:Source)=>Snapshot:一个函数,数据源作为函数的参数获取快照信息,可以理解为选择器,对外部数据源的数据进行过滤,查找所需的数据源。subscribe:(source:Source,callback:()=>void)=>()=>void:订阅函数,有两个参数,Source可以理解为useMutableSource的第一个参数,callback可以理解为useMutableSource的第二个参数使用MutableSource,当数据源发生变化时,执行快照获取新数据。useMutableSource的特点useMutableSource和useSubscription在功能上是相似的:都需要一个带有记忆的‘可配置对象’来从外部检索值。两者都需要一个订阅方法来订阅和取消订阅提要。除此之外,useMutableSource有一些特点:useMutableSource需要一个源作为显式参数。即需要将数据源对象作为第一个参数传入。useMutableSource和getSnapshot读取的数据是不可变的。关于MutableSource的版本号useMutableSource会跟踪MutableSource的版本号然后读取数据,所以如果两者不一致,可能会导致读取异常。useMutableSource会检查版本号:第一次挂载组件时读取版本号。当组件重新渲染时,确保版本号一致,然后读取数据。否则,将发生错误。确保数据来源和版本号的一致性。设计规范当通过getSnapshot读取外部数据源时,返回值应该是不可变的。正确写法:getSnapshot:source=>Array.from(source.friendIDs)错误写法:getSnapshot:source=>source.friendIDs数据源必须有一个全局版本号,代表整个数据源:正确写法:getVersion:()=>source.version错误的写法:getVersion:()=>source.user.version接下来参考github上的例子,说说使用方法:例子1例子1:订阅路由变化在历史模式中。比如有一个场景是订阅非人为情况下的路由变化,显示对应的location.pathname,看看使用useMutableSource是如何处理的。在这个场景中,外部数据源是位置信息。//通过createMutableSource创建外部数据源。//数据源对象为window。//使用location.href作为数据源的版本号,如果href发生变化,说明数据源发生了变化。constlocationSource=createMutableSource(window,()=>window.location.href);//获取快照信息,这里是location.pathname字段,可以重复使用,当路由发生变化时,快照会调用函数形成新的快照信息。constgetSnapshot=window=>window.location.pathname//订阅函数。constsubscribe=(window,callback)=>{//通过popstate以history方式监听路由变化。当路由发生变化时,执行快照函数获取新的快照信息。window.addEventListener("popstate",callback);//取消监听return()=>window.removeEventListener("popstate",callback);};functionExample(){//通过useMutableSource,放数据源对象,快照函数,传入订阅函数,形成pathName。constpathName=useMutableSource(locationSource,getSnapshot,subscribe);//...}来描述这个过程:首先,通过createMutableSource创建一个数据源对象,数据源对象为window。使用location.href作为数据源的版本号,如果href发生变化,则说明数据源发生了变化。获取快照信息,这里是location.pathname字段,可以重复使用,当路由发生变化时,会调用快照函数,形成新的快照信息。通过popstate以history方式监控路由变化。当路由发生变化时,执行快照函数获取新的快照信息。通过useMutableSource传入数据源对象、快照函数、订阅函数,组成pathName。也许这个例子不足以让你理解useMutableSource的作用。再举个例子,看看useMutableSource是如何适配redux的。示例2示例2:redux中的useMutableSource使用redux,可以通过useMutableSource——useSelector编写自定义hooks,useSelector可以读取数据源的状态,当数据源发生变化时,重新执行快照获取状态来实现订阅更新。我们来看看useSelector是如何实现的。constmutableSource=createMutableSource(reduxStore,//使用reduxstore作为数据源。//State是不可变的,可以作为数据源的版本号()=>reduxStore.getState());//保存数据通过创建上下文mutableSource来获取源。constMutableSourceContext=createContext(mutableSource);//订阅存储变化。存储变化,执行getSnapshotconstsubscribe=(store,callback)=>store.subscribe(callback);//自定义hooksuseSelector可以在每个connect内部使用,通过useContext获取数据源对象。functionuseSelector(selector){constmutableSource=useContext(MutableSourceContext);//使用useCallback使getSnapshot内存。constgetSnapshot=useCallback(store=>selector(store.getState()),[selector]);//最后,useMutableSource本质上是用来订阅状态变化的。returnuseMutableSource(mutableSource,getSnapshot,subscribe);}大致流程如下:使用redux的store作为数据源对象mutableSource。state是不可变的,可以作为数据源的版本号。通过创建上下文保存数据源对象mutableSource。声明一个订阅函数来订阅商店的变化。存储更改,执行getSnapshot。每个connect内部都可以使用自定义hooksuseSelector,通过useContext获取数据源对象。使用useCallback使getSnapshot记忆化。最后,useMutableSource本质上是用来订阅外部状态变化的。注意,在创建getSnapshot的时候,getSnapshot需要记忆,就像上面process中的useCallback处理getSnapshot一样。如果不记忆,组件会被频繁渲染。在最新的react-redux源码中,使用了一个新的api来订阅外部数据源,但不是useMutableSource而是useSyncExternalStore,具体是因为useMutableSource没有提供内置的selectorAPI,所以需要重新每次selector变化时订阅store,如果没有UseCallback等api内存处理,那么会重新订阅。具体请参考useMutableSource→useSyncExternalStore。三实践接下来我会用一个例子来实践一下createMutableSource,让大家更清楚这个过程。这里仍然使用redux和createMutableSource来引用外部数据源。这里使用了18.0.0-alpha版本的react和react-dom。importReact,{unstable_useMutableSourceasuseMutableSource,unstable_createMutableSourceascreateMutableSource}from'react'import{combineReducers,createStore}from'redux'/*numberReducer*/functionnumberReducer(state=1,action){switch(action.type){case'ADD':1返回状态+'DEL':returnstate-1default:returnstate}}/*注册reducer*/constrootReducer=combineReducers({number:numberReducer})/*combineStore*/constStore=createStore(rootReducer,{number:1})/*注册外部数据Source*/constdataSource=createMutableSource(Store,()=>1)/*订阅外部数据源*/constsubscribe=(dataSource,callback)=>{constunSubScribe=dataSource.subscribe(callback)return()=>unSubScribe()}/*TODO:情况1*/exportdefaultfunctionIndex(){/*获取数据快照*/constshotSnop=React.useCallback((data)=>({...data.getState()}),[])/*hooks:使用*/constdata=useMutableSource(dataSource,shotSnop,subscribe)返回

拥抱React18🎉🎉🎉

喜欢:{data.number}
Store.dispatCH({类型:'添加'})}>Like
}第一部分使用combineReducers和createStore创建reduxStore的过程。重点在第二部分:首先通过createMutableSource创建数据源,Store为数据源,data.getState()为版本号。第二点是快照信息,这里的快照就是store中的state。所以shotSnop还是通过getState获取状态。一般情况下,shotSnop应该作为一个Selector,所有的状态都映射到这里。三是通过useMutableSource传入数据源、快照、订阅函数,获取的数据就是引用的外部数据源。接下来看一下效果:四大原理分析useMutableSource已经在Reactv18的规划中,那么其实现原理和细节可能会在V18正式上线前进行调整,1createMutableSourcereact/src/ReactMutableSource.js->createMutableSourcefunctioncreateMutableSource(source,getVersion){constmutableSource={_getVersion:getVersion,_source:source,_workInProgressVersionPrimary:null,_workInProgressVersionSecondary:null,};returnmutableSource}createMutableSource的原理很简单,createContext类似于创建一个2Recreateobject,createuseMutableSource关于useMutableSource的原理其实并没有那么神秘。原来是开发者把外部数据源注入state,然后写订阅函数。useMutableSource的原理就是做开发者该做的事情??????,免去开发者编写相关代码的麻烦。本质上是useState+useEffect:useState负责更新。useEffect负责订阅。然后看原理。react-reconciler/src/ReactFiberHooks.new.js->useMutableSource函数useMutableSource(hook,source,getSnapshot){/*获取版本号*/constgetVersion=source._getVersion;constversion=getVersion(source._source);/*使用useState保存当前快照,触发更新。*/let[currentSnapshot,setSnapshot]=dispatcher.useState(()=>readFromUnsubscribedMutableSource(root,source,getSnapshot),);dispatcher.useEffect(()=>{/*包装函数*/constandleChange=()=>{/*触发更新*/setSnapshot()}/*订阅更新*/constunsubscribe=subscribe(source._source,handleChange);/*取消订阅*/returnunsubscribe;},[source,subscribe])}以上代码保留了最核心的逻辑:首先通过getVersion获取数据源的版本号,用useState保存当前Snapshot,用setSnapshot触发更新。在useEffect中,订阅并绑定封装好的handleChange函数,调用setSnapshot更新真正的组件。所以useMutableSource本质上是useState。五小结今天讲了useMutableSource的背景、用法、原理。想阅读的同学可以cloneReactv18新版本尝试新特性,对理解useMutableSource有很大帮助。在下一章中,我们将继续围绕Reactv18。参考文档useMutableSourceRFC