# 怎么减少组件渲染

  • 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 和 queue

    • memoizedState:存储 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 之间的事件不再共享了