SolidJS是一个语法像React函数组件,内核像Vue的前端框架。本周我们阅读了文章SolidJS简介以了解其核心概念。为什么引入SolidJS而不引入其他前端框架?因为SolidJS在教React团队正确实现Hooks,这在当时只有React概念和虚拟DOM概念跟风的时代是非常难得的。这也是开源技术的魅力所在:任何观点都可以自由挑战,只要你是对的,就有可能脱颖而出。概述整篇文章都是从新手的角度讲解SolidJS的用法,但本文假设读者有React基础,所以只讲解核心区别。渲染函数只执行一次。SolidJS只支持FunctionComponent的写法。不管内容有没有状态管理,组件是否接受父组件透传Props,渲染函数只会触发一次。因此,它的状态更新机制与React有本质区别:React状态发生变化后,通过重新执行Render函数体来响应状态变化。Solid状态发生变化后,通过重新执行状态代码块来响应状态变化。与React的整个渲染函数重新执行相比,固态响应粒度非常细,即使在一段JSX中调用了多个变量,也不会重新执行整段JSX逻辑,而只是变量部分被更新:constApp=({var1,var2})=>(<>var1:{console.log("var1",var1)}var2:{console.log("var2",var2)});上面的代码在单独改变var1时发生变化,只打印var1不打印var2,这在React中是不可能的。这一切都源于SolidJS对抗React的核心理念:面向面状态驱动而非视图驱动。也正是因为这个区别,渲染函数只执行一次,顺便推导出可变更新粒度的结果。也是其高性能的基础,也解决了ReactHooks不够直观的顽固问题。更完善的Hooks实现SolidJS使用createSignal实现类似ReactuseState的能力。虽然看起来很相似,但实现原理和使用时的想法完全不同:constApp=()=>{const[count,setCount]=createSignal(0);返回setCount(count()+1)}>{count()};};我们需要用SolidJS的思维来完全理解这段代码,而不是React的思维来理解它,即使它看起来太像Hooks。一个显着的区别是将状态代码推送到外层是完全没问题的:const[count,setCount]=createSignal(0);constApp=()=>{returnsetCount(count()+1)}>{count()};};这是理解SolidJS概念最快的方式,即SolidJS根本不理解React的概念,SolidJS理解的数据驱动是纯数据驱动Views,无论数据定义在哪里,Views在哪里是,可以建立绑定。自然地,这种设计并不依赖渲染函数被多次执行。同时,因为使用了依赖收集,所以不需要手动声明deps数组。也可以在条件分支后面写createSignal,因为没有执行顺序的概念。派生状态可以用回调函数声明:constApp=()=>{const[count,setCount]=createSignal(0);constdoubleCount=()=>count()*2;返回setCount(count()+1)}>{doubleCount()};};这是一个不如React方便的地方,因为React付出了巨大的代价(数据改变整个函数体后重新执行),所以你可以用更简单的方式定义派生状态://ReactconstApp=()=>{const[计数,setCount]=useState(0);constdoubleCount=计数*2;//这个块比SolidJS定义的返回更简单(setCount((count)=>count+1)}>{doubleCount});};因为它的成本太大了。我们继续分析为什么SolidJS这种看似简单的派生状态写法可以奏效。原因是SolidJS收集了所有用到count()的依赖,doubleCount()用了,渲染函数用了doubleCount(),仅此而已,依赖自然就挂了。该实现过程简单、稳定。没有魔法。SolidJS也支持派生字段的计算缓存,使用createMemo:constApp=()=>{const[count,setCount]=createSignal(0);constdoubleCount=()=>createMemo(()=>count()*2);返回setCount(count()+1)}>{doubleCount()};};也不需要写deps依赖数组,SolidJS通过依赖收集来驱动count变化doubleCount这一步,这样在访问doubleCount()的时候,就不需要一直执行它回调的函数体,造成额外的性能开销。状态监控对比React的useEffect,SolidJS提供了createEffect,但是相比之下,没有写deps,它真的是监控数据,不是组件生命周期的一部分:constApp=()=>{const[count,setCount]=createSignal(0);createEffect(()=>{console.log(count());//当计数改变时重新执行});};这再次体现了为什么SolidJS有资格“教”React团队实现Hooks:nodepsdeclaration。将监听器与生命周期分开,这是React经常混淆的。在SolidJS中,生命周期函数有onMount和onCleanUp,状态监控函数有createEffect;而React所有的生命周期和状态监控功能都是useEffect。虽然看起来比较简洁,但是要判断哪些人精通ReactHooks并不容易。就是监控,也就是生命周期。模板编译为什么SolidJS可以如此神奇的解决React的这么多历史问题,而React却不行?核心原因还是在SolidJS加入的模板编译过程。以官方Playground提供的demo为例:functionCounter(){const[count,setCount]=createSignal(0);constincrement=()=>setCount(count()+1);return({count()});}编译为:const_tmpl$=/*#__PURE__*/template(``,2);functionCounter(){const[count,setCount]=createSignal(0);constincrement=()=>setCount(count()+1);return(()=>{const_el$=_tmpl$.cloneNode(true);_el$.$$click=increment;insert(_el$,count);return_el$;})();}首先是组件JSX部分被提取到全局模板中。初始化逻辑:在模板中插入变量;更新状态逻辑:由于在insert(_el$,count)时已经绑定了count和_el$,所以下次调用setCount()时,只需要更新绑定的_el$就可以了,不用管它在哪里.为了更完整的实现这个功能,使用模板的Node必须完全分离。我们可以测试稍微复杂一点的场景,比如:这段代码编译后的模板结果为:const_el$=_tmpl$.cloneNode(true),_el$2=_el$.firstChild,_el$4=_el$2.nextSibling;_el$4.nextSibling;_el$.$$click=increment;insert(_el$,count,_el$4);插入(_el$,()=>count()+1,null);将模板分为一个整体和三个子块,分别是字面量、变量和字面量。为什么不添加最后一个变量?因为最后的变量插入可以直接放在_el$的末尾,而中间的插入位置需要insert(_el$,count,_el$4)给父节点和子节点实例。SolidJS精读之谜已解开,作者自问自答。为什么createSignal没有类似于hooks的顺序限制?ReactHooks使用deps来收集依赖项。下次执行渲染函数体时,因为没有办法识别“为哪个Hook声明了deps”,只能靠顺序作为识别依据,所以需要一个稳定的顺序,所以不能出现条件分支在前。SolidJS本身的渲染函数只执行一次,所以不存在React重新执行函数体的场景,createSignal本身只是创建一个变量,createEffect只是创建一个monitor,回调函数内部处理逻辑,而与视图的绑定是通过依赖收集完成的,因此也不受条件分支的影响。为什么createEffect没有useEffect关闭问题?因为SolidJS函数体只执行一次,组件实例中不会出现N个闭包,所以不存在闭包问题。为什么React是错误响应?React响应组件树的变化,通过组件树自上而下的渲染来响应更新。但是,SolidJS只响应数据,甚至可以在渲染函数之外声明数据定义。所以虽然React说它是响应式的,但开发者真正响应的是UI树的逐层更新。在这个过程中,会出现关闭问题。手动维护deps,条件分支后不能写hooks,有时分不清当前更新是父组件rerender还是state变化引起的。这一切都表明React不允许开发人员真正只关心数据的变化。如果他们只关心数据变化,为什么组件重新渲染的原因可能是因为“父组件重新渲染”?为什么SolidJS去掉了虚拟dom仍然很快?虚拟dom虽然避免了整体dom刷新带来的性能损失,但是也带来了diff开销。对于SolidJS,它提出了一个问题:为什么要避免dom的整体刷新,而不能局部更新呢?是的,部分更新并非不可能。模板渲染后,单独抽取jsx的动态部分,结合依赖收集,变量变化时可以实现点对点的更新,不需要domdiff。为什么信号变量不能用count()写成count?作者没有找到答案。理论上Proxy应该可以完成这个显式的函数调用动作,除非它不想引入Mutable开发习惯,让开发习惯更加Immutable。道具的绑定不支持解构。由于响应式的特性,解构会失去代理的特性://?constApp=(props)=>
{props.userName}
;//?constApp=({userName})=>
{用户名}
;虽然也提供了splitProps来解决这个问题,但是这个功能还是不太自然。这个问题比较好的解决办法是通过babel插件来规避。createEffect不支持不带deps的异步,虽然很方便,但是在异步场景下还是无解:constApp=()=>{const[count,setCount]=createSignal(0);createEffect(()=>{asyncfunctionrun(){awaitwait(1000);console.log(count());//不会被触发}run();});};总结一下,SolidJS的核心设计只有一个,就是让数据驱动真正回归数据,而不是绑定到UI树上,React就在这一点上误入歧途。SolidJS虽好,但相关组件生态尚未兴起,巨大的迁移成本是其难以快速替换到生产环境的最大问题。前端生态要想无缝升级,似乎首先要思考“代码范式”,以及代码范式之间如何转换。范式确定后,社区将竞相完成落地,生态迁移不会有任何困难。.然而,上述假设是不成立的。技术迭代总是以BreakChange为代价,很多时候我们只能放弃旧项目,在新项目中实践新技术,就像Jquery时代一样。讨论地址为:Jingdu《SolidJS》·Issue#438·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号
版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)