最常问的初级前端问题:
React的diff算法和复杂度
- 同层比较
- 类型判断:不同type直接卸载旧子树,挂载新的子树;相同的type,就复用节点实例,递归对比children;
- 列表diff:
- 有稳定的key:通过key建立匹配与移动、插入、删除的决策。这是比较的算法复杂度在O(n)的关键。
- 没有稳定的key:会有性能问题
react的常用hook
- 状态与数据流
- useState:局部状态
- useRender:复杂状态机
- useContext:跨层级依赖注入
- 副作用与生命周期
- useEffect:异步副作用
- useLayoutEfffect:在浏览器paint前同步执行
- 引用和示例
- useRef:持久化可变容器,保存dom引用
- 并发体验:
- useTransition:将非紧急更新标记为低优先级,避免卡交互;
React用于性能优化的hook
- 减少重复计算 、稳定引用
- useMemo,缓存计算结果
- useCallback:缓存函数引用
- useRef:缓存不需要驱动UI的可变值
- React 18+的hook:并发、交互体验
- useTransition 、startTransition:把非紧急更新标记为低优先级
- useDefferedValue:把某个值延迟给昂贵渲染用,保持输入更丝滑
React的useRef解决rereder问题、闭包问题
-
保存可变值(不触发rerender)
const countRef = useRef(0); countRef.current +=1 ; console.log(countRef.curent) //会变成2,但是不会触发组件rerender
-解决闭包旧值的问题 (定时器、订阅回调里获取最新的值)
function demo(){
const [count,setCount] = useState(0);
const countRef = useRef(count);
useEffect(()=>{
countRef.current = count;
},[count]);
useEffect(()=>{
const id = setInterval(()=>{
console.log("latest",countRef.current);
},1000);
return()=>clearInterval(id);
},[]); //只在挂载后执行一次,卸载时执行cleanUp
}
-
获取DOM节点进行操作
function InputFocus(){ const inputRef = useRef(null); useEffect(()=>{ inputRef.current?.focus();//挂载后自动聚焦一次 },[]) return <input ref={inputRef} /> }
React的Fiber
- 定义:Fiber是React 16引入的核心架构重写:把一次渲染工作拆成很多可中断的,可恢复的,可复用的Fiber节点,可以对不同的fiber node去分配不同的优先级,必要时可以暂停低优先级的任务,保证交互的流畅。fiber的目标是提升对动画,手势,布局等场景的适配能力。
- 注意:Fiber是让渲染阶段具体“可打断性”,但commit阶段为了保证一致性,通常不可随意中断
React 18 - 19发生了哪些更新
- 并发基础
- 更多场景默认批处理state更新,减少重复渲染
- 增加useTransition用于区别紧急与非紧急更新
- streamSSR + suspense:支持流式服务端渲染与suspense协作
- 围绕“Action”异步提交、表单、乐观更新的能力增强
- Action约定
- 增加hooks、API
React的渲染机制
-
渲染机制定义
-
- Schedule调度:在状态更新(setState),props变化,Context变化等会调度一次更新
-
- Render渲染阶段:react生成函数组件,生成新的react element tree,做diff算法,计算需要的dom变更
-
- commit提交:将变更应用到DOM上。DOM变更后,useLayoutEffect同步执行,在浏览器paint之前;paint后执行useEffect;
-
-
代码阅读题(看代码说明react的渲染机制)
-
一次点击后发生了什么
function App() { const [n, setN] = React.useState(0); console.log("render", n); React.useEffect(() => { console.log("effect", n); return () => console.log("cleanup", n); }, [n]); return ( <button onClick={() => { console.log("click"); setN(n + 1); console.log("after setN"); }} > + </button> ); }1.首次挂载: 2.render 0 3.effective 0 4.点击一次 5.click 6.after setN 7.render 1 8.(commit阶段后)cleanUp 0 9.(paint阶段后)effect 1
-
连写两次setState为什么只加1,怎么加2
function App() { const [n, setN] = React.useState(0); console.log("render", n); return ( <button onClick={() => { setN(n + 1); setN(n + 1); }}> + </button> ); }
日志输出:render 0 - 点击后 - render 1 (不会render两次,因为被batching合并了) 修改使用到的都是同一个闭包的旧值
setN(x => x + 1);
setN(x => x + 1);
-
父组件更新,子组件也rerender,React.memo没拦住
const Child = React.memo(function Child({ onInc }) { console.log("Child render"); return <button onClick={onInc}>child</button>; }); function Parent() { const [n, setN] = React.useState(0); console.log("Parent render"); const onInc = () => setN(n + 1); return ( <> <div>{n}</div> <Child onInc={onInc} /> <button onClick={() => setN(n + 1)}>parent+</button> </> ); }
日志输出: Parent render - Child render - 点击“parent+” - onInc引用发生改变(react.memo是浅比较)
如何修复
const onInc = React.useCallback(() => {
setN(x => x + 1);
}, []);