1. 对 React 的理解、特性
React 是靠数据驱动视图改变的一种框架,它的核心驱动方法就是用其提供的 setState 方法设置 state 中的数据从而驱动存放在内存中的虚拟 DOM 树的更新
更新方法就是通过 React 的 Diff 算法比较旧虚拟 DOM 树和新虚拟 DOM 树之间的 Change ,然后批处理这些改变。
遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效
使用虚拟 DOM 来有效地操作 DOM,遵循从高阶组件到低阶组件的单向数据流
- 声明式编程:React采用声明式编程范式,允许开发者描述UI应该呈现的状态,而非具体的操作步骤。这种抽象简化了开发过程,并且有利于React在内部执行高效的DOM更新。
- 虚拟DOM:React引入了虚拟DOM的概念,即在内存中维护一个与实际DOM相对应的数据结构。每当组件的状态变化时,React首先更新虚拟DOM,然后通过高效的Diff算法找出最小化的DOM更新操作,再将这些变化反映到实际DOM上,从而极大提升了渲染性能。
- 组件化:React强调组件化开发,鼓励将UI拆分成可复用的独立单元。组件具有明确的输入(Props)和内部状态(State),使得代码易于组织和测试。
- 单向数据流与可预测性:React推崇单向数据流的设计理念,数据从父组件流向子组件,通过props传递,而子组件通过回调函数通知父组件状态变化,这种设计有助于理解和调试应用状态。
- 函数组件与Hooks:随着React 16.8版本引入的Hooks,函数组件获得了状态管理和生命周期功能,使得无需类就能写出完整功能的组件,进一步简化了代码结构,提高了可读性和可复用性。
- React Fiber:React 16 引入的调度算法改进,提供了更细粒度的任务划分与优先级调度,增强了应用在复杂场景下的流畅性。
- 兼容性与扩展性:React生态丰富,支持服务端渲染(SSR)、静态站点生成(SSG)、移动应用开发(React Native)等多种应用场景,具备良好的兼容性和扩展能力。
React 的设计理念
React官网在React哲学一节开篇提到:
我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。React 最棒的部分之一是引导我们思考如何构建一个应用。
由此可见,React 追求的是 "快速响应",那么,"快速响应"的制约因素都有什么呢?
- CPU的瓶颈:当项目变得庞大、组件数量繁多、遇到大计算量 的操作或者设备性能不足使得页面掉帧,导致卡顿。
- IO的瓶颈:发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。
2. React如何捕获错误
错误边界(Error Boundaries):
- React 16及更高版本引入了错误边界这一概念,它是一种特殊的React组件,能够在其子组件树中捕获任何渲染错误或其他JavaScript错误。当错误边界内的任何子组件抛出错误时,错误边界能够捕获这个错误,记录日志,并且可以选择性地显示恢复界面,而不是让整个应用程序崩溃。
除此之外还可以通过window.onerror或unhandledrejection事件监听器在全局范围内捕获未处理的错误。
window.addEventListener('error', function(event) { ... })
window.addEventListener('unhandledrejection', function(event) { ... })
3. tsx转换成真实DOM过程
- TypeScript 编译阶段:
-
- .tsx 文件包含了 TypeScript 类型注解和 JSX 语法,首先通过 TypeScript 编译器(如tsc或者配合Babel的@babel/preset-typescript插件)进行编译。
- TypeScript 编译器负责检查类型注解的正确性,并且把包含类型信息的 TypeScript+JSX 代码转换为普通的 JavaScript 代码,同时保持 JSX 结构不变。
- JSX编译阶段:
-
- 开发者使用JSX编写React组件。
- JSX并非浏览器原生支持的语法,所以需要通过像Babel这样的编译器将其转换为标准的JavaScript代码。
- Babel将JSX转换成React.createElement(component, props, ...children)的调用形式,其中component是组件名称或原生DOM元素标签名,props是组件属性对象,children是子元素数组。
- 创建虚拟DOM(VDOM):
-
- React.createElement()调用会产生一个虚拟DOM节点对象,它是一个轻量级的JavaScript对象,模拟了DOM节点的结构和属性。
- 整个组件树会转换成由这些虚拟DOM节点构成的虚拟DOM树。
- 渲染虚拟DOM:
-
- 当调用ReactDOM.render()方法时,React接收虚拟DOM树作为参数。
- React会将这个虚拟DOM树与实际DOM进行比较(首次渲染时不存在比较,直接创建)。
- DIFF算法与更新DOM:
-
- 在组件状态或props更改导致重新渲染时,React会生成一个新的虚拟DOM树,并与旧的虚拟DOM树进行高效差异比较(称为Reconciliation或Diffing过程)。
- Diff算法找出最小化的DOM操作集合(增删改查节点)。
- React随后将这些操作应用到实际的DOM上,仅更新需要改变的部分,而不是完全替换整个DOM树。
- 真实DOM更新:
-
- 最终,React通过ReactDOM模块与浏览器底层交互,执行必要的DOM操作,将虚拟DOM树的改动反映到浏览器的真实DOM中
4. 对Fiber架构的理解
React16 之前的版本比对更新 VirtualDOM 的过程是采用循环加递归实现的,这种比对方式有一个问题、就是一旦任务开始进行就断,如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtuaIDOM 树比对更新完成之后主线程才能被释放,主线程才能执任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿,非常的影响用户体验。
核心问题:递归无法中断,执行重任务耗时长, 无法实现增量渲染,垃圾资源和优质资源混在一起 。JavaScript 又是单线程,无法同时执行其他任务,导致任务延迟页面卡顿,用户体验差。
- 无法中断
- 垃圾资源和优质资源一起占用空间
- 无法增量渲染
- 交互聚焦等高优先任务需要等待 造成卡顿的感觉
4.1. 解决方案
思想借鉴计算机操作系统 CPU 的调度和机制
1. 利用浏览器空闲时间执行任务 ,拒绝长时间占用主线程
2.放弃递归只采用循环,因为循环可以被中断,并且可以回滚的
3.组件更新任务拆分,将任务拆分成一个个的小任务
4.2. 实现思路
在 Fiber 方案中,为了实现任务的终止再继续,DOM能对算法被分成了两部分
1.构建 Fiber(可中断)
2.提交 Commit(不可中断)
DOM 初始渲染: virtualDOM -> Fiber -> Fiber[] -> DOM
DOM 更新操作: newFiber vs oldFiber -> Fiber[] -> DOM
在React的Fiber架构中,协调(Reconciliation)阶段,也被称为Render阶段,其主要工作包括以下几个关键步骤:
- Tree Walk (树遍历):
-
- Fiber架构下的协调阶段首先会对整个组件树进行遍历。不同于原来的递归同步渲染,Fiber采用了可中断的工作单元(work-in-progress tree,简称WIP tree)的方式来构建一个新的虚拟DOM树。
- Diff算法 :
-
- 当组件树发生变化时,React会执行其差异算法(diffing algorithm),但它在Fiber架构下进行了优化。Fiber不是一次性比较完整的老树与新树,而是增量式地对比每个fiber节点及其子树,寻找需要更新的部分,并决定如何最有效地反映这些变化到真实的DOM上。
- 调度优先级与抢占 :
-
- Fiber架构允许React按照优先级来调度和暂停不同的更新任务。这意味着React可以根据更新的性质(比如是否涉及到交互反馈、是否可见等)来判断哪些更新应该优先处理,哪些可以延后。在遍历过程中,低优先级的任务可以被高优先级的任务暂时中断,然后在适当的时候恢复。
- 副作用收集 :
-
- 在遍历过程中,React会记录每一个组件需要执行的副作用(side effects),这包括但不限于DOM更新(state/props变化导致的UI更新)、生命周期方法调用(如 shouldComponentUpdate**、** getDerivedStateFromProps**、** render****等)以及Hook(如 useState**、** useEffect****等)产生的效果。
- 生成Effect List :
-
- 协调阶段会生成一个Effect List,其中包含了待执行的所有副作用。这些副作用将在后续的Commit阶段被执行,从而实现DOM的实际更新和其他必要的清理或设置工作。
**总之,协调阶段的核心目标是基于新的props和state,通过高效地比对和分析组件树来确定哪些部分需要更新,并准备好这些更新的具体计划,而真正的DOM更新操作则推迟到后续的Commit阶段执行。**React 中的Fiber架构引入了两个核心的协调(reconciliation)阶段:Render阶段和Commit阶段。
在 Commit阶段 ,React的主要任务是将Render阶段确定的所有UI更改实际应用到DOM中。以下是Commit阶段的关键步骤和功能:
- 开始提交 : 通过 commitRoot****函数启动Commit阶段,这个函数接收Fiber树的根节点作为参数。
- Effect List处理 :
-
- React会遍历Render阶段生成的 Effect List ,这是一个包含了所有需要执行副作用(side effects)的Fiber节点的链表。
- 这些副作用可能包括DOM更新、组件生命周期方法(如 componentDidMount**、** componentDidUpdate**、** componentWillUnmount****等)的调用、以及Hook(如 useEffect**、** useLayoutEffect****等)注册的回调函数。
- 三个子阶段 :
-
- Before Mutation阶段 :在这个阶段,React会在实际改变DOM结构之前执行一些不需要依赖DOM状态的操作,例如清除样式或调整CSS动画。
- Mutation阶段 :在此阶段,React会根据Fiber节点的状态和更新队列对DOM进行实际的修改操作,包括添加、删除或更新DOM元素。
- Layout阶段 (有时也称为Paint阶段):在DOM变更后,React可能会触发浏览器的布局重排(reflow)和重绘(repaint)。对于使用 useLayoutEffect****Hook注册的回调,它们会在这个阶段同步执行。
- 同步与异步副作用 :
-
- 同步副作用(如 useLayoutEffect**)在这个阶段同步执行。**
- 异步副作用(如 useEffect****不带依赖数组的情况或者由调度器安排的微任务)则会在Commit阶段结束后的一个合适的时机(通常是浏览器事件循环的微任务阶段)执行。
- 不可中断性 :
-
- Commit阶段是同步且不可中断的过程,确保在这一阶段内所有的DOM更新和相关操作能够原子化地完成,不会被其他高优先级的任务打断。
总的来说,Commit阶段的目标就是将React内部的计算结果转化为可见的用户界面变化,同时处理所有与DOM交互和组件生命周期相关的逻辑。
4.3. fiber 对象
(1)fiber之前的数据驱动原理
React 是以数据驱动视图更新为核心的框架,其关键机制在于运用 setState 方法来变更组件状态,进而引发内存中虚拟DOM树的更新。更新过程依赖于React Diff算法,该算法通过比对新旧虚拟DOM树的差异,并批处理这些变化,实现视图的精准高效更新。
在React 16以前的版本中,组件树更新是通过堆栈调和(Stack Reconciliation)机制来实现的,这是一种递归遍历的方式,不具备可中断性,意味着它会一口气遍历完整个组件树直至完成。这样的处理方式在面对大规模数据或复杂的视图层次时,有可能导致主线程长时间被占用,从而影响应用程序对用户交互和其他高优先级事件的即时响应能力。
在Fiber架构引入之前,React在应对setState()调用(包括初次渲染)时遵循两个连续步骤:
- 调度阶段(Reconciler 瑞坑撒一了) :这个阶段React用新数据生成新的 Virtual DOM,遍历 Virtual DOM,然后通过 Diff 算法,快速找出需要更新的元素,放到更新队列中去。
- 渲染阶段(Renderer 软的弱):这个阶段 React 根据所在的渲染环境,遍历更新队列,将对应元素更新。在浏览器中,就是更新对应的 DOM 元素。
表面上看,这种设计也是挺合理的,因为更新过程不会有任何 I/O 操作,完全是 CPU 计算,所以无需异步操作,执行到结束即可。
(2)fiber之前的diff 缺点
这个策略像函数调用栈一样,会深度优先遍历所有的 Virtual DOM 节点,进行 Diff 。它一定要等整棵 Virtual DOM 计算完成之后,才将任务出栈释放主线程。对于复杂组件,需要大量的 diff 计算,会严重影响到页面的交互性。大量的 diff操作会
- 主线程阻塞(长时间占据主现场)
-
- 主线程阻塞:在React 15及更早版本中,当组件树发生更新时,React会通过递归算法一次性完成整个组件树的渲染过程,这个过程如果涉及大量组件,会导致主线程长时间阻塞,无法处理其他的UI交互,从而造成卡顿和延迟,降低用户体验。
- 无法中断与恢复渲染(必须一次完成)
-
- 无法中断与恢复渲染:原有的渲染过程不具备中断和恢复的能力,一旦开始渲染,就必须等到整个过程结束,即使在中间有更高优先级的任务也需要等待
- 无法实现增量渲染(无法分批)
-
- 无法实现增量渲染:以往的React无法有效区分渲染任务的重要性和紧急程度,所有更新任务都被视为同等重要的,无法做到逐步、增量地渲染UI。
- 资源优化不足(垃圾资源卡壳)
-
- 资源优化不足:旧版React无法根据应用的具体需求动态分配资源,无法高效利用有限的CPU周期来优化渲染性能。
只要 stack reconciler 持续使用主线程的时间,超过 16ms,页面绘制渲染就没法获得控制权,就容易出现渲染掉帧的现象。
(3) Fiber是React新的调度算法
React 在 Fiber 之前是使用基于递归的树结构来表示虚拟 DOM,类似于 基于 children 数组,主要特点是:
- 可以方便地进行递归遍历和操作,但在处理大型树或深度嵌套的情况下可能会出现性能问题
- 递归遍历进行时无法中断(中断无法记录位置)
Fiber 树的组织方式:基于链表 且 每个节点记录: 父节点 return、first child、next sibling进行深度优先遍历
虽然损失了一些可读性,这个结构却有很多优势:
- 调整节点位置很灵活,只要改改指针
- 方便进行各种方式的遍历
- 可以随时从某一个节点出发还原整棵树
「React 构建出新Virtual DOM 树,通过 Diffing 算法和老树对比」。但实际上 Fiber 树是边构建、边遍历、边对比的,这样最大程度减少了遍历次数,也符合「可中断」的设定 。
而旧的 react架构是:
- 构建新旧虚拟 DOM 树:React 首先会根据组件的状态和属性,递归构建出新的虚拟 DOM 树和旧的虚拟 DOM 树。
- 比较新旧虚拟 DOM 树:React 会在新旧虚拟 DOM 树之间进行深度优先遍历,对比两棵树的节点,找出差异。
- 应用差异到实际 DOM:一旦找出差异,React 就会将这些差异应用到实际的 DOM 上,从而更新页面内容。
(4)Fiber是深度优先 遍历 并区分内外层循环
Fiber 树是边创建边遍历的,每个节点都经历了「创建、Diffing、收集副作用(要改哪些节点) 」的过程。其中,创建、Diffing要自上而下,因为有父才有子;收集副作用要自下而上最终收集到根节点。
现在我们回头看遍历过程。外层循环每一步(也就是 beginWork 每次执行)都是自上而下的,并保证每个节点只走一次;内层循环每一步(在 completeUnitOfWork 里)都是自下而上的。显然,beginWork 负责创建、Diffing,completeUnitOfWork 负责收集副作用。
(5)树的构建和 diff
首先明确一点,所谓的 Diffing 算法并不是独立存在的,不是说先把树建完再执行 Diffing 算法找出差距,而是将 Diffing 算法体现在构建过程中对老节点的复用策略。
在React中最多会同时存在两棵 Fiber树:
- 当前屏幕上显示内容对应的Fiber树称为 current Fiber 树
- 正在构建的Fiber树称为 workInProgress Fiber 树,我们这里讨论的所有遍历都在这棵树上
当一次协调发起,首先会开一棵新 workInProgress Fiber 树,然后从根节点开始构建并遍历 workInProgress Fiber 树。
如果构建到一半被打断,current 树还在。如果构建并提交完成,直接把 current 树丢掉,让 workInProgress Fiber 树成为新的 current 树。
所谓 Diffing 也是在这两棵树之间,如果构建过程中确认新节点对旧节点的复用关系,新旧节点间也会通过 alternate 指针相连。
(6)Diffing 算法思想
正常情况下,完全找到两棵树的差异,是个时间复杂度很高的操作。但 Diffing 算法通过一些假设,权衡了执行开销和完整性。
假设一:不同类型的节点元素会有不同的形态
当节点为不同类型的元素时,React 会拆卸原有节点并且建立起新的节点。举个例子,当一个元素从 a 变成 img,从 Article 变成 Comment,都会触发一个完整的重建流程。
该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。
假设二:节点不会进行跨父节点移动
只会对比两个关联父节点的子节点,多了就加少了就减。没有提供任何方式追踪他们是否被移动到别的地方。
假设三:用户会给每个子节点提供一个 key,标记它们"是同一个"
当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。在新增 key 之后,使得树的转换效率得以提高。比如两个兄弟节点调换了位置,有 key 的情况下能保证二者都复用仅做移动,但无 key 就会造成两个不必要的卸载重建。
useFiber 做了什么
基于可复用节点和新属性复制一个 workInProgress 节点出来,并将二者通过 alternate 关联。这就是 useFiber 做的事。
总结:
- Fiber 树通过 beginWork 同时进行创建和"向下"遍历
- 创建过程也是 current(旧)、workInProgress(新)两棵树 Diffing 的过程,决定哪些旧节点需要复用、删除、移动,哪些新节点需要创建
- 只有父节点相互复用,才会触发子节点 Diffing,所以跨父节点的移动是铁定 Diffing 不到的
- 复用的条件是 key 和 type 都相同,所以 key 能提升复用率
- 子节点间的 Diffing 是一个"先做简单题"的过程,假设的优先级为:新子节点只有一个 ---> 子节点只发生末尾的增删 ---> 其他情况
- 对应的,Diffing 策略也分为:单节点 Diffing ---> 一轮循环 ---> 二轮循环
- Diffing 过程中会把结果(操作)以 Effect 的形式挂到节点上
React Fiber 架构主要分为三个阶段,即 Reconciliation(协调)、Render(渲染)和 Commit(提交)阶段。在每个阶段,Fiber 执行不同的任务来完成整个更新过程:
- Reconciliation(协调)阶段:
-
- Fiber 在协调阶段执行以下任务:
-
-
- 优先级调度:根据任务的优先级调度执行顺序,确保高优先级任务优先执行。
- Diff 比较:将新旧虚拟 DOM 树进行深度优先遍历,找出两棵树之间的差异(diff)。
- 更新标记:标记出需要更新的组件,并且生成更新任务队列。
- 构建 Fiber 树:构建 Fiber 树结构,将组件树映射到 Fiber 节点上。
-
- Render(渲染)阶段:
-
- Fiber 在渲染阶段执行以下任务:
-
-
- 生成 Fiber 树:根据更新任务队列生成 Fiber 树,这个过程称为"reconciliation"。
- 执行组件逻辑:执行组件的生命周期方法(如 componentWillMount、componentWillReceiveProps 等)和函数组件的函数体,生成新的 Virtual DOM。
- 更新 Fiber 节点:更新 Fiber 节点的状态(如 effectTag)和 props。
-
- Commit(提交)阶段:
-
- Fiber 在提交阶段执行以下任务:
-
-
- 提交 DOM 变更:将更新后的 Virtual DOM 提交到实际的 DOM 上进行更新。
- 执行副作用:根据 Fiber 树上的 effectTag,执行相应的 DOM 操作,如添加、删除、更新等。
-
Fiber数据结构是一个链表,这样就为Fiber架构可中断渲染提供可能
function FiberNode(){
this.tag = tag; //元素类型
this.key = key;//元素的唯一标识。
this.elementType = null; //元素类型
this.type = null;//元素类型
this.stateNode = null;//元素实例的状态节点
// Fiber
this.return = null;//该组件实例的父级。
this.child = null;//该组件实例的第一个子级。
this.sibling = null;//该组件实例的下一个兄弟级
this.index = 0;//该组件实例在父级的子级列表中的位置。
this.ref = null;//该组件实例的ref属性
this.refCleanup = null;//ref的清理函数
this.pendingProps = pendingProps;//待处理的props(最新的)
this.memoizedProps = null;//处理后的props(上一次)
this.updateQueue = null;//TODO
this.memoizedState = null;//类组件保存state信息,函数组件保存hooks信息
this.dependencies = null;//该组件实例的依赖列表
this.mode = mode;//该组件实例的模式 (DOM模式和Canvas模式)
// Effectsx
this.flags = NoFlags$1;//副作用标签 ,之前的版本是effectTag
this.subtreeFlags= NoFlags$1;//子节点副作用标签。
this.deletions = null;//待删除的子树列表。
this.lanes = NoLanes;//任务更新的优先级区分
this.childLanes = NoLanes;//子树任务更新的优先级区分
this.alternate = null;//组件实例的备份实例,用于记录前一次更新的状态。更新时候 workInProgress会复用当前值
}
5. fiber 处理副作用的时候为什么是自下而上的呢
Fiber架构中处理副作用(side effects)时采用自下而上的方式,这是为了实现更高效的调度和渲染过程,以及更好地支持优先级调度和中断。
在Fiber架构中,每个React组件都可以被视为一个fiber节点,这些节点以树状结构组织,形成了React组件树。在渲染过程中,React会遍历整个组件树,并根据需要执行各种操作,比如计算Virtual DOM、处理更新、执行副作用等。为了保证在处理副作用时的灵活性和性能,Fiber架构采用了以下策略:
- 异步调度和优先级:自下而上的处理副作用可以更好地支持React的异步调度和优先级调度。在React中,更新被分成多个优先级等级,每个优先级都有不同的截止时间。通过自下而上的处理方式,可以在渲染过程中根据优先级动态地中断和恢复任务执行,从而更好地响应用户输入和保证页面的流畅性。
- 错误边界和恢复:自下而上的处理方式使得React能够更容易地实现错误边界和错误恢复机制。当子组件发生错误时,父组件可以更容易地捕获错误并进行处理,而不会中断整个渲染过程。
- 局部更新和增量渲染:通过自下而上的处理方式,React可以更精细地控制组件的更新范围,实现局部更新和增量渲染。只有在需要更新的部分才会执行副作用,从而提高了渲染的效率。
总之,自下而上的处理副作用是为了更好地支持React的异步调度和优先级调度,以及实现更高效的局部更新和增量渲染。这种处理方式使得React能够更好地适应复杂的应用场景,并提供更好的用户体验。
6. React Fiber 架构主要阶段
React Fiber 架构主要分为三个阶段,即 Reconciliation (协调 瑞 kang 色里ation)、Render(渲染)和 Commit(提交)阶段。在每个阶段,Fiber 执行不同的任务来完成整个更新过程:
- 调度阶段(Scheduling): 在调度阶段,React Fiber 根据优先级决定哪些任务需要被执行,并将这些任务安排到适当的时间片段(或"帧")中执行。调度器负责管理任务的优先级和调度顺序,以确保页面能够保持响应并优先处理重要任务。
- Reconciliation(协调)阶段:
-
- Fiber 在协调阶段执行以下任务:
-
-
- 优先级调度:根据任务的优先级调度执行顺序,确保高优先级任务优先执行。
- Diff 比较:将新旧虚拟 DOM 树进行深度优先遍历,找出两棵树之间的差异(diff)。
- 更新标记:标记出需要更新的组件,并且生成更新任务队列。
- 构建 Fiber 树:构建 Fiber 树结构,将组件树映射到 Fiber 节点上。
-
- Render(渲染)阶段 : 渲染器阶段 【生成更新操作的数据结构】
-
- Fiber 在渲染阶段执行以下任务:
-
-
- 生成 Fiber 树:根据更新任务队列生成 Fiber 树,这个过程称为"reconciliation"。
- 执行组件逻辑:执行组件的生命周期方法(如 componentWillMount、componentWillReceiveProps 等)和函数组件的函数体,生成新的 Virtual DOM。
- 更新 Fiber 节点:更新 Fiber 节点的状态(如 effectTag)和 props。
-
- Commit(提交)阶段 : 【执行更新操作并将其应用到实际的DOM上。两者虽然都涉及将虚拟DOM转换为真实DOM】
-
- Fiber 在提交阶段执行以下任务:
-
-
- 提交 DOM 变更:将更新后的 Virtual DOM 提交到实际的 DOM 上进行更新。
- React会将这些更新操作转换为底层浏览器API调用,例如 appendChild 、 removeChild 、 setAttribute 等,然后将这些操作批量执行,更新页面的显示。
- 执行副作用:根据 Fiber 树上的 effectTag,执行相应的 DOM 操作,如添加、删除、更新等。
-
7. 什么是代数效用
代数效应(Algebraic Effects)是一种编程范式,它为在纯函数式编程环境中管理和控制副作用提供了一种形式化的手段。在函数式编程中,通常强调无副作用的纯函数,然而实际编程中经常需要处理诸如I/O操作、异常处理、资源管理等副作用。代数效应通过将这些副作用建模为可组合的代数结构,使得程序员可以明确地定义、触发和处理这些副作用。
7.1.1. 什么是代数效应?
- 代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离(PS:不说人话😄)
- 我的理解是可以在同步代码中实现异步效果,可以参考JS中Generator的使用,但要理念比其先进。(PS: React中saga的原理实现就是这个原理,后续会写其源码实现)
- 应用:React中的Suspense和和Hook实现参考了代数效应
- 代数效应的代码理解(PS: 图中你没见过的语法都是虚构的,仅帮助理解)
7.1.2. 代数效应的应用
- React15中的缺陷:上文中提到解决UI渲染瓶颈的问题采用时间切片处理,但React15中的渲染逻辑是递归处理,一旦开始就不能终止。若是在其更新过程中有新的交互,触发数据更新就不能及时响应。这是React15中的缺陷。
- React16中如何解决 : React16中引入代数效应的理念,增加调度机制 ,给fiber节点打上标记,对应不同的渲染优先级。在渲染过程中优先级高的fiber节点能插队,实现异步更新。当异步更新完成后,又回来接着更新上次的内容,这里就是代数效应的应用
8. 介绍requestIdleCallback
window.requestIdleCallback() 方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间 timeout,则有可能为了在超时前执行函数而打乱执行顺序。
你可以在空闲回调函数中调用 requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。
这是一个实验中的功能,强烈建议使用timeout选项进行必要的工作,否则可能会在触发回调之前经过几秒钟。
浏览器在一帧内可能会做执行下列任务,而且它们的执行顺序基本是固定的:
- 处理用户输入事件
- Javascript执行
- requestAnimation 调用
- 布局 Layout
- 绘制 Paint
上面说理想的一帧时间是 16ms (1000ms / 60),如果浏览器处理完上述的任务(布局和绘制之后),还有盈余时间,浏览器就会调用 requestIdleCallback 的回调。例如
但是在浏览器繁忙的时候,可能不会有盈余时间,这时候requestIdleCallback回调可能就不会被执行。 为了避免饿死,可以通过 requestIdleCallback 的第二个参数指定一个超时时间
另外不建议在requestIdleCallback中进行DOM操作,因为这可能导致样式重新计算或重新布局(比如操作DOM后马上调用 getBoundingClientRect),这些时间很难预估的,很有可能导致回调执行超时,从而掉帧。
9. MessageChannel
目前 requestIdleCallback 是一个实验功能。所以目前 React 自己实现了一个。它利用 MessageChannel 模拟将回调延迟到 绘制操作 之后执行:
-
当前帧结束时间: 我们知道requestAnimationFrame的回调被执行的时机是当前帧开始绘制之前。也就是说rafTime是当前帧开始时候的时间,如果按照每一帧执行的时间是16.66ms。那么我们就能算出当前帧结束的时间, frameDeadline = rafTime + 16.66。
-
当前帧剩余时间:当前帧剩余时间 = 当前帧结束时间(frameDeadline) - 当前帧花费的时间。关键是我们怎么知道'当前帧花费的时间',这个是怎么算的,这里就涉及到js事件循环的知识。react中是用
let frameDeadline // 当前帧的结束时间
let penddingCallback // requestIdleCallback的回调方法
let channel = new MessageChannel()// 当执行此方法时,说明requestAnimationFrame的回调已经执行完毕,此时就能算出当前帧的剩余时间了,直接调用timeRemaining()即可。
// 因为MessageChannel是宏任务,需要等主线程任务执行完后才会执行。我们可以理解requestAnimationFrame的回调执行是在当前的主线程中,只有回调执行完毕onmessage这个方法才会执行。
// 这里可以根据setTimeout思考一下,setTimeout也是需要等主线程任务执行完毕后才会执行。
channel.port2.onmessage = function() {
// 判断当前帧是否结束
// timeRemaining()计算的是当前帧的剩余时间 如果大于0 说明当前帧还有剩余时间
let timeRema = timeRemaining()
if(timeRema > 0){
// 执行回调并把参数传给回调
penddingCallback && penddingCallback({
// 当前帧是否完成
didTimeout: timeRema < 0,
// 计算剩余时间的方法
timeRemaining
})
}
}
// 计算当前帧的剩余时间
function timeRemaining() {
// 当前帧结束时间 - 当前时间
// 如果结果 > 0 说明当前帧还有剩余时间
return frameDeadline - performance.now()
}
window.requestIdleCallback = function(callback) {
requestAnimationFrame(rafTime => {
// 算出当前帧的结束时间 这里就先按照16.66ms一帧来计算
frameDeadline = rafTime + 16.66
// 存储回调
penddingCallback = callback
// 这里发送消息,MessageChannel是一个宏任务,也就是说上面onmessage方法会在当前帧执行完成后才执行
// 这样就可以计算出当前帧的剩余时间了
channel.port1.postMessage('haha') // 发送内容随便写了
})
}
requestHostCallback 在非 DOM 环境下用 setTimeout 实现的,在 DOM 是使用 MessageChannel 实现的,通过 MessageChannel 双通道来处理任务,messageChannel属于宏认为,异步执行
Channel Messaging API 的 MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。,备注: 此特性在 Web Worker 中可用。
在React中,MessageChannel可以用来模拟requestIdleCallback,因为它可以创建一个异步的消息通道,允许在主线程执行任务时进行调度,而不会阻塞主线程的执行。
requestIdleCallback是一个浏览器原生API,它允许开发者在浏览器的空闲时间执行任务,从而减少对主线程的占用,提高页面的性能和响应性。然而,并非所有浏览器都支持这个API,因此需要一种方式来模拟它的行为。
MessageChannel提供了一个解决方案。它允许我们创建一个通信管道,通过postMessage()方法在管道中发送消息,并通过监听message事件来接收消息。通过利用这个特性,我们可以创建一个循环,在浏览器空闲时不断地向管道发送消息,从而模拟requestIdleCallback的行为。
10. requestAnimationFrame()
你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
当你准备更新在屏动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。回调函数执行次数通常是每秒 60 次,但在大多数遵循 W3C 建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,在大多数浏览器里,当 requestAnimationFrame() 运行在后台标签页或者隐藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。
11. diff的原理
浏览器下使用 react-dom 的渲染器,会先把 vdom 转成 fiber,找到需要更新 dom 的部分,打上增删改的 effectTag 标记,这个过程叫做 reconcile,可以打断,由 scheducler 调度执行。reconcile 结束之后一次性根据 effectTag 更新 dom,叫做 commit。
这就是 react 的基于 fiber 的渲染流程,分成 render(reconcile + schedule)、commit 两个阶段。
当渲染完一次,产生了 fiber 之后,再次渲染的 vdom 要和之前的 fiber 对比下,再决定如何产生新的 fiber,目标是尽可能复用已有的 fiber 节点,这叫做 diff 算法。
react 的 diff 算法分为两个阶段:
第一个阶段一一对比,如果可以复用就下一个,不可以复用就结束。
第二个阶段把剩下的老 fiber 放到 map 里,遍历剩余的 vdom,一一查找 map 中是否有可复用的节点。
最后把剩下的老 fiber 删掉,剩下的新 vdom 新增。
这样就完成了更新时的 reconcile 过程。
其实 diff 算法的核心就是复用节点,通过一一对比也好,通过 map 查找也好,都是为了找到可复用的节点,移动过来。然后剩下的该删删该增增。
diff算法的大致步骤:
- 树对比入口:
-
- React首先会对新旧两棵虚拟DOM树(旧树是上次渲染的结果,新树是这次即将渲染的结果)的根节点进行比较。
- 组件标识符比较:
-
- 如果新旧两棵树的根节点是React组件,React会比较它们的类型(即构造函数或类),只有当类型相同的情况下,才会进行下一步的比较。不同类型的话,React会直接卸载旧组件并创建新组件。
- 元素类型及属性比较:
-
- 如果节点是DOM元素(不是组件),React会比较元素类型(标签名)以及它们的属性。如果类型不同,React会直接替换整个DOM元素;如果类型相同,仅更新变化的属性。
- 文本节点比较:
-
- 如果是文本节点,React直接比较文本内容,如果内容不同则更新文本节点。
- 子节点递归比较:
-
- 对于子节点,React采用深度优先遍历的方式,逐一对比新旧树的子节点。这里的优化策略包括:
-
-
- 同层比较:React只会对同一层级的节点进行比较,不会跨层级寻找节点。
- 移动标记:若发现一个新节点与旧节点中的某个子节点相匹配,React会选择移动(reorder)而不是删除并重新创建节点,以减少DOM操作。
- keyed children:对于列表类型的子节点,React通过key属性来识别节点的身份,以优化列表更新。
-
- 组件状态比较:
-
- 如果节点是React组件,React还会考虑组件自身的内部状态(state)是否发生了变化,根据状态来决定是否需要重新渲染组件及其子树。
- 删除多余节点:
-
- 在遍历过程中,React会标记出旧树中未在新树中出现的节点,遍历结束后会批量删除这些节点。
通过以上步骤,React能够最小化对真实DOM的操作,极大地提升页面更新的性能。而在React Fiber架构中,diff算法变得更加灵活,引入了可中断的优先级调度,使得React在处理大量或复杂的更新时能够更加有效地分配资源。
通过以上步骤,React的diff算法能够在O(n)的时间复杂度内完成虚拟DOM树的比较,从而实现在大量DOM更新时依然保持较高的性能。
几个组件 没有更新和移除 只是进行了顺序调换
如果几个组件没有更新和移除,只是进行了顺序调换,React Diff算法会尽可能地尝试最小化DOM操作,从而减少不必要的更新。
当组件顺序发生调换时,React Diff算法会比较新旧虚拟DOM树的顺序,并识别出顺序变化的节点。这种情况下,React Diff算法会将顺序变化的节点视为移动操作,而不是插入或移除操作。
具体步骤如下:
- 识别顺序变化: React Diff算法会遍历新旧虚拟DOM树,识别出顺序发生变化的节点,即新旧列表中相同位置的节点类型不同的情况。
- 标记移动操作: 对于顺序变化的节点,React Diff算法会将其标记为移动操作,而不是插入或移除操作。这意味着React不会销毁和重新创建这些节点,而是尝试将其移动到正确的位置,以保持页面的稳定性和性能。
- 生成更新操作: 一旦识别出顺序变化的节点,并将其标记为移动操作,React Diff算法会生成相应的更新操作,描述了如何调整虚拟DOM树以反映最新的顺序。
- 应用更新: 最后,生成的更新操作会被提交给渲染引擎,渲染引擎会将这些更新操作转换为实际的DOM操作,并将其应用到页面上,从而实现组件顺序的调换。
通过这种方式,React Diff算法能够在不重新渲染和销毁组件的情况下,高效地处理组件顺序的变化,从而提高页面的性能和响应性。
组件不变的情况下 重新排序会导致渲染吗
- 如果不主动给组件赋值不可变更的 key 则会重新渲染 并重新执行一次 useEffect
- 如果主动赋值 key 则不会重新渲染 不会重新执行 useEffect
12. 提高组件的渲染效率
在React中提高组件渲染效率并避免不必要的渲染主要有以下几个策略:
- 使用PureComponent或React.memo:
-
React.PureComponent
自动进行浅比较(shallow comparison
),只有当props或state发生改变时才会触发组件重新渲染。继承自React.PureComponent
的组件会默认检查props和state对象是否严格相等。- 对于函数组件,可以使用
React.memo
对其进行包裹,React.memo
同样会进行浅比较,只有当props发生变化时才会重新渲染组件。
- 自定义shouldComponentUpdate生命周期方法:
-
- 在class组件中,可以覆盖
shouldComponentUpdate(nextProps, nextState)
方法,根据传入的新props和新state判断是否有必要调用render
方法,从而避免不必要的渲染。
- 在class组件中,可以覆盖
- 使用React Hooks进行优化:
-
- 使用
React.useState
和React.useReducer
时,可以根据业务逻辑精确控制state的变化,避免不必要的状态更新。 - 使用
React.useMemo
来缓存计算结果,仅当依赖的props或state改变时才重新计算。 - 使用
React.useCallback
来缓存函数引用,避免在props没变的情况下因为回调函数引用变了而导致不必要的子组件重渲染。
- 使用
- 优化数据结构:
-
- 避免在props或state中传递深度嵌套的对象或数组,因为React的默认浅比较无法检测到深层数据的变化。若必须使用复杂数据结构,应当在适当的地方使用
shouldComponentUpdate
、useMemo
或手动进行深比较。
- 避免在props或state中传递深度嵌套的对象或数组,因为React的默认浅比较无法检测到深层数据的变化。若必须使用复杂数据结构,应当在适当的地方使用
- 优化事件处理器:
-
- 将事件处理器封装在
useCallback
中,保证其在props不变时引用始终一致,避免无意义的组件重渲染。
- 将事件处理器封装在
- 减少不必要的state和props更新:
-
- 只有当数据实际变化时才更新state,避免频繁调用
setState
。 - 使用Context API或者Redux等状态管理库时,确保只在数据变化时触发全局状态更新。
- 只有当数据实际变化时才更新state,避免频繁调用
通过以上这些方法,可以最大程度地减少不必要的组件渲染,从而提升React应用的性能。在实践中,需要根据组件的具体情况进行权衡和选择最适合的优化方案。
13. React render方法的原理
React的render
方法是React组件的核心方法之一,它的基本原理和作用在于将组件的状态和属性转化为可以在浏览器中渲染的虚拟DOM表示,然后将这个虚拟DOM转化为实际的DOM操作,最终更新到浏览器的真实DOM中。
以下是render
方法的基本原理和过程:
- 首次渲染 :
当组件实例化并初次插入到DOM中时,React会调用该组件的render
方法。这个方法必须返回一个React元素(可以是原生DOM元素、组件元素或Fragment),React会根据这个返回值创建一个虚拟DOM树。 - 虚拟DOM的创建与比对:
-
render
方法返回的是一个虚拟DOM树,这是一个轻量级的JavaScript对象结构,与实际的DOM树结构相似但并非真实的DOM节点。- 当组件的
props
或state
发生变化时,React会重新执行render
方法生成新的虚拟DOM树。 - React使用其内部的高效算法------虚拟DOM Diff算法,比较新旧两棵虚拟DOM树的差异。
- 最小化DOM操作:
-
- 根据虚拟DOM的比较结果,React确定最少必要的DOM操作,如添加、更新或删除DOM节点,而不是每次都完全重建DOM树。
- 这种增量更新机制极大提高了React应用的性能,因为它避免了频繁地直接操作DOM带来的性能损耗。
- DOM更新:
-
- 最终,React将这些最小化的DOM操作应用到实际DOM树上,确保用户界面得到准确、高效的更新。
React render
方法的作用就是将组件的状态和属性转化为虚拟DOM,通过虚拟DOM Diff算法来决定实际DOM的最小更新操作,从而实现高性能的用户界面更新。在类组件中, render****方法是必需的,而在函数组件中,函数体本身充当了 render****方法的角色。
14. 能render时访问refs吗
不能。这是因为refs是在组件挂载到DOM之后填充的,并且在render()执行期间,React还没有完成这个过程。
refs会在组件渲染完成后才被赋值,即在组件生命周期的某个阶段(比如componentDidMount或getDerivedStateFromProps(已弃用)后的一个回调钩子中)才能保证已经被正确设置并指向真实的DOM节点或组件实例。
因此,如果你尝试在render()方法内部访问ref,它可能会是null或未定义的,因为在那个时刻,React还没有机会将其关联到实际的DOM元素或组件实例上。
正确的做法是在生命周期方法中或者其他合适的时机(例如事件处理函数中)访问refs。例如,在componentDidMount或useEffect Hook(对于函数组件)中,可以安全地通过this.refName.current(对于类组件)或 useRef Hook 返回的 .current 属性(对于函数组件)来访问refs指向的内容。
15. React refs 的理解
React 中的 ref
是一种在组件之间直接访问 DOM 节点或在函数组件中访问类组件实例的能力。React 的 ref
底层原理涉及以下几个关键点:
- 创建和分配 Refs:
-
- React 提供了几种创建 ref 的方式,如
React.createRef()
(适用于类组件)、useRef()
Hook(适用于函数组件)以及forwardRef()
(用于在函数组件之间传递 ref)。 - 创建 ref 后,可以通过
ref={myRef}
的方式将 ref 分配给具体的 DOM 元素或类组件实例。
- React 提供了几种创建 ref 的方式,如
- React 内部对 Ref 的处理:
-
- 当 React 渲染组件时,它会注意到带有
ref
属性的元素,并将 ref 的.current
属性指向相应的 DOM 节点或组件实例。 - 对于 DOM 节点,React 在组件挂载时将 ref 的
.current
设置为对应的 DOM 元素。 - 对于类组件,
.current
指向的是组件的实例对象。 - 对于函数组件,通过
forwardRef
结合useImperativeHandle
Hook,可以让函数组件暴露特定方法或属性给父组件。
- 当 React 渲染组件时,它会注意到带有
- 生命周期中的 Ref 更新:
-
- 当组件的生命周期发生变化(如挂载、更新或卸载)时,React 会更新 ref 的
.current
属性。 - 在组件卸载时,
.current
会被设置为null
,以避免对已不存在的 DOM 节点或组件实例进行引用。
- 当组件的生命周期发生变化(如挂载、更新或卸载)时,React 会更新 ref 的
- Ref 的传播与转发:
-
- 使用
React.forwardRef
API,我们可以创建一个能够将接收到的ref
传递给其内部子组件的组件,这对于函数组件尤为有用,因为它允许函数组件也能获得和操作内部 DOM 节点或子组件实例。
- 使用
- 底层实现:
-
- 在 React 的 Fiber 架构中,refs 是在调度和 reconciliation 过程中进行处理的。
- React 通过
commitWork
阶段将更新的 ref 信息应用到实际的 DOM 树中。
总而言之,React 的 ref
系统通过对组件生命周期的精细控制和对 DOM 操作的抽象,为开发者提供了便捷地访问和操作底层 DOM 节点的能力,同时也支持组件间的直接交互。React 内部通过巧妙的数据结构和算法确保了 ref 更新的高效和准确。
- Ref的底层绑定
当React在渲染过程中遇到带有ref属性的元素时,会通过内部机制将ref对象与对应的DOM节点或组件实例建立联系。对于DOM元素,React会在组件挂载后立即将ref对象的current属性指向对应的DOM节点。
对于类组件,ref的current属性将会指向组件的实例。这样就可以访问类组件的实例方法和内部状态。
对于函数组件,通过forwardRef和useImperativeHandle可以定制函数组件对外暴露的实例行为,使其也可以拥有类似类组件实例的行为。
- 生命周期中的Ref管理
在组件生命周期的不同阶段,React会适当地更新ref的current属性。例如,在组件卸载时,React会将ref的current设为null,以防止对已卸载的DOM节点或组件实例进行引用。 - Ref的更新和同步
React的Fiber架构在调度和Reconciliation过程中处理ref。在确定了DOM更新计划后,React会在commit阶段把ref的更新同步到实际的DOM树中。 - React Fiber与Ref的关系
React Fiber通过commitRootImpl等内部方法,在DOM更新阶段遍历Fiber节点,当遇到有ref的节点时,会将ref.current指向正确的DOM节点或组件实例。
总的来说,React通过一套内部机制,在不影响React组件抽象的同时,为开发者提供了一种间接但灵活地访问和操作DOM节点的方式。通过ref,开发者可以在React的声明式编程模型中实现对DOM的命令式操作,这对处理聚焦、测量尺寸、动画等场景至关重要。
ref 是 React 提供的用于获取组件实例或 DOM 元素引用的一种机制,而 forwardRef 是用于在函数式组件中转发 ref 的高阶组件。使用 ref 可以在类组件和函数式组件中获取引用,而 forwardRef 则是用于在函数式组件中处理引用的特殊情况。
16. 高阶组件
React 中的高阶组件(Higher-Order Component,简称 HOC)是一种高级的React组件抽象概念,它本质上是一个函数,此函数接受一个React组件作为参数,并返回一个新的封装过的React组件。高阶组件主要用于代码复用、逻辑抽象和交叉关注点的处理,比如权限控制、数据预取、主题样式切换等场景。
什么是高阶组件:
在函数式编程的概念中,高阶函数是指接受函数作为输入或者输出函数的函数。在React中,高阶组件遵循同样的原则,它是接收组件并返回新组件的函数,这样可以使得我们能够在不修改原始组件代码的情况下为其增加额外的功能。
怎么编写高阶组件:
下面是一个基础的高阶组件示例,它接收一个组件WrappedComponent
并返回一个新的组件,为传入的组件增加了某些通用逻辑或特性:
function withEnhancement(WrappedComponent) {
return class EnhancedComponent extends React.Component {
componentDidMount() {
// 在这里添加额外的生命周期逻辑
}
render() {
// 可能会修改或添加props,或者包裹WrappedComponent
const newProps = { ...this.props, extraProp: 'someValue' };
return <WrappedComponent {...newProps} />;
}
};
}
// 使用高阶组件
const EnhancedMyComponent = withEnhancement(MyComponent);
应用场景:
- 跨组件状态管理:当多个组件需要共享相同的数据源或状态时,可以通过高阶组件统一管理和分发状态。
- Props代理:HOC可以拦截和修改传入子组件的props,例如注入依赖、处理副作用逻辑或进行数据转换。
- 生命周期方法的封装:可以集中处理诸如数据加载、订阅和取消订阅等生命周期事件。
- 组件装饰:为组件添加全局功能,如路由、错误边界、日志记录、性能检测等。
- 权限控制:根据用户的权限信息,决定是否渲染某个组件或者限制组件的部分功能。
- 响应式数据获取:在组件挂载前或更新时自动获取数据,然后将其作为props传递给被包裹的组件。
- 主题切换:允许在不同主题间切换,通过高阶组件动态修改组件的样式或其他主题相关属性。
总结起来,高阶组件提供了一种强大的抽象手段,帮助开发者更好地组织和复用代码,保持组件层级扁平化,并且能够集中处理与特定业务逻辑或框架无关的通用需求。随着React Hooks的引入,虽然很多原本通过HOC实现的需求可以转由自定义Hooks完成,但在某些情况下,特别是在处理复杂的组件组合和扩展时,高阶组件依然是一种有效的设计模式。
通过上面的了解,高阶组件能够提高代码的复用性和灵活性,在实际应用中,常常用于与核心业务无关但又在多个模块使用的功能,如权限控制、日志记录、数据校验、异常处理、统计上报等
数据流管理:例如在Redux中,connect函数就是一个典型的高阶组件,用来连接React组件与Redux Store。
生命周期管理:为多个组件提供统一的生命周期逻辑,如初始化、清理资源、错误处理等。
条件渲染:根据某些条件动态决定是否渲染某个组件,或者渲染不同的组件版本。
API调用封装:在组件渲染前后执行API调用,并将数据注入到组件props中。
权限控制
日志记录
数据校验
异常处理
统计上报
17. 受控组件和非受控组件
React 受控组件(Controlled Components) :
受控组件是指组件内部的状态完全由React的state管理,用户输入的值立即反映到组件的state中。在受控组件中,表单元素(如<input>
、<textarea>
或<select>
)的值不是直接由DOM本身管理,而是通过React组件的state进行控制。例如,<input>
元素的值由value
属性关联到组件的state变量,每次用户输入时,都会触发onChange事件,此时组件会根据事件处理函数更新state,进而更新value
属性,始终保持DOM元素的值与state同步。
特点:
- 值由React组件控制,用户输入不会直接修改DOM元素的值。
- 必须提供onChange事件处理函数,以更新组件状态。
- 表单提交时,可以通过组件自身的state获取用户输入的最新数据。
优点:
- 提供了对用户输入行为的完全控制,方便在表单验证、实时反馈等方面做出快速反应。
- 状态管理集中,有利于组件间的协作和数据一致性。
缺点:
- 需要额外的代码来处理onChange事件和更新state,增加了开发复杂度。
- 不适用于某些不需要实时响应用户输入,或者希望由DOM自身处理的场景。
常用场景:
- 实现复杂的表单验证逻辑,需要实时获取并校验用户输入的表单。
- 用户输入内容需要与其他组件或数据源紧密联动,例如搜索框实时查询。
React 非受控组件(Uncontrolled Components) :
非受控组件则是指DOM元素的值由浏览器本身管理,不受React组件的state直接影响。在非受控组件中,表单元素的值是由DOM自身的defaultValue属性初始化,用户输入的改变并不会立刻同步到React组件的state中。获取非受控组件的值通常通过ref来访问DOM节点的value属性。
特点:
- 用户输入直接改变DOM元素的值,不依赖React组件的state。
- 不需要提供onChange事件处理函数来更新state。
- 获取当前值时,通常需要使用ref。
优点:
- 开发简单,更接近原生HTML表单的使用方式。
- 适用于只需要在表单提交时获取用户输入值的简单场景。
缺点:
- 无法实时跟踪表单值的变化,不利于实现即时验证或联动其他组件的功能。
- 多个组件共享数据时需要额外考虑数据同步问题。
常用场景:
- 简单的表单,仅在提交时才关心用户输入的值,而不关心输入过程。
- 需要保留浏览器默认行为,比如在文本框中粘贴内容时保持原有格式。
总结来说,受控组件适合于需要对用户输入实时响应和严格控制的场景,而非受控组件更适合于简单的表单处理,或者是需要利用原生DOM特性的场合。开发者应根据具体需求灵活选择合适的方式来处理表单输入。
18. 组件之间如何通信
父组件向子组件通信:父组件通过 props 向子组件传递需要的信息。
子组件向父组件通信:: props+回调的方式。
跨级组件的通信方式?
- Context API :
React的Context API提供了一种在组件树中共享数据的方法,而无需手动通过每个级别的props传递。你可以创建一个Context对象,并使用<MyContext.Provider>在组件树中的某个位置提供数据。然后,任何子组件(无论多深)都可以使用<MyContext.Consumer>或useContext Hook来访问这些数据。 - Redux :
Redux是一个流行的状态管理库,它允许你在应用程序的任何地方管理和访问状态。通过使用Redux,你可以创建一个全局可访问的store来存储应用程序的状态,并使用connect函数或useSelector和useDispatch Hooks将组件与store连接起来。 - props React Hooks(如useState和useReducer)与自定义Hooks :
对于较简单的跨级通信,你可以使用useState和useReducer Hooks在父组件中管理状态,并通过自定义Hooks或props将状态和方法传递给子组件。自定义Hooks允许你封装和重用状态逻辑。 - MobX :
MobX是另一个状态管理库,它提供了一种更响应式的方法来管理和更新应用程序的状态。MobX鼓励使用可观察的对象和反应机制来自动更新UI。 - 事件总线(Event Bus)或发布-订阅模式 :
你可以实现一个简单的事件总线或使用现有的库(如mitt、tiny-emitter等),允许组件订阅事件并在事件发生时接收通知。这种方法在需要解耦组件时特别有用。 - 全局状态容器(如window对象) :
虽然不推荐作为常规做法,但有时你可以将状态附加到全局对象(如window)上以实现跨级通信。这种方法应该谨慎使用,因为它可能导致难以追踪的状态更新和潜在的命名冲突。 - 父组件回调 :
通过父组件向子组件传递回调函数,子组件可以在需要时调用这些函数来通知父组件状态的变化。这种方法适用于较简单的场景和较浅的组件层次结构。 - 使用第三方库 :
除了Redux和MobX之外,还有其他一些第三方库可以帮助实现跨级组件通信,如reactn、unstated、zustand等。这些库提供了不同的抽象和机制来处理状态管理。
父组件获取子组件的状态和方法
- 回调函数
- 使用 refs
1、通常建议遵循React数据流向单向数据绑定的原则,尽量避免直接访问子组件的状态。
2、使用回调函数是一种更符合React设计理念的方式,它促进了组件之间的解耦和可复用性。
3、Refs 主要用于获取DOM节点或在必要时获取子组件实例进行一些特殊操作,而不鼓励常规情况下频繁获取子组件的状态。
19. hook 的跨级组件的通信方式
- Context API with useContext Hook
-
- 顶层组件创建createContext 包裹组件,子组件使用
- useContext
- useReducer with useContext
-
- 首先在顶层组件(或接近顶层的位置)创建一个Context,并通过 useReducer 初始化和管理状态。然后将 useReducer 返回的状态和dispatch方法作为值放入Context中。这样一来,任何需要访问或修改这个状态的子组件,不管它们在层级结构中的位置有多深,只要通过 useContext 消费这个Context,就能直接获取到状态和dispatch方法,实现跨层级的状态共享和更新
- Redux or MobX with their respective Hooks 如果你的应用规模较大,可以使用Redux或MobX配合它们的React Hooks(如useSelector、useDispatch(Redux Toolkit)或useObservable、useComputed(MobX))来进行跨级甚至全局范围内的状态管理。
- 自定义Hook + Context 你可以创建一个自定义Hook,封装对Context的消费,然后在任何层级的组件中使用这个自定义Hook。
- 事件总线(Event Bus) 使用第三方库(如pubsub-js)或者自己实现一个简单的事件发布订阅系统,通过发布和订阅事件来实现跨级通信。但在React中,这种方法相比Context API等更为低效且不易于维护,故不太推荐。
- 祖先组件作为中介 如果组件层次结构相对清晰,可以向上寻找共同的祖先组件作为通信的媒介,祖先组件可以使用useReducer、useState等方式维护共享状态,再通过props向下传递给需要通信的子组件。
的异步操作,然后再 dispatch 回结果 action。
19.1. 基于 hook 的状态库
以下是一些流行的基于 Hooks 的状态库:
- Recoil:由 Facebook 推出的一个用于 React 的状态管理库,它使用 Hooks API 并提供了灵活的、可预测的状态管理。Recoil 允许你创建可观察和可变更的状态,并通过 Hooks 在组件中轻松访问这些状态。
- Redux Toolkit:虽然 Redux 本身不是基于 Hooks 的,但 Redux Toolkit 是 Redux 的官方工具集,它简化了 Redux 应用的开发过程,并提供了与 Hooks 集成的功能。通过使用 Redux Toolkit,你可以更容易地在 React 组件中使用 Redux 状态。
- MobX:MobX 是一个简单易用、可扩展的状态管理库,它支持 React Hooks。MobX 允许你通过可观察的对象和数组来管理状态,并使用 Hooks 在 React 组件中访问这些状态。
- Zustand:Zustand 是一个轻量级、快速且灵活的状态管理库,专为 React Hooks 设计。它提供了一个简单的 API 来创建和管理全局状态,并允许你在 React 组件中通过 Hooks 访问这些状态。
- Jotai:Jotai 是一个基于 React Hooks 的简单状态管理库。它提供了一个原子化的状态管理机制,允许你在 React 组件中轻松地定义、读取和更新状态。
- Valtio:Valtio 是另一个基于 React Hooks 的状态管理库,它提供了一个代理对象来管理状态。你可以像操作普通对象一样操作代理对象,而 Valtio 会自动处理状态的更新和渲染。
这些库都提供了与 React Hooks 集成的状态管理解决方案,可以帮助你更轻松地构建复杂的 React 应用程序。你可以根据自己的需求和偏好选择合适的库来使用。
20. 对React Hooks的理解
hooks的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能,在我们日常使用中,使用hooks能够解决大多数问题,并且还拥有代码复用机制,因此优先考虑hooks
解决老的函数式组件在React Hook出现之前,函数式组件(也称为无状态组件)的主要特点与优缺点如下:
早期函数式组件优点:
**简洁性:**函数式组件代码结构简单,易于阅读和理解,因为它仅负责接收props并基于props返回JSX元素,不涉及复杂的生命周期方法和状态管理。
**效率:**由于没有内部状态和生命周期方法,函数式组件在每次props改变时都会重新渲染,而这种简单的渲染方式往往更快,减少了不必要的计算和DOM操作。
**易测试:**由于它们是纯函数,不依赖外部状态或上下文,因此单元测试更加容易和可靠。
**记忆化:**React在某些情况下能够利用PureComponent或shouldComponentUpdate优化,减少不必要的渲染,即使对于无状态组件。
函数式组件缺点:
无状态:最大的限制在于它们不能拥有自身的state,所有数据必须由父组件通过props传递,难以实现局部状态管理。
无生命周期方法:这意味着无法在组件挂载、更新、卸载等阶段执行自定义操作,比如数据获取、订阅、清理等。
逻辑复用困难:若需复用包含副作用或状态相关的逻辑,往往需要借助高阶组件(HOC)或Render Props等模式,这会导致组件层级过深,代码组织不够直观。
Hooks 是 useState、useEffect、useMemo 等 hook方法的总称,提供了一种在函数组件中实现状态逻辑、生命周期方法、副作用处理以及其他各种功能的方法,使得函数组件也能拥有原本只有类组件才能拥有的能力。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
至于为什么引入hook,官方给出的动机是解决长时间使用和维护react过程中常遇到的问题,例如:
【状态复用困难】难以重用和共享组件中的与状态相关的逻辑
【可维护性差】逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面
【this 心智负担高】类组件中的this增加学习成本,类组件在基于现有工具的优化上存在些许问题
【函数组件太弱】由于业务变动,函数组件不得不改为类组件等等
为函数式组件赋能
在以前,函数组件也被称为无状态的组件,只负责渲染的一些工作
因此,现在的函数组件也可以是有状态的组件,内部也可以维护自身的状态以及做一些逻辑方面的处理
在React Hooks推出之前,函数式组件非常适合于那些只需要根据传入props进行渲染的简单场景,但对于复杂的交互逻辑和状态管理,开发者不得不转向类组件或采用间接的方式来弥补这些不足。React Hooks的引入极大地增强了函数式组件的能力,使得它们既能保持简洁又能拥有状态管理和生命周期功能,有效地解决了上述缺点。
21. hook 模拟生命周期
|-----------------------------------------------|--------------------------------------------------------------------------------|
| 类组件生命周期方法 | 对应的 Hooks 功能 |
| constructor
| N/A(直接在函数组件中初始化状态即可,如使用 useState
初始化状态) |
| componentDidMount
| useEffect(fn, [])
(传入一个空数组作为依赖项,表示在组件挂载后执行一次) |
| componentDidUpdate(prevProps, prevState)
| useEffect(fn, [dependencies])
(传入依赖项数组,当这些依赖项变化时执行) |
| componentWillUnmount
| useEffect
返回的清理函数(在组件卸载前执行清理操作) |
| shouldComponentUpdate(nextProps, nextState)
| React.memo
(对于函数组件,用于优化不必要的渲染) 或者在 useEffect
的依赖数组中精确定义需要监听的变化 |
| getDerivedStateFromProps
| 避免在函数组件中使用派生状态。推荐使用 useState
或 useReducer
,并在 useEffect
中根据props更新state。 |
补充说明:
- 对于
componentDidUpdate
,React Hooks并没有直接对应的Hook来模拟,但可以通过在useEffect
的依赖数组中声明需要观察的props或state变量来实现类似的效果。 - 若要模拟类组件生命周期中的状态更新逻辑,可以结合
useState
、useReducer
以及useEffect
等Hooks来实现。
22. Hooks需要注意的问题
(1)不要在循环,条件或嵌套函数中调用Hook,必须始终在 React函数的顶层使用Hook
(2)使用useState时候,使用push,pop,splice等直接更改数组对象的坑
(3)useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
(4)善用useCallback
22.1. 为什么不能再 if、循环、函数中使用
React的函数式组件每次渲染都会有新的状态生成,而且每一次渲染都有一个状态序列,如果在if里面调用,就可能导致在某些情况下的渲染,状态序列会丢失,序列就会乱,导致异常。
React hook 底层是基于链表实现的,每次组件被 render 的时候都会按顺序执行所有 hooks,而且正因为底层是链表,每个 hook 的 next 是指向下一个 hook 的,所以我们写代码是不能在不同的 hooks 调用里使用条件判断/函数嵌套之类的 ,因为这会导致执行顺序不对,从而出错
那讲一讲为什么Hooks要保证这个状态序列?
我们使用useState声明状态的时候,只给状态定义了初始值,并没有给状态加key
从写法上看出来,类组件的状态是对多形式存储的,每个状态都有自己的key和value.
而函数式组件中,useState方法只接受了状态初始值作为参数,没有key,所以函数值组件不能以对象的形式存储,只能以线性表的形式存储,比如说数组和链表。实际上Hooks状态就是用链表来存储的
但无论是数组还是链表,都需要保持顺序,这样才能使每次渲染的序列对应得上
那为什么循环里面也不能调用?
因为循环中也有条件语句判断
那Vue3的setup也是函数式组件,为什么就不存在这种问题?
虽然都是函数式组件,但还是有区别。
vue3的渲染函数只执行1次:在组件第一次渲染时,就会执行一次setup函数,并且创建响应式对象,然后使用闭包进行缓存。后续组件渲染会直接使用缓存渲染函数,不需要重新执行setup函数,所以Vue的状态始终就在一个数据结构里面,顺序不会发生改变。
React返回的是JSX模板,本质上是一个对象,不是函数,无法形成闭包重复使用
而React之所以不用闭包,是为了实现并发模式(每一次更新的状态都要保存下来,中断后可以恢复)。
23. useState实现原理
以下是useState
在React中的底层实现原理的大致步骤概述:
- 注册状态 :
当React在渲染函数组件的过程中遇到useState(initialState)
时,它会在组件实例的内部创建一个新的状态槽(slot),并将initialState
作为初始值存入该槽位。由于函数组件没有实例的概念,React通过Fiber节点来模拟实例行为,所以实际上是给对应的Fiber节点添加了状态。 - 状态钩子的创建 :
React会返回一个状态对------当前状态值和更新函数。状态值就是从状态槽中读取的初始值;更新函数则是由React自动生成的一个闭包函数,它能够找到正确的状态槽并更新其值。 - 状态更新 :
当调用setState(newState)
时,React不会立即修改状态,而是创建一个更新任务,将其加入到一个优先级队列中。这是因为React采用了异步更新策略,以批量处理多个状态更新,提高性能。 - 调度过程 :
调度器(Reconciler/Renderer)会按照一定的优先级顺序处理队列中的更新任务。当轮到处理某个组件的状态更新时,调度器会查看新旧状态是否不同,如果不同,则标记该组件及其子孙组件为需要重新渲染。 - 渲染阶段 :
在渲染阶段,React会重新执行函数组件体以获取最新的UI描述(虚拟DOM)。此时,useState
会返回上一次渲染时的状态值,但如果在本次渲染周期中有待处理的更新,则会返回已更新的状态值。 - 差异比较与DOM更新 :
React使用虚拟DOM diff算法找出前后两次渲染之间的差异,然后只对实际DOM进行必要的更新操作。 - 闭包的作用 :
setState
函数通过闭包绑定到了正确的组件实例(即Fiber节点),这样在任何地方调用这个函数都能够准确地修改相应组件的状态,而不影响其他组件。
综上所述,useState
在React的底层是通过一套复杂但高效的系统实现的,包括状态槽的管理、更新任务的调度、异步更新机制、以及闭包的应用等技术手段,从而使得函数组件也能具备维护自身状态的能力。
fiber 角度下的 useState
在Fiber架构下,React为每个函数组件实例(即Fiber节点)内部维护了一个状态对象。当调用 useState(initialState) 时:
- 状态创建: React为当前渲染的函数组件Fiber节点创建一个状态槽,并将传入的 initialState 保存在这个槽位中。
- 状态访问与更新: useState 返回一对值:当前状态和一个更新状态的函数。这个更新函数通过闭包捕获了与当前Fiber节点关联的状态槽,因此可以准确地定位和更新状态。
- 调度与更新: 当调用 setState(newState) 时,React并不会立即更新状态和重新渲染组件,而是将更新操作添加到一个优先级队列中。React Fiber调度器会依据任务优先级决定何时执行这些更新。在重新渲染过程中,Fiber节点的状态会根据 useState 返回的新状态值进行更新。
- 记忆化与回收: React在处理Fiber树时,会利用记忆化机制,确保状态和副作用函数与正确的Fiber节点关联,并在组件卸载时正确地清理资源。
因此,useState 能够在函数组件中管理状态,得益于React Fiber架构对其内部状态管理机制的改进和增强。Fiber不仅负责调度和更新组件,还提供了在函数组件中存储和更新状态的能力。
24. useEffect 实现
useEffect
是React Hooks中的一个重要Hook,用于处理副作用、订阅数据源和执行DOM操作等。以下是useEffect
在React中的底层实现原理的大致步骤:
- 注册效应函数 :
当React在渲染过程中遇到useEffect(callback, deps)
时,它会捕获当前作用域下的callback
函数(副作用函数)以及依赖数组deps
。每次组件渲染完毕后,React都会检查是否存在已经注册过的效应函数,并且比较新的依赖项列表与旧的是否发生变化。 - 首次渲染时执行 :
在组件的首次渲染完成后,React会立刻执行useEffect
中传入的回调函数,不论依赖项数组是否为空。这相当于"挂载"阶段执行的生命周期方法如componentDidMount
。 - 依赖变化时执行 :
在后续的渲染周期中,React会对比新旧依赖项数组。如果数组内容发生了变化(例如引用变更,或者数组元素内容有变),React就会认为依赖关系有所更新,并触发新的副作用函数执行。这对应于类组件中的componentDidUpdate
生命周期方法。 - 清理工作 :
如果在副作用函数中返回了一个清理函数(比如取消定时器、解绑事件监听器等),那么在下一次副作用函数执行前,React会确保先调用上一次的清理函数,保证资源的有效释放。这相当于类组件中的componentWillUnmount
生命周期方法。 - 优化选项 :
可以通过传递空数组[]
作为useEffect
的第二个参数,告诉React这个副作用函数只在组件挂载和卸载时运行,不依赖任何props或state的变化。这有助于避免不必要的重复执行。
useEffect
Hook的工作机制基于渲染周期后的回调执行和依赖项变化的追踪,利用这种机制,React能够在函数组件中有效地管理和调度各种副作用操作,保持组件逻辑清晰的同时,也实现了与类组件生命周期方法类似的灵活性和控制力。
- 任务调度: 在 React Fiber 架构下,所有的更新任务(包括 useEffect 回调函数的执行)都被视为可中断和可恢复的工作单元。当组件渲染完成后,React Fiber 会根据优先级调度执行 useEffect 回调函数。这意味着即使在组件树的深度遍历过程中,也可以暂停当前的工作并转而去处理更高优先级的任务,如处理浏览器事件或进行渲染。
- 依赖收集与更新: 当 useEffect 被调用时,React 会记录下其依赖项数组。在后续的渲染周期中,React 通过 Fiber 结构跟踪和比较依赖项的变化,决定是否需要重新执行 useEffect 回调函数。Fiber 节点存储了组件的状态信息和副作用信息,这是决定何时执行 useEffect 的关键。
- 清理工作与调度: useEffect 回调函数可以返回一个清理函数,用于在组件卸载或 useEffect 更新前执行清理操作。React Fiber 在调度任务时会管理这些清理函数的执行,确保资源的释放和状态的一致性。
- 并发模式支持: Fiber 架构支持 React 的并发模式(Concurrent Mode),在这种模式下,useEffect 会更加智能地配合调度系统,适应可暂停、可恢复的渲染过程,使得多个 useEffect 可以并发执行,从而提高应用的整体性能和响应性。
25. useEffect 与 useLayoutEffect 的区别
useEffect
和 useLayoutEffect
都是 React 中的 Hook,用于处理副作用,例如订阅事件、执行异步操作、更新DOM属性等。它们的主要区别在于执行时机和浏览器渲染流水线的影响:
- 执行时机:
-
- useEffect**:在所有的 DOM 变更已经完成并同步到浏览器界面之后,浏览器的事件循环下一次迭代中异步执行。这意味着在** useEffect****里的代码运行时,用户已经可以看到渲染的结果。
useLayoutEffect
:在所有的 DOM 变更完成后立即同步执行 ,紧接在 DOM 更新之前。这意味着useLayoutEffect
内部的代码更改会影响到当前帧的渲染结果,因此在它执行期间,浏览器会暂停布局和绘制工作,等待同步代码执行完毕。
-
-
- 案例:树状组件渲染完后修补对齐线
- div 宽高改变时
- 使用 useLayoutEffect 要特别谨慎,因为如果执行的副作用中有耗时操作,可能会导致页面的"卡顿"。
-
- 对渲染性能的影响:
-
useEffect
:由于其异步执行的特性,不会阻塞浏览器的渲染流程,所以不会引起明显的卡顿或布局闪烁。useLayoutEffect
:由于其同步执行,若在其回调函数中修改了样式或其他影响布局的属性,将会强制浏览器重新计算布局,这可能造成性能上的影响,特别是在循环或大量元素上进行同步更新时。
- 何时使用:
-
useEffect
:适用于大部分副作用场景,尤其是涉及网络请求、设置定时器、订阅事件等不直接影响布局的任务。useLayoutEffect
:在那些需要在渲染后立即操作DOM并确保DOM更新与之同步的场景中使用,比如某些动画效果、依赖DOM尺寸变动的操作,或者为了防止UI闪烁而需要在绘制前调整样式的情况。
总结来说,useEffect
更适合非阻塞渲染管线的异步操作,而 useLayoutEffect
则用于那些需要严格保证在渲染完成前执行,并可能影响布局的操作。在大部分情况下,优先考虑使用 useEffect
;只有在特定场景下,当确实需要同步更新并避免布局抖动时,才应该选择 useLayoutEffect
。
useLayoutEffect 的出现是为了解决 useEffect 的页面闪烁问题。useEffect 是在组件挂载后异步执行的,并且执行事件会更加往后,如果我们在 useEffect 里面改变 state 状态,那么页面会出现闪烁(state 可见性变化导致的)。而 useLayoutEffect 是在渲染之前同步执行的,在这里执行修改 DOM 相关操作,就会避免页面闪烁的情况。
26. 在 hook 中如何模componentwillMount
componentWillMount 是 React 16 及更早版本中类组件的一个生命周期方法。在 React 组件的整个生命周期中,componentWillMount 在以下时间点执行:
- 组件初次挂载前: 在组件即将第一次被插入到 DOM 中之前,componentWillMount 生命周期方法会被调用。这意味着它将在组件渲染(render)方法执行前调用,而且是在任何 DOM 操作或实际渲染发生之前。注意:componentWillMount 不会在组件更新时再次调用,只在组件生命周期的初始阶段执行一次。
**废弃原因:**在类组件生命周期中不再是最佳实践,特别是因为它在服务器端渲染(SSR)和客户端渲染之间存在不一致行为,以及在某些情况下不利于并发渲染。React官方推荐使用替代的方法来模拟或迁移原有componentWillMount的功能。
由于 React 16.3 版本引入了新的生命周期方法,并宣布弃用 componentWillMount,在 React 17 及更高版本中,这个生命周期方法已被彻底移除,推荐使用 UNSAFE_componentWillMount(带前缀 UNSAFE_,警示开发者谨慎使用)或者在构造函数(constructor)中初始化组件状态,而在组件挂载后需要执行的操作可以放在 componentDidMount 生命周期方法中执行。如果需要在渲染前执行同步操作并且支持 React 16 以下版本,可以继续使用 UNSAFE_componentWillMount,但请注意,React 官方强烈建议避免在这些方法中执行副作用操作,因为它们不会遵循严格的异步更新模式。在 React 16.3 及以上版本中,推荐使用 getDerivedStateFromProps(在某些场景下)或 constructor 初始化数据,以及 componentDidMount 来执行副作用操作。
const useComponentWillMount = (cb) => {
const willMount = useRef(true)
if (willMount.current) cb()
willMount.current = false
}
值得注意的是,这并不能完全等同于 componentWillMount,因为存在代码顺序带来的问题,比如:
console.log('111')
useComponentWillMount(() => console.log('222'))
// output:
// 111
// 222
27. context 中provider是干什么的
在React中,Provider 通常是指与React的Context API相关的特定组件。Context API是React提供的一个功能,允许你在组件树中传递数据,而不必手动逐层传递每一个层级。这特别有用,尤其是当你需要在深层嵌套的组件之间共享某些状态时。
当你使用React.createContext()创建一个Context对象时,你会得到两个组件:Provider 和 Consumer。
- Provider:这是一个React组件,它允许你向下游组件传递数据或状态。你可以将数据作为props传递给Provider,然后,在Provider下面的任何组件(无论嵌套有多深)都可以通过相应的Consumer或使用useContext Hook来访问这些数据。
- Consumer:这也是一个React组件,它允许你从Provider中"消费"数据。在函数组件中,更常见的是使用useContext Hook来访问Context中的数据,而不是直接使用Consumer组件。
28. context 是如何实现让 view 刷新
React 中 Context 实现视图刷新的底层原理涉及到 React 的调和算法(Reconciliation Algorithm)和 React 组件的更新机制。当 Context 中的数据发生变化时,React 会触发组件树的重新渲染,从而更新视图。下面是大致的实现原理:
- Context 的 Provider 提供数据: 在组件树中,通过 Context 的 Provider 提供数据。Provider 组件在其内部维护着一个数据源(通常是一个 JavaScript 对象),当数据发生变化时,Provider 会通知 React 的调度器。
- Consumer 订阅数据变化: 在组件树中,使用 Context 的 Consumer 或者 useContext hook 来订阅数据变化。这些组件会将自身标记为依赖于 Context 数据的组件。
- 数据变化触发重新渲染: 当 Context 中的数据发生变化时,Provider 会通知 React 的调度器。调度器会触发更新流程,首先会执行 render 阶段,调用所有依赖于 Context 数据的组件的 render 函数。
- 比较更新前后的虚拟 DOM 树: React 使用虚拟 DOM 树来描述 UI 结构,render 阶段会生成更新前后的两棵虚拟 DOM 树。React 会比较这两棵树的差异,找出需要更新的部分。
- 更新实际的 DOM: 在 commit 阶段,React 根据前面生成的更新信息来实际更新 DOM。只更新需要变化的部分,而不是整体重新渲染整个页面。
- 触发生命周期钩子和副作用: 在 commit 阶段,React 会触发生命周期钩子(如 componentDidUpdate)以及一些副作用(如 useEffect 中的回调函数)。
通过这样的流程,当 Context 中的数据发生变化时,React 会实现只更新受影响的组件,而不是整体重新渲染整个页面。这样就可以实现高效的视图更新。
29. 不要滥用useContext?
useContext
是React提供的一个Hook,它允许组件无需通过props层层传递就能直接访问Context中的数据。然而,在使用过程中需要注意适度,避免滥用的原因主要有以下几点:
- 过度耦合 :过多地使用
useContext
可能导致各个组件与全局状态高度耦合。即使这些组件原本不需要关心全局状态,也可能因为直接消费Context而导致维护性和可读性降低,同时也增加了组件重构和复用的难度。 - 难以追踪依赖 :当多个组件都通过
useContext
访问同一个Context时,一旦这个全局状态发生改变,所有依赖它的组件都有可能被迫重新渲染,即便它们并不关心这次状态变化的具体内容。这可能导致不必要的性能损失。 - 架构复杂度:随着应用规模扩大,如果随意创建和使用Context,会导致全局状态管理变得混乱且难以维护。合理的做法通常是集中管理和划分不同作用域的Context,而不是处处使用。
- 不利于逻辑解耦:过度依赖全局上下文可能导致业务逻辑难以拆分和独立管理。理想的做法是将相关性强的状态和逻辑封装在自包含的组件或Redux等状态管理库中,保持每个模块的内聚性。
因此,在实际开发中,应当审慎地使用 useContext
,只在必要的全局共享状态场景下使用,并结合其他最佳实践(如Redux、 Recoil、MobX等)以及React的其他特性(如 useReducer
、useState
等),合理规划和组织应用的状态管理结构。
- componentwillMount
30. setState 到页面重新渲染经历了什么
setState
方法在 React 中调用后,直到页面重新渲染之间经历了一系列的步骤。以下是这个过程的详细解释:
- 状态合并:
-
- 当调用
setState
时,React 不会立即改变组件的状态,而是将传入的新状态对象与当前状态进行合并。合并通常是浅层合并,这意味着如果新状态包含深层次的对象属性更改,那么只有第一层属性会合并,深层对象的更改可能不会生效,除非显式替换整个深层对象。
- 当调用
- 异步处理:
-
- React 将
setState
操作视为异步的,特别是当在事件处理器或生命周期方法中调用时。这意味着调用setState
并不会立即导致重新渲染。实际上,React 可能会把多个连续的setState
调用合并成一个,以减少不必要的渲染次数。
- React 将
- 批处理:
-
- React 有一个批处理过程,它可以收集在一个事件循环 tick 内的多个
setState
调用,然后一次性去更新状态。这样有助于在高并发更新时优化性能。
- React 有一个批处理过程,它可以收集在一个事件循环 tick 内的多个
- 状态更新调度:
-
- React 会在微任务队列中安排状态更新的任务,这通常发生在事件处理结束、生命周期钩子调用之后,或者在某些异步操作(如网络请求完成)之后。
- 组件生命周期方法:
-
- 在状态更新前,React 可能会触发
componentWillReceiveProps
(在旧版 React 中)或getDerivedStateFromProps
(在 React 16.3+ 版本中),接着是shouldComponentUpdate
(如果有实现的话)来决定是否需要继续渲染。
- 在状态更新前,React 可能会触发
- 重新渲染决策:
-
- 如果根据新的 props 或 state,React 确定组件需要重新渲染,它会进入
render
方法来生成新的虚拟 DOM 树。
- 如果根据新的 props 或 state,React 确定组件需要重新渲染,它会进入
- 虚拟 DOM 比较:
-
- 新的虚拟 DOM 与旧的虚拟 DOM 进行对比,找出最小化的差异(diffing算法),确定哪些实际 DOM 节点需要更新、添加或删除。
- DOM 更新:
-
- 根据 diff 结果,React 更新实际的 DOM 树,仅做必要的改动。
- 生命周期方法调用:
-
- 在 DOM 更新完成后,React 会触发
getSnapshotBeforeUpdate
(如果有的话)来抓取更新前后的状态差异,随后触发componentDidUpdate
生命周期方法,这时组件已经反映出了新的状态和 UI。
- 在 DOM 更新完成后,React 会触发
31. React.PureComponent 和React.memo
React.PureComponent
应用场景:React.PureComponent 是一个内置的类组件,它继承自 React.Component,并且重写了 shouldComponentUpdate 生命周期方法,用于决定何时需要更新组件。
原理:shouldComponentUpdate(nextProps, nextState) 方法默认执行浅比较(shallow comparison):比较当前组件的 props 和 state 与即将接收到的新 props 和 state 是否相等。如果两者完全相等(浅比较下引用不变或值类型相等),则返回 false,阻止组件进行重新渲染;否则返回 true,组件将会进行正常渲染。
注意:浅比较意味着对于复杂的数据结构(如嵌套对象或数组),如果内部的值发生了变化但是引用地址没变,PureComponent 无法检测到这些变化,会导致组件不会更新。
React.memo
应用场景:React.memo 是一个高阶组件(HOC),专门用于优化函数组件的性能,相当于函数组件版本的 PureComponent。
原理:React.memo 会包裹提供的函数组件,并为其添加一层优化机制。当组件的 props 发生变化时,React.memo 也会进行类似的浅比较。
默认情况下,React.memo 会比较前后两次传递给组件的 props 对象是否相等,如果不相等,则重新渲染组件;如果相等,则跳过渲染。
自定义比较函数:React.memo 允许传入第二个参数作为自定义的比较函数,这个函数接收新旧两个props对象作为参数,由开发者自行决定是否应该触发组件重新渲染。
React.PureComponent 适用于类组件,而 React.memo 适用于函数组件,它们通过浅比较来决定组件是否需要重新渲染,从而达到性能优化的目的 。但请注意,这两种方式都不适用于含有深层嵌套数据结构或依赖内部状态变更的组件优化。在这种情况下,应手动进行深比较或者使用更高级别的优化手段。
32. 什么可能触发 React 重新渲染
你是对的,我可以将它们合并为一个类别,因为它们实际上是相互关联的。让我们重新组织一下:
- Props 或 State 的变化以及调用setState():
-
- 当组件的props或state发生变化时,或者调用组件的
setState()
方法时,React会触发重新渲染。这两种情况都是由于组件的数据发生了变化,导致React需要重新计算并更新UI。
- 当组件的props或state发生变化时,或者调用组件的
- 父组件重新渲染:
-
- 如果父组件重新渲染,那么它的子组件也会重新渲染。这是因为React会递归地重新渲染整个组件树。
- Context 的值变化:
-
- 如果使用了React的Context API,并且Context的值发生了变化,订阅了该Context的组件也会触发重新渲染。
- Hooks的调用:
-
- 使用Hooks的函数组件中,如果使用了
useState
、useReducer
、useContext
等钩子,并且它们的返回值发生了变化,那么组件会重新渲染。
- 使用Hooks的函数组件中,如果使用了
- Force Update:
-
- 调用组件的
forceUpdate()
方法会强制触发重新渲染,不管props或state是否发生变化。
- 调用组件的
- 父组件的shouldComponentUpdate()返回true:
-
- 如果一个父组件实现了
shouldComponentUpdate()
方法并返回了true,那么即使子组件的props没有变化,也会触发子组件的重新渲染。
- 如果一个父组件实现了
- 使用React.memo()或PureComponent进行优化:
-
- 如果组件是使用
React.memo()
或继承自React.PureComponent
,那么React会对组件进行浅比较以确定是否重新渲染。这可以减少不必要的渲染。
- 如果组件是使用
这样,我们就将与组件重新渲染相关的情况更加明确地归类了。
33. React.lazy和Suspense的底层原理
React.lazy
-
动态导入(Dynamic Import): React.lazy函数接受一个返回动态导入语句的函数作为参数。动态导入是ES模块语法的一个特性,允许在运行时异步加载模块。例如:
import React, { lazy } from 'react';
const AsyncComponent = lazy(() => import('./AsyncComponent'));
这里,import('./AsyncComponent')返回一个Promise,当组件需要渲染时,Promise会异步解析并加载对应的模块。React.lazy将这个Promise包装成一个可渲染的React组件,当Promise resolve时,解析得到的实际组件将被用来替换占位符。
- 异步组件封装 : React.lazy 返回一个特殊的代理组件,这个组件内部维护了对动态导入Promise的引用。当React尝试渲染这个代理组件时,它会检查Promise是否已经resolve。如果已经resolve,就渲染解析得到的实际组件;如果Promise还在pending状态,则暂停渲染流程,等待Promise resolve。
Suspense
主要解决一下 2 个问题
- 组件动态加载(懒加载)的用户体验优化 : 在大型单页应用中,为了提高首屏加载速度和总体性能,常常采用组件懒加载(或称为代码分割)策略,即只在需要时加载特定组件及其依赖。然而,传统的懒加载实现可能导致界面在组件加载期间出现空白或闪烁,影响用户体验 。Suspense通过提供一种统一且优雅的方式,使得组件在加载过程中可以显示自定义的占位内容(如加载指示器或骨架屏),而不是空白或突兀的跳转。当组件加载完成时,Suspense会平滑地过渡到实际内容,极大地提升了用户体验。
- 异步数据加载的协调与错误处理 : 不仅限于组件加载,Suspense 还旨在解决异步数据加载(如API请求)与界面渲染的协调问题。在传统的实现中,开发者需要手动处理异步请求的状态(如pending、fulfilled、rejected),并在适当的时候更新界面。这往往涉及大量的状态管理和错误处理代码,容易出错且难以维护。Suspense允许开发者在数据加载过程中显示统一的占位内容,当数据加载成功或失败时,自动切换到相应的内容或错误提示。它简化了异步数据加载的编程模型,使得数据获取与界面更新更加解耦,提高了代码的清晰度和可维护性。
Suspense:原理
- 使用React.lazy创建动态加载的组件,并通过Suspense组件设置异步边界和占位内容。
- 在首次渲染时,React识别到异步加载的组件,暂停当前渲染路径,显示fallback内容。
- 后台模块加载完成,Promise resolve,React调度器在微任务阶段注意到加载完成事件。
- React重新调度受影响的Fiber节点,渲染实际的异步组件,Suspense切换到实际内容。
- 若加载过程中发生错误,Suspense捕获错误,显示错误状态占位内容。
在 React 18 中,支持了服务端 Suspense,并且使用并发渲染特性扩展了其功能。
React 18 中的 Suspense 在与 Transition API 结合时效果最好。如果你在 Transition 期间挂起,React 不会让已显示的内容被后备方案取代。相反,React 会延迟渲染,直到有足够的数据,以防止出现加载状态错误。
协同工作原理
当使用React.lazy和Suspense配合时,Suspense组件负责监控其子树中由React.lazy创建的动态组件的加载状态。当动态组件的加载Promise处于pending状态时,Suspense会显示fallback内容;当Promise resolve,实际组件可用时,Suspense会替换掉fallback内容,渲染出实际组件。如果加载过程中出现错误,Suspense同样会展示fallback内容中的错误状态。
底层实现上,React通过调度算法和组件更新机制来协调这些异步加载和状态切换。它使用Fiber架构来追踪和管理组件的更新状态,包括异步加载的进度和挂起(suspended)状态。当检测到一个组件需要异步加载时,React会将其标记为suspended,并安排适当的重新渲染。一旦加载完成,React会重新调度这些suspended组件的渲染工作,完成界面的更新。
总结来说,React.lazy和Suspense底层原理涉及动态导入、异步组件封装、渲染暂停与恢复、异步边界以及错误边界等概念。它们通过React的调度机制和Fiber架构紧密协作,实现组件的动态加载、占位展示和错误处理,从而优化应用的加载性能和用户体验。
34. React-Router的实现原理是什么
- 基于 hash 的路由:通过监听hashchange事件,感知 hash 的变化
-
- 改变 hash 可以直接通过 location.hash=xxx
- 基于 H5 history 路由:
-
- 改变 url 可以通过 history.pushState 和 resplaceState 等,会将URL压入堆栈,同时能够应用 history.go() 等 API
- 监听 url 的变化可以通过自定义事件触发实现
基于 Hash 的路由(Hash-Based Routing)
优点:
- 兼容性良好:几乎在所有浏览器中都可以使用,即使是老旧的浏览器,包括不支持HTML5 History API的浏览器。
- 无需服务器配置:由于Hash改变并不会触发页面刷新,所以浏览器不会向服务器发送新的请求,这就意味着无需对服务器进行任何特定配置以支持前端路由。
缺点:
- URL 不够美观:URL中包含哈希符号(#),例如 http://example.com/#/about,这在一定程度上影响了用户体验和SEO(搜索引擎优化)。
- 浏览器历史记录管理受限:每个Hash变化都被浏览器认为是同一页面的不同状态,所以在浏览器的历史记录栈中会产生大量条目,不利于用户导航回退。
- 无法进行真正的网页跳转:由于hash变化不会触发页面加载,所以不能通过改变hash实现真正的页面跳转和前进后退功能,需要借助onhashchange事件自行管理。
基于 HTML5 History 的路由(History-Based Routing)
优点:
- 美观的URL:能够提供更直观、更友好的URL,例如 http://example.com/about,这种URL看起来更像是传统web应用的链接。
- 更好的用户体验:可以利用浏览器的前进、后退按钮进行导航,且不会在历史记录中产生过多条目。
- 有利于SEO:对于需要搜索引擎优化的应用,History API可以让每个路由映射到唯一的真实URL,便于搜索引擎索引和理解页面结构。
缺点:
- 兼容性限制:需要现代浏览器支持HTML5 History API,老版本浏览器(如IE9及以下版本)可能不支持。
- 需要服务器支持:因为切换路由时会改变URL路径,当用户直接访问这些路径或刷新页面时,服务器需要配置重定向或服务端渲染以避免404错误。
- 实现相对复杂:相较于基于Hash的路由,History API的实现稍微复杂一些,需要更多的代码来处理浏览器的路由变化和页面跳转。
35. react 中怎么实现重定向
重定向(Redirect)在Web开发中指的是将用户的请求从一个URL转向另一个URL的行为,通常是当用户访问某个页面时,服务器或前端框架决定将用户引导至另一个页面。
- 使用 <Redirect> 组件(适用于React Router v4及更高版本): 在一个路由配置或组件内部使用<Redirect>组件,当组件渲染时,它会立刻执行重定向。
- 编程式重定向: 通过history对象或者useHistory Hook,可以在组件内部进行动态重定向。
- 在路由配置中实现重定向: 在设置路由规则时,可以直接配置重定向规则
36. react-router 里的 Link 标签和 a 标签的区别
HTML <a> 标签:
- 原生HTML元素,点击后会触发浏览器的默认行为,即发送一个新的HTTP请求到指定的URL,页面会发生整页刷新。
- 通过 href 属性指定目标URL。
React Router <Link> 标签:
- 是React Router提供的一个组件,专为SPA(Single Page Applications)设计。
- 点击后不会导致整页刷新,而是通过React Router的内部机制来改变当前应用的状态,只重新渲染需要更新的部分,实现页面局部刷新。
- 通过 to 属性指定目标路由,而不是URL,可以是绝对路径或相对路径。
- <Link> 标签同样支持激活样式(activeClassName、activeStyle)等功能,可根据当前路由是否匹配来添加特定样式。
- a 标签用于传统的、页面间全量加载的导航。
- Link 标签则是为React Router中的路由导航设计,用于在SPA中实现无刷新的页面跳转,能够更好地整合到React应用的客户端路由系统中。
37. React-Router 4的Switch有什么用?
React Router 4 中的 Switch 组件用于渲染一组 Route 组件,它会遍历其子 Route 组件并依次检查它们是否与当前的location匹配。重点在于 ,Switch 会确保只渲染第一个匹配路径的 Route,一旦找到匹配的 Route,它会停止遍历剩余的 Route 子组件。
这样做的好处是可以避免在多个路径同时匹配时,多个组件同时被渲染的情况,确保任何时候路由匹配时只有一个组件是活跃并渲染出来的。这对于构建更有序和明确的路由逻辑非常有用,尤其是在有嵌套路由或多分支路由的情况下,能够确保页面展现的正确性和一致性
38. Redux原理
**核心原理:**Redux 的核心原理是通过一个单一的、不可变的状态树来管理整个应用程序的状态,并通过纯函数来处理状态的更新,从而实现可预测、易于理解和可维护的应用程序状态管理。
工作步骤:
action 被发出(dispatched)和到达 reducer 处理这两个阶段之间是Redux 中间件(Middleware)扩展机制。中间件可以看作是对 Redux dispatch 流程的一种拦截器,它允许开发者在 action 传播的过程中执行额外的操作,例如日志记录、异步处理、事务控制、取消操作、异常处理等。
中间件是纯函数reducer 之前的操作:
中间件在Redux中的角色更像是一个拦截器,它并不直接修改或替换Reducer,而是拦截每一次Action的分发,对Action进行加工处理,然后可以选择是否将处理后的Action传递给下一个中间件或者直接传递给Reducer。因此,中间件能够在不影响Reducer纯粹性的前提下,增强了Redux在处理异步逻辑和扩展功能方面的灵活性。
单一数据源,单向数据流
- 单一状态树(Single Source of Truth): Redux规定应用的所有状态都被集中储存在一个单一的对象树(state tree)中,这个状态树只能通过触发action来修改。
- Action: (对象,描述对状态的操作)Action是改变状态的唯一方式。它是应用中发送的信息,是一个描述发生了什么的普通对象。每个action都有一个type属性,用来标识这次操作的类型。action 用来操作 reducer
- Reducer:(输入变跟和最新的 state 返回新的 state) Reducer是纯函数,它接收当前的state和一个action,根据action的类型和内容返回新的state。Reducer不能直接修改传入的state,而是需要创建一个新的state对象。Redux应用中的所有reducer组成一个大的reducer,通常使用combineReducers函数来组合。
- Store: Store是Redux应用的核心,它负责保存应用的状态(state tree),并且提供了getState()方法获取当前状态,dispatch(action)方法来分发action到所有的reducer,以及subscribe(listener)方法来注册监听状态变化的回调函数。
- Dispatching Actions: 组件不直接修改状态,而是通过调用store.dispatch(action)方法发起一个action,这个action会被传递到reducer进行处理。
- Subscribing to Changes: 组件可以通过订阅store的变更来更新自己。当store的状态发生变化时,所有订阅的组件都会收到通知,并根据新的state重新渲染。
- 中间件(Middleware) : Redux的中间件提供了一种插件式的机制,可以介入到action被dispatch到reducer处理的中间环节,进行额外的处理,如日志记录、异步操作处理(如Redux Thunk或Redux Saga)等。
主要解决什么问题:
1、组件间通信
由于connect后,各connect组件是共享store的,所以各组件可以通过store来进行数据通信,当然这里必须遵守redux的一些规范,比如遵守 view -> aciton -> reducer的改变state的路径
2、通过对象驱动组件进入生命周期
对于一个react组件来说,只能对自己的state改变驱动自己的生命周期,或者通过外部传入的props进行驱动。通过redux,可以通过store中改变的state,来驱动组件进行update
3、方便进行数据管理和切片
redux通过对store的管理和控制,可以很方便的实现页面状态的管理和切片。通过切片的操作,可以轻松的实现redo之类的操作
39. redux中间件?
Redux 中间件(Middleware)是 Redux 库中一个强大的扩展机制,它位于 action 被发出(dispatched)和到达 reducer 处理这两个阶段之间。中间件可以看作是对 Redux dispatch 流程的一种拦截器,它允许开发者在 action 传播的过程中执行额外的操作,例如日志记录、异步处理、事务控制、取消操作、异常处理等。
- redux中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数。
- 柯里化函数两端一个是 middewares,一个是store.dispatch
中间件通过链式调用的方式来组织,形成一个中间件栈。当一个 action 被 dispatch 时,它会依次经过中间件栈中的每一个中间件。每个中间件都有机会查看 action,对其进行操作(如修改、延迟 dispatch 或发起异步请求),然后决定是否将 action 传递给下一个中间件或者直接发送给 reducers 进行状态更新。
中间件的结构通常遵循一个标准的函数签名,即接受 `store` 的 `dispatch` 方法和 `getState` 方法作为参数,然后返回一个新的增强过的 `dispatch` 函数。这个新的 `dispatch` 函数会在执行原先的 dispatch 行为前后插入自定义的逻辑。
Redux 中常见的中间件如 `redux-thunk` 和 `redux-saga` 分别用于简化异步操作的处理。`redux-thunk` 允许 dispatch 一个函数而不是单纯的 action 对象,这个函数可以在运行时生成和 dispatch 多个 action。而 `redux-saga` 通过生成器函数实现复杂的异步流程控制,它可以监听 actions 并触发一系列
- Redux Thunk:
-
- 它是最基础且广泛使用的异步中间件,允许你编写可返回函数而非纯对象的 action 创建器。这样函数就能在内部进行延迟 dispatch 或异步操作,并在完成后 dispatch 真正的 action。
- Redux Promise Middleware:
-
- 这个中间件能够自动处理 promise 类型的 action,当 promise resolve 或 reject 时,它会 dispatch 相应的成功或失败 action。
- Redux Saga:
-
- 提供了一个更强大的异步流程控制机制,基于 ES6 的 Generator 函数。它允许你编写 sagas(类似事件处理器)来监听 actions 并触发副作用(如异步 API 调用),还可以组合多个复杂的异步流程。
- Redux-Observable:
-
- 基于 RxJS 的 Observable 流,提供了响应式编程的方式来处理异步逻辑。类似于 Redux Saga,但它使用 Observables 和 Epics(观察 epic 并生成新的 actions 的函数)。
- Redux Loop:
-
- 尽管不是专门针对异步操作设计,Redux Loop 提供了一种模式,让应用能够在 action 处理完毕后执行额外的副作用(包括异步操作),并且可以根据副作用的结果再次 dispatch action。
40. redux 如何驱动视图更新
Redux 中,当 state 发生变更时,视图的更新是由 React-Redux 库中的 Provider****组件和 connect****高阶组件共同协作来驱动的。
- connect 高阶组件通过 mapStateToProps 函数监听Redux Store的变化,并将新的state映射为传递给被包装组件的props。
- 当Redux Store的state更新时, connect 内部已经订阅了store的变更通知,因此它会运行 mapStateToProps 函数计算新的props。
- 如果新计算出的props与之前的props不同,则 connect 会自动触发包装组件的重新渲染(尽管不是通过调用 setState ,而是通过返回一个新的props对象给React组件)
具体流程如下:
- Store 更新 State:
当应用中通过store.dispatch(action)
分发了一个 action 之后,会触发 Redux store 中的 reducer 函数。reducer 会根据接收到的 action 生成新的 state,然后通过store.replaceReducer(newState)
方法来更新 store 中的 state。 - Store 通知订阅者:
Redux store 有一个subscribe
方法,它可以接受一个监听函数作为参数。每当 state 发生变化时,store 会自动调用所有已订阅的监听函数。在React应用中,通常在根组件包裹一层<Provider store={store}>
,这样Provider组件就会订阅store的变更。 - React-Redux 中的 Provider 组件:
Provider 组件的作用是将 Redux store 作为上下文提供给其后代组件。当 store 发生变化时,Provider 会通过React的上下文机制告知所有连接到Redux store的子组件。 - connect 高阶组件:
在React组件中,通过import { connect } from 'react-redux'
导入connect
高阶组件,它可以将React组件与Redux store连接起来。通过connect(mapStateToProps, mapDispatchToProps)(YourComponent)
的方式装饰React组件。
-
mapStateToProps
函数负责将store的一部分state映射到组件的props。mapDispatchToProps
则可以将dispatch方法注入到组件的props中,以便组件可以直接分发action。
- 视图更新:
当store的state发生改变时,mapStateToProps
返回的新props与上次渲染时相比若有变化,React会通过Virtual DOM diff算法检测到props的变化,并触发相应组件的重新渲染。这样,与Redux store连接的React组件就能自动获取到新的state,并根据新的props重新渲染视图。
总之,Redux中的state变更通过React-Redux库将store中的state映射到React组件的props中,进而通过React本身的props变更检测机制自动驱动视图的更新。
41. Context 和 redux 触发view刷新的底层原理对比
- 共同点:
-
- 都是基于 React 的单向数据流模型实现的。
- 都使用了订阅-发布模式(或者说观察者模式),在状态变化时通知相关组件进行更新。
- 区别:
-
- Context :Context 是 React 提供的一种跨层级传递数据的机制,它允许您将数据传递给组件树中的所有组件,而无需手动传递 props。当 Context 中的数据发生变化时,只有订阅了这个 Context 的组件才会被重新渲染,而不是所有组件。这是通过 React 内部的一种优化实现的,称为 "reactive context",它可以精确地确定哪些组件应该重新渲染。
- Redux:Redux 是一个独立于 React 的状态管理库,它使用单一的、不可变的状态树来管理应用程序的状态。当 Redux 中的状态发生变化时,Redux store 会通知所有订阅了状态变化的组件,并触发这些组件的重新渲染。Redux 使用了观察者模式来实现状态变化的订阅和通知。
虽然 Context 和 Redux 在状态变化时都使用了订阅-发布模式,并且都只会重新渲染订阅了状态变化的组件,但它们的实现机制略有不同。Context 是 React 提供的一种轻量级的状态管理机制,适用于一些简单的状态共享场景;而 Redux 则是一个更为强大和灵活的状态管理库,适用于复杂的状态管理和数据流控制场景。
42. react中页面重新加载时怎样保留数据?
- **Redux:**将页面的数据存储在redux中,在重新加载页面时,获取Redux中的数据;
- **data.js:**使用webpack构建的项目,可以建一个文件,data.js,将数据保存data.js中,跳转页面后获取;
- **sessionStorge:**在进入选择地址页面之前,componentWillUnMount的时候,将数据存储到sessionStorage中,每次进入页面判断sessionStorage中有没有存储的那个值,有,则读取渲染数据;没有,则说明数据是初始化的状态。返回或进入除了选择地址以外的页面,清掉存储的sessionStorage,保证下次进入是初始化的数据
- history API: History API 的
pushState
函数可以给历史记录关联一个任意的可序列化state
,所以可以在路由push
的时候将当前页面的一些信息存到state
中,下次返回到这个页面的时候就能从state
里面取出离开前的数据重新渲染。react-router 直接可以支持。这个方法适合一些需要临时存储的场景。
43. 没有看到使用react却需要引入react?
本质上来说JSX是React.createElement(component, props, ...children)
方法的语法糖。在React 17之前,如果使用了JSX,其实就是在使用React, babel
会把组件转换为 CreateElement
形式。在React 17之后,就不再需要引入,因为 babel
已经可以帮我们自动引入react。
44. 事件合成
之前多版本并存的主要问题在于React 事件系统默认的委托机制,出于性能考虑,React 只会给document挂上事件监听,DOM 事件触发后冒泡到document,React 找到对应的组件,造一个 React 事件(SyntheticEvent)出来,并按组件树模拟一遍事件冒泡(此时原生 DOM 事件早已冒出document了)。
因此,不同版本的 React 组件嵌套使用时,e.stopPropagation()无法正常工作(两个不同版本的事件系统是独立的,都到document已经太晚了
为了解决这个问题,React 17 不再往document上挂事件委托,而是挂到 DOM 容器上:
React引入了"合成事件"(SyntheticEvent)的概念,这是一种跨浏览器的事件抽象层,它模拟了W3C标准事件行为,并提供了统一的接口给开发者。
- 事件委派:
-
- React并不直接将事件处理器绑定到各个DOM元素上,而是采用事件委派的策略,将所有的事件监听器绑定到最顶层的容器元素上(在React 17之前是 document ,17及以后版本默认绑定在根DOM节点 #root 上)。
- 当任何子元素触发事件时,事件会按照浏览器的事件传播机制冒泡到顶层容器,此时React的事件监听器会捕获这些事件。
- 事件对象标准化:
-
- React创建了自己的事件对象------合成事件,当原生事件发生时,React会创建一个与原生事件相对应的 SyntheticEvent 对象,并将其传递给相应的事件处理函数。
- SyntheticEvent对象是对原生事件对象的包装,它具有与原生事件相同的接口和属性,但确保了在各种浏览器下的行为一致性。
45. 介绍一下React的patch流程
React的patch流程,即React的虚拟DOM(Virtual DOM)更新机制,主要包括以下几个步骤:
- 生成新的虚拟DOM树 :
当组件的state或props发生变化时,React会重新执行render
方法,生成新的虚拟DOM树。虚拟DOM是一种轻量级的JavaScript对象表示,它模仿了实际DOM结构。 - 比较(Diffing) :
React使用了一种优化过的算法(通常称为" reconciliation algorithm"或" diff算法")来比较新的虚拟DOM树与上一次渲染后的虚拟DOM树。这个算法并不是简单地对整个DOM树进行深比较,而是采取了一些优化策略,如:
-
- 仅对同级节点进行比较,不同层级的节点变化不会相互影响。
- 对于相同类型的组件,仅比较props和state的变化。
- 对于列表,通过key来高效地识别新增、移除和移动的元素。
- 生成差异对象(Delta) :
Diff算法得出哪些部分需要更新后,会生成一个差异对象,其中包含了需要在实际DOM中进行更新的具体操作。 - 执行更新操作(Patching) :
React根据差异对象,对实际DOM进行最小化操作。这些操作可能包括:
-
- 更新元素的属性。
- 插入、删除或移动DOM元素。
- 更新文本内容。
- 批处理更新 :
React还会将多个组件的更新操作进行批处理(batching),在事件循环的下一个tick统一执行,以提高性能。
在整个patch流程中,React通过高效的Diff算法和最小化DOM操作策略,最大程度地减少了对实际DOM的直接操作,从而提高应用的性能和响应速度。在React Fiber架构下,这个流程变得更加灵活,能够支持异步渲染和优先级调度,使得UI更新更加平滑和高效。
46. React 性能优化的手段
React 中进行性能优化的手段可以从多个维度进行分类,以下是一些关键类别及其对应的优化策略:
1. 组件优化
- 使用PureComponent或React.memo:对于仅根据props和state改变才重新渲染的组件,使用
React.PureComponent
或者对其包装一层React.memo
,它们都能通过浅比较props来避免不必要的重新渲染。 - shouldComponentUpdate/React Hooks中的useMemo/useCallback:在类组件中实现
shouldComponentUpdate
生命周期方法来手动控制是否更新组件。在函数组件中,使用useMemo
缓存计算结果,useCallback
缓存回调函数,防止因依赖项不变而引起的无效渲染。
2. 状态管理与变更
- 减少不必要的setState调用:合并多次对同一状态的修改,例如使用
useState
hook时,可以利用函数式的setState来一次性更新多个状态值。 - 选择性地更新state:只在props或state真正发生变化时才进行更新,避免频繁或大面积的state变更引发大量子组件重新渲染。
3. Virtual DOM与Diff算法优化
- 合理构建组件层级:保持组件树扁平化,减少不必要的嵌套层次,使React的diff算法更高效。
- 利用key属性:为列表元素提供稳定的唯一key,帮助React识别并最小化DOM变动。
- 少用 dom 层级 多使用箭头标签替代
4. 事件处理优化
- 使用合成事件:React的合成事件系统可以减少全局事件监听器的数量,提高事件处理效率。
- 避免内联函数绑定:在事件处理函数中,避免每次渲染时创建新的函数引用,而是使用箭头函数或者
useCallback
来缓存函数引用。
5. 懒加载与代码分割
- 动态导入:使用React.lazy和Suspense来按需加载组件,减轻初始加载负担,提高首屏加载速度。
- 使用优先级加载CSS、JavaScript和图片资源。
6. 优化渲染过程
- 使用ReactDOM.createPortal:将某些组件渲染到根DOM之外,比如渲染到document.body,可以避免不必要的re-render。
- CSS动画与交互优化:配合requestAnimationFrame等API来处理复杂的动画,减少不必要的布局重排和重绘。
7、工具辅助
- Profiler工具:利用React DevTools的Profiler面板分析组件渲染性能瓶颈。
- 性能监控与警告:设置性能指标监控点,及时发现和修复潜在性能问题。
8、前端通用优化
- 静态资源压缩与HTTP缓存:优化CSS、JavaScript文件大小,合理设置HTTP缓存策略。
- 服务端渲染(SSR):针对SEO友好和首屏加载速度,结合Next.js等框架进行服务器端渲染。
47. fiber如何定义任务的优先级?
为了实现低优先级任务给高优先级任务让步,React弄了一套优先级机制,并且实现了一套任务调度,有点类似于实现了属于React自己的事件循环机制。(不得不说React玩得可真花,而且这里面还用到了二进制和小顶堆的知识。
具体来说就是,React的Scheduler给任务的优先级划分成5个级别(Nopriprity除外):
除了0之外,数字越小的优先级越高。ImmediatePriority 是最高优先级,用于表示紧急任务,主要是一些离散型事件,像用户点击和输入,以及开发者手动标记的高优先级任务。
其次是UserBlockingPriority,主要是一些连续型事件,比如scroll,drag,mouseover,这些虽然也是事件但相对点击和输入来说并不那么紧急。
另外NormalPriority是普通优先级,LowPriority 低优先级,IdlePriority是空闲优先级,优先级最低。
至于怎么判断具体任务属于哪个优先级,React另外弄了一套Lane优先级的机制,使用二进制来表示优先级,然后在运行时会将Lane优先级转换成Scheduler优先级。
在React中,任务的优先级是通过一套优先级系统来定义的,这套系统主要体现在Fiber架构下的并发调度策略上。React 17及后续版本中采用了Lanes模型来表示任务的优先级。
在Lanes模型中,任务的优先级是通过31位的位字段来表示,不同的位代表了不同的优先级通道(lane)。每一个优先级都可以映射到一个或多个位上,这样通过位运算就可以轻松地判断任务之间是否存在优先级重叠以及决定哪个任务应该优先执行。
以下是React中的一些常见优先级分类:
- 用户交互优先级:与用户直接交互相关的任务,比如键盘输入、鼠标点击等,这类事件引发的更新具有很高的优先级。
- 同步优先级:同步更新通常需要立即执行,以保持一致性。
- 异步优先级:这些优先级通常与setTimeout/setInterval等异步API相关,或者是React的异步渲染功能调度的更新。
- Suspense优先级:用于处理异步数据加载的情况,当数据不足时挂起渲染,待数据获取后继续。
React通过Scheduler库来管理这些优先级,当需要安排一个新的更新时,React会创建一个Update对象并为其分配一个或多个lane。然后,Scheduler会基于这些lane的优先级来安排任务进入任务队列,并采用工作调度算法选择最合适的任务执行。
例如,当一个低优先级的任务正在执行,而一个高优先级的任务被提交时,高优先级的任务会打断低优先级的任务,优先被执行。这样做有助于保证即使在复杂的更新序列中,React也能实时响应用户的操作和其他高优先级更新。
48. react 中provider底层原理
- Context对象 :
当你通过React.createContext()创建一个Context对象时,React内部会初始化一个包含Provider和Consumer组件的对象。这个Context对象将用于在组件之间共享数据。 - Provider组件 :
Provider组件本质上是一个特殊的React组件,它接受一个value prop,并将这个值存储在当前的React上下文中。这个值可以是任何类型的数据,包括原始值、对象、数组等。当Provider组件被渲染时,它会将当前的值与Context对象关联起来,并将这个关联的信息存储在React的Fiber树中。 - Fiber树与上下文关系 :
React Fiber是React内部用来跟踪组件树结构的数据结构。每个Fiber节点都保存了关于组件的信息,包括其类型、实例、输入、输出等。当Provider组件被渲染时,React会将当前Fiber节点与一个特定的Context关联起来,并将Provider的value存储在Fiber的上下文中。 - 数据传递 :
当Provider的值发生变化时,React会重新渲染与该Context相关的所有组件。这是通过React的更新机制实现的,当Provider的props或state发生变化时,React会触发更新过程,重新渲染Provider及其子组件 。在这个过程中,与当前Context相关联的所有组件都会接收到新的上下文值。 - Consumer与useContext :
虽然Provider负责提供数据,但数据的消费通常通过Consumer组件或使用useContext Hook来完成。在底层,当React渲染一个使用特定Context的Consumer或使用useContext的组件时,它会沿着Fiber树向上查找与该Context相关联的最近的Provider。一旦找到,React就会将Provider的value传递给Consumer或使用useContext的组件。 - 优化与性能 :
React通过一些优化手段来避免不必要的渲染和更新。例如,当Provider的值没有发生变化时,React可能会跳过与该Context相关的某些组件的更新。此外,React还使用了一种称为"context bridging"的技术来减少不必要的上下文查找,从而提高渲染性能。
49. connect 底层原理
- 创建高阶组件 :
connect函数接收一个mapStateToProps函数和一个mapDispatchToProps函数(这两个函数都是可选的)作为参数,并返回一个新的函数。这个新函数接受一个React组件作为参数,并返回一个新的、经过增强的React组件。 - 状态映射 :
如果提供了mapStateToProps函数,它会被用来从Redux store中的状态中提取出需要的部分,并将这些状态映射到新的props上。这样,每当store中的相关状态发生变化时,连接(connect)的组件都会接收到新的props。 - 分发动作 :
如果提供了mapDispatchToProps函数,它可以将action creators映射到新的props上,从而允许连接的组件调用这些props来分发动作。这些动作会触发Redux的reducer函数,进而更新store中的状态。 - 订阅Store变化 :
在connect函数内部,新组件会订阅Redux store的变化。这意味着每当store的状态发生变化时,新组件都会重新渲染。这是通过Redux的store.subscribe()方法实现的,它允许注册一个监听器函数,该函数会在每次action被分发并改变store状态后被调用。 - 性能优化 :
为了优化性能,connect函数还实现了shouldComponentUpdate生命周期方法(或在函数组件中使用React.memo等),以防止不必要的重新渲染。它会比较新旧props,只有当它们实际发生变化时,才会触发组件的重新渲染。 - 装饰器模式 :
从设计模式的角度来看,connect函数实际上是一种装饰器模式的应用。它"装饰"了原始的React组件,为其添加了额外的功能(即与Redux store的连接)。 - 纯函数和记忆化 :
在Redux的实现中,connect函数还利用了纯函数和记忆化(memoization)的概念来提高性能。例如,mapStateToProps和mapDispatchToProps函数应该是纯函数,以便在给定相同输入时总是产生相同的输出。此外,Redux可能会对这些函数的输出进行记忆化,以避免在每次状态更新时都重新计算它们。
50. Context Provider 和Redux Provider
当在React中同时使用Context和Redux时,可能会感到混乱,因为它们都使用了Provider
组件。让我们来理清一下:
- Context Provider:
-
-
在React中,
Context
允许您在组件树中传递数据,而无需在每个级别手动传递props。您可以使用Context.Provider
来提供数据,并使用Context.Consumer
或useContext
来从组件中访问该数据。 -
示例:
const MyContext = React.createContext();
function MyComponent() {
return (
<MyContext.Provider value="someValue">
<ChildComponent />
</MyContext.Provider>
);
}function ChildComponent() {
const value = useContext(MyContext);
return{value};
}
-
- Redux Provider:
-
-
Redux是一个状态管理库,它允许您在应用程序中统一管理状态。Redux使用
Provider
来将Redux存储绑定到React应用程序。Redux的Provider
确保您的整个应用程序都可以访问Redux存储中的状态。 -
示例:
import { Provider } from 'react-redux';
import store from './store';ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
-
如何理清混淆:
- Context和Redux的用途不同:Context用于在组件树中传递数据,而Redux用于管理应用程序的状态。
- 使用Context时,通常用于较小范围的数据传递,而Redux则更适用于大型应用程序的状态管理。
- 在React应用程序中,您可以同时使用Context和Redux。例如,您可以使用Context传递一些全局配置或用户信息,而Redux用于管理应用程序的核心状态。
最重要的是要理解它们的用途和作用范围,以便在项目中正确地使用它们。