Skip to main content

React

关于 React 相关知识点总结, 持续更新中……

Fiber

什么是fiber,fiber解决了什么问题

在React16以前,React更新是通过树的深度优先遍历完成的,遍历是不能中断的,当树的层级深就会产生栈的层级过深,页面渲染速度变慢的问题,为了解决这个问题才引入了 fiberReact fiber 可以说就是 虚拟DOM,它是一个链表结构,返回了return、children、siblings,分别代表父 fiber,子 fiber 和兄弟 fiber,随时可中断。

fiber是协程,是比线程更小的单元,可以被人为中断和恢复,当 React 更新时间超过1帧时,会产生视觉卡顿的效果,因此我们可以通过fiber把浏览器渲染过程分段执行,每执行一会就让出主线程控制权,执行优先级更高的任务,从而实现增量渲染增量渲染指的是把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里。增量渲染是为了实现任务的可中断、可恢复,并按优先级处理任务,从而达到更顺滑的用户体验。

fiber 是一个链表结构,它有三个指针,分别记录了当前节点的下一个兄弟节点,子节点,父节点。当遍历中断时,它是可以恢复的,只需要保留当前节点的索引,就能根据索引找到对应的节点。

Fiber

它主要解决了 React 在处理大型应用和复杂更新场景时的性能问题。Fiber 的目标是提高 React 在渲染和更新组件时的性能,并支持更高效的任务调度。 Fiber 是一种架构,让render 阶段可以调度任务的优先级 也是一种数据结构,通过两颗缓存树 current FiberworkInProgress Filber ,一个用于展示,一个用于操作,当一轮操作完成后,react 才会切换到操作完成的Fiber 节点上,另一棵树会销毁重新构建用于操作。 每个节点进行对比是会进行一次 shouldYield() 判断,利用空闲时间去更新。 高优先级任务使用 RequestAnimatonFrame, 低优先级使用 RequestIdleCallback

从某种意义上来说 fiber 就是 vdom,但是 fiber != vdom 这里所谓的调度就是,调度器允许给任务分配不同的优先级,高优先级限制性,

diff算法

  • 用JS对象模拟DOM(虚拟DOM)
  • 把此虚拟DOM转成真实DOM并插入页面中(render)
  • 如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
  • 把差异对象应用到真正的DOM树上(patch)

react 的 diff 主要有三种策略:

  • tree diff 两个树对比时,只会比较同一层级的节点,会忽略掉跨层级
  • component diff 在对比两个组件时,首先会判断它们两个的类型是否相同,如果不同,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点
  • element diff 对于同一层级的一组节点,会使用具有唯一性的key来区分是否需要创建,删除,或者是移动。React diff 提供了三种节点操作,分别为:
    • INSERT_MARKUP(插入)
    • MOVE_EXISTING(移动)
    • REMOVE_NODE(删除)

diff 的大致流程:

react 检测到组件状态或属性发生改变,会出发重新渲染,从而生成新的vdom树, 然后diff算法对新旧vdom 树进行深度优先遍历,比较节点的类型属性以及存在的key, 如果节点发生变化,会销毁旧的节点,及其子节点,重新生成新的节点,属性则会保留节点,只更新属性, 最后react 根据计算结果得到的更新之后的vdom 更新在真是dom,保持一致。 主要作用是减少直接操作dom 导致的重排重绘,减少开销,提高效率

React diff 采用的是递增法,就是通过比较新的列表中的节点,在原本列表的位置是否有递增,来判断节点是否需要移动。

虚拟dom

就是 js和dom之间的一种映射吧,可以展现dom的结构还有属性,主要作用是在记录差异,最后把差异更新到真正的dom中 虚拟DOM 实际就是一个JS 对象,其结构主要包括:

  • type 实际的标签
  • props 标签内部的属性
  • children 节点内容,依次循环 相比较真实的DOM 结构,其内部结构很简单,真实DOM 结构中还有属性和一系列方法, 虽然他们都是默认的,相比较而言,虚拟Dom 没有那么重,操作起来对比真实Dom 要简单跨界很多,性能也会更好。

虚拟Dom 大致工作流程

  • 挂载阶段 React 结合 JSX, 构建出虚拟Dom 树,然后通过其与Dom 的映射关系, 然后通过 render 触发渲染
  • 更新阶段 页面发生变化,在作用到Dom 之前,会先映射生成新的虚拟dom 树,然后经过 diff 算法,对比新旧虚拟Dom 树,对比具体的改变,然后再将改变作用到真实Dom。

React 中 key 作用

key 是给每一个 vnode 的唯一 id,可以依靠 key,更准确,更快的拿到 oldVnode 中对应的 vnode 节点

循环中不建议使用index,当我们在操作更新数据的时候,会导致所有的dom 重新渲染,效率低下,还有就是有可能导致更新不符合预期 每次都是用不一样的key,比如时间戳或者随机数等等,会造成每次key不一样,导致许多组件实例和DOM节点被不必要地重新创建,这可能导致性能下降和子组件中的丢失状态

事件处理系统

React 的事件系统(Event System)是一种统一的事件处理机制,它允许开发者在 React 应用中处理原生浏览器事件,如点击、输入、滚动等。React 事件系统的主要目标是提供一种跨浏览器的、一致的事件处理方式,并实现性能优化。

React 事件系统的实现主要包括以下几个方面:

  • 合成事件(Synthetic Events):React 将原生浏览器事件包装为合成事件对象,这些合成事件具有与原生事件相似的接口,但提供了跨浏览器的一致性。这意味着无论在哪个浏览器中运行,React 的事件处理行为都是相同的。

  • 事件委托(Event Delegation):React 事件系统采用事件委托的方式处理事件。这意味着 React 并不是将事件监听器绑定到每个实际的 DOM 节点上,而是将事件监听器绑定到组件树的根节点(通常是 document)。当事件触发时,React 利用事件冒泡机制捕获事件,然后根据事件目标和事件类型确定需要触发的事件处理函数。这种方式可以减少事件监听器的数量,降低内存占用,提高性能。

  • 事件池(Event Pooling):为了降低内存占用,React 使用事件池来重用合成事件对象。在事件处理函数执行完毕后,React 会将合成事件对象的属性清空,并将其放回事件池,以便后续的事件处理重用。这有助于减少垃圾回收的开销,提高性能。

  • 自动绑定(Autobinding):在类组件中,React 会自动绑定事件处理函数到组件实例。这意味着在事件处理函数中,开发者可以通过 this 关键字访问组件实例。这种自动绑定便于开发者在事件处理函数中访问组件状态和属性。在函数组件中,可以使用 React Hooks(如 useState 和 useEffect)来处理组件状态和副作用。

总之,React 事件系统通过合成事件、事件委托、事件池和自动绑定等技术实现了一种跨浏览器、高性能的事件处理机制。这使得 React 开发者能够更轻松地处理浏览器事件,同时保持应用性能。

生命周期

直接上图,新旧对比

旧版

新版本

下面就不介绍旧版本了,相信现在很少人会使用旧版本了

16.8 以后生命周期

  • constructor

constructor() 是在React组件挂载之前被调用,在为React.Component子类实现构造函数时,应在其他语句之前调用 super() (将父类的this对象继承给子类)。

通常,React构造函数仅用于以下两种情况:

  • 来初始化函数内部 state
  • 为 事件处理函数 绑定实例
  • getDerivedStateFromProps

getDerivedStateFromProps() 在调用 render方法之前调用,在初始化和后续更新都会被调用

返回值:返回一个对象来更新 state, 如果返回 null 则不更新任何内容

参数: 第一个参数为即将更新的 props, 第二个参数为上一个状态的 state , 可以比较props 和 state来加一些限制条件,防止无用的state更新

注意:getDerivedStateFromProps 是一个静态函数,不能使用this, 也就是只能作一些无副作用的操作

render() 方法是class组件中唯一必须实现的方法,用于渲染dom, render()方法必须返回reactDOM

  • componentDidMount

componentDidMount() 在组件挂载后 (插入DOM树后) 立即调用,componentDidMount() 是发送网络请求、启用事件监听方法的好时机,并且可以在 此钩子函数里直接调用 setState()

  • shouldComponentUpdate

shouldComponentUpdate() 在组件更新之前调用,可以控制组件是否进行更新, 返回true时组件更新, 返回false则不更新, 包含两个参数,

  • 第一个是即将更新的 props 值,
  • 第二个是即将跟新后的 state 值, 可以根据更新前后的 props 或 state 来比较加一些限制条件,决定是否更新,进行性能优化

可以使用内置 PureComponent 组件替代

不要 shouldComponentUpdate 中调用 setState(),否则会导致无限循环调用更新、渲染,直至浏览器内存崩溃

  • getSnapshotBeforeUpdate

    getSnapshotBeforeUpdate() 在最近一次的渲染输出被提交之前调用。也就是说,在 render 之后,即将对组件进行挂载时调用。

    它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给 componentDidUpdate()。如不需要传递任何值,那么请返回 null

  • componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行,包含三个参数,第一个是上一次props值。 第二个是上一次state值。如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),第三个是“snapshot” 参数传递。

可以进行前后props的比较进行条件语句的限制,来进行 setState() , 否则会导致死循环

  • componentWillUnmount

    componentWillUnmount() 在组件即将被卸载或销毁时进行调用。此生命周期是取消网络请求、移除监听事件、清理 DOM 元素、清理定时器等操作的好时机。

执行顺序

  • 创建时

    • constructor()
    • static getDerivedStateFromProps()
    • render()
    • componentDidMount()
  • 更新时

    • static getDerivedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()
  • 卸载时

    • componentWillUnmount()

函数组件和类组件

类组件的根基是 OOP(面向对象编程),所以它有继承、有属性、有内部状态的管理。

函数组件的根基是 FP (函数式编程)。它属于“结构化编程”的一种,与数学函数思想类似。也就是假定输入与输出存在某种特定的映射关系,那么输入一定的情况下,输出必然是确定的。

区别:

  • 类组件需要声明constructor,函数组件不需要
  • 类组件需要手动绑定this,函数组件不需要
  • 类组件有生命周期钩子,函数组件没有
  • 类组件可以定义并维护自己的state,属于有状态组件,函数组件是无状态组件
  • 类组件需要继承class,函数组件不需要
  • 类组件使用的是面向对象的方法,封装:组件属性和方法都封装在组件内部 继承:通过extends React.Component继承;函数组件使用的是函数式编程思想

函数组件优点

  • 告别难以理解的class组件,生命周期,比如:useEffect聚合了多个生命周期函数。
  • 解决业务逻辑难以拆分的问题,难以复用的问题 类组件封装只能通过 HOCrender props
  • 使状态逻辑复用变的简单可行
  • 函数组件从设计理念来看,更适合react(函数式编程)

函数组件缺点

  • hooks还不能完整的为函数组件提供类组件的能力,举两个例子:

    1、需要根据 props 状态,更新state,在类组件,可以直接使用 getDerviedStateFromProps 来更新state,在函数组件中也可以写代码根据props更新state,但这样做会造成重复渲染。 如果遇到需要根据props更新state的情况,应该考虑做状态提升。 如果你发现在某个组件中必须要根据props更新state又无法做状态提升,那么该组件应该写成类式组件,而不是函数式组件。

    2、错误边界, ErrorBoundary, componentDidCatch 无法在 hooks 中找到对应

  • 函数组件给了我们一定程度的自由,却也对开发者的水平提出了更高的要求

    约定式,useXXX 开头,数组 [状态,修改状态的方法] = UseXXX(initData)

  • Hooks 在使用层面有着严格的规则约束

    hooks 只能在顶层, 不能在条件,函数,循环,嵌套中使用

  • 函数组件中不能监听组件的生命周期, 错误边界等。

Fragment

Fragment提供了一种将子列表分组又不产生额外DOM节点的方法, 直接上代码

// 
<React.Fragment>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</React.Fragment>

// 可以简写为空标签的形式
<>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</>

高阶组件(HOC)

所谓高阶组件就是 参数是组件,返回值也是一个组件的函数。

高阶组件的应用场景:

  • 复用逻辑
  • 修改 props
  • 条件渲染
  • 提供额外的功能

直接上例子:

export function CommonLoading(ArgComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <ArgComponent {...props} />;
};
}

export default CommonLoading;

再次封装

import CommonLoading from "./CommonLoading";

function DataList({ data }) {
return (
<div>
{data.map((item, index) => (
<div key={item.id}>{item}</div>
))}
</div>
);
}

const LoadingDataList = CommonLoading(DataList);

export default LoadingDataList;

从上面的例子可以看到,导入了 CommonLoading 高阶组件,然后将 DataList 组件传递到 CommonLoading,包装成新的组件 LoadingDataList

使用中

import React, { useState, useEffect } from "react";
import LoadingDataList from "./LoadingDataList";

function Index() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const response = await fetch(...);
setData(data);
setIsLoading(false);
};

fetchData();
}, []);

return (
<div>
<h1>Data List</h1>
<LoadingDataList data={data} isLoading={isLoading} />
</div>
);
}

export default Index;

由上可以看出,在我们使用 useStateuseEffect Hooks 来获取数据,并在数据获取过程中设置 isLoading 为 true。我们将 dataisLoading 作为属性传递给 LoadingDataList 组件。当数据正在加载时,组件将显示加载指示器;当数据加载完成时,组件将显示数据列表。

错误边界

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误。并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

捕捉的错误不包括:自身抛出的错误,事件处理,异步代码,服务端渲染。

function ErrorBoundary(WrappedComponent) {
return class ErrorBoundaryComponent extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError() {
return { hasError: true };
}

componentDidCatch(error, info) {
// 处理错误记录
console.error("Error:", error, "Info:", info);
}

render() {
if (this.state.hasError) {
return <div>Something went wrong. Please try again later.</div>;
}
return <WrappedComponent {...this.props} />;
}
};
}

export default ErrorBoundary;

// 使用
import React from "react";
import ErrorBoundary from "./ErrorBoundary";

function UserPage({ user }) {
// 用户相关
}

const userErrorBoundary = ErrorBoundary(UserPage);
export default userErrorBoundary;

Portal

Portal 字面含义为 入口/门,在react 中,它提供了让子组件渲染在除了父组件之外的DOM节点的方式, 它可以接收两个参数,第一个是需要渲染的React元素,第二个是渲染的地方(DOM元素)

主要用途是组件定义渲染的地方,例如:弹框,提醒,提示框等

ReactDOM.createPortal(child,container)

React 18 更新了哪些东西?

  • 1、引入了新的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()
  • 2、setState自动批处理

  • 3、react组件返回值更新

    • 在react17中,返回空组件只能返回null,显式返回undefined会报错
    • 在react18中,支持null和undefined返回
  • 4、strict mode更新

    当你使用严格模式时,React会对每个组件返回两次渲染,以便你观察一些意想不到的结果,在react17中去掉了一次渲染的控制台日志,以便让日志容易阅读。react18取消了这个限制,第二次渲染会以浅灰色出现在控制台日志

  • 5、Concurrent Mode

    并发模式不是一个功能,而是一个底层设计。 它可以帮助应用保持响应,根据用户的设备性能和网速进行调整,它通过渲染可中断来修复阻塞渲染机制。 在concurrent模式中,React可以同时更新多个状态,区别就是使同步不可中断更新变成了异步可中断更新

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

  • 6、hooks (详细可见下一章节 Hooks)

    • useSyncExternalStore
    • useInsertionEffect
    • useId