react19走起!无Signal,坚守不可变数据与单向数据流!
Date:
react过度重新渲染的问题,使得开发人员历来花费了大量时间来解决这个性能问题——对导致重新渲染的代码进行持续追踪和优化工作一直是工程师们的重复任务。
而react19,据说框架将自动处理重新渲染,手动干预将不再必要。
去年底,react19发布:
react19一发布,就被尤大大吐槽……
Signal 明明可以解决React的性能问题——更新颗粒过大的问题——使得用户不需要再写 shouldComponentUpdate 来优化性能,Signal 甚至可以解决 useEffect 的各种坑 (是的, React官方文档里有6篇就是教你怎么避开 useEffect的坑) , Angular、Preact、SoildJs、Vue 都在拥抱 Signal ,React执拗的不愿意使用 Signal
React使用虚拟DOM和协调算法,当状态变化时,重新渲染组件树,然后通过Diffing算法找出变化部分,更新真实DOM。这个过程可能会导致不必要的子组件重新渲染,所以用户需要通过shouldComponentUpdate或React.memo来优化,避免性能问题。而Signal的核心是细粒度的响应式更新,状态变化直接定位到需要更新的部分,不需要整个组件树重新渲染。
Signal 是一种细粒度的响应式编程模型,它通过跟踪状态变量的具体依赖关系,实现精准的局部更新。其核心优势在于:
性能优化: Signal 自动追踪依赖关系,仅更新与状态变化直接关联的 UI 部分,无需手动优化。
规避 useEffect 的复杂性:Signal 通过自动依赖追踪(如 Vue 的 watchEffect、SolidJS 的 createEffect),让副作用与状态变化自然绑定,减少心智负担。
通过响应式机制,在复杂应用中更高效地管理状态与 UI 的同步,同时降低开发者优化性能的成本。
那为什么React不采用Signal呢?
React推崇的是不可变数据和单向数据流,强调组件的纯函数特性,这样虽然可能在性能上有损耗,但代码更可预测,易于调试。
而Signal属于响应式编程,状态变化自动触发更新,可能在复杂应用中更高效,但可能牺牲一定的可预测性。
链接:https://www.zhihu.com/question/638475234/answer/3354463805
问题都是由于React 最初的 UI = f(state) 函数式哲学延伸来的,因为把整个组件视作纯函数,所以搞出了 VDOM, 又不得不对现实妥协,现实世界的组件从来就不是那么纯粹,所以还得支持声明周期和副作用,于是整出了 useEffect 这些,而组件的副作用和生命周期被封装在这个函数 f 里面,每次 state 变化所有的东西都得重新运行一遍,所以又搞出了 useMemo, useCallback, useXXX 来补救。
反观 Solid 的路子就很直接,组件不是纯函数,组件内的代码只运行一次,只有需要更新的 DOM 才会响应数据变化,和 React 是不同的心智模型,所以尽管 Solid 的 createSignal 和 React 的 useState 看起来很像,但其实完全不是同一个东西,signal 没有 hooks 的那些规则限制,写在组件外面也没有问题,就很自由,这才是本来应该的样子,React 明显走了太多的弯路,并且越走越远。
React Hooks的出现,尤其是useState和useEffect,虽然带来了灵活性,但也引入了闭包陷阱、依赖数组等问题,导致开发者需要处理很多边缘情况。而Signal可能简化这些,比如自动跟踪依赖,无需手动声明依赖数组,这可能减少useEffect的问题。
或许React所谓的 注重开发体验和可维护性,即使牺牲部分性能——通过并发模式(Concurrent Mode)和Suspense等特性,通过异步渲染和优先级调度来优化性能,而不是转向响应式模型。
React官方确实强调不可变数据和声明式组件,他们认为显式的状态管理更可控,而响应式系统可能带来隐式的依赖追踪,增加调试难度。
React 的组件是纯函数,接收 props 返回 UI 描述。这种模型简化了 UI 的声明式表达,虽然牺牲了更新粒度。React 更倾向于通过算法优化(如 Fiber 架构、并发渲染)弥补性能问题,而非改变组件模型。
此外,React的并发特性需要组件渲染可中断,而响应式系统可能与之不兼容,或者需要不同的实现方式。
所以,react坚守 不可变数据与单向数据流,核心在于 调试和并发渲染!总之:
Signal 阵营:追求极致的性能与开发体验,通过响应式模型降低心智负担。
React 阵营:优先保障代码的可维护性与显式控制——让开发者显式控制更新边界!
react19 Ref/forwardRef
Reac文档有个很有意思的细节:useRef、useEffect这两个API的介绍,在文档中所在的章节叫Escape Hatches(逃生舱)。
为什么ref、effect被归类到「逃生舱」中?比如执行ref.current.appendChild插入子节点!
同样是操作DOM,这些属于「React控制范围内的因素」,通过ref执行这些操作就属于失控的情况。
「高阶组件」无法直接将ref指向DOM,这一限制就将「ref失控」的范围控制在单个组件内,不会出现跨越组件的「ref失控」。——「为了将ref失控的范围控制在单个组件内,React默认情况下不支持跨组件传递ref」。
forwardRef
使用forwardRef(forward在这里是「传递」的意思)后,就能跨组件传递ref。
const MyInput = forwardRef((props, ref) => { return <input {...props} ref={ref} />; }); function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
forwardRef 设计对复杂场景(如多个 ref 或高阶组件)的支持不够友好。
useImperativeHandle
除了「限制跨组件传递ref」外,还有一种「防止ref失控的措施」,那就是useImperativeHandle,他的逻辑是这样的:既然「ref失控」是由于「使用了不该被使用的DOM方法」(比如appendChild),那我可以限制「ref中只存在可以被使用的方法」。
useImperativeHandle(ref,() => ({ validate: () => {}, setCustomValidity: (message: string) => {}, getValue: () => inputRef.current?.value, }), [setErrorMessage], )
这就杜绝了「开发者通过ref取到DOM后,执行不该被使用的API,出现ref失控」的情况。
React 19
React 19 允许函数组件直接声明 ref 为 props,无需 forwardRef(deprecate):
function MyComponent({ ref }) { return <div ref={ref}>...</div>; }
现在react的解释是:
React 19 通过让所有组件类型(函数组件、类组件、memo 组件等)统一通过 props 接收 ref,消除了这种差异,使组件行为更可预测。
React 19 允许通过 props 直接传递 ref,使得组合 ref 或传递多个 ref 更灵活
其实,个人感觉最根本都原因是:旧的 ref 机制在 Server Components 和并发渲染时
forwardRef 的“显式转发”模式在 Server Components 中可能引发序列化问题。
隐式传递 ref 更符合 React 未来对组件树的静态分析需求。
总之:React 19 通过让 ref 成为普通 props,移除了这种“魔法”,降低了学习与使用成本。
React编译器
从React的早期开始,我们针对此类情况的解决方案一直是手动记忆化。在之前的API中,这意味着应用useMemo、useCallback和memo API来手动调整React在状态变化时重新渲染的部分。
React 中的重新渲染通常是级联的。每次触发组件的重新渲染时,它都会触发每个嵌套组件的重新渲染,这会触发每个组件内部的重新渲染,依此类推,直到到达 React 组件树的末尾。
React.memo它会停下来检查其 props 是否已更改(阻止react渲染链)。如果没有任何 props 更改,重新渲染将被停止。然而,如果哪怕一个 prop 发生了变化,React 将继续进行重新渲染,就好像没有记忆化一样!
为了解决这个问题,我们有两个钩子:useMemo 和 useCallback。这两个钩子将在重新渲染之间保留引用。useMemo 通常用于对象和数组,useCallback 用于函数。将 props 包装在这些钩子中就是我们通常所说的“记忆化 props”。
有了有很多文章,向你展示大多数开发人员如何过度使用 useMemo等,并提供一些避免这种情况的技巧。
因此,React 团队创建了React 编译器。React 编译器现在将管理这些重新渲染。React 将自行决定何时以及如何改变状态并更新 UI。
因为Compiler 可以解决:一个 React 组件仅在其状态或属性发生变化时才会重新渲染,而无论其父组件是否进行了重新渲染都不会影响到它。
React 编译器是由 React 核心团队开发的 Babel 插件,在 2024 年 10 月发布了 Beta 版本。
在构建时,它试图将“正常”的 React 代码转换为默认记忆化组件、它们的 props 和钩子依赖项的代码。最终结果是“正常”的 React 代码,表现得就好像一切都被包裹在 memo、useMemo 和 useCallback 中。
几乎!实际上,它执行更复杂的转换,并尝试尽可能高效地适应代码。
React 编译器感觉三言两语讲不完,还是单独发一篇文章讲……
也可以瞅下:https://dev.to/adevnadia/how-react-compiler-performs-on-real-code-5gkf
React 19 新hooks
在 React19 之后,你不再需要使用 useMemo() 钩子,因为 React 编译器将自行进行记忆。
useMemo 在组件返回虚拟Dom之前执行,执行回调函数并返回其返回值,这一点对于优化确定不需要更新的组件很有用,后文会提及。
useLayoutEffect 在组件返回虚拟DOM之后同步执行
useEffect 最后执行
同时,也不需要useContext、之类,因为有了use(不是hooks,是API)
use
官方文档:https://react.dev/reference/react/use
use()这个钩子将简化我们如何使用 promises、async 代码和 context——统一处理异步资源和上下文!
const fetchUsers = async (id) => await fetch(`/api/user/${id}`) const OtherComponent = use(import('./OtherComponent'));//替代 React.lazy,动态加载组件: const ThemeContext = createContext(); const UsersItems = () => { const { theme, toggleTheme } = use(ThemeContext)// 不再使用 useContext(),而是使用 use(context)。 const user = use(fetchUsers(1)) // use 钩子执行 fetchUsers,而不是使用 useEffect 或 useState 钩子。 const [posts, friends] = use(Promise.all([ffetchUserPosts(userId), fetchUserFriends(userId) ])); return <div><OtherComponent user={user}/></div> }
使用限制:只能在组件内部使用、需要配合 Suspense 使用、不能在事件处理器中使用
useTransition
useTransition主要是用于当有大数据量渲染时减少重复渲染次数,并且返回一个等待状态(不接收任何参数)。
const [isPending, startTransition] = useTransition()
useTransition在不阻塞 UI 的情况下更新状态
export default function App() { const [val, setVal] = useState(''); const [searchVal, setSearchVal] = useState(''); const [loading, startTransition] = useTransition(); const handleChange = (e) => { setVal(e.target.value); startTransition(() => {// 真正的待执行函数 setSearchVal(Array(100000).fill(e.target.value)); }); }; return ( <div className="App"> <input value={val} onChange={handleChange} /> {loading ? (<p>loading...</p>) : ( searchVal.map((item, index) => <div key={index}>{item}</div>))} </div> ); }
useTransition可以在不等待列表渲染完成的情况下完成操作,在列表渲染过程中依然保持响应。
所以,useTransition是利用react的并发渲染来处理缓慢的状态更新(无法解决防抖、节流问题)!
具体参看:《react能力值+1!useTransition是如何实现的?》
startTransition 解决的是渲染阻塞的问题
requestIdleCallback 适合轻量级、非紧集任务,解决主线程阻塞问题。
web worker 适合处理复杂的计算任务,如大量数据处理、图像处理等,并在独立的线程中运行,避免阻塞主线程。
useDeferredValue
useDeferredValue(react18提供的一个hook)它的工作方式类似于useTransition,允许我们「将某些更新标记为非关键并将它们移至后台」——不紧急/紧急、延迟、可中断!
通常建议在没有访问状态更新函数时使用它,例如,当值来自props时。
export default function MyComponent() { const [inputValue, setInputValue] = useState(""); const deferredValue = useDeferredValue(inputValue); let start = performance.now(); while (performance.now() - start < 1000) {} return ( <div> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <p>即时值: {inputValue}</p> <p>延迟值: {deferredValue}</p> </div> ); }
当键盘输入时,可以明显看到页面上即时值先进行了更新,而延迟值等了一会儿才在页面上更新。
useActionState
在 React 19 中,基于 Actions 的概念,引入了useOptimistic来管理乐观更新,以及一个全新的 Hook React.useActionState用于处理常见的 Actions 场景。在react-dom中,添加了<form> Actions 来自动管理表单,以及useFormStatus来支持表单中常见的 Actions 场景。
const [error, submitAction, isPending] = useActionState( async (previousState, newName) => { const error = await updateName(newName); if (error) { // You can return any result of the action. // Here, we return only the error. return error; } // handle success return null; }, null, );
在设计系统中,通常需要编写设计组件,这些组件需要获取其所处 <form> 表单的相关信息,但又不想通过 props 逐级向下传递。虽然可以通过 Context 来实现这一点,但为了让这种常见情况更简单,React 19 添加了一个全新的 Hook:useFormStatus。
useFormStatus
这个主要是为了表单,终于原生支持表单提交按钮状态分离(比如button的loading)
const { pending, data, method, action } = useFormStatus(fn, initialState, permalink?);
参数:
fn:当表单提交或按钮被按下时要调用的函数。
initialState:你希望初始状态是什么值。它可以是任何可序列化的值。在首次调用后,此参数将被忽略。
permalink:这是可选的。一个 URL 或页面链接,如果 fn 将在服务器上运行,则页面将重定向到 permalink。
返回值:
pending:如果表单处于挂起状态,则为 true,否则为 false。
data:一个实现 FormData 接口的对象,包含父级 正在提交的数据。
method:HTTP 方法 – GET 或 POST。默认情况下为 GET。
action:一个函数引用。
下面是表单组件
import { useFormState} from 'react-dom'; const FormState = () => { const submitForm = (prevState, queryData) => { const name = queryData.get("username"); console.log(prevState); // 表单之前的状态 // TODO } const [ message, formAction ] = useFormState(submitForm, null) return <form action={formAction}> <label>姓名</label> <input type="text" name="username" /> <button>提交</button> {message && <h1>{message.text}</h1>} </form> } export default FormState;
这个表单的submit按钮可以分开写:
function Submit() { const status = useFormStatus(); return <button disabled={status.pending}>{status.pending ? '正在提交...' : '提交'}</button>; }
useOptimistic
在执行数据变更操作时,另一种常见的 UI 模式是:在异步请求正在进行的过程中,以乐观的方式展示最终状态。在 React 19 中,新增了一个名为useOptimistic的 Hook,以简化这一过程:
function ChangeName({currentName, onUpdateName}) { const [optimisticName, setOptimisticName] = useOptimistic(currentName); const submitAction = async formData => { const newName = formData.get('name'); setOptimisticName(newName); const updatedName = await updateName(newName); onUpdateName(updatedName); }; return ( <form action={submitAction}> <p>Your name is: {optimisticName}</p> <p> <label>Change Name:</label> <input type='text' name='name' disabled={currentName !== optimisticName} /> </p> </form> ); }
useOptimistic Hook 将在 updateName 请求过程中立即渲染 optimisticName。当更新完成或出现错误时,React 将自动切换回 currentName 值。
React 19总公19个hooks总结
https://zh-hans.react.dev/reference/react/useDeferredValue
状态管理 Hook
useState
useReducer
上下文 Hook
useContext
引用 Hook
useRef
useImperativeHandle
副作用 Hook
useEffect
useLayoutEffect
useInsertionEffect
性能优化 Hook
useMemo
useCallback
调度 Hook
useTransition
useDeferredValue
其他 Hook
useId
useSyncExternalStore
useDebugValue
React 19 新增 Hook
use
useActionState
useFormStatus
useOptimistic
参考文章:
刚刚,React 19 正式发布! https://cloud.tencent.com/developer/article/2474898
React 19 新特性 – 附带代码示例的更新 https://www.cnblogs.com/web-666/p/18113242
魔术师卡颂的回答 - https://www.zhihu.com/question/521311581/answer/2530942394
转载本站文章《react19走起!无Signal,坚守不可变数据与单向数据流!》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2025_0303_9507.html