React 面试知识点汇总
文章目录
react fiber 解决了什么问题
React15 以递归方式执行更新,stack Reconciler,同步更新无法阻断,无法处理高优先级的任务,会造成卡顿。即:
- 解决同步渲染无法响应更高优先级任务的问题
- 提供停下并继续执行的能力
React16 将同步更新变为异步可中断更新,架构大致分为三层:
- Scheduler(调度器)– 调度任务优先级
- Reconciler(协调器)– 找出有变化的组件(虚拟 dom)
- Renderer(渲染器)– 将变化的组件渲染到页面上
其中 Scheduler 和 Reconciler 会因为有更高优任务需要先更新、或当前帧没有剩余时间等原因被中断。
React Fiber 即 React 内部实现的一套状态更新机制,Fiber Reconciler,支持任务不同优先级,可中断与恢复,并且恢复之后可以复用之前的中间状态。
这种架构下 setState 状态更新是异步,某一些方法是 UNSAFE_。
Fiber
Fiber 中包含了三个部分内容:
- 作为静态数据结构的属性
- 链接其他 Fiber 形成 Fiber 树
- 作为动态工作单位的属性(删除/更新等)。
使用双缓存 Fiber 树,在 React 中最多会同时存在两颗 Fiber 树,当前屏幕上显示的 Fiber 树称为 current Fiber 树,正在构建的 Fiber 树称为workInProgress Fiber 树,并通过 alternate 属性链接。
根节点 fiberRoot 的 current 指针指向的切换来完成 current Fiber 树的切换。
ReactDOM.render
- 创建
fiber: 创建fiberRoot和rootFiber,并初始化updateQueue。fiberRoot是整个应用的根节点,rootFiber是要渲染组件所在组件树的根节点。 - 等待创建
update来开启一次更新。
setState
- 调用
enqueueSetState,计算优先级,创建 update 并入队 updateQueue,挂载 fiber 节点上。 - 从节点向上遍历到 rootFiber,
markUpdateLaneFromFiberToRoot并处理优先级。 - 从根节点 rootFiber 开始,遍历到叶子节点,每次遍历到的节点都会执行 beginWork。
- 冒泡执行
completeWork。
beginWork
主要工作是创建或复用子节点,可以根据 current === null 来区分是 mount 阶段还是 update 阶段。
复用子节点的依据:oldProps 和 newProps 不变,fiber 的 type 不变;优先级足够。
diff 在这个阶段。
completeWork
处理 fiber 的 props,创建 dom 和 effectList。
随后调用 commitRoot,进入 commit 阶段。
commit 阶段
主要工作分为三部分:
- before mutation(DOM 操作前):遍历
effectList并调用commitBeforeMutationEffects函数。- 处理调用
getSnapshotBeforeUpdate生命周期钩子 - 调度
useEffect
- 处理调用
- mutation(DOM 操作):遍历
effectList,执行commitMutationEffects。- 根据
effectTag调用不同的处理函数处理Fiber(插入、删除等操作,更新ref)。 - 执行
useLayoutEffect的销毁函数。 - 如果是删除节点会调用
componentWillUnmount生命周期钩子、解绑ref,调度useEffect的销毁函数。
- 根据
- layout(DOM 操作后):遍历
effectList,执行commitLayoutEffects。- 切换
fiberRoot指向的 current Fiber 树。 - 区分 mount 还是 update 来调用
componentDidMount或componentDidUpdate生命周期钩子。 this.setState的第二个回调函数也会在这个时候调用。useLayoutEffect的回调会在这个阶段调用- 赋值
ref。 - 调度
useEffect的销毁与回调函数。
- 切换
setState 是同步的还是异步的
只要进入了 react 的调度流程,那就是异步的。没有进入 react 的调度流程,那就是同步的。setTimeout、setInterval,直接在 DOM 上绑定原生事件等。都不会进入 react 的调度流程。
diff 算法
为了降低算法复杂度,react 的 diff 会预设 3 个限制:
- 只对同级元素进行 diff。如果一个 DOM 节点在前后两次更新中跨越了层级,那么 React 不会尝试复用他。
- 两个不同类型的元素会产生出不同的树。如果元素由 div 变为 p,React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点。
- 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定。
通过 child 类型( object 、number 、string 还是 array )来区分子节点是单节点还是数组,分别进行 diff。
单节点
- 当
child !== null且key相同且type不同时执行deleteRemainingChildren将child及其兄弟fiber都标记删除。 - 当
child !== null且key不同时仅执行deleteChild将child标记删除(后面还有兄弟节点没有被遍历到,可能还可以复用)。
多节点
会经历两轮遍历:第一轮处理更新的节点,第二轮不属于更新的节点。
第一轮:
i = 0, 比较child[i]和oldFiber,可复用则i++,比较child[i]和oldFiber.sibling。- 如果不可复用分为两种:
key不同导致不可复用,直接退出第一轮遍历;key相同但type不同导致不可复用,将oldFiber标记为deletion,并继续遍历。
- 如果
child遍历完(即i === child.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。
第二轮根据第一轮遍历结果分多种情况讨论:
child与oldFiber同时遍历完,则只需要在第一轮遍历进行组件更新(或标记删除);child没遍历完,oldFiber遍历完,意味着child的剩余节点都是新增节点,只需要依次标记为placement;child遍历完,oldFiber没有遍历完,意味着oldFiber的剩余节点被删除,只需要依次标记为deletion;child与oldFiber都没有遍历完,意味着有节点在这次更新中改变了位置:- 存一个
oldFiber的key为 key,oldFiber为 value 的Map; - 以最后一个可复用的节点在
oldFiber中的位置lastPlacedIndex为参考; - 当前
child节点的上一次更新的位置位于lastPlacedIndex左边 (child[i].alternate.index < lastPlacedIndex),则说明需要移动;
- 存一个
hooks
- remove this
- 函数式编程
- 利于状态复用、状态与 UI 分离
hooks 本质是闭包。
多个 hooks 本质是链表,状态与声明顺序相关。
React 事件
为什么要有 react 合成事件:
- 抹平浏览器之间的差异;
- 可以用来自定义事件;
- 利用事件委托机制,差不多所有事件都代理到了 document 对象上
- 可以干预事件的分发,派发优先级
React 的事件是在 DOM 事件流的冒泡阶段被触发执行。绑定在元素上面的原生事件会比合成事件先触发。
React-Router 原理
hash 路由
早期的前端路由是使用 hash 来实现的。改变 url 的 hash 值,不会刷新页面,但是可以感知到事件 hashchange 来进行页面修改:
|
|
hash 的兼容性较好,但是存在几个问题:
- 搜索引擎对 hash 的页面不友好
- 带有 hash 的页面难以追踪用户行为
history 路由
HTML5 为 history 新增了 history.pushState()、history.replaceState()、history.state 等 api,可以对路由进行除 go()、forward()、back() 之外的操作。
History 接口可见:HashRouter vs BrowserRouter
history 实现路由的好处:
- 更符合路由格式
- 对搜索引擎友好
- 方便统计用户行为
缺点:
- 兼容性不如 hash
- 需要相应的服务支持路由,不然刷新页面会返回 404
文章作者 xuyou
上次更新 2021-04-17