继上一篇精读《Records & Tuples 提案》之后,已经有人在思考这个提案可以帮助React解决哪些问题了。比如这个Records&TuplesforReact提到了很多React的痛点是可以解决的。其实我更担心的是浏览器对Records&Tuples的性能优化的不够好。这将是它能否大规模应用,或者我们能否信任它来解决问题的最关键因素。这篇文章是建立在浏览器可以完美优化其性能的前提下的。一切看起来都很好。基于这个假设,让我们看看Records&Tuplesproposal能解决什么问题!OverviewRecords&TuplesProposal提案在之前的精读中已经介绍过了。如果你不熟悉它,你可以先去proposalsyntax。保证不变性虽然React也可以用Immutable的思想来开发,但是大多数情况下无法保证安全性,比如:constHello=({profile})=>{//propmutation:throwsTypeErrorprofile.name='Sebastienupdated';返回
Hello{profile.name}
;};functionApp(){const[profile,setProfile]=React.useState(#{name:'Sebastien',});//状态突变:抛出TypeErrorprofile.name='Sebastienupdated';return
;}归根结底,我们不会一直使用freeze来冻结对象。在大多数情况下,我们需要人为地确保引用不被修改。潜在风险依然存在。但是用Record来表示状态,TS和JS都会报错,立马阻止了问题的蔓延。部分替换useMemo,比如下面这个例子,为了保证apiFilters引用不变,需要useMemo:constapiFilters=useMemo(()=>({userFilter,companyFilter}),[userFilter,companyFilter],);const{apiData,loading}=useApiData(apiFilters);但是Record模式不需要memo,因为js引擎会帮你做类似的事情:const{apiData,loading}=useApiData(#{userFilter,companyFilter})在useEffect部分很啰嗦其实就是类似于替换useMemo,即:constapiFilters=#{userFilter,companyFilter};useEffect(()=>{fetchApiData(apiFilters).then(setApiDataInState);},[apiFilters]);您可以使用apiFilters作为参考查看稳定的原始对象。如果确实发生变化,那一定是值发生了变化,所以才会触发fetch。如果把上面的#号去掉,每次刷新组件都会去取数,其实是多余的。在props属性中定义不可变的props更方便,不需要提前使用useMemo:
;将fetch结果转成Record目前确实不行,除非使用性能很差的JSON.stringify或deepEqual,用法如下:constfetchUserAndCompany=async()=>{constresponse=awaitfetch(`https//myBackend.com/userAndCompany`,);返回JSON.parseImmutable(awaitresponse.text());};即利用Record提案的JSON.parseImmutable将后端返回值转为Record,这样即使重新查询查询,如果返回结果完全不变,也不会导致重新渲染,orlocalchanges只会导致部分重新渲染,目前我们只能在这种情况下允许完全重新渲染。但是,这对浏览器对Record的实现的优化提出了非常严格的要求,因为假设后端返回的数据是几十MB,我们不知道这个内置的API会带来多少额外的开销。假设浏览器使用了一种非常神奇的方法来实现几乎为零的开销,那么我们应该始终使用JSON.parseImmutable而不是JSON.parse进行解析。生成查询参数也使用了parseImmutable方法,这样前端就可以准确的发送请求,而不是每次qs.parse生成一个新的引用都发送一个请求://Thisisanon-performant,butworkingsolution.//Lib作者应该提供一个方法,例如qs.parseRecord(search)constparseQueryStringAsRecord=(search)=>{constqueryStringObject=qs.parse(search);//注意:Record(obj)转换函数不是递归的//这里有一个递归的转换方法://https://tc39.es/proposal-record-tuple/cookbook/index.htmlreturnJSON.parseImmutable(JSON.stringify(queryStringObject),);};constuseQueryStringRecord=()=>{const{search}=useLocation();返回useMemo(()=>parseQueryStringAsRecord(search),[search,]);};还提到了一个有趣的点,就是配套的工具库可能会提供类似qs.parseRecord(search)的方法来包装JSON.parseImmutable,也就是说这些生态库想要“无缝”其实还需要做一些API修改访问记录提案。避免循环产生新的引用即使原来的对象引用保持不变,我们可以写几行代码,.filter来改变它,不管返回结果是否改变,引用肯定会改变:constAllUsers=[{id:1,name:'Sebastien'},{id:2,name:'John'},];constParent=()=>{constuserIdsToHide=useUserIdsToHide();constusers=AllUsers.filter((user)=>!userIdsToHide.includes(user.id),);返回
;};constUserList=React.memo(({users})=>(
{users.map((user)=>({user.name}))}
));为了避免这个问题,useMemo是必要的,但在Record提案中不是必需的:constAllUsers=#[#{id:1,name:'Sebastien'},#{id:2,name:'John'},];constfilteredUsers=AllUsers。过滤器(()=>真);AllUsers===filteredUsers;//true作为Reactkey的想法比较有意思。如果Record提案保证引用是严格不可变的,那么我们可以不用任何其他手段就可以使用项目本身作为键,这样维护成本就会大大降低。constlist=#[#{country:'FR',localPhoneNumber:'111111'},#{country:'FR',localPhoneNumber:'222222'},#{country:'US',localPhoneNumber:'111111'},];<>{list.map((item)=>(
))}>当然这还是建立在浏览器实现了Record的前提下非常高效,假设浏览器使用deepEqual作为初稿来实现这个规范,那么上面的代码可能会导致没有卡住的页面直接崩溃退出。TS支持可能ts会支持通过以下方式定义不可变变量:[]);//穷人的获取useEffect(()=>{fetchUsers(usersFilters).then(setUsers);},[usersFilters]);返回<用户users={users}/>;};那么我们就可以真正保证usersFilters是不可变的。因为现阶段ts不能保证变量引用在编译时是否会发生变化。优化css-in-js使用Record和普通对象作为css属性。css-in-js有什么区别?constComponent=()=>(
这有一个暖粉色背景。 );由于css-in-js框架会为新的className生成一个新的引用,所以如果不主动保证引用不可变,那么在渲染过程中className会一直变化,不仅影响调试而且也会影响性能,而Record可以避免这种担心。综上所述,Record提案并没有解决以前无法解决的问题,而是解决了复杂逻辑才能用更简洁的原生语法解决的问题。这样做带来的好处主要是“不容易写出问题代码”,或者说js语言的Immutable入门成本更低。现在来看,这个规范比较严重的关注点是性能,stage2并没有对浏览器的性能提出要求,而是给出了一些建议,在stage4之前给出了具体的性能优化建议。提到了一些具体的做法,包括快速判断真假,也就是数据结构操作的优化。快速真值判断可以使用相似的hash-cons来快速判断结构是否相等,可能是将一些关键的判断信息存储在哈希表中,这样就不需要递归判断结构了。快速误判可以通过维护一个哈希表来快速判断,或者我觉得也可以使用一些数据结构的经典算法,比如布隆过滤器,用于高效快速判断的场景。Record减轻了什么样的精神负担?事实上,如果应用开发是helloworld复杂度,那么React也能很好地适应immutable。例如,我们传递给React组件的props都是布尔值、字符串或数字:
;比如上面的例子,你根本不需要关心引用的变化,因为我们使用的原始类型是无法改变引用本身的。比如18是不能突变成19的,如果子组件真的要19的话,只能新建一个。简而言之,我们没有办法改变我们传递的原始类型。如果我们永远在这种环境中开发,React与immutable的结合会很棒。但好景不长。我们总是要面对对象和数组的场景。但是,这些类型在js语法中不是原始类型。我们了解到,还有一个术语“引用”。两个不同值的对象可能===全等。可以认为Record从语法层面消除了这个顾虑,即#{a:1}也可以看成18、19这样的数字,不可能有人改,所以从语法层面,你会像19一样。放心,像#{a:1}这样的数字是不会变的。当然,这个提案面临的最大问题是“如何把一个有子结构的类型当作原始类型”。或许JS引擎把它当成一个特殊的字符串来处理更合适,但难点在于这违反了整个语言系统对子结构的默认识别,Boxpacking语法特别别扭。总结一下看完这篇文章后的想象,React和Records&Tulpes的结合肯定会很好,但是前提是浏览器的性能优化一定要和“参考对比”大致一样,比较难得,有这种对性能要求苛刻的特性,因为没有性能的加持,它的便利性将毫无意义。讨论地址为:精读《Records & Tuples for React》·第385期·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号