React
架构
- 老架构
- Reconciler(协调器)负责找出组件的变化
- 调用组件的render方法将JSX转成虚拟DOM
- 将虚拟DOM和上次的虚拟DOM进行比较
- 通过对比找出本次更新的DOM
- 通知Renderer将更新的DOM最终渲染到页面上
- Renderer(渲染器)负责将变化的组件渲染到页面上
- 浏览器端主要用的是ReactDOM,用于将虚拟DOM渲染到页面
- 在移动端中可以使用ReactNative进行渲染
- 老架构中渲染器是通过递归的方式渲染当前组件及其子组件
- 当组件嵌套较深或组件渲染耗时较长时,会阻塞浏览器的UI绘制过程
- 因此在React 16中提出了使用可中断的异步更新来替代同步更新
- Reconciler(协调器)负责找出组件的变化
- 新架构
- Scheduler(调度器)调度任务的优先级,高优任务优先进入Reconciler
- 把Reconciler提供的回调函数包装在一个任务对象中
- 在内部实现一个任务队列,按照任务优先级进行排列
- 循环消费任务队列,直到清空
- Reconciler(协调器)与老架构类似,但新增了时间切片和可中断渲染的能力
- 装载渲染器,在commit阶段可以通过调用渲染器的api来生成对应的真实节点
- 接收ReactDOM.render或者setState触发的更新请求
- 将生成Fiber节点和Fiber树的过程作为回调函数传入Scheduler
- Renderer(渲染器)与老架构类似,新增了对Fiber节点树的渲染支持
- 实现HostConfig协议,能够在 Reconciler 调用时将 Fiber 树生成DOM或者HTML字符串
- 提供render方法引导React应用的启动
- Scheduler(调度器)调度任务的优先级,高优任务优先进入Reconciler
JSX
- 一种React的语法糖,相当于调用了React.createElement方法,需要Babel进行转换
- createElement方法会返回一个ReactElement对象,通过 $$typeof 字段来标识类型
- 转换后的对象还包含 type、key、_owner、props、ref 等属性
- JSX的标签大写时会在创建时当做React组件,小写时会被当成html的标签
- render函数中的JSX只能有一个根节点的原因
- 虚拟DOM是一个树状结构,同一棵树不能有多个根节点
- render函数中只能return一个返回值
Fiber
- React 16版本的虚拟DOM,每一个Fiber节点都是一个工作单元,在更新过程中可以进行中断和恢复
- 包含一些静态数据结构,用以表示当前组件的信息,如 tag、type、key 等
- 包含一些动态的数据结构,用以保存组件更新的状态和要进行的动作,如 pendingProps、memoizedProps、updateQueue 等
- 包含其他关联节点的信息,return 指向父节点、sibling 指向下一个兄弟节点、child 指向子节点
- Fiber树的构建和替换使用了双缓存技术,即事先在内存中进行构建并在更新时机直接替换
- 当前已经渲染的 Fiber 树被称为 currentFiber,下一次将渲染的 Fiber 树被称为 workInProgressFiber,两棵树通过 alternate 属性相互关联
Render
- 流程概览
- ReactDOM.render 方法会创建一个 fiberRoot对象 用于存储整个应用的全局状态变化信息
- App 组件则会对应一个 HostRootFiber 对象作为当前组件树的根节点,并将 fiberRoot.current 指向该节点
- Reconciler 调用 App 组件中的 render 方法,生成一个新的 workInProgressFiber 节点,该节点的 alternate 指向 HostRootFiber
- Reconciler 进行一次 commit 操作,通知 Renderer 将整个 fiberRoot 树渲染到页面上
- 在渲染完成后,fiberRoot.current 指向新的 HostRootFiber节点(原来的workInProgressFiber节点)
- 当组件中内容发生变化时,Reconciler 会再次调用 render 方法生成新的 workInProgressFiber 节点
- 为了提升性能,新的 fiber 节点生成时会通过 Diff 算法复用之前的 fiber 节点
- 优先级管理
- 使用 Lane(车道模型, react18之后)进行优先级管理,针对不同的操作设置了不同的 lane
- 分为4个优先级:同步优先级(setState)、用户交互优先级(点击)、默认优先级(渲染)、低优先级(过渡动画、空闲认)
- 不同 lane 的优先级不同,对应的是执行任务的超时时间不同,优先级高的任务超时时间更长
- 在一次render中如果触发了多次setState,会根据优先级判断做防抖和节流以提升性能
- 调度原理
- 在Scheduler中维护了一个任务队列,每一个任务都来自于Fiber的callback属性
- 任务队列会进行循环执行,当队列中任务清空时或者任务执行时间过长时都会退出循环,每一次循环都称为时间切片
- 在React中默认的单任务执行最大时长为5ms,当超出时则会在下一个任务执行前将线程控制权交还给浏览器
- 使用 window.requestIdleCallback API去判断任务是否能在当前帧的剩余时间内执行完成
- 在 fiber 树构造时,每完成一个节点的构造都会去判断剩余时间,若剩余时间不足,则会从当前构造的节点处返回,在下次主线程空闲时继续执行后续构造,该方式即为可中断的异步渲染
- Diff算法
- Diff算法为比较currentFiber节点和JSX对象,并生成 workInProgressFiber 的过程
- 单节点Diff
- 首先判断当前节点是否存在一个对应的老节点,若不存在,则说明是新增,直接返回新节点
- 否则判断两个节点的key是否相同,若不同,则说明是替换,将老的节点删除,并创建新节点
- 否则继续判断两个节点的type是否相同,若不同,则将老节点及其兄弟节点都删除,并创建新节点
- 若新老节点的 key 和 type 都相同,则认为可以复用,直接返回老节点即可
- 多节点Diff
- 会经历两轮遍历,第一次处理更新操作的节点,第二次处理非更新操作的节点
- 第一轮遍历会依次将 children[i] 和 currentFiber 以及 children[i++] 和 currentFiber.sibling 进行对比,当发现节点不可复用时提前结束遍历
- 当第一轮遍历无提前结束时,说明所有节点都可以复用,直接返回老节点
- 若children遍历完成,currentFiber未完成,则说明是删除操作,需要对未完成的 currentFiber 兄弟节点标记删除
- 若children遍历未完成,currentFiber完成,则说明是新增操作,需要生成新的workInProgressFiber节点
- 若children和currentFiber都未完成,则说明是节点位置发送了变更 * 对剩余的currentFiber进行遍历,并记录 currentFiber[i] 在currentFiber 中位置为 oldIndex * 通过 key 查找到children中对应相同 key 的节点在 children 中位置为 lastPlacedIndex * 若 lastPlacedIndex <= oldIndex,则说明该节点不需要移动 * 否则说明节点需要移动,并将移动到 lastPlacedIndex 对应的位置
- 渲染DOM
- 渲染前
- 重置一些全局变量,处理DOM,提供生命周期钩子函数等。最重要的则是执行 useEffect 等副作用函数
- 在生成Fiber节点时会将拥有副作用的节点存在一条单向链表 effectList 中(产生副作用的条件包含插入、更新、删除DOM或者使用了useEffect、useLayoutEffect等hooks)
- 处理副作用时,会异步遍历 effectList 列表,以保证在副作用函数的执行不阻塞浏览器渲染
- 渲染
- 根据 effectList 中节点的 effectTag 类型来执行不同的渲染逻辑
- Placement:新增节点,调用 render 中的 appendChild 或者 insertBefore 等方法
- Update:调用render中的commitUpdate方法来更新节点
- Deletion:首先递归调用Fiber节点及其子节点的生命周期函数,然后调用render中的commitDeletion方法来删除节点,之后销毁ref并调用useEffect的销毁函数
- 渲染后
- 清除副作用队列,辅助垃圾回收
- 设置ref的值
- 渲染前
合成事件
- React中的事件是自己实现的一套兼容原生事件API的合成事件
- 大部分的事件都是通过事件委托的方式绑定在了React渲染的根元素上,也有一些是直接绑定在原生的DOM上
- 事件触发流程
- 用户点击组件按钮,事件冒泡到根元素
- 根元素的监听事件通过 dispatchEvent 去派发事件
- dispatchEvent 首先去查找事件元素对应的 fiber 节点
- 找到 fiber 节点后通过节点上绑定的合成事件以及事件映射表将合成事件和原生事件进行对应
- 之后创建合成事件,并将其加入到事件的执行队列
- 最后执行事件队列中的合成事件
- 优点
- 抹平不同浏览器直接的差异,始终使用同一套事件API
- 将事件进行集中管理,可以提升性能,减少内存消耗
React 有哪些通信方式
- 父向子组件:props
- 子向父:props+回调
- 兄弟组件:利用共同的父元素,比如hook中的useContext
- 全局状态管理器:redux,mobx
- 发布订阅的设计模式
setState之后发生了哪些事情
先讲React的架构,包含了Renderer、Scheduler和Reconciler三部分,然后具体说了每一部分大概是做什么,之后讲setState其实就是触发组件的一次渲染过程,
具体过程如下:setState会生成一份新的组件内状态数据并重新执行Reconciler中的render方法render方法去创建一个新的fiber节点树,每一个树节点的创建都是Reconciler中的一个工作单元所有的创建fiber节点工作单元生成后,这些工作单元的执行和调度会由Scheduler中的任务队列来执行任务队列每次取出一个创建fiber节点的任务执行,执行完成之后会调用浏览器的requestIdleCallback方法来判断当前刷新帧剩余时间是否够执行下一个任务如果时间够就执行下一个创建fiber节点任务,不够的话就先将创建任务暂停,等下一个刷新帧继续执行当所有的创建任务都执行完成之后,就生成了一棵新的fiber节点树,之后就是通过新旧两棵树去做diff算法获得要更新的树,后面的diff和渲染部分这里就不多介绍了
为什么react hook不能放到条件语句里面
- 保证hook顺序一致性:如果在条件里面,调用顺序会根据条件变化,导致无法追踪和管理组件状态
- 避免无限循环和不可预测问题,如果依赖组件渲染结果,hook又影响渲染,导致无限循环
- 简化hook实现,可以在渲染时候一次性收集所有hook,然后处理,而不是在渲染过程中再不断检测和处理hook调用
hook原理
hooks 是函数编程范式,依赖于Fiber架构 和Hook的对象保存管理。 React会将这些函数的内部的状态和副作用保存在一个Hook对象中,并将这些对象保存在当前组件的Hook链表中,当组件重新渲染的时候,React会遍历这个链表,根据每个Hook对象保存的状态和副作用来更新组件的状态,并执行副作用函数。
Reconiler 协调器
复制管理虚拟DOM的更新,当组件的状态或属性改变后,Reconiler会根据新属性或状态创建一个新的虚拟dom树,reconiler会使用Diff算法将新旧dom树做比较,React使用了O(n)的时间复杂的完成dom比较,确定需要更新Dom节点,
Renderer 渲染器
拿到Reconiler协调器的结果后,如果当前环境是浏览器,会调用ReactDom进行渲染,渲染过程中采用的是递归方式渲染当前组件和子组件,所以当层级较深后,会阻塞浏览器的UI绘制。
Scheduler 调度器
React16之后,新增了Scheduler,Scheduler维护了一个任务队列,根据任务优先级排列。高优先级的任务先进入Reconiler中。
新Reconiler
增加了时间切片和中断渲染的能力。生成的不再是DOM树,而是Fiber,使用的是双向链表的结构存储。
新Render 渲染器
增加了对Fiber节点的渲染支持,将Fiber树生成DOM或HTML字符串。
React18并发模式
React18的并发模式是在React16Fiber基础上实现的并发渲染模式。 通过Suspense、useDefferredValue、useTransition 实现异步渲染。能分阶段对用户界面进行更新。然后React18改进了批处理方案,也能更好支持并发模式。从官方文档里的描述:通过准备多个版本的UI进行渲染。
React源码分为哪些模块
- 采用monorepo仓库管理,分为react、react-devtool、react-dom、react-reconciler、
- react-dom 中包含了对JSX的解析
- react-reconciler 包含了协调器,diff算法就在这里面
为什么react的diff不静态标记
vue的模板语法可以通过静态分析,通过正则表达式解析模板,判断哪些结点不会变。而react的jsx使用createElement执行调用,具有高度动态化。无法从模板层面进行分析。
函数组件和class组件
- 函数组件更简洁,不需要定义类和生命周期,代码量更少
- 函数组件性能更好,能充分利用react新特性,实现状态管理和副作用管理
- 函数组件可测试性更好,因为是纯函数,可预测性更好
- 函数组件是官方推荐的写法
- 方便组合复用,结合hooks,使得代码更加灵活
- 函数组件每次渲染都是新的,是无状态的,class组件state和props都是固定的地址
Hook为什么单链表结构
- 方便状态管理和状态更新,每次渲染时候只需遍历一次链表,就可以获取所有状态值,并正确更新状态
- 为了支持中断和恢复,支持并发,使用链表更方便记录位置和状态。
JSX渲染到屏幕的过程
- React内部实现了协调器、渲染器、以及React16之后实现了调度器。实现的。
- 在协调器中,react通过调用render将JSX转换为虚拟DOM(内部是通过babel进行转换的)。react会进行Diff算法比较,找出需要更新dom
- 在渲染器中,对需要更新的DOM进行递归渲染,在浏览器中是使用的ReactDOM渲染的,在移动端使用的ReactNative渲染的。
- 在React16之后。使用调度器将任务划分优先级,将虚拟DOM划分为Fiber树,使用双向链接结构存储,便于暂停和恢复。将Fiber进行渲染。
- 在React18后,使用了并发模式,通过实现了useDeffereredValue,useTranstation等方式将渲染任务延迟处理,实现渲染。
React优化方式
React.lazy(利用webpack的功能),useMemo,useCallback,Suspense,useTransition。useDeferredValue
React HOC
HOC高阶组件,就是一个纯函数,通过传入一个组件并返回一个新组件。 可能的问题包括:组件层级过深,增加代码复杂度和理解成本;props传递不清,可能导致命名相互覆盖;
Mixin:已经废弃了,通过将一个对象的属性拷贝到另一个对象上,也是为了解决代码复用问题。MIxin 隐藏依赖关系,代码直接互相可能互相依赖或耦合。当使用多个Mixins之间可能互相冲突。导致状态管理变得复杂。