React

什么是 React

React用于动态构建用户界面的 JS UI库。

React特点

  • 使用虚拟DOM和Diff算法,尽量复用DOM节点,减少与真实DOM的交互
  • 使用 JSX,代码的可读性更好
  • 组件化模式,提高代码复用率、且让代码更好维护
  • 声明式编程, 让编码人员无需直接操作DOM,提高开发效率

React声明式编程:我们只声明了我们想要的内容:比如一个包含用户姓名的无序列表,而不用关心具体的DOM操作。React会负责渲染和更新DOM,我们只需关注描述界面的声明式代码。

React的设计思想

  • 组件化:每个组件都符合开放-封闭原则,封闭是针对渲染工作流来说的,指的是组件内部的状态都由自身维护,只处理内部的渲染逻辑。开放是针对组件通信来说的,指的是不同组件可以通过props(单项数据流)进行数据交互

  • 数据驱动视图:UI=f(data),通过这个公式得出,如果要渲染界面,不应该直接操作DOM,而是通过修改数据(state或prop),数据驱动视图更新

  • 虚拟DOM:由浏览器的渲染流水线可知,DOM操作是一个昂贵的操作,很耗性能,因此产生了虚拟DOM。虚拟DOM是对真实DOM的映射,React通过新旧虚拟DOM对比,得到需要更新的部分,实现数据的增量更新

React生命周期

React的生命周期:React实例从被创建到被销毁的过程,React组件中包含一系列勾子函数(生命周期回调函数), 会在特定的时刻调用。

只有 class 组件才有生命周期,因为 class 组件会创建对应的实例,而函数组件不会 。

React 生命周期主要包括三个阶段:挂载阶段,更新阶段,卸载阶段
在这里插入图片描述

class Test extends React.Component{
    constructor(props){ // 构造函数
        super(props)
        this.state = {}
    }
    // 初始化,更新时会调用
    static getDerivedStateFromProps(props,state){
        // 必须返回一个对象,会和state合并
        return {}
    }
    // 初始化渲染时使用
    componentDidMount(){}
    // 组件更新时调用 返回false 不更新
    shoudComponentUpdate(prevProps,nextState){ return true }
    // 组件更新时调用,返回的值会设置在componentDidUpdate的第三个参数
    getSnapshotBeforeUpdate(prevprops,nextState) { return ''}
    // 组件更新后调用
    componentDidUpdate(preProps,preState,valueFromSnaspshot){}
    // 组件卸载时调用
    componentWillUnmount() {}
    // 组件抛出错误
    static getDerivedStateFromError(){}
}

挂载阶段

挂载阶段:组件实例被创建和插入 DOM 树的过程

  • constructor 初始化阶段,可以进行state和props的初始化
  • static getDerivedStateFromProps 静态方法,不能获取this
  • render 创建虚拟DOM的阶段
    componentDidMount 第一次渲染后调用,挂载到页面生成真实DOM,可以访问DOM,进行异步请求和定时器、消息订阅

更新阶段

当组件的props或state变化会触发更新

  • shouldComponentUpdate 返回一个布尔值,默认返回true,可以通过这个生命周期钩子进行性能优化,确认不需要更新组件时调用
  • render 更新虚拟DOM
  • getSnapShotBeforeUpdate 获取更新前的状态
  • componentDidUpdate 在组件完成更新后调用,更新挂载后生成真实DOM
    当组件的props或state变化会触发更新

卸载阶段

  • componentWillUnmount 组件从DOM中被移除的时候调用,通常在这个阶段清除副作用,比如定时器、事件监听等

错误捕获

  • static getDerivedStateFromError 在errorBoundary中使用
  • render是class组件中唯一必须实现的方法

重要的勾子

  • render:初始化渲染或更新渲染调用。
  • componentDidMount:在组件挂载成功之后调用,该过程组件已经成功挂载到了真实 DOM 上。一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
  • componentWillUnmount:在组件卸载成功之前调用,做一些收尾工作, 如: 清理定时器、取消订阅消息

已经废弃的勾子

  1. componentWillMount
  2. componentWillReceiveProps
  3. componentWillUpdate

React事件机制

什么是React事件
React基于浏览器的事件机制实现了一套自身的事件机制,它符合W3C规范,包括事件触发、事件冒泡、事件捕获、事件合成和事件派发等

在这里插入图片描述
在这里插入图片描述

React事件和普通的HTML事件有什么不同?

  • 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰camelCase;
  • 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
<button onclick="alert('Button clicked')">Click me</button>
function MyComponent() {
  return (
    <button onClick={()=>{alert('Button clicked')}>Click me</button>
  );
}
  • react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

为什么React事件不能通过return false阻止事件的默认行为?
因为React基于浏览器的事件机制实现了一套自己的事件机制,和原生DOM事件不同,它采用了事件委托的思想,通过dispatch统一分发事件处理函数

React怎么阻止事件冒泡

  • 阻止合成事件的冒泡用e.stopPropagation()
  • 阻止合成事件和最外层document事件冒泡,使用e.nativeEvent.stopImmediatePropogation()
  • 阻止合成事件和除了最外层document事件冒泡,通过判断e.target避免

React事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:

  • 兼容所有浏览器,更好的跨平台;
  • 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
  • 方便 react 统一管理和事务机制。

事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。

JSX

JSX 是 JavaScript 语法扩展,可以在JS中编写XML的语言。它不能被浏览器直接识别,需要通过webpack、babel之类的编译工具转换为JS执行。
JSX是React.createElement的语法糖,使用JSX等价于React.createElement,React.createElement将返回一个ReactElement的js对象,编译工作交由babel操作
JSX 和 React 是相互独立的东西。JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。

import React from 'react';
//JSX不是字符串, 也不是HTML/XML标签,最终产生的就是一个JS对象
var ele = <h1>Hello, JSX!</h1> 
// 等价于
var element = React.createElement(
  "h1",
  null,
  "Hello, world!"
);

为什么在文件中没有使用react,也要在文件顶部import React from “react”?

只要使用了jsx,就需要引用react,因为jsx本质就是React.createElement

为什么React自定义组件首字母要大写?
从jsx到真实DOM需要经历jsx->虚拟DOM->真实DOM。如果组件首字母为小写,它会被当成字符串进行传递,在创建虚拟DOM的时候,就会把它当成一个html标签,而html没有app这个标签,就会报错。组件首字母为大写,它会当成一个变量进行传递,React知道它是个自定义组件就不会报错了

React组件为什么只能有一个根元素?(React组件为什么不能返回多个元素)
React的虚拟DOM是一个树状结构,树的根节点只能是1个,如果有多个根节点,无法确认是在哪棵树上进行更新

state 和 props有什么区别?

  • props 是传递给组件的(类似于函数的形参),是父组件向子组件传递数据的方式,state 是在组件内被组件自己管理的(类似于在一个函数内声明的变量), 代表了随时间会产生变化的数据,应当仅在实现交互时使用

  • props 在组件内部是不可修改的,但 state 在组件内部可以进行修改

  • 组件可以选择把它的 state 作为 props 向下传递到它的子组件中

组件通信

单层级通信

  • 父组件给子组件通信,传递props
  • 子组件向父组件通信,使用回调函数(父组件向子组件传递一个函数,通过函数回调,拿到子组件传过来的值)
  • 兄弟组件通信,通过父组件当中间件传递,子组件a传递给父组件,父组件再传递给子组

跨多层组件通信(如祖孙组件通信)

  • 使用React的Context
  • 使用状态管理框架,如 redux

在React中,forwardRef 和 useImperativeHandle 用来帮助父组件直接访问并操作子组件内部某些特定DOM元素或者自定义的方法。具体来说,forwardRef 用于将 ref 传递给函数组件,而 useImperativeHandle 则是用来定制函数组件暴露给父组件的实例方法或属性。

下面是一个简单的示例,展示如何使用 forwardRef 和 useImperativeHandle:

import React, { forwardRef, useImperativeHandle, useRef } from 'react';

// 定义一个子组件,它接收父组件传递过来的ref
const ChildComponent = forwardRef((props, ref) => {
  // 使用useRef创建一个内部引用,例如用于存储一个DOM元素
  const internalRef = useRef();

  // 在useImperativeHandle中定义子组件希望暴露给父组件的方法或属性
  useImperativeHandle(ref, () => ({
    // 假设我们想让父组件能够调用子组件内部的一个方法,比如滚动到顶部
    scrollToTop: () => {
      if (internalRef.current) {
        internalRef.current.scrollTop = 0;
      }
    },
    // 或者暴露其他任意自定义方法
    customMethod: () => {
      console.log('This is a custom method called by the parent');
    },
  }));

  // 在返回的DOM节点上挂载内部引用
  return (
    <div ref={internalRef}>
      {/* 子组件的内容 */}
    </div>
  );
});

// 父组件使用子组件,并通过ref属性来操作子组件
function ParentComponent() {
  const childRef = useRef(null);

  const handleScrollToTop = () => {
    if (childRef.current) {
      childRef.current.scrollToTop();
    }
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleScrollToTop}>Scroll to Top</button>
    </div>
  );
}

在这个例子中,ChildComponent 接受了由 forwardRef 传递进来的 ref 参数,并通过 useImperativeHandle 自定义了暴露给父组件的方法——scrollToTop。父组件通过这个 ref 能够直接调用子组件内部的方法,实现了跨组件的交互。

context 多层级通信

context的作用是为了避免在组件间层层传递变量,我们可以通过createContext(null)来创建一个新的context,新创建的context包含一个provider 和一个consumer

传递时,需要用Provider包裹父组件,通过value携带参数,在Provider包裹下的层层组件中,通过consumer包裹子组件来读取传递的变量

React Fiber

React Fiber是什么?

Fiber是一个JavaScript对象,可以理解为是一个更强大的虚拟DOM,代表React的一个工作单元,它包含了与组件相关的信息。一个简化的Fiber对象长这样:

{
  type: 'h1',  // 组件类型
  key: null,   // React key
  props: { ... }, // 输入的props
  state: { ... }, // 组件的state (如果是class组件或带有state的function组件)
  child: Fiber | null,  // 第一个子元素的Fiber
  sibling: Fiber | null,  // 下一个兄弟元素的Fiber
  return: Fiber | null,  // 父元素的Fiber
  // ...其他属性
}

当React开始工作时,它会沿着Fiber树形结构进行,试图完成每个Fiber的工作(例如,比较新旧props,确定是否需要更新组件等)。如果主线程有更重要的工作(例如,响应用户输入),则React可以中断当前工作并返回执行主线程上的任务。

因此,Fiber不仅仅是代表组件的一个内部对象,它还是React的调度和更新机制的核心组成部分。

为什么需要Fiber?

在React 16之前的版本中,是使用堆栈调和(Stack Reconciliation)的方式处理组件树更新,这种方法一旦开始就不能中断,直到整个组件树都被遍历完。这种机制在处理大量数据或复杂视图时可能导致主线程被阻塞,从而使应用无法及时响应用户的输入或其他高优先级任务。因为人眼可识别的帧率是1s 60帧,即16ms一帧,如果diff计算时间超过16ms,阻塞渲染,就会感觉卡顿。为了避免这种情况,需要执行更新操作时不能超过16ms,如果超过16ms,就需要先暂停,让给浏览器进行渲染操作,后续再继续执行更新计算。

为了解决上述问题,React 16引入Fiber 架构,React 把渲染更新过程拆分成多个子任务,当React向浏览器请求调度时,浏览器在一帧中如果还有空闲时间,子任务会被执行,当每个子任务执行完成后,React 都会检查是否还有剩余时间,如果有剩余时间则继续执行下一个子任务,如果没有,则挂起当前任务,将控制权交回给主线程,允许浏览器执行高优先级任务,当浏览器空闲时在继续执行react任务,这样任务会被分散到多个帧里面,中间可以返回至主进程控制执行其他任务,从而实现渐进式渲染,提升用户体验。

window.requestIdleCallback() 是一个浏览器提供的 JavaScript API,它允许开发者请求在浏览器空闲时段执行回调函数。

React向浏览器请求调度流程如下:

img

Fiber工作原理

Fiber工作原理中最核心的点:可以中断和恢复,这个特性增强了React的并发性和响应性

实现可中断和恢复的原因:Fiber的数据结构里提供的信息让React可以追踪工作进度、管理调度和同步更新到DOM

Fiber工作原理中的关键点

  • 单元工作:每个Fiber节点代表一个单元,所有Fiber节点共同组成一个Fiber链表树(有链接属性,同时又有树的结构),这种结构让React可以细粒度控制节点的行为。

  • 链接属性childsiblingreturn 字段构成了Fiber之间的链接关系,使React能够遍历组件树并知道从哪里开始、继续或停止工作。比如B1return A1,代表B1的父节点是A1

    20230721115421

  • 双缓冲技术: React在更新时,会根据现有的Fiber树(Current Tree)创建一个新的临时树(Work-in-progress (WIP) Tree),WIP-Tree包含了当前更新受影响的最高节点直至其所有子孙节点。Current Tree是当前显示在页面上的视图,WIP-Tree则是在后台进行更新,WIP-Tree更新完成后会复制其它节点,并最终替换掉Current Tree,成为新的Current Tree。因为React在更新时总是维护了两个Fiber树,所以可以随时进行比较、中断或恢复等操作,而且这种机制让React能够同时具备拥有优秀的渲染性能和UI的稳定性。

2.png

  • State 和 Props:memoizedPropspendingPropsmemoizedState 字段让React知道组件的上一个状态和即将应用的状态。通过比较这些值,React可以决定组件是否需要更新,从而避免不必要的渲染,提高性能。

  • 副作用的追踪flagssubtreeFlags 字段标识Fiber及其子树中需要执行的副作用,例如DOM更新、生命周期方法调用等。React会积累这些副作用,然后在Commit阶段一次性执行,从而提高效率。

Fiber架构

  • Scheduler(调度器)—— 根据优先级调度任务。它会根据任务的优先级对任务进行调用执行。在有多个任务的情况下,它会先执行优先级高的任务。如果一个任务执行的时间过长,Scheduler 会中断当前任务,让出线程的执行权,避免造成用户操作时界面的卡顿。在下一次恢复未完成的任务的执行。
  • Reconciler(协调器)—— 找出变化的节点
  • Renderer(渲染器)—— 将变化更新到页面

Fiber工作流程

Fiber 的工作流程分为两个阶段:reconciliation(调和)阶段和 commit(调度) 阶段

第一阶段:reconciliation(调和)

  • 目标: 确定哪些部分的UI需要更新。
  • 原理: 这是React构建工作进度树的阶段,会比较新的props和旧的Fiber树来确定哪些部分需要更新。

调和(处理诸如新的 Fiber 创建、旧 Fiber 删除或现有 Fiber 更新等操作)

第二阶段:reconciliation(调和)

  • 目标: 更新DOM并执行任何副作用。

  • 原理: 遍历在Reconciliation阶段创建的副作用列表进行更新。

  • render 阶段:找出所有节点的变更,比如节点新增、删除、属性变更等,这些变更统称为副作用,此阶段会构建一棵Fiber tree,以虚拟dom节点为维度对任务进行拆分,即一个虚拟Dom节点对应一个任务,最后产出的结果是effect list,从中统计出知道哪些节点需要更新、哪些节点需要增加、哪些节点需要删除。

  • commit 阶段:将render 阶段计算出需要处理的副作用一次性执行,此阶段不能暂停,否则会出现UI更新不连续的现象。

新的diff过程:

  1. 首次渲染时候构建一个和虚拟dom树一样结构的fiber树
  2. 组件更新时候,遍历新旧fiber树,diff区别,diff操作是分片进行,16ms内如果没完成,就先暂停等待下个渲染空闲时间再继续。
  3. diff完成之后进行commit,将变化提交,进行对应的dom操作,为防止界面抖动,commit是一次性完成的。

为了将这个元素渲染到DOM上,React需要创建一种内部实例,用来追踪该组件的所有信息和状态。在早期版本的React中,我们称之为“实例”或“虚拟DOM对象”。但在Fiber架构中,这个新的工作单元就叫做Fiber。

React 如何实现时间分片和可中断渲染?

React 实现时间分片和可中断渲染是通过 Fiber 架构和调度器来实现的。具体如下所示:

  • Fiber 架构:Fiber 是一种数据结构,它表示 React 组件树中的每个节点。每个 Fiber 节点包含了组件的状态和描述如何构建组件的信息。Fiber 节点形成一个链表,称为 Fiber 树;
  • 任务调度: React 的调度器负责任务的调度和执行。调度器使用优先级来确定任务的重要性和执行顺序。任务的优先级决定了任务在调度器中的位置,优先级高的任务会优先执行;
  • 时间分片:时间分片是将大的渲染任务拆分成小的可中断的任务单元。React 将整个渲染过程分成多个时间片段,每个时间片段都有一个固定的时间段,例如 5ms 来执行任务。在每个时间片段内,React 执行一部分工作,然后让出主线程,允许浏览器执行其他任务,从而实现渐进式渲染;
  • 可中断渲染: Fiber 架构使得 React 能够在渲染过程中中断任务的执行,并在必要时重新调度任务。如果浏览器需要执行其他高优先级任务,如用户交互事件,React 会中断当前渲染任务,并优先执行其他任务。一旦高优先级任务完成,React 会恢复中断的渲染任务,确保及时响应用户操作;
  • 执行中断: 在 React 执行渲染任务时,会周期性地检查是否有其他高优先级的任务需要执行。如果有,React 会中断当前任务,让出主线程,并执行其他紧急任务。一旦高优先级任务完成,React 会恢复中断的渲染任务,继续执行渲染过程;
    通过以上的实现,React 能够在渲染过程中灵活地控制任务的执行顺序,使得页面可以更及时地响应用户操作,提高用户体验。同时,时间分片和可中断渲染还能够确保在大型组件树的渲染过程中,不会阻塞主线程,保持页面的流畅性。

React 如何实现增量渲染?

React 实现增量渲染是通过 Fiber 架构和协调器来实现的。增量渲染是一种渐进式渲染方式,它将整个渲染过程拆分成多个增量步骤,并将这些步骤分布在多个时间片段中执行,从而实现渲染的分阶段、增量式进行。
通过时间分片的方式将整个渲染过程分成多个时间片段。这样页面的渲染过程被分成多个增量步骤进行,避免了长时间的渲染阻塞。React 使用虚拟 DOM 和 Diff 算法来对比前后两个状态的差异,然后仅更新真正需要变化的部分,而不是重新渲染整个组件树。这样,React 只需要更新变化的部分,从而实现增量渲染。
增量渲染使得页面可以更快地显示,从而保持页面的流畅性和响应性,提高了用户体验。

Concurrent并发模式

React 并发模式(React Concurrent Mode)是 React 的一项新设计模式,旨在提高渲染性能和用户体验。在传统的 React 中,更新组件树时会阻塞用户界面的响应,可能导致卡顿和延迟。而并发模式通过将任务分解为多个小步骤,让 React 在执行渲染和布局时可以中断和恢复任务,从而提供更平滑和响应式的用户体验。
在 React 并发模式中,引入了两个主要概念:任务调度和优先级任务调度器负责决定哪些任务执行、何时执行以及中断和恢复任务。优先级允许 React 根据任务的紧迫性来安排任务的执行顺序,确保响应度更高的任务能够优先执行。
利用并发模式,React 可以将渲染过程分解为多个小任务,并根据优先级来动态调整任务执行的顺序。这样,在浏览器空闲时间或网络请求等异步操作期间,React 可以暂停当前任务,执行其他具有更高优先级的任务,以实现更爽快的用户交互体验。
并发模式两个方向的优化,分别是 CPU密集型 和 I/O密集型
CPU 密集型任务是指React 将组件转化为虚拟 DOM 的过程。
在Reconciler过程中,React 需要执行组件函数,生成表示组件结构和内容的虚拟 DOM 对象。这涉及到对组件的属性和状态进行计算、逻辑处理和组装,需要占用大量的 CPU 资源。尤其在组件树较大、嵌套层级较深的情况下,计算虚拟 DOM 的过程可能会非常复杂和耗时。
I/O 密集型任务是指在组件渲染或更新过程中涉及到大量的异步操作或需要等待外部资源响应的任务。这些任务可能会阻塞主线程的执行,导致页面的卡顿或不流畅。
React 在处理 I/O 密集型任务方面的主要优化:

  • 异步渲染: React 通过引入异步渲染机制,将渲染工作分成多个小任务,并以适当的时机执行,避免阻塞主线程。这使得 React 能够更好地响应用户输入和处理 I/O 操作;
  • 批量更新:React 通过批量处理组件更新,将多个更新操作合并为一个更新批次,减少了对实际 DOM 的操作次数,提高了性能。特别是在处理大量连续的数据更新时,这种批处理机制可以显著减少不必要的重排和重绘;

总而言之,React 并发模式通过任务调度和优先级机制,提供了更好的用户体验和性能,使得 React 应用程序能够更加平滑地响应用户操作。

虚拟DOM、diff算法

虚拟DOM

原生JS渲染页面:数据 => 真实DOM

React渲染页面:数据 => 虚拟DOM(内存中的数据)=> 真实DOM
原生的JS DOM操作非常消耗性能,浏览器在处理DOM的时候会很慢,处理JavaScript会很快
虚拟DOM是一个用来描述真实DOM结构的js对象。虚拟DOM要比真实DOM轻很多,没有真实DOM上默认的属性和方法。
优点:减少了DOM操作,减少了重排与重绘,保证性能的下限,极大的优化了大量操作DOM时产生的性能损耗
缺点:首次渲染DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。

"重排(Reflow)"和"重绘(Repaint)"是浏览器渲染页面时的两个关键过程:
重排(Reflow):当DOM树中的元素尺寸、位置或其他可以改变布局的属性发生变化时,浏览器需要重新计算元素的几何信息(如宽高、位置),然后重新构建渲染树的过程。这是一个相对昂贵的操作,因为涉及到所有后续元素的重新布局。
重绘(Repaint):当一个元素的外观发生改变,但不影响其几何属性(如尺寸、位置)时,比如背景色变化、文字颜色更改等,浏览器只需要对这个元素及其子元素进行重新绘制,而不需要重新计算布局。
因此,每一次重排操作后通常会伴随着至少一次重绘,因为布局变化后肯定需要视觉上的更新。然而,不是所有的重绘都需要先进行重排。例如,如果只是简单地改变了某个元素的颜色,并没有影响到布局,则只会触发重绘而不触发重排。这样可以提高页面的性能表现。

diff算法

diff算法是用于比较新旧虚拟节点树之间差异的一种算法。在state 更新的时候,组件会重新 render,产生新的 vdom,在浏览器平台下,为了减少 dom 的创建,React 会对两次的 render 结果做 diff,对比两次渲染结果,找到可复用的部分,然后剩下的该删除删除,该新增新增,提高性能。

在 16 之前,React 是直接递归渲染 vdom 的,setState 会触发重新渲染,对比渲染出的新旧 vdom,对差异部分进行 dom 操作。通过三大策略完成了优化:

  • DOM 节点的跨层级移动的操作特别少,可以忽略不计
  • 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同的树形结构
  • 对于同一层级的一组子节点,可以通过唯一的 id 进行区分

分别对应tree diff、component diff、element diff:

  • tree diff: DOM 节点的跨级操作比较少,那么 diff 算法只会对相同层级的 DOM 节点进行比较。如果发现节点不存在 那么会将该节点以及其子节点完全删除,不会再继续比较。如果出现了 DOM 节点的跨层级的移动操作,那么会删除该节点以及其所有的子节点,然后再移动后的位置重新创建。
  • component diff:如果是同一类型的组件(函数组件和类组件,原生 HTML 元素(如 div、span 等)),那么会继续对比 VM 数(tree diff),不是同一类型的组件,那么会将其和其子节点完全替换,不会再进行比对
  • element diff:当节点处于同一层级的时候时,有三种操作:插入、移动、删除, React对于同一层级的同组子节点,添加唯一的 key 进行区分。通过 key 发现新旧集合中的节点有相同的节点,就只需要进行移动操作就可以。(当组件D在集合 A、B、C、D中,且集合更新时,D没有发生更新,只是位置发生了改变,如:A、D、B、C,D的位置由4变换到了2,此时进行移动操作即可)

在 16 之后,为了优化性能,会先把 vdom树 转换成 fiber链表,然后再渲染。整体渲染流程分成了两个阶段

  • render 阶段(reconcile + schedule):从 vdom 转换成 fiber,并且对需要 dom 操作的节点打上 effectTag 的标记
  • commit 阶段:对有 effectTag 标记的 fiber 节点进行 dom 操作,并执行所有的 effect 副作用函数。

先把 vdom 转成 fiber,找到需要更新 dom 的部分,打上增删改的 effectTag 标记,这个过程叫做 reconcile,可以打断,由 scheducler 调度执行。reconcile 结束之后一次性根据 effectTag 更新 dom,叫做 commit。

img

diff 算法在 reconcile 阶段的作用:

  • 第一次渲染不需要 diff,直接 vdom 转 fiber。
  • 再次渲染的时候,会产生新的 vdom,这时候要和之前的 fiber 做下对比,决定怎么产生新的 fiber,对可复用的节点打上修改的标记,剩余的旧节点打上删除标记,新节点打上新增标记。

SSR的时候没有 diff,每次都是 vdom 渲染出新的字符串。现在框架SSR的原理是将组件变为字符串,并且通过模板引擎将数据注入到字符串中,最后返回一个完整的 html 页面

React 的 diff 算法是分成两次遍历的:

第一轮遍历,一一对比 vdom 和老的 fiber,如果可以复用就处理下一个节点,否则就结束遍历。

如果所有的新的 vdom 处理完了,那就把剩下的老 fiber 节点删掉就行。

如果还有 vdom 没处理,那就进行第二次遍历:

第二轮遍历,把剩下的老 fiber 放到 map 里,遍历剩下的 vdom,从 map 里查找,如果找到了,就移动过来。

第二轮遍历完了之后,把剩余的老 fiber 删掉,剩余的 vdom 新增。这样就完成了新的 fiber 结构的创建,也就是 reconcile 的过程。

举个例子,比如父节点下有 A、B、C、D 四个子节点,那渲染出的 vdom 就是这样的:

那具体怎么实现 React 的 diff 算法呢?

比如父节点下有 A、B、C、D 四个子节点,那渲染出的 vdom 就是这样的:

img

经过 reconcile 之后,会变成这样的 fiber 结构:

img

那如果再次渲染的时候,渲染出了 A、C、B、E 的 vdom,这时候怎么处理呢?结论很简单:再次渲染出 vdom 的时候,也要进行 vdom 转 fiber 的 reconcile 阶段,但是要尽量能复用之前的节点。

img

第一轮遍历:

img

一一对比新的 vdom 和 老的 fiber,发现 A 是可以复用的,那就创建新 fiber 节点,打上更新标记。

C 不可复用,所以结束第一轮遍历,进入第二轮遍历。

img

把剩下的 老 fiber 节点放到 map 里,key 就是节点的 key,然后遍历新的 vdom 节点,从 map 中能找到的话,就是可复用,移动过来打上更新的标记。

遍历完之后,剩下的老 fiber 节点删掉,剩下的新 vdom 新增。这样就完成了更新时的 reconcile 的过程

Hooks 链表

链表(Linked List)是一种常见的数据结构,用于存储和组织数据。它由一系列节点(Node)组成,每个节点包含了数据和指向下一个节点的引用(指针或链接)。链表的特点是节点之间通过指针相互连接,形成一个链式结构,并且节点可以按需动态分配内存,灵活地插入、删除和修改数据。
在这里插入图片描述

Hooks 链表是 React 内部用于在函数组件中管理多个 Hooks 状态的数据结构。它是在 Render 阶段创建的,用于记录函数组件中所有使用的 Hooks 及其对应的状态信息。通过 Hooks 链表,React 能够准确地跟踪每个 Hook 的调用顺序和状态,从而实现组件的状态管理和更新。从而实现更高效、可预测的组件渲染和更新。每个函数组件都有自己的 Hooks 链表,Hooks 链表存储在对应的 Fiber 节点中,保证了每个组件的 Hooks 是独立且安全的。
无论是初次挂载还是更新,每调用一次hooks函数,都会产生一个hook对象与之对应。以下是hook对象的结构。

{
    baseQueue: null,
    baseState: 'hook1',
    memoizedState: null,
    queue: null,
    next: {
        baseQueue: null,
        baseState: null,
        memoizedState: 'hook2',
        next: null
        queue: null
    }
}

产生的hook对象依次排列,形成链表存储到函数组件fiber.memoizedState上。在这个过程中workInProgressHook指针通过记录当前生成(更新)的hook对象,可以间接反映在组件中当前调用到哪个hook函数了。每调用一次hook函数,就将这个指针的指向移到该hook函数产生的hook对象上。
例如:

const HooksExp = () => {
const [ stateHookA, setHookA ] = useState(‘A’)
useEffect(() => { console.log(‘B’) })
const [ stateHookC, setHookC ] = useState(‘C’)
return

Hook Example

}
上面的例子中,HooksExp组件内一共调用了三个hooks函数,分别是useState、useEffect、useState。那么构建hook链表的过程,可以概括为下面这样,重点关注workInProgressHook的指向变化。

调用useState(‘A’):

fiber.memoizedState: hookA
                                       ^
                       workInProgressHook

调用useEffect:

fiber.memoizedState: hookA -> hookB
                                                     ^
                                     workInProgressHook

调用useState(‘C’):

fiber.memoizedState: hookA -> hookB -> hookC
^
workInProgressHook

hook函数每次执行,都会创建它对应的hook对象,去进行下一步的操作。创建hook对象的过程实际上也是hooks链表构建以及workInProgressHook指针指向更新的过程。

为什么hooks不能在循环、条件判断或者子函数中调用?
react用链表结构来严格保证hooks的顺序,
初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。如果使用循环、条件很有可能导致读到的链表在顺序上出现差异错位,执行错误的 Hook。
React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。

React 18 新特性

  • setState自动批处理
    在react17中,只有react事件会进行批处理,原生js事件、promise、setTimeout、setInterval不会
    react18,将所有事件都进行批处理,即多次setState会被合并为1次执行,提高了性能,在数据层,将多个状态更新合并成一次处理(在视图层,将多次渲染合并成一次渲染)
    批处理是指当 React 在一个单独的渲染事件中批量处理多个状态更新以此实现优化性能。
  • 引入了新的root API,支持new concurrent renderer(并发模式的渲染)
//React 17
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"

const root = document.getElementById("root")
ReactDOM.render(<App/>,root)

// 卸载组件
ReactDOM.unmountComponentAtNode(root)  

// React 18
import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App"
const root = document.getElementById("root")
ReactDOM.createRoot(root).render(<App/>)

// 卸载组件
root.unmount()  
  • 去掉了对IE浏览器的支持,react18引入的新特性全部基于现代浏览器,如需支持需要退回到react17版本

  • react组件返回值更新
    在react17中,返回空组件只能返回null,返回undefined会报错
    在react18中,支持null和undefined返回

新的 Hook

  • useId
    在服务器和客户端生成相同的唯一一个id,避免hydrating的不兼容

  • useTransition
    过渡(transition)更新是 React 中一个新的概念,用于区分紧急和非紧急的更新
    紧急更新:对应直接的交互,如输入,点击,按压等。
    过渡更新:将 UI 从一个视图过渡到另一个。
    useTransition 让你能够将一些状态更新标记为过渡更新。默认情况下,状态更新都被视为紧急更新。React 将允许紧急更新(例如,更新一个文本输入内容)打断过渡更新(例如,渲染一个搜索结果列表)。

  • useDeferredValue
    useDeferredValue 允许推迟渲染树的非紧急更新。React 会在第一次渲染在屏幕上出现后立即尝试延迟渲染。延迟渲染是可中断的,它不会阻塞用户输入。

Concurrent Mode
React 并发模式(React Concurrent Mode)是 React 的一项新设计模式,旨在提高渲染性能和用户体验。在传统的 React 中,更新组件树时会阻塞用户界面的响应,可能导致卡顿和延迟。而并发模式通过将任务分解为多个小步骤,让 React 在执行渲染和布局时可以中断和恢复任务,从而提供更平滑和响应式的用户体验。
在 React 并发模式中,引入了两个主要概念:任务调度和优先级任务调度器负责决定哪些任务执行、何时执行以及中断和恢复任务。优先级允许 React 根据任务的紧迫性来安排任务的执行顺序,确保响应度更高的任务能够优先执行。
利用并发模式,React 可以将渲染过程分解为多个小任务,并根据优先级来动态调整任务执行的顺序。这样,在浏览器空闲时间或网络请求等异步操作期间,React 可以暂停当前任务,执行其他具有更高优先级的任务,以实现更爽快的用户交互体验。
总而言之,React 并发模式通过任务调度和优先级机制,提供了更好的用户体验和性能,使得 React 应用程序能够更加平滑地响应用户操作。

useDeferredValue和startTransition用来标记一次非紧急更新

React性能优化方法

避免重复渲染

  • 使用 React.PureComponent对类组件进行优化,通过浅比较控制 shouldComponentUpdate 的返回值避免子组件重新渲染。

  • 使用 React.memo 对函数组件进行优化。它通过浅比较 props,确保只有在相关数据发生变化时才触发组件的重新渲染。(在 React 工作流中,如果只有父组件发生状态更新,即使父组件传给子组件的所有 Props 都没有修改,也会引起子组件的 Render 过程。如果子组件的 Props 和 State 都没有改变,子组件的不应该重新 Render )

  • useMemo用于缓存计算结果,避免在每次渲染时都重新计算。使用场景:

    • 当处理数据的时间复杂度较高时,比如将数组中的所有值加起来,但这个数组的length非常大,这时可以使用useMemo
    • 计算结果作为props传递给包装在React.memo中的子组件
    • 计算结果作为某些 Hook 的依赖,比如是useCallback或useEffect的依赖
  • useCallback用于缓存回调函数,避免在每次渲染时都重新创建回调。使用场景:

    • 函数作为props传递给包装在React.memo中的子组件
    • 作为某些 Hook 的依赖,比如是useCallback或useEffect的依赖
    • 推荐使用ahooks中的useMemoizedFn,与useCallback效果相同,但它不传入依赖项,函数引用地址不变
  • 祖孙组件通信,使用React.createContext 和React.useContext,这样就少了中间组件的渲染阶段

  • 列表项使用 key 属性,提高渲染效率

  • 避免使用内联css样式和匿名函数

  • 使用React.Fragment或<></>避免添加额外的DOM

  • 路由组件懒加载,React.Lazy和React.Suspense

    • 通过React的lazy函数配合import()函数动态加载路由组件,路由组件代码会被分开打包
    • 通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面

React Hooks

Hooks使用限制
在React中,Hooks 是 React 16.8 新增的特性,是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的特殊JS函数,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hooks的优点:

  • 告别难以理解的class组件
  • 解决业务逻辑难以拆分的问题
  • 使状态逻辑复用变的简单可行

主要有两个限制

  • 只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用

  • react用链表结构来严格保证hooks的顺序,在调用时按顺序加入链表中,如果使用循环、条件或嵌套函数很有可能导致读到的链表在顺序上出现差异错位,执行错误的 Hook。

  • 只能在 React 的函数组件中调用 Hook,不要在普通JS函数中调用

    由于Hooks依赖React的渲染机制和函数组件的特性,所以只能在React的函数组件中调用。在普通的JavaScript函数中使用Hooks是没有意义的,因为它们无法享受到React提供的状态管理和渲染优化的好处

常见的Hook

  • useState - 状态钩子,定义组件的state
  • useEffect - 执行副作用操作
  • useContext - 获取context对象
  • useCallback - 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染
  • useMemo - 缓存传入的props,避免重复渲染,缓存大量计算
  • useRef - 获取组件的真实节点

hooks 主要是利用闭包来保存状态,state 当做一个单链表的形式,用一个useState 就去表里取一下,使用链表保存一系列hooks,将链表中的第一个hooks 与 fiber关联,在fiber 树更新时,就能从hooks 中计算出最终输出的状态和执行相关的副作用。

副作用(Side Effects)
在React中,**副作用(Side Effects)**是指执行与组件渲染结果无关的操作。这些操作可以包括但不限于:

  • 发送网络请求
  • 订阅或取消订阅事件
  • 操作浏览器缓存或本地存储
  • 修改全局状态
  • 改变DOM

自定义hook

自定义 Hook 是一个函数,其名称以 use开头,是 React Hooks 聚合产物,其内部有一个或者多个 React Hooks 组成,用于解决一些复杂逻辑。 通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
Hook 的名称必须以 use 开头,然后紧跟一个大写字母,就像内置的 useState 或者本文早前的自定义 useOnlineStatus 一样。 这个公约保证你始终能一眼识别出组件并且知道它的 state,Effect 以及其他的 React 特性可能“隐藏”在哪里。例如如果你在组件内部看见 getColor() 函数调用,就可以确定它里面不可能包含 React state,因为它的名称没有以 use 开头。但是像 useOnlineStatus() 这样的函数调用就很可能包含对内部其他 Hook 的调用!

何时使用:每当你写 Effect 时,考虑一下把它包裹在自定义 Hook 是否更清晰
自定义 hooks的步骤:

  • 引入 react 和自己需要的 hook
  • 创建自己的hook函数
  • Hook 可以返回任意值,可以只执行逻辑,不返回值,可以返回一个数组,数组中第一个内容是数据,第二个是修改数据的函数
  • 暴露自定义 hook 函数出去
  • 引入自己的业务组件

自定义 Hooks 的应用范围非常广泛,几乎可以用于任何需要共享逻辑的情况。通过合理利用自定义 Hooks,我们可以提高代码的可维护性、可复用性和可读性
可以创建一个自定义 Hooks 用于从 API 中获取数据、对数据进行转换或者缓存数据,里面包含hook,案例如下,里面包含ahook中useRequest:

// 后端字典
export enum DictTypeEnum {
  // 公告类型
  NOTICE_TYPE = 'notice_type',
  // 病种信息
  TYPE_DISEASE = 'type_disease',
  // 婚姻状态
  MARITAL_STATUS = 'marital_status',
  // 复诊任务类型
  FUV_TASK_TYPE = 'fuv_task_type',
  // 文化程度
  EDUCATION = 'education',
  // 护士名称
  NURSE_NAME = 'nurse_name',
  // 处方用法
  PRESCRIPTION_USAGE = 'prescription_usage',
  // 处方频次
  PRESCRIPTION_FREQUENCY = 'prescription_frequency',
  // 处方单位
  PRESCRIPTION_UNIT = 'prescription_unit',
  // 处方生产厂商
  PRESCRIPTION_VENDOR = 'prescription_vendor',
}

// 获取后端字典数据列表
export const useGetOptionsByType = (
  type: DictTypeEnum,
  labelValue?: boolean,
) => {
  const {data} = useRequest(
    () => requestUtil.dict.dictSysDictTypeDictTypeGet(type),
    {
      manual: false,
    },
  );
  return data?.data?.rows?.map(item => ({
    label: item.dictLabel,
    value: labelValue ? item.dictLabel : item.dictValue,
  }));
};

useEffect如何区分生命周期

useEffect(异步执行副作用): 在函数组件中执行副作用操作 (用于模拟类组件中的生命周期钩子) , 在执行 DOM 更新之后调用 。

useEffect可以看成是 componentDidMountcomponentDidUpdatecomponentWillUnmount 三者的结合。

  • componentDidMount: 组件挂载完成 (开启监听, 发送ajax请求)
  • componentDidUpdate:组件更新完成
  • componentWillUnmount:组件即将卸载(收尾工作, 如: 清理定时器)

useEffect和useLayoutEffect的区别

相同点

  • useEffect和useLayoutEffect都是用于处理副作用,也就是改变DOM、设置订阅、操作定时器等
  • 使用方法一样,底层也一样,都是调用mountEffectlmpl方法,基本可以直接替换

不同点

  • useEffect在React渲染过程中是被异步调用的,用于绝大部分场景,会等待浏览器完成画面渲染之后才会延迟调用,并且在依赖改变时还会更新。而useLayoutEffect会在所有DOM变更后同步调用,主要处理页面闪烁灯问题,因为是同步,所以在较大计算量时会造成阻塞,使用 useLayoutEffect 时,它的回调函数会在 DOM 更新之后、浏览器进行下一次绘制之前立即执行可以用于进行 DOM 操作或获取 DOM 元素的尺寸和位置等计算

useMemo和useCallback

useMemo 的主要作用是在组件重新渲染时,用来缓存计算结果,以避免不必要的重复计算。它接收两个参数:一个回调函数和一个依赖数组。回调函数用于进行计算,而依赖数组用于指定在数组中列出的依赖项发生变化时,才重新计算并返回新的值,否则会返回上一次缓存的值。

const memoizedValue = useMemo(() => {
  // 进行耗时的计算
  return someValue;
}, [dependency1, dependency2]);

在上面的示例中,只有当 dependency1 或者 dependency2 发生变化时,useMemo 才会重新计算并返回新的值,否则会复用之前的值。

useCallback 的作用是在组件重新渲染时,返回一个记忆化的回调函数,以避免不必要的函数重新创建。它也接收两个参数:一个回调函数和一个依赖数组。当依赖项发生变化时,会返回一个新的回调函数,否则会复用之前的回调函数。

const memoizedCallback = useCallback(() => {
  // 处理事件的回调函数
}, [dependency1, dependency2]);

在这个示例中,只有当 dependency1 或者 dependency2 发生变化时,useCallback 才会返回一个新的回调函数,否则会返回之前的回调函数。

总结区别:
useMemo 主要用于缓存计算结果,适用于任何需要缓存值的场景。
useCallback 主要用于缓存回调函数,适用于需要传递给子组件的事件处理函数,以避免不必要的重新渲染。
另外,在大多数情况下,你不必在每个函数组件中都使用 useMemo 或 useCallback。
只有当你在性能测试中发现了性能问题,或者在特定情况下需要优化函数的创建和计算时,再考虑使用这些钩子。

setState的同步异步,调用setState后会发生什么

setState的同步异步
setState是一个异步方法,但是在setTimeout/setInterval等定时器里逃脱了React对它的掌控,变成了同步方法
每个setState都会被react加入到任务队列,多次对同一个state使用setState只会返回最后一次的结果,因为它不是立刻就更新,而是先放在队列中,等时机成熟在执行批量更新。
React18以后,使用了createRoot api后,所有setState都是异步批量执行的

调用setState后会发生什么

  • 调用setState后,react会将传入的参数对象和组件当前的状态合并,然后触发调和过程返回新的状态,react会根据新的状态构建新的DOM树,然后重新渲染

React Router常用API

react-router的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,可以实现无刷新的条件下切换显示不同的页面,可以通过前端路由实现单页面(SPA)应用。

react-router-dom的常用API

  • router组件(BrowserRouter、HashRouter)

    <BrowserRouter>HashRouter就是router根组件,用于包裹整个应用

  • 路由匹配组件(Routes 、 Route、switch)

    <Switch>适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹配

    v6版本中<Routes>替换<Switch><Routes><Route>要配合使用,且必须要用<Routes>包裹<Route>,当URL发生变化时,<Routes> 都会查看其所有子 <Route> 元素以找到最佳匹配并呈现组件

  • Navigation组件(Link、NavLink、Navigate、Redirect)

    <Link>:修改URL,且不发送网络请求(路由链接)

    <NavLink>:与<Link>组件类似,且可实现导航的“高亮”效果。

    <Navigate>:只要<Navigate>组件被渲染,就会修改路径,切换视图。

    <redirect>:用于路由的重定向

  • Outlet:当<Route>产生嵌套时,渲染其对应的后续子路由。

React Router有几种模式,以及实现原理?

React Router对应的hash模式history模式对应的组件为<HashRouter><BrowserRouter>,作为最顶层组件包裹其他组件。

hash模式:通过监听浏览器 onhashchange 事件变化,查找对应路由应用。

history模式:通过 html5 的history API 中新增的 pushState() 和 replaceState() 方法修改浏览器记录,改变页面路径。
区别

  • 兼容:hash 可以兼容到 IE8,而 history 只能兼容到 IE10。
  • 网络请求:使用 hash 模式,地址改变时通过 hashchange 事件,只会读取哈希符号后的内容,并不会发起任何网络请求。而 history 模式,每访问一个页面都要发起网络请求,每个请求都需要服务器进行路由匹配、数据库查询、生成HTML文档后再发送响应给浏览器,这个过程会消耗服务器的大量资源,给服务器的压力较大。
  • 服务器配置:hash 不需要服务器任何配置。history 进行刷新页面时,无法找到url对应的页面,会出现 404 问题。因为域名后面的路由是由前端控制的,后端只能保留域名部分,所以就会造成页面丢失的问题,需要服务器端将所有请求重定向到初始的HTML文件,并让React应用程序接管路由处理
    hash 模式更为简单直接,而对于复杂的多页面应用,history 模式可能更适合

类组件和函数组件

从实际开发、性能优化、趋势分析三方面分析:

  • 实际开发中,类组件是基于面向对象编程的,主打继承、生命周期等核心概念,而函数组件的内核是函数式编程,主打immutable/引用透明等
  • 性能优化上,类组件主要依靠React.PureComponent进行浅比较,从而控制shouldComponentUpdate 的返回值避免子组件重新渲染,而函数组件依靠React.memo缓存渲染结果来提高性能
  • 从趋势上,React官方更推崇 组合优于继承 的设计概念,所以未来函数组件成为主推方案的概率会大些

react组件设计模式

无状态组件(展示组件):只作展示、独立运行、不额外增加功能的组件,特点是复用性强,可分为代理组件样式组件布局组件

有状态组件(灵巧组件):包含业务逻辑和数据状态的组件称为有状态组件,或者灵巧组件,特点是功能丰富、复杂度高、复用性低,有状态组件分为 容器组件高阶组件

  • 容器组件 - 几乎没有复用性,主要用于拉取数据和组合组件
  • 高阶组件 - 实际是一个函数概念,一个函数可以接收另一个函数作为参数,然后返回一个函数,称为高阶函数

高阶组件(HOC)

高阶组件是参数为组件,返回值为新组件的函数。
作用:强化组件,复用逻辑,提升渲染性能等作用。

HOC是一个函数,它接受一个组件并返回一个新的组件。
HOC可以用来修改props或state之外的行为,增加新的props,在渲染前后执行逻辑等。
HOC可以链式调用,每个HOC添加某些功能,但这可能会导致嵌套地狱,因为组件包裹层可能会很深。

// 高阶组件 判断登录状态
const checkLogin = (Demo) => {
    return props => {
        const isLogin = true; // 这里是登录判断条件,根据实际处理,未登录则显示登录组件
        return isLogin ? <Demo {...props} /> : <LoginPage />
    }
}
// 调用高阶组件,函数写法
class Demo extends React.Component{
	...
}
const DemoPage = checkLogin(Demo)

createElement和cloneElement的区别

  • React.createElement和React.cloneElement都是用来创建react元素的,区别是传参不一样
  • createElement传入的第一个参数是react元素,而cloneElement第一个参数是element

受控组件和不受控组件

受控组件和非受控组件是两种不同的表单输入组件的概念,区别在于受不受react组件的控制

  • 受控组件是指通过react组件的 state 属性来维护维护值的表单组件,并通过 onChange 事件处理程序更新。受控组件提供了对表单数据的精确控制,比如实时现场验证
  • 非受控组件是指通过 DOM 元素自身维护值的表单组件。通过使用 ref 属性,可以访问到非受控组件的当前值。非受控组件更加简单,适用于简单的表单场景,不需要高度控制输入的值。比如提交时验证

组件状态保存(类似vue的keep-alive)

  • 状态保存:在交互过程中,因为某些原因需要临时离开交互场景,则需要对状态进行保存,类似的场景有已填写但未提交的表单数据的保存,
  • vue的keep-alive是把虚拟DOM 保存在内存中,React认为容易造成内存泄漏,所以官方不提供状态保存方案

1、手动保存状态

配合componentWillUnmount生命周期,通过redux之类的状态管理框架对数据进行保存,通过componentDidMount进行数据恢复,但数据量大的时候比较麻烦

2、通过路由实现保存 react-router

这个方法实现比较麻烦,原理是因为react状态丢失是由于路由切换卸载了组件引起的,所以从根本上改变路由对组件的渲染行为,有以下方式

  • 重写组件 - 可参考 react-live-route,实现成本比较高
  • 重写路由库 - 可参考 react-keeper, 实现成本和风险更高

3、模拟真实功能

github 有类似的实现插件 react-activation,实现原理是:由于react 会卸载掉处于固有组件层级内的组件,所以我们需要将children子属性抽取处理,渲染到一个不会被卸载的组件内,再使用DOM操作将其真实内容移到对应的内容,实现此功能

路由懒加载及实现原理

懒路由加载:用的时候才加载,如果不用路由懒加载,页面在第一次进入的时候,就请求了所有组件的数据,如果组件过多,页面可能会出现卡顿现象,应该是用户按哪个链接再请求哪个组件

实现原理

  • 通过React.lazy配合Webpack import()函数动态加载路由组件,路由组件代码会被分开打包

    React.lazy 方法返回的是一个 lazy 组件的对象,与 Promise 类似它具有 Pending、Resolved、Rejected 三个状态,分别代表组件的加载中、已加载、和加载失败三中状态。

  • 通过React.Suspense渲染 React.lazy 异步加载的组件,主要通过捕获组件的状态去判断如何加载,当这个组件的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件。如果遇到网络问题或是组件内部错误,页面的动态资源可能会加载失败,为了优雅降级,可以使用 Error boundary(错误边界) 来解决这个问题。

Redux是什么

什么时候需要状态管理工具?
状态管理工具的作用,就是状态的共享,当共享状态发生变化,所有使用方都会触发重新渲染。所以,状态需要被多方共享的时候,才需要使用状态管理工具了。比如:

  • 当前登录的用户信息,姓名,角色,所属组织等
  • 静态数据字典的缓存
  • 需要 keep-alive 的数据(不一定用)
  • 页面功能复杂,模块化后,模块之间仍需要共享的数据
    当react中context、React.forwardRef转发以及useImperativeHandle向父组件暴露一个自定义的 ref的存在,你可以不需要使用状态管理工具。这个因人而异。 类似的状态管理工具还有moxb、dva、

Redux是一个状态管理库,Redux Toolki是Redux官方推荐的编写 Redux 逻辑的方法。使用场景:

  • 跨层级组件数据共享与通信
  • 一些需要持久化的全局数据,比如用户登录信息

Store 一个全局状态管理对象
Reducer 一个纯函数,根据旧state和props更新新state
Action 改变状态的唯一方式是dispatch action
在这里插入图片描述

redux是一个实现状态集中管理的容器,遵循三大基本原则

  • 单一数据源
  • state是只读的
  • 使用纯函数来执行修改
  • store的数据,通过dispatch来派发action

redux工作流程

view 调用store的dispatch接收action传入store,reducer进行state操作,然后view通过store提供的getState获取最新的数据

reducer是纯函数,它规定应用程序的状态怎样因响应action而变化,reducer通过接收先前的状态和action来工作, 然后返回一个新的状态。
它根据操作的类型确定需要执行哪种更新,然后返回新的值,如果不需要完成任务,就会返回原来的状态

immutable

  • immutable 是指不可改变的数据,原理是 用结构共享数据的方法保存数据,当数据被修改时,会返回一个对象,但是这个的对象会尽可能的利用之前的数据结构而不会对内存造成浪费
  • 使用immutable可以给react应用带来性能的优化,主要体现在减少渲染次数。在react性能优化的时候,当传入子组件 props 或 state 不止一层,或者传入的是 Array 和 Object 类型时,React.PureComponent浅比较就失效了,这时也可以在 shouldComponentUpdate() 中使用使用 deepCopydeepCompare 来避免不必要的 render() ,但 deepCopydeepCompare 一般都是非常耗性能的。这个时候我们就需要 Immutable Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比较,提高性能。

在React项目是如何捕获错误的?

错误边界(Error boundary):是一种 React 组件,用来捕获后代组件错误,并打印这些错误,同时展示降级 UI,渲染出备用页面,而并不会渲染那些发生崩溃的子组件树
使用方法:
React组件在内部定义了getDerivedStateFromError或者componentDidCatch,它就是一个错误边界。getDerviedStateFromError和componentDidCatch的区别是前者展示降级UI,后者记录具体的错误信息,它只能用于class组件

vue的react的区别

相同点

  1. 组件化开发

Vue 3和React都采用了组件化开发的方式,使得代码具有更好的可维护性和重用性。在Vue 3中,组件可以被定义为一个对象,并且包含一个template、script和style标签。在React中,组件可以被定义为一个类或者函数,并且采用JSX语法来描述组件的结构和行为。

  1. 响应式数据绑定

Vue 3和React都支持响应式数据绑定,这意味着当数据变化时,UI界面会自动更新。在Vue 3中,数据变化会触发视图的重新渲染,而在React中则会调用组件的render方法重新生成虚拟DOM树。

  1. 虚拟DOM机制

Vue 3和React都采用了虚拟DOM机制来进行高效的页面更新。虚拟DOM是一个轻量级的JavaScript对象,它描述了UI界面的状态和结构,当数据发生变化时,框架会通过比较前后两个虚拟DOM树的差异来进行页面更新。

  1. 组件之间的通信

Vue 3和React都支持父子组件之间的通信。在Vue 3中,通过props和$emit方法可以实现父子组件之间的数据传递和事件监听;在React中,则通过props和回调函数来实现同样的功能。
不同点

  1. 响应式数据绑定的实现方式不同

Vue 3使用了Proxy API来实现响应式数据绑定,这使得代码更加简洁易懂,并且能够支持动态添加和删除属性。而React则需要使用setState方法来触发视图的重新渲染,这使得代码相对复杂一些。

Vue 3的响应式API还允许开发者在模板中直接使用响应式数据,而React则需要手动将数据传递给组件。

  1. 组件状态管理的实现方式不同

Vue 3引入了Composition API,使得组件状态管理更加灵活和可维护。开发者可以将逻辑相关的代码封装为单独的函数,从而实现更好的代码复用和组织。

React则通过生命周期方法和hooks来管理组件状态,虽然也能够实现相同的功能,但是代码相对较为冗长。

  1. 组件渲染方式不同

Vue 3采用了template语法来描述组件的结构和行为,这使得代码可读性更高,并且能够更好地与设计师协作。在模板中可以使用if、for等语句来实现复杂的逻辑控制。

React则采用JSX语法来描述组件的结构和行为,这使得代码更加灵活,并且能够更好地与JavaScript集成。但是,由于JSX需要手动添加标签,因此代码可读性相对较差。

  1. API设计风格不同

Vue 3的API设计倾向于提供语法糖和便捷方法,使得开发者能够更加高效地编写代码。例如,Vue 3中提供了v-model指令来实现双向数据绑定,在处理表单等情况下非常方便。

React则倾向于提供一些基础API,并且鼓励开发者自行封装复杂的功能。这样做可以让代码更加灵活和可扩展,但是需要花费更多的时间和精力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值