React 整体渲染流程
为什么要有React Fiber?
见:React Fiber的意义
jsx、ReactElement、虚拟DOM 、Fiber的关系
树里的一个 React 元素(ReactElement)对应一个 Fiber,一个 Fiber 也对应树里的一个元素。
见:JSX、ReactElement、FiberNode、DomElement
常见的生命周期函数都是在哪些渲染阶段被调用
见:常见的生命周期函数都是在哪些渲染阶段被调用
diff算法的过程
单节点和多节点Diff:Diff算法
fiberRootNode与rootFiber的结构
fiberRootNode 是“整棵应用的根管理对象(Root 容器)”,而 rootFiber 是“Fiber 树里的第一个 Fiber 节点(HostRootFiber)”。
见:FiberRootNode存储的信息和Fiber Node存储的信息
setState后React都做了什么
见:从一次 useState 的 setState 开始后发生了什么
React Scheduler为什么一定要把任务包在宏任务里执行?
思考能否使用如下方案:直接同步执行,限制好执行时间(5ms)
答案是,不行。
Scheduler本质上做的不仅仅是分片,还要把控制权交还给浏览器
- 纯同步执行 + 自己看时间,理论上可以“分段”,但本质上你还是在当前调用栈里,浏览器拿不到控制权,不能及时处理输入/渲染。
- React 需要的是协作式调度:做一小段 -> 让出线程 -> 下轮再继续。这就必须依赖异步边界(MessageChannel/setTimeout 这种 task 机制)。
React Scheduler 为什么不用requestIdleCallback实现
- 兼容性差 Safari 并不支持 https://caniuse.com
- 控制精细度 React 要根据组件优先级、更新的紧急程度等信息,更精确地安排渲染的工作
- 执行时机requestIdleCallback(callback) 回调函数的执行间隔是 50ms(W3C规定),也就是 20FPS,1秒内执行20次,间隔较长。
- 差异性 每个浏览器实现该API的方式不同,导致执行时机有差异有的快有的慢
requestIdleCallback的替代方案?
MessageChannel
选择 MessageChannel 的原因,是首先异步得是个宏任务,因为宏任务中会在下次事件循环中执行,不会阻塞当前页面的更新。MessageChannel 是一个宏任务。
没选常见的 setTimeout,是因为MessageChannel 能较快执行,在 0~1ms 内触发,像 setTimeout 即便设置 timeout 为 0 还是需要4~5ms。相同时间下,MessageChannel 能够完成更多的任务。
并发渲染
支持 new concurrent renderer(并发模式的渲染)
- 并发模式不是一个功能,而是一个底层设计。
- 它可以帮助应用保持响应,根据用户的设备性能和网速进行调整。
- 通过渲染可中断来修复阻塞渲染机制。在 concurrent 模式中,React 可以同时更新多个状态。
- 区别就是使同步不可中断更新变成了异步可中断更新。
- useDeferredValue 和 startTransition 用来标记一次非紧急更新。
starTransition:用于标记非紧急的更新,用 starTransition 包裹起来就是告诉 React,这部分代码渲染的优先级不高,可以优先处理其它更重要的渲染。
useTransition:除了能提供 startTransition 以外,还能提供一个变量来跟踪当前渲染的执行状态。
React 的并发机制允许 React 在渲染过程中根据任务的优先级进行调度和中断,从而确保高优先级的更新能够及时渲染,而不会被低优先级的任务阻塞。
并发机制的工作原理
时间分片(Time Slicing): React 将渲染任务拆分为多个小片段,每个片段在主线程空闲时执行。这使得浏览器可以在渲染过程中处理用户输入和其他高优先级任务,避免长时间的渲染阻塞用户交互。
优先级调度(Priority Scheduling): React 为不同的更新分配不同的优先级。高优先级的更新(如用户输入)会被优先处理,而低优先级的更新(如数据预加载)可以在空闲时处理。
可中断渲染(Interruptible Rendering): 在并发模式下,React 可以中断当前的渲染任务,处理更高优先级的任务,然后再恢复之前的渲染。这确保了应用在长时间渲染过程中仍能保持响应性。
并发机制的优势
- 提升响应性: 通过优先处理高优先级任务,React 能够更快地响应用户输入,提升用户体验。
- 优化性能: 将渲染任务拆分为小片段,避免长时间的渲染阻塞,提升应用的整体性能。
- 更好的资源利用: 在主线程空闲时处理低优先级任务,充分利用系统资源。
如何启用并发模式
要在 React 应用中启用并发模式,需要使用 createRoot API:
1 2 3 4 5 6 7
| import React from 'react' import ReactDOM from 'react-dom/client' import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root')) root.render(<App />)
|
在并发模式下,React 会自动根据任务的优先级进行调度和渲染。
useTransition 本质:把一部分更新标记到“过渡 lane(低优先级车道)”,让紧急更新(输入、点击)先渲染,过渡更新可以被打断、延后。
useDeferredValue 本质:把某个值的更新“映射”到过渡 lane,上游状态先变,依赖这个值的重渲染滞后一点,起到“值级别的 useTransition”效果。
什么是lane模型
见:Lane模型
状态更新与批量更新机制
同一个上下文中的setState会被合并
React 17以及之前:
- onClick 里的多个 setState 会被批处理
- setTimeout / Promise.then 里的更新通常不会自动批处理(会各自触发)
React 18(createRoot)
引入了更广泛的自动批处理:
- React 事件里批处理
- setTimeout、Promise、原生事件回调里也会自动批处理(多数场景)
setState 是同步还是异步的
useState怎么实现的第一次拿初始状态,后续拿之前状态
判断挂载还是Update,如果是Update阶段根据BaseState和Update对象计算出最新的state
useState等hooks为什么不能写在函数组件外面
因为hook要挂到当前Fiber上
React18 的自动批处理与 flushSync
- 在 react17 中,只有 React 事件会进行批处理,原生 js 事件、promise,setTimeout、setInterval 不会。
- react18 中,将所有事件都进行批处理,即多次 setState 会被合并为 1 次执行,提高了性能。
flushSync:退出批量更新,强制立即刷新视图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import React,{useState} from "react" import {flushSync} from "react-dom"
const App=()=>{ const [count,setCount]=useState(0) const [count2,setCount2]=useState(0)
return ( <div className="App"> <button onClick=(()=>{ // 第一次更新 flushSync(()=>{ setCount(count=>count+1) }) // 第二次更新 flushSync(()=>{ setCount2(count2=>count2+1) }) })>点击</button> <span>count:{count}</span> <span>count2:{count2}</span> </div> ) } export default App
|
其他 React18/19 新特性
- 放弃对 IE 浏览器的支持:react18/19 引入的新特性全部基于现代浏览器,如需支持需要退回到 react17 版本。
- Suspense 不再需要 fallback 捕获。
- 支持 useId:在服务器和客户端生成相同的唯一一个 id,避免 hydrating 的不兼容。
- useSyncExternalStore:用于解决外部数据撕裂问题。
- useInsertionEffect:这个 hooks 只建议在 css in js 库中使用,这个 hooks 执行时机在 DOM 生成之后,useLayoutEffect 执行之前,无法访问 DOM 节点引用,一般用于提前注入脚本。
React Hooks
见:Hooks
封装自定义 Hooks
实现 useTimeout hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function useTimeout(callback, delay) { const memorizeCallback = useRef();
useEffect(() => { memorizeCallback.current = callback; }, [callback]);
useEffect(() => { if (delay !== null) { const timer = setTimeout(() => { memorizeCallback.current(); }, delay); return () => { clearTimeout(timer); }; } }, [delay]); };
|
实现一个 dom 可见性的 hook
实现的一个关键点是 Ref 的更新在 useEffect 执行前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import { useEffect, useState, useRef } from "react"; const useInView = ( options = { root: null, rootMargin: "0px 0px", threshold: 1, }, triggerOnce = false, ) => { const [inView, setInView] = useState(false); const targetRef = useRef(null);
useEffect(() => { const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setInView(true); if (triggerOnce) { observer.unobserve(entry.target); } } else { setInView(false); } }); }, options);
if (targetRef.current) { observer.observe(targetRef.current); }
return () => { if (targetRef.current) { observer.unobserve(targetRef.current); } }; }, [options, triggerOnce]);
return [targetRef, inView]; };
export default useInView;
|
状态管理相关
React Hooks 如何实现类似 Redux 的状态管理
useContext + useReducer。
Redux 性能问题
Redux vs Mobx
Redux RTX
ImmerJS
React Router 路由模式与实现原理
几种模式
<BrowserRouter> /<HashRouter>
hash 模式
URL 示例:https://example.com/#/about、https://example.com/#/users/123
history模式
URL 示例:https://example.com/about、https://example.com/users/123
history模式最重要的是部署需要配合Nginx,所有路由都返回 index.html