react知识点总结

199 阅读15分钟

react的核心可以用ui=fn(state)来表示,更详细可以用

const state = reconcile(update);
const UI = commit(state);

上面的fn可以分为如下一个部分:

  • Scheduler(调度器): 排序优先级,让优先级高的任务先进行reconcile

  • Reconciler(协调器): 找出哪些节点发生了改变,并打上不同的Flags(旧版本react叫Tag)

  • Renderer(渲染器): 将Reconciler中打好标签的节点渲染到视图上

  • 工作流程如下图

image.png

一图胜千言:

react源码3.1

react源码3.2

1.diff算法的过程

协调器是在render阶段工作的,简单一句话概括就是Reconciler会创建或者更新Fiber节点。在mount的时候会根据jsx生成Fiber对象,在update的时候会根据最新的state形成的jsx对象和current Fiber树对比构建workInProgress Fiber树,这个对比的过程就是diff算法。

2.diff算法

diff算法发生在render阶段的reconcileChildFibers函数中,diff算法分为单节点的diff和多节点的diff(例如一个节点中包含多个子节点就属于多节点的diff),单节点会根据节点的key和type,props等来判断节点是复用还是直接新创建节点,多节点diff会涉及节点的增删和节点位置的变化

react的多节点diff算法

新的dom数组和当前的fiber链表进行对比,有两轮遍历

第一轮遍历:处理更新 的节点。 第二轮遍历:处理剩下的不属于 更新 的节点。

第一轮遍历,从头开始遍历 newChildren ,逐个与 oldFiber 链中的节点进行比较,判断 DOM 节点是否可复用。如果节点的 key 不同,则不可复用,直接跳出循环,第一轮遍历结束。如果 key 相同,但是 type 不同,则会重新创建节点,将 oldFiber 标记为 Deletion ,并继续遍历。

lastPlacedIndex 是最后一个可复用的节点在 oldFiber 中的位置索引,用于后续判断节点是否需要复用。

第一轮遍历完毕后,会有以下几种情况:

  1. newChildrenoldFiber 同时遍历完
  2. newChildren 没遍历完,oldFiber 遍历完
  3. newChildren 遍历完,oldFiber 没遍历完
  4. newChildrenoldFiber 都没遍历完

newChildrenoldFiber 同时遍历完,这个是最理想的情况,只需在第一轮遍历进行组件 更新,此时 Diff 结束。

newChildren 没遍历完,oldFiber 遍历完,这说明 newChildren 中剩下的节点都是新插入的节点,只需遍历剩下的 newChildren 创建新的 Fiber 节点并以此标记为 Placement

newChildren 遍历完,oldFiber 没遍历完,意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的 oldFiber ,依次标记 Deletion 。

newChildren 与 oldFiber 都没遍历完,这是 Diff 算法最难的部分。 newChildrenoldFiber 都没遍历完,则有可能存在移动了位置的节点,所以为了快速地找到 oldFiber 中可以复用的节点,则创建一个以 oldFiber 的 key 为 key ,oldFiber 为 value 的 Map 数据结构。

然后会遍历剩余的 newChildren ,逐个在 map 中寻找 oldFiber 中可复用的节点,如果找到可复用的节点,则将 oldIndex 与 lastPlacedIndex 比较,如果 oldIndex 与 lastPlacedIndex 小,则该节点需要右移,将新的 Fiber 节点标记为 Placement 。否则,将 lastPlacedIndex 更新为 oldIndex 。

遍历完 newChildren 后,map 中还有节点剩余,则那些节点属于多余的节点,需要标记为删除(Deletion)。

fiber对比及移动的策略

react对于位置移动的fiber节点,采用的是仅向右移的策略,大体上是,遍历newChldren数组,对比新节点对应的可复用的老节点在老fiber中的index(位置),如果位置在左边(oldIndex<lastPlacedIndex)则老fiber需要向右移,并将它放入新的fiber的sibling指针,如果在右边(oldIndex>lastPlacedIndex),则不需要移动,但是lastPlacedIndex要更新为更大的oldIndex,目的是为了始终使用最右边的老fiber作为位置基准。 更多react与vue的diff详细比较,推荐该文章

案例

12345 old
12435 new

第一轮直接复用 :对比结束 1 2 可复用,lastPlacedIndex = 0

第二轮 移动
4  oldIndex = 3 > lastPlacedIndex:0 =》 不用移动
lastPlacedIndex = 3
3  oldIndex = 2 < lastPlacedIndex:3  =》 打上 Placement 标志 

5  oldIndex = 4 > lastPlacedIndex:3 => 不用移动
lastPlacedIndex = 4



在 commit 阶段,深度优先遍历每个新 fiber 节点,对 fiber 节点对应的 DOM 节点做以下变更:

删除 deletions 数组中 fiber 对应的 DOM 节点

**!如有 Placement 标志,将节点移动到往后第一个没有 Placement 标记的 fiber 的 DOM 节点之前。**

更新节点。以 DOM 节点为例,在生成 fiber 树的「归」阶段,会找出属性的变更集,在 commit 阶段更新属性。

React Diff 流程图

31.png

lastPlacedIndex:最后一个可复用节点的在 oldFiber 中的位置索引

oldIndex:当前遍历到的 oldFiber 节点在 oldFiber 中的位置索引

React 会调用 updateSlot 方法,在 updateSlot 中判断 fiber 节点能否复用,只要是 key 相同,updateSlot 都会返回 newFiber ,key 不同,则会返回 null ,第 1 轮遍历结束。

32.png

3.effectList

在 beginWork 中我们知道有的节点被打上了 effectTag 的标记,有的没有,而在 commit 阶段时要遍历所有包含 effectTag 的 Fiber 来执行对应的增删改,那我们还需要从 Fiber 树中找到这些带 effectTag 的节点嘛,答案是不需要的,这里是以空间换时间,在执行 completeUnitOfWork 的时候遇到了带 effectTag 的节点,会将这个节点加入一个叫 effectList 中,所以在 commit 阶段只要遍历 effectList 就可以了(rootFiber.firstEffect.nextEffect 就可以访问带 effectTag 的 Fiber 了)每个 fiber 节点上都保存了该 fiber 节点的子节点的 effectList,通过 firstEffect、nextEffect、LastEffect 来保存,在 completeWork 的时候就会将每个 fiber 的 effectList 更新到其父 Fiber 节点上,所以 complete 之后,rootFiber 上就保存了完整的 effectList,我们在 commit 阶段就直接遍历 rootFiber 上的 effectList 来执行副作用即可

EffectList 不是全局变量,只是在 Fiber 树创建过程中,一层层向上收集有 effect 的 Fiber 节点,最终的 root 节点就会收集到所有有 effect 到 Fiber 节点,我们就把这条包含 effect 节点的链表叫做 EffectList。

由于收集的过程是深度优先,子级会先被收集,所以遍历的时候也会先操作子级,所以如果有面试官问子级和父级的生命周期或者 useEffect 谁先执行,就很清楚的知道会先执行子级操作了。

4.concurrent异步可中断、带优先级的更新

它是一类功能的合集(如fiber、schduler、lane、suspense),其目的是为了提高应用的响应速度,使应用cpu密集型的更新不在那么卡顿,其核心是实现了一套异步可中断、带优先级的更新。

我们知道一般浏览器的fps是60Hz,也就是每16.6ms会刷新一次,而js执行线程和GUI也就是浏览器的绘制是互斥的,因为js可以操作dom,影响最后呈现的结果,所以如果js执行的时间过长,会导致浏览器没时间绘制dom,造成卡顿。react17会在每一帧分配一个时间(时间片)给js执行,如果在这个时间内js还没执行完,那就要暂停它的执行,等下一帧继续执行,把执行权交回给浏览器去绘制。

5.react的工作机制(简述)

render阶段

render阶段的主要工作是构建Fiber树和生成effectList

reconciler 调和

1.mounted通过jsx到react.createElement创建curentFiber

2.update时,通过jsx到react.createElement对比curentFiber(diff算法)schedule调度 生成 workinprogressFiber,workinprogressFiber成为curentFiber

3.在 Fiber 树构建过程中,每当一个 Fiber 节点的 effectTag 字段不为 NoEffect 时(代表需要执行副作用),就把该 Fiber 节点添加到 EffectList,在 Fiber 树构建完成后,Fiber 树的 Effect List 也就构建完成

commit阶段

render 渲染

4.遍历effectList执行对应的dom操作或部分生命周期。

概述

从 vdom 转换成 fiber 的过程就叫做 reconcile,转换过程中会顺便创建对应的 dom 元素,然后全部转换完后一次性 commit 到 dom。

这个过程不是一次性的,是通过 scheduler 调度执行的,那也就可以分批次进行,也就是可打断的含义。

这就是 React 的 fiber 架构下的渲染流程。

react 把 schedule 和 reconcile 叫做 render 阶段,这个阶段就是把 vdom 转为 fiber。(schedule 只是让 reconcile 可以分多次执行,可以打断,但做的事情是不变的,所以 schedule 也是 render 阶段的一部分)

之后把 fiber 更新到 dom 的过程就叫做 commit 阶段。

6、react 工作的2大循环

这张图也就对应了在16版本及以后react采用的fiber 架构,所对应的其中2个部分,一个是scheduler部分,也就是负责任务调度循环,另一个就是reconciler部分,负责fiber构造循环

fiber 构造循环,也就是刚才所讲的采用FSD的方式向下遍历,可以分为“递”和“归”2个阶段 ,而递和归又分别对应mount和update两种场景

在递的阶段,从根节点rootFiber开始,遍历到叶子节点,每次遍历到的节点都会执行beginWork,并且传入当前Fiber节点,然后创建或复用它的子Fiber节点,并赋值给workInProgress.child。

在归的阶段, 在归阶段遍历到子节点之后,会执行completeWork方法,执行完成之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork,当全部兄弟节点执行完之后,会向上冒泡到父节点执行completeWork,直到rootFiber。

7. Scheduler 、Reconciler运行原理

图-核心逻辑解析-4.jpg

在 React 中宏观来看,针对浏览器、Scheduler 、Reconciler 其实是有3层 Loop。浏览器级别的 eventLoop,Scheduler 级别的 workLoop,Reconciler 级别 workLoopConcurrent 。

  • 浏览器的 eventLoop 与 Scheduler 的关系

    • 每次 eventLoop 会执行宏任务的队列的宏任务,而 React 中的 Scheduler 就是用宏任务 messageChannel 触发的。由于requestIdleCallback存在兼容和触发时机不稳定的问题,scheduler中采用MessageChannel来实现requestIdleCallback,当前环境不支持MessageChannel就采用setTimeout。

    • 当 eventLoop 开始执行跟 Scheduler 有关的宏任务时,Scheduler 会启动一次 workloop,就是在遍历执行 Scheduler 中已存在的 taskQueue 队列的每个 task。

  • Scheduler 与 Reconciler 的关系

    • Scheduler中的 workLoop 中每执行一次 task,是通过调用 Reconciler 中的 performConcurrentWorkOnRoot 方法,即每一个 task 可以理解为是一个 performConcurrentWorkOnRoot 方法的调用。
    • performConcurrentWorkOnRoot 方法每次调用,其本质是在执行 workLoopConcurrent 方法,这个方法是在循环 performUnitOfWork 这个构建 Fiber 树中每个 Fiber 的方法。

因此可以梳理出来,3个大循环,从最开始的 eventLoop 的单个宏任务执行,会逐步触发 Scheduler 和 Reconciler 的任务循环执行。

任务的中断与恢复,实现中断与恢复的逻辑分了2个部分,第一个是 Scheduler 中正在执行的 workloop 的任务中断,第二个是 Reconciler 中正在执行的 workLoopConcurrent 的任务中断

  • Reconciler 中的任务中断与恢复:在 workLoopConcurrent 的 while 循环中,通过 shouldYield() 方法来判断当前构建 fiber 树的执行过程是否超时,如果超时,则中断当前的 while 循环。由于每次 while 执行的 fiber 构建方法,即 performUnitOfWork 是按照每个 fiberNode 来遍历的,也就是说每完成一次 fiberNode 的 beginWork + completeWork 树的构建过程,会设置下一次 nextNode 的值 ,可以理解为中断时已经保留了下一次要构建的 fiberNode 指针,以至于不会下一次不知道从哪里继续。

  • Scheduler 中的任务中断与恢复:当执行任务时间超时后,如果 Reconciler 中的 performConcurrentWorkOnRoot 方法没有执行完成,会返回其自身。在 Scheduler 中,发现当前任务还有下一个任务没有执行完,则不会将当前任务从 taskQueue 中取出,同时会把 reconciler 中返回的待执行的回调函数继续赋值给当前任务,于是下一次继续启动 Scheduler 的任务时,也就连接上了。同时退出这次中断的任务前,会通过 messageChannel 向 eventLoop 的宏任务队列放入一个新的宏任务。

  • 所以任务的恢复,其实就是从下一次 eventLoop 开始执行 Scheduler 相关的宏任务,而执行的宏任务也是 Reconciler 中断前赋值的 fiberNode,也就实现了整体的任务恢复。

8. 浏览器的绘制事件执行顺序( requestIdleCallback和requestAnimationFrame有什么区别?)

  • requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。

  • 我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60的时候是比较流畅的,而FPS为个位数的时候就属于用户可以感知到的卡顿了,那么在一帧里面浏览器都要做哪些事情呢,如下所示: image.png 假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调

同步任务>promise等微任务>制作render树(display = ‘block’)>requestAnimationFrame>制作render树(height = ‘200px’(对比前一颗render的相同元素产生动画))>第一帧重绘完成>setTimeout等宏任务

HOOKS

useState

用于创建状态,参数可以传入一个变量或者一个函数(可以对初始值做一些计算),解构的形式来取值,state和setState

setState是异步的,同时多次执行只会执行最后一次,但是在settimout中是同步的

setState 会创建 update 对象挂到 fiber 对象上,然后调度 performSyncWorkOnRoot 重新渲染。 在 react17 中,setState 是批量执行的,因为执行前会设置 executionContext。但如果在 setTimeout、事件监听器等函数里,就不会设置 executionContext 了,这时候 setState 会同步执行。可以在外面包一层 batchUpdates 函数,手动设置下 excutionContext 来切换成异步批量执行。 在 react18 里面,如果用 createRoot 的 api,就不会有这种问题了。 setState 是同步还是异步这个问题等 react18 普及以后就不会再有了,因为所有的 setState 都是异步批量执行了。

useReducer

用于在修改值之前执行一些固定逻辑,可以传3个参数:1.reducer(state,action)=>newState,2,initState,3initializer类似usestate的 '()=>state'

useEffect

回答

jsx + react.createElement => vDom:js对象 vDom 与 curentFiber对比 (diff 算法); diff 算法: React16之前组件的更新是递归执行,所以更新一旦开始,中途就无法中断,当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。

react16之后diff算法使用fiber架构,同时配合 Schedduler 的任务调度器(Scheduler是一个独立的包,不仅仅在React中可以使用。),在 Concurrent(并行) 模式下可以将 React 的组件更新任务变成可中断、恢复的执行,就减少了组件更新所造成的页面卡顿。

记录了chid(子),silbing(兄弟),return(父) 可以中断与恢复, 使用MessageChannel中的onmessage的回调函数是在一帧的paint完成之后调用的原理,利用绘制后的浏览器空余时间来执行任务; 在不支持MessageChannel的环境下会使用setTimeout来实现:比MessageChannel执行时机更慢,更久(差4ms)

如何判断是否要中断

使用schedule调度的形式进行计算,在源码中使用shouldYield()方法来判断是否要继续执行,

如何中断继续

以task形式度存于taskQueue ,workLoop会去taskQueue栈顶取task进行执行他的callback(构建fiber)执行完会将它从执行队列删除,当任务需要中断时会把本次callback存起来,且不删除队列,下次执行的时候则会继续执行

为什么hook需要写在第一层,不能写到函数或条件判断中

因为函数组件中的hook在mount时会创建一个有序hook链表,每个hook有一个next属性指向下一个hook。如果写到函数或条件判断中不能保证每次函数组件执行的时候hook链表和mount的时候一样导致异常。

react中为什么需要使用setState而不是直接state.value去改变状态

直接修改不能触发页面更新,immutability react的数据不可变性,setState会创建update,然后在queue队列中进行批量更新; 要知道setState本质是通过一个队列机制实现state更新的。 执行setState时,会将需要更新的state合并后放入状态队列,而不会立刻更新state,队列机制可以批量更新state,进而重新渲染页面。包括计算虚拟DOM树、找出差异、应用差异。

如果不通过setState而直接修改this.state,那么这个state不会放入状态队列中。不会触发重新渲 染。

or

执行setState后会调用dispatchAction,dispatchAction做的事情就是讲Update加入queue.pending中,然后开始调度。进而重新渲染页面。而直接state.value去改变状态则不会。

setState是同步还是异步

setState 并不是单纯的同步函数或者异步函数,他的同步和异步的表现差异体现在调用的场景不同。在React 的生命周期和合成事件中他表现为异步函数。而在DOM的原生事件等非合成事件中表现为同步函数

React 为什么需要合成事件

合成事件全部挂载到 document 节点上

  • 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次。

  • 统一规范,解决 ie 事件兼容问题,简化事件逻辑。

  • 跨端复用。