SolidJS是一个号称比React还要react的框架。SolidJS与React一样,也是使用JSX编写代码,但是与React不同的是,SolidJS没有使用React中使用的VDOM技术,而是直接处理DOM树。这种处理方式就赋予了SolidJS接近原生Javascript的性能。
其实看看SolidJS的说明,就会发现SolidJS文档里提到的JSX、Fragments、Context、Portals、Suspense、Streaming SSR、Progressive Hydration、Error Boundaries、Concurrent Rendering都是在React里非常常见的。如果值看到这里,可能会觉得SolidJS和React基本上是一样的。但是SolidJS与React肯定是不一样的,只是由于SolidJS与React的一些概念十分类似,所以也就让熟悉React的人可以很方便的迁移到SolidJS。
这篇文章的主要目的就是记录和辅助一下如从React迁移到SolidJS。
应用的启动
React中将应用根绑定到DOM界面中是使用ReactDOM
中的createRoot()
和render()
方法。例如:
|
|
在SolidJS里,这个被简化成了一个render()
函数。例如:
|
|
同时,SolidJS也提供了,hydrate()
、renderToString()
、renderToStream()
等渲染方法,允许SolidJS可以像React一样,实现SSR等功能。
组件创建
SolidJS的组件创建和目前React函数式组件的创建十分类似。组件都是以一个函数的形式出现,组件所表显的内容都由函数的返回值确定。例如在React中,一个最简单的组件是这样创建的:
|
|
在SolidJS中定义组件也是一样的。当需要定义其他复杂的组件的时候,所使用的JSX格式与React中也是一致的。
生命周期控制
生命周期在React改用函数式组件定义的以后,得到了简化,从之前类组件中的componentDidMount
和componentDidUnmount
等生命周期方法,改成了使用useEffect
Hook的实现。例如:
|
|
但是在SolidJS中,生命周期是通过onMount()
和onCleanup()
两个函数来定义的,并且与React不同,SolidJS把组件的生命周期与组件的副作用分开处理了,这样就使得组件中的代码功能更加清晰。例如在SolidJS中实现上面的组件:
|
|
从上面这个例子可以看出来,SolidJS中定义组件的代码相较于React中的组件定义各个功能区域更加的清晰,每个功能区域所实现的内容也更加的明了。
条件渲染
条件渲染在任何一个前端框架中都是非常常见的操作,在React中,根据条件渲染组件的内容通常都是利用判断语句来控制被控组件的输出。例如:
|
|
React中可以这样来实现是因为React中是通过VDOM比较来完成渲染内容的判断的。但是SolidJS没有VDOM,是直接渲染DOM的,所以如果也按照React的方式来渲染纵然可以,但是会导致DOM的额外刷新操作,所以SolidJS提供了专门用于条件渲染的一系列组件:<Show>
、<For>
、<Index>
、<Switch>
、<Match>
等。
对于上面React的示例,这种简单的分条件渲染可以直接使用SolidJS提供的<Show>
组件实现。
|
|
<Show>
组件可以接受一个fallback
属性,可以用来提供当条件不满足时候的的渲染内容,相当于else
分支。
复杂分支条件的渲染在React中一般是通过多个判断的罗列来完成的,但是在SolidJS里如果也使用大量的<Show>
来罗列输出,就有一些繁琐了,所以SolidJS提供了<Switch>
和<Match>
组件。例如以下两段React和SolidJS代码的功能是一致的。
|
|
对比上面React和SolidJS的两段代码,虽然SolidJS的代码更长,但是表意更加明确。不过为了提升性能做出的牺牲,代码长一些也是值得的。
循环渲染
输出一个列表或者Map,也是组件中非常常用的的操作。循环输出一个列表的操作也是SolidJS诟病React性能低下的一个地方。在React使用.map()
方法循环输出列表内容的时候,如果列表的内容发生了变化,那么React就需要重复输出整个列表来判断VDOM是否发生了变化。SolidJS里为了能更明确的操作DOM,于是就提供了<For>
和<Index>
组件来循环输出列表内容。
以下是React和SolidJS输出相同内容列表的代码对比。
|
|
<For>
组件提供的是自动创建带有key
的循环输出,如果不需要输出key
,那么可以使用SolidJS提供的<Index>
组件来循环输出列表。
动态组件
动态组件是在DOM渲染的时候,不能确定具体要渲染的组件是什么,而被渲染到DOM中的组件是在运行时才能确定的。其实在前面的条件渲染中就已经是动态组件的概念了,只是动态组件的概念提出来是为了对条件渲染进行进一步的优化。React对组件的动态渲染其实并不是十分关心,有VDOM树做为一层结果缓存,React实际上并不需要特意的去响应某一个组件的改变和渲染。
但是SolidJS并不是这样,直接操作DOM的特性要求SolidJS必须清晰的明确的知晓究竟是哪一个组件发生了变更,从而确定所需要操作的DOM部分。为了提供动态组件的优化,SolidJS提供了一个专用的<Dynamic>
组件,这个组件可以接受一个组件和需要传递给这个组件的Props。
以下是一个动态表单的示例。
|
|
示例中的这种组件选择渲染的模式不论在SolidJS中还是在React中都是十分常见的。<Dynamic>
实际上仅接受一个component
属性来确定其要渲染的组件,列举在<Dynamic>
组件中的其他属性,都将被传递给确定的所需要渲染的组件,包括<Dynamic>
组件的children
内容。
错误处理
React中的组件错误处理通常是分为两种的,一种是类组件,一种是函数式组件。在类组件中,错误处理是通过componentDidCatch
来实现的。但是在函数式组件中,就没有一个专用的Hook来处理组建中发生的错误。在React函数式组件中,错误都是通过try/catch
来完成捕获和处理的。
还是一如之前提到的,SolidJS需要直接对DOM进行操作,所以就不是那么方便使用try/catch
来进行错误处理。SolidJS引入了一个专用的组件<ErrorBoundary>
来捕获组件树中出现的错误。例如以下示例中,在<ErrorBoundary>
组件包裹的组建中出现错误的时候,<ErrorBoundary>
组件就可以使用其fallback
属性设定的回退值来代替出现错误的组件。
|
|
其实React中也存在第三方制作提供的错误边界处理组件,其原理也是基本类似的。
组件属性
组件属性是组件在使用时获取基本数据的方式。无论是在React还是SolidJS中,组件都是可以接受属性参数的。在之前的一些示例中,已经展示了函数式组件接受一个名为props
的参数来保存和获取其组件参数。React和SolidJS中对于传入组件的属性处理时,主要传入的属性发生了变化,那么组件就会需要重新渲染。
常用的组件属性中会包含传入组件中需要做为组件的子元素展示的children
属性,这个属性无论在React中还是在SolidJS中,都是直接位于props
下的。在React中,携带有children
属性的props
的类型为PropsWithChildren<T>
;在SolidJS中,这个props
的类型为ParentProps<T>
。
除了普通的组件属性和组件的子元素以外,组件常常还需要接受一个名为ref
的属性,这个属性的类型定义为Ref<T> = T | ((val: T) => void)
。如果组卷的props
参数中需要携带这个属性的时候,需要自行包含。例如ParentProps<{ ref: Ref<JSX.Element> }>
。但是这个属性在React中的表达就复杂多了:ForwardRefExoticComponent<T>
。
ref
引用的使用区别,SolidJS要更加的简单,因为SolidJS总是会创建实际的DOM元素。而React首先创建的是VDOM元素,但此时并不能够保证在使用ref
的时候,ref
一定可以引用到具体的实际DOM元素。
本地状态
一个组件的渲染结果往往是组件的渲染方法与组件的各种本地状态的组合。在React中,组件的本地状态通常是使用useState
或者useMemo
等Hook定义的。useState
实际上就是定义了一个关联了内容修改方法的变量,这个内容修改方法实际上的功能就是标记组件已经发生了变化。如果直接在组件里使用普通变量的话,框架本身是无法获知变量的内容发生了变化的。
例如在React中,可以这样来创建和使用组件的本地状态。
|
|
其实React还没有SolidJS那么依赖于组件状态变化的标记功能,因为React主要是通过对比VDOM树的不同来决定DOM的更新的。SolidJS因为要直接操作DOM,所以必须明确知晓具体的哪一个组件发生了变化。这也就是为什么在SolidJS中,组件的状态的创建是采用createSignal()
函数的。这里的 signal 实际上就是在通知SolidJS哪个组件发生了变化。所以在SolidJS中,组件的本地状态实际上更应该称为 信号函数。
在SolidJS中,上面的这个示例可以改写为以下这个样子。
|
|
setCount()
除了可以接受一个具体的值来更新组件中的本地状态以外,还可以接受一个函数作为参数。这种情况下,这个函数需要接受一个参数,这个参数的内容是这个本地状态的原始值,函数的返回值则是本地状态的新值,例如上面示例中大大本地状态更新还可以写成setCount(c => c + 1)
。
createSignal
函数创建的第一个元素实际上是一个函数。这是因为在Javascript中,闭包函数可以捕获其周围的变量,从而实现不同调用之间的内容隔离。
对比React中本地状态的使用和SolidJS中的本地状态使用,可以看到SolidJS中的组件本地状态的操作会更加简单和清晰。在SolidJS中使用本地状态(信号函数)时还有一个特点,就是任何依赖于本地状态的函数,也都是一个本地状态(信号函数)。例如以下这个示例。
|
|
在这个示例中,doubleCount()
这个函数实际上就是一个单纯的状态函数,它所对应的状态值的更新是根据另一个信号函数count()
而更新的。
此外,createSignal
并不局限于在组件内部使用。在组件之外创建的 signal 会形成一个类似于共享的全局状态的内容,其内容在更新的时候,会使得所有使用这个状态的组件位置发生更新。并且通过createSignal
创建的本地状态,还可以支持嵌套使用,也就是在一个 signal 中记录另一个 signal。
记忆值
记忆值是React和SolidJS中提供的用来减少DOM更新的一种组件本地状态,也可以称这种状态值为 惰性状态值 或者 计算值,这种状态值大多由其他的本地状态值或者组件属性值计算得到。无论组件中的其他本地状态如何发生变化,这些记忆值只会在与它们存在关联的状态值发生变化的时候变化。这也就避免了在组件发生更新的时候,需要计算的值发生不必要的更新。
在React中,记忆值的创建是通过useMemo
Hook完成的。例如:
|
|
在SolidJS中,记忆值的创建是使用createMemo
完成的,而且实际上更加的简单。例如上面这个示例可以改写为下面的样子。
|
|
这里需要注意的是,createMemo()
创建出来的记忆值依旧是一个函数,在获取其记忆的值的时候,也是需要调用函数。所以在使用SolidJS的时候,需要注意熟悉Javascript中函数和闭包的特性。
即时计算
计算值与记忆值的处理区别是计算值是在渲染前完成计算的。在React中计算值就是存在于组件中的普通表达式,但是在SolidJS中,就需要额外的声明,以明确其计算阶段的归属。
SolidJS提供的createComputed()
函数来实现在渲染前完成即时计算的。这个createComputed()
的使用与createMemo()
十分相像。
createComputed()
和createMemo()
在使用的时候需要注意其功能区别,createComputed()
是即时计算,createMemo()
是延迟计算的。
createComputed
创建的仅仅是一个计算,而不是一个计算出的新状态,这也是createComputed
没有返回值的原因之一。而且除了createComputed
自身以外,没有任何启动的渠道可以获取到createComputed
的返回值。
监听值
不管是使用createMemo
还是使用createComputed
还是直接使用本地状态计算产生新的状态值,其实质都是针对原有的状态值建立了隐式的监听。SolidJS除了这种隐式监听的方式以外,还提供了显式定义的方法。只是在使用这种显示定义方法时,需要配合副作用功能的创建。
以下是对于前面示例的一个改版示例。
|
|
批量更新
多个组件本地状态的批量更新是一定会引起组件的重新渲染的,但是因为React中有VDOM这一层缓冲,所以大量状态的批量更新是可以在生成VDOM树和比较VDOM区别的时候进行一定的优化的。但是SolidJS是直接操作DOM的,如果连续调用本地状态更新函数,那么就会使一个组件反复进行重新渲染。
所以在SolidJS中批量更新状态值就需要使用SolidJS提供的优化方法batch
。例如以下示例。
|
|
所有排列在batch
中的状态更新方法,都会被优化为一个更新动作,使SolidJS只需要重新渲染一次相关的组件,而不是反复的多次渲染。
副作用
副作用是React和SolidJS中都十分重要的一项功能。副作用允许组件在渲染的时候可以完成一些与渲染操作无关的事情。React中的副作用定义是通过useEffect
Hook实现的,而且React中的useEffect
Hook同时还整合了组件的生命周期方法。这种设计在一定程度上使useEffect
的使用变得复杂。
SolidJS中的副作用是通过createEffect()
函数创建的。以下是一个创建并使用副作用方法的示例。
|
|
从以上示例中可以看出SolidJS中副作用的执行是基于本地状态的更新的,SolidJS会自动为副作用增加其中依赖的监听,不需要像React中需要手动设置副作用的监听。如果需要取消一个依赖的监听,则需要在副作用中使用untrack
函数获取状态(信号)的内容。例如以下示例中,b
的更新就不会触发副作用的执行。
|
|
createEffect
创建的副作用是在渲染完成以后执行的,如果需要副作用在渲染之前执行,那么副作用需要使用createRenderEffect
。
SolidJS中的createEffect
与React中useEffect
不同的是,createEffect
接受的闭包参数可以接受一个参数,这个参数是这个副作用上一次执行的结果,这种使用方法可以参考这个抽象的说明:createEffect(previousValue => newValue, initialValue)
。
事件处理
事件处理是React和SolidJS中处理用户操作界面元素的操作。SolidJS中对于事件的处理是十分简单的,几乎就是简单的将HTML中产生的事件直接交由组件中定义的处理函数处理。React中对于事件处理函数的定义时存在一个建议的,因为事件处理函数中常常需要使用到组件中定义的本地状态,所以为了避免组件重新渲染时带来的事件处理函数重新声明带来的相能写消耗,就建议使用useCallback
Hook来定义事件处理函数。
相对React来说,SolidJS中定义事件处理函数就十分简单了,直接定义一个普通函数即可。在React和SolidJS中事件处理函数定义的不同可以通过以下两个示例对比。
|
|
onClick={[handleClick, itemId, itemAmount]}
。这种情况下,处理函数的定义中需要首先接收绑定时提供的额外参数,然后再接收代表事件的对象,即function handleClick(id, amount, event)
。
在SolidJS中,监听一般的HTML事件可以直接使用HTML规范中定义的事件。此外SolidJS还支持使用on:
的语法来支持一些自定义的事件的处理。
上下文
上下文在React和SolidJS中提供的都是依赖注入的功能,可以在组件树中向某一节点及其后代节点插入一个共享的可供访问和使用的对象。在上下文的使用方法上,React和SolidJS基本上是一样的,都是创建一个上下文对象,然后使用这个对象提供的Provider
方法将其注入到组件树中。
通过上下文注入的内容一般没有什么限制,可以是任意类型的对象。以下是在React和SolidJS中分别实现相同的上下文对象注入的示例。
|
|
不管是React,还是SolidJS的createContext
函数接受的都是上下文所需要共享出去的对象的默认值。而在Context.Provider
里提供的具体值才是组件树中各个组件从上下文里获取到的内容。
全局状态
在React中,全局状态一般是由第三方库提供的,比如常见的Redux、MobX、Zustand等。SolidJS中的全局状态是由一个官方全局状态库solid-js/store
提供的。
solid-js/store
提供的最基本的一个函数是createStore
,这个函数可以创建一个存储对象。通过createStore
创建的对象主要是提供了比createSignal
创建的状态对象更方便于使用的状态存储。createStore
创建出来的存储对象也是遵守单向数据流的概念的,
以下是创建和使用全局状态的简单示例。
|
|
solid-js/store
也提供了一个createMutable
这种可以创建追踪变化值的函数,但是这样创建出来的对象往往会打破单向数据流的概念,所以一般是不建议使用的。
在上面示例中,出现了一个 getter 方法,这个方法可以看做是状态中的计算状态,默认情况下 getter 会在每次使用的时候进行运算。所以在使用的时候可能会需要对计算得到的结果进行缓存以优化性能。比如上面示例就可以优化成以下样子。
|
|
通过createStore
中定义出来的 Store 所提供的更新方法,可以使用其返回的 setter 方法非常方便的设置其下任意一级内容的具体值。例如上面示例中的setState("count", "action", a => a + 1)
,其意义就是设置state.count.action
的值,也就是一个值的访问路径,这个内容的新值采用其后的闭包计算得到。设置方法接受的闭包的参数实际上就是之前访问路径所指定值的原始值。
额外的,solid-js/store
还提供了一个produce
方法,可以简化值的设定,可以允许使用更加自然的值设定方法。produce
函数的使用与 Immer 库中的produce
函数的使用基本上是一致的。例如使用produce
函数来完成上面示例中的更新就是以下样子。
|
|
使用这种方式来更新 Store 里的内容虽然需要额外书写一些东西,但是整体来看,这样的确是更加自然。
异步控制
异步是前端项目中与服务端交互或者完场一些耗时任务所需要的必要操作。异步也是现代程序中,尤其是Javascript中的一个基石般的概念。在React中,异步函数的使用基本上与同步函数无异,但是在SolidJS中,异步函数还被提供了一种独特的用法。
异步函数在被用于与服务端交互获取数据的时候,获取到的内容通常都是项目运行所使用的数据资源。所以SolidJS就提供了createResouce
函数,允许把一个本地状态与一个异步函数结合起来做成一个资源。createResource
有两种使用方法,一种是接受两个参数,一种是一个参数。
可以接受两个参数的createResource
所接受的第一个参数,实际上是传入异步函数的参数,这个参数既可以是一个普通变量,也可以是一个能够获取某个值的函数(比如一个 signal)。以下是一个常见的从服务端异步获取数据的示例。
|
|
在这个示例中,当调用setUserId
改变userId
状态的时候,SolidJS就会自动取调用fetchUser
获取服务端的信息,之后将解析到的信息置入user
里。
createResource
函数其实返回的列表有两个元素,以下是createResource
函数返回值的类型定义,可以在使用的时候参考。
|
|
对于只能接受一个参数的createResource
版本,这个可以被接受的参数仅仅是异步函数本身了。其实对于任何一个版本的createResource
函数,也对于传入的异步函数做了一些定义,实际上也提供了允许异步函数接受和使用createResource
中一些特性的能力。以下是createResource
中声明的异步函数的签名。
|
|
在这个函数签名中的info.value
表示上一次调用异步函数后获取到的内容,info.refetching
表示当前的调用是否是一次手动激活的异步调用。
SolidJS中的独特功能
SolidJS作为一个后发的框架,除了从React中吸取了大量的设计经验,还参考其他的框架增加了许多十分便捷的功能。
自定义指令
自定义指令(Custom Directive)是一种类似于Mixin的功能,允许定义一个公共处理功能或者资源,提供给众多不同类型的组件使用。自定义指令在SolidJS中实际上就是一个普通的函数,但是被规定接受两个参数,第一个参数是这个自定义指令被绑定到的组件,第二个是自定义指令中值的访问方法,所以这个函数的签名是function directive(el: JSX.Element, accessor: () => any): void
。
以下是一个自定义指令的示例。
|
|
onCleanup
做释放清理。