React
关于 React 相关知识点总结, 持续更新中……
Fiber
什么是fiber,fiber解决了什么问题
在React16以前,React更新是通过树的深度优先遍历完成的,遍历是不能中断的,当树的层级深就会产生栈的层级过深,页面渲染速度变慢的问题,为了解决这个问题才引入了 fiber
,React fiber
可以说就是 虚拟DOM,它是一个链表结构,返回了return、children、siblings
,分别代表父 fiber
,子 fiber
和兄弟 fiber
,随时可中断。
fiber
是协程,是比线程更小的单元,可以被人为中断和恢复,当 React
更新时间超过1帧时,会产生视觉卡顿的效果,因此我们可以通过fiber把浏览器渲染过程分段执行,每执行一会就让出主线程控制权,执行优先级更高的任务,从而实现增量渲染,增量渲染指的是把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里。增量渲染是为了实现任务的可中断、可恢复,并按优先级处理任务,从而达到更顺滑的用户体验。
fiber
是一个链表结构,它有三个指针,分别记录了当前节点的下一个兄弟节点,子节点,父节点。当遍历中断时,它是可以恢复的,只需要保留当前节点的索引,就能根据索引找到对应的节点。
Fiber
它主要解决了 React 在处理大型应用和复杂更新场景时的性能问题。Fiber 的目标是提高 React 在渲染和更新组件时的性能,并支持更高效的任务调度。
Fiber 是一种架构,让render 阶段可以调度任务的优先级
也是一种数据结构,通过两颗缓存树 current Fiber
和 workInProgress 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
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聚合了多个生命周期函数。
- 解决业务逻辑难以拆分的问题,难以复用的问题 类组件封装只能通过
HOC
、render props
- 使状态逻辑复用变的简单可行
- 函数组件从设计理念来看,更适合react(函数式编程)
函数组件缺点
-
hooks还不能完整的为函数组件提供类组件的能力,举两个例子:
1、需要根据 props 状态,更新state,在类组件,可以直接使用
getDerviedStateFromProps
来更新state,在函数组件中也可以写代码根据props更新state,但这样做会造成重复渲染。 如果遇到需要根据props更新state的情况,应该考虑做状态提升。 如果你发现在某个组件中必须要根据props更新state又无法做状态提升,那么该组件应该写成类式组件,而不是函数式组件。2、错误边界,
ErrorBoundary
,componentDidCatch
无法在 hooks 中找到对应 -
函数组件给了我们一定程度的自由,却也对开发者的水平提出了更高的要求
约定式,useXXX 开头,数组 [状态,修改状态的方法] = UseXXX(initData)
-
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;
由上可以看出,在我们使用 useState
和 useEffect
Hooks 来获取数据,并在数据获取过程中设置 isLoading
为 true。我们将 data
和 isLoading
作为属性传递给 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