# 怎么减少组件渲染
memo 组件浅比较
useMemo 缓存计算结果
useCallback 缓存函数
组件拆分
批量处理 setState
# react 常用的性能优化方法
memo、PureComponent
useMemo、useCallback
优化 useEffect 的依赖数组
合理的状态提升、组件拆分
高频操作用防抖和节流
建议避免使用 内联函数 内联对象,避免子组件重复渲染
# react diff 算法中 key 的作用
新旧虚拟 dom 树的同级列表按
key
进行比较react 会将旧列表的每个元素按 key 存入一个哈希表中
react 遍历新列表,查对应的 key,如果找到了就复用并更新,没找到就插入新节点
移动已有元素
- react 遍历新列表查找对应的 key 的过程中,如果找到了且该元素位置不同,那么就会移动新虚拟 dom 中该节点位置(react 会进行批处理)
删除未使用的旧节点
- 新列表遍历完成后,如果哈希表里面还有旧元素不在新列表里,react 会移除这些 dom 元素
# fiber 在整个 react 渲染流程中的角色
fiber 核心概念
fiber
是一个数据结构,表示组件树中的每个单元,每个组件对应一个 fiber 节点,fiber 节点包含了组件的 类型、props、state、子组件 等信息增量渲染
fiber 将渲染过程拆解成了多个小任务(工作单元),这些任务可以暂停、恢复、丢弃、重新执行;优先级调度
fiber 允许对任务设置不同的优先级,react 会根据优先级调度这些任务
fiber 的工作流
调和阶段/渲染阶段
,这个阶段是 ’增量的‘ 而且是 ’可中断的‘,react 会构建新的 fiber 树,决定哪部分需要更新,这个阶段只生成更新计划,不去真的修改 dom提交阶段
,这个阶段是 ’同步的‘ 而且是 ’不可中断的‘。一旦 fiber 树构建完成并确定了哪部分需要更新,react 会一次性更新并修改真实 dom
调和阶段
增量工作单元
,fiber 将每个组件的更新拆分为一个个的独立工作单元,形成一个链式结构优先级调度
,react 自定义了 6 种优先级,决定了哪些任务可以延迟,哪些任务应该立即完成中断和恢复
,fiber 中,调和阶段是可以被中断的,可以给高优先级任务让步,完成高优先级任务之后,react 会继续执行之前暂停的任务,直到完成所有更新
提交阶段
提交阶段不可中断
,提交阶段不可中断包含步骤
beforeMutation 阶段,在 dom 更新之前执行的生命周期,如
getSnapshotBeforeUpdate
mutaion 阶段,react 会将确定更新的 dom 更新到页面上,包括 插入、删除、更新 dom 节点
layout 阶段,执行
componentDidMount
compontDidUpdate
声明周期方法。触发 layout 的相关副作用
# jsx 到渲染到屏幕的过程
jsx 编译
。浏览器无法理解 jsx 文件,所以 react 使用 babel 等编译工具将 jsx 转成 jsx 代码,用的是React.createElement
创建虚拟 dom 树
,react 每次渲染都通过两颗虚拟 dom 树比较,确定哪些地方发生了变化创建 fiber 树
,react 根据虚拟 dom 树,构建 fiber 树,包含组件和元素的状态和更新信息- fiber 树和虚拟 dom 树是互相独立的,虽然他们包含的信息类似。但是 fiber 树是专门为了调度、增量渲染、优先级管理等优任务服务的,他允许 react 进行更细粒度的任务控制
调和与 diff 算法
,通过 fiber 树的调和,react 计算新旧 fiber 树的差异,生成更新计划,但是不立即执行更新react 会根据新的虚拟 dom 树生成的 fiber 树与上一次保存的旧的 fiber 树进行比较,找到需要更新的部分
新旧虚拟 dom 树的表是 react15 及以前的版本的工作模式
新旧 fiber 树差异生成了更新计划,更新计划是一个链表结构,每个链表链接着需要更新的 fiber 节点,react 会按优先级顺序执行
真实 dom 更新
如果节点没有变化,那么 react 保持 dom 不变
如果节点改变,那么 react 更新 dom 对应的部分
如果添加或者删除了节点,那么 react 添加或删除对应的 dom 元素
使用 ReactDom.render() 方法将虚拟 dom 树插入到实际的 dom 中,这个过程是最终渲染,根据虚拟 dom 生成真实 dom
浏览器渲染
,更新后的真实 dom 通过浏览器进行重新渲染,更新页面显示
# 虚拟 dom 的好处和缺点
性能提升、高效的批量更新
跨平台能力、跨浏览器的兼容性
合成事件系统也是依赖虚拟 dom
缺点:额外的计算开销、存在一定的内存消耗、小项目可能不如直接操作 dom
缺点:不适用于高频 ui 更新、复杂动画的场景
# React 的事件代理机制
react 在组件挂载的时候,将事件绑定到最顶层的 dom 元素上,比如 document
事件触发时,react 会在事件冒泡阶段捕获到该事件,并在组件内部根据虚拟 dom 的结构调用相应的事件处理程序
# react 的批量渲染是怎么实现的
在 react 的一个事件处理程序(onclick onchange)或者声明周期方法(useEffect useLayoutEffect componentDidMount componentDidUpdate)里面,将多个状态更新合并在一起,在一次渲染中统一处理
批量更新依赖其内部的调度算法,该算法优先处理高优先级的任务,推迟低优先级任务到合适的时机
- 16 之后引入了fiber架构,允许 react 在执行任务的时候打断渲染,支持异步渲染和时间切片,优先处理更紧急的任务,在此加持下,react 批量更新可以结合异步渲染,将多个状态更新推迟到同一帧进行合并处理
批量处理的边界情况
如果状态更新在异步回调里面,比如 setTimeout 和 promise,react 无法合并这些更新,不过也可以 ReactDOM.unstable_batchedUpdates 手动触发批量更新。
18 里面,异步回调也可以批量更新了
好处:性能优化减少重复渲染,一致性(避免中间状态不一致),控制渲染时机
# react 16 和 18 的区别
setState 自动批处理。16 版本的 promise.then 和 setTimeout 回调里面的 setState 是不能合并处理的,18 可以自动合并处理;不过 16 可以使用 unstable_batchedUpdates 进行批处理
并发渲染。允许 react 在不阻塞主线程的情况下处理多任务渲染。
并发相关:新的 startTransition API,支持标记非紧急更新。可以用这个 API 标记非紧急更新,从而使得紧急任务优先渲染。这个依赖于 18 的并发渲染机制
startTransition
用于将某些更新标记为“非紧急”任务
startTransition(() => { setState('1212123'); });
useTransition
用于将某些状态的优先级更新为’过度‘级别,它返回一个布尔值,用来表示当前的过渡状态,以及一个函数用于触发过渡更新。
const [isPending, startTransition] = useTransition();
useDeferredValue
用于标记那些可以延迟处理的低优先级状态
const [val, setVal] = useState(''); const deferVal = useDeferredValue(val);
调度器 Scheduler
可以指定优先级,允许一个回调;不常用Scheduler.unstable_runWithPriority(priority, callback)
:以指定的优先级运行一个回调。Scheduler.unstable_scheduleCallback(priority, callback)
:调度一个具有指定优先级的回调。
并发相关:默认开启 fiber 架构。16 是引入,18 是默认开启,并且进行了优化以支持并发渲染
ssr 相关:Suspense 改进。16 的 suspense 只支持代码拆分,18 的 suspense 被扩展为支持服务器渲染时的异步数据加载
ssr 相关:useIdHook。可以生成唯一 ID,在 ssr 和客户端渲染保持一致
ssr 相关:ssr 改进。支持流式 HTML 传输,可以更快的显示初始页面
# react 中 lane 架构将事件的优先级分为几种 哪些事件属于哪些优先级
同步优先级。最高优先级的任务,立即执行,不会有延迟,react 会在当前事件循环内完成所有更新
- 如:浏览器事件、用户输入事件、状态更新
离散优先级。瞬时触发的用户交互时间,通常是用户输入的时间,react 会尽快处理
- 如:点击、鼠标移动、键盘输入等,通常这些事件会头即使的用户反馈
用户块事件优先级。用户阻塞的交互事件,优先级仅次于离散事件,通常是不需要立即响应的事件,react 可以稍微延迟处理,但是不能让用户感觉到明显的卡顿
- 如:滚动、拖拽
默认优先级。通常是不需要立即响应的事件,react 在空闲时间处理。通常是非交互的任务
- 如:定时器的回调、非用户主动触发的 setstate
过度优先级。非紧急、可延迟的 ui 更新
- 如:路由变更、分页加载
空闲优先级。最低的优先级,react 在其他任务都完成了才去处理这些任务
- 如:后台数据同步、统计更新
# reacthooks 在源码里是怎么实现的, hooks 的值与 callback 是怎么存储的
hooks 的核心概念。核心机制是
链表
结构。每个 hook 都有一个链表节点,节点包含了 memoizedState 和 queuememoizedState
:存储 Hook 的状态值(如 useState 的状态值)queue
:存储 setState 相关的更新队列和回调(如 useState 的更新函数),queue 是 hook 独立拥有的环形链表
hooks 在 react 内部的存储。react 通过链表结果存储每个 hook。每个函数组件都有自己的 hook 链表,并且 react 会一次遍历这些 hook。这个链表通过内部的 fiber 对象进行维护,每个 fiber 对象都和一个组件实例关联。
memoizedState
:在 fiber 上保存的 hook 的链表头;每个 hook 的状态值都存储在这里updateQueue
:在 hook 中用来更新队列(用于调度 setState 的更新)
useState 的实现原理
调用 useState:实际上是调用 useReducer,useReducer 是 react 内部处理状态更新的基础
调用 useState 之后,react 会创建一个 hook 对象,这个对象会存储到当前组件实例对应的 fiber 对象的 memoizedState 中(即存到了 hook 链表中)
总结
hooks 的值,每个 hook 的值存储在一个链表节点的 memoizedState 属性中,这个链表挂载在正在渲染的 fiber 对象上。每次渲染的时候,react 通过链表遍历每个 hook,查找对应的 memoizedState,然后更新它。
hooks 的回调函数,实际是封装了一个 dispatchAction 的函数,这个回调函数存储在 hook 的 queue.dispatch 中;每个 setState 调用都会触发 dispatchAction,而 dispatchAction 会将更新推入一个更新队列,然后触发渲染
react 使用连边管理和遍历 hooks,每个组件渲染的时候都有自己独立的 hook 链表
扩展
函数组件有一个 hook 链表
每个函数组件都有一个与之关联的 fiber 对象,这个 fiber 对象有一个属性
memoizedState
,执行整个 hook 连的头节点每次组件渲染的时候,fiber 会从
memoizedState
开始,按顺序遍历整个链表,依次处理每个链表节点(即每个 hook)
每个 hook 对应链表中的一个节点
每次调用 useState useEffect 或其他 hook,都会为该 hook 创建一个链表节点,而且该节点会连接到 hook 链表中
每个节点存储了 hook 相关的状态,包含
memoizedState
(存储状态值) 和queue
(存储更新队列)
react 渲染流程
函数组件每次渲染时,从
fiber.memoizedState
开始,依次遍历整个 hook 链表,重新计算和更新每个 hook 的状态每次调用某个 hook 的时候,react 会从链表中找到对应的节点,然后读取和更新其状态
# react 的事件合成系统,17 之后不再挂载到 document 上,而是 root 元素上,为什么
与原生的
document
事件共享了一个全局事件处理程序,可能会与 原生代码、库代码 产生冲突提高了兼容性,现在 root 元素的事件处理被限定在了 react 的应用作用范围内
减少副作用,这样 react 只处理 react 应用内部的事件,不会产生其他副作用(比如:页面除了 root 外的其他元素的事件也被 react 监听到了)
缺点,监听全局事件(窗口大小调整、窗口滑动、键盘事件等等)不再被 react 监听,需要开发中手动绑定 window 或者 document 事件
缺点,如果是多 react 实例的项目,那么 react 之间的事件不再共享了