文章为 Youtube 视频 《Every React Concept Explained in 12 Minutes》 的总结和归纳,感兴趣的同学请自行前往观看,懒就看我总结的吧 ~ 如果你已经对这些概念很熟悉,不妨跳转到本文底部针对这些概念的 面试题 检验下?
好的,废话不多说,正文开始。
本篇文章将介绍 React 的核心概念和基本用法,包括 组件、JSX、props、key、state、hooks、context、portal、Suspense和ErrorBoundary, React中的渲染过程、事件处理和副作用、如何保持 React 组件的纯洁性、 strict mode 的使用以及如何 使用 useEffect 处理外部系统,ErrorBoundary 处理错误边界。 旨在让对 React 认识不多的同学们快速认识这些核心概念
众所周知, React 是一个 JavaScript 库, 它通过组件和 JSX 语法来构建用户界面。 组件是 React 应用程序的基本构建快, 可以制作应用程序的可见部分,如按钮和整个页面。 React 使用虚拟 DOM和协调机制来更新真实 DOM,并提供内置事件处理和状态管理功能。 使用 React 可以创建纯净的组件, 避免不必要的重新渲染和性能问题。
Components - 组件
- 组件是每个 React 应用程序的构建快,每个组件就是 UI 的一部分,一个网站就像搭乐高积木一样,可以是很小的一个积木,也可以是多个积木组合起来拼成更大的积木。并且组件是可复用的,你可以使用任意多次。
- 小到一个元素, 如
<Button / >, <Inputs / >, ..., - 或者布局组件, 如
<Header />, <Content />, <Footer />, <Sidebar> .. - 甚至整个页面, 如
<404 />,
- 小到一个元素, 如
那 组件 到底是什么? - React 组件其实就是返回一段 JSX 的 JavaScript 函数。
什么又是 JSX 啊? 下面会介绍,先来看下一个典型的组件长什么样子。
function SweetButton(){
return <button className=“btn”>Click me</button>
}
JSX - JavaScript XML
JSX 是 JavaScript 语法的一种拓展, 可以使用 JS 写出类似 HTML 的代码, 直观和简洁地描述 UI, 通过Babel转译为JS代码从而让浏览器得以识别, 它实际上是 createElement('button',null,'🙂') 的语法糖, 但因为 JSX 写起来更便捷, 每个人都喜欢用,甚至影响到了其他框架,如 Vue,Solid,Preact 等都可以使用 JSX 。
// JSX 代码
const element = <h1>Hello, world!</h1>;
// Babel 转换后的代码
const element = React.createElement("h1", null, "Hello, world!");
- 因为是 JavaScript, 所以不能像编写 HTML 属性那样,需要驼峰命名 camelCase来编写
<button class="btn"> // 这是 HTML
<button className="btn"> // 这是 JSX
Curly Braces - 大括号
遇到 () 想到 HTML,遇到 {} 想到 JavaScript
JSX 可以编写 HTML,那很酷,但那是静态的!如何能让它动起来呢? 这便是 JSX 添加交互的桥梁 - {} Curly Braces 大括号, JSX 具备 JavaScript 的全部功能, 当你想要使用 JavaScript,那么便先输入 {} 吧!
// E.g.1 动态数据
const wife = '刻晴';
<div>嗨,{wife}</div>
// E.g.2 动态属性
const url = './xxx.jpg'
<img src={url} />
// E.g.3 动态样式
const background = 'red'
<div style={{ background }} />
// E.g.4 条件判断
const isLoggedIn = true;
<p>{isLoggedIn ? 'You are logged in' : 'Please log in'}</p>
<button disabled={!isLoggedIn}>Submit</button>
Fragments - 碎片
因为 JavaScript 函数只能返回一个返回值,对应地在 React 中只能返回一个父元素,如果你尝试这么写,
会遇到 JSX expressions must have one parent element 这个报错
function App(){
return (
<Header />
<Main />
)
}
解决它的方法之一是将所有元素包裹在同一个父元素中,如空标签<div>, 或者不想添加额外标签的 Fragments
function App(){
return (
<>
<Header />
<Main />
</>
)
}
Props - 属性
Ok,现在我了解了 React 组件,但是如果想将数据传递到另一个组件中,就需要了解叫 props 的东西, 意为 属性(properties) 的意思。
- props 指的是对象上的属性,每个组件的参数中获得的属性
- 如果组件 A 包裹组件 B,那么我们可以说组件 A 是组件 B 的父组件
- props 是一个只读的对象, 子组件无法修改父组件的 props (单项数据流)
<Hello text={'你好哇👋'}> // 1. 创建 props, 并赋值
function Hello(props) {
return <h1>{props.text}</h1> // 2. 使用 props
}
Children - 特殊的 props
上面👆我们了解到 Props,那么 Props 可以传递任何东西吗? - Yes, 你甚至可以将其他组件作为 props 传递到该组件,这便是 Children
你只需要将组件打开,并放置你想要传递的组件,然后到该组件使用 Children 接受该组件即可。
如果你对编程范式有了解,你可能意识到这叫
(组合 - Composition), 以更好的方式组织 React 组件。Children 对于创建布局组件非常有用。
<Parent>
<Child />
</Parent>
function Partent(props){
return <div>{props.children}</div>
}
Keys - 键
key 是 React 的内置 prop,key prop 的作用是告诉 React 如何区分不同的组件
通常在使用 map() 函数创建一个列表时,key 需要是稳定的、唯一的字符串或数字,用于标识一个组件,如果你忘记添加了 key, React 会告诉你 Each child in a list should have a unique "key" prop , 如果没有添加 key 或者使用会变化的 key,这可能会导致 React 组件渲染不一致等错误, 开发人员可以使用 key 属性暗示哪些子元素在不同的渲染中可能是稳定的。
const people = [{
id: 0,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
}]
const listItems = people.map(
person => {
<li key={person.id}>{person.name}</li>
});
常见错误使用 key 的案例
key={Math.random()}
点击查看解释
key 每次都是随机, 这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建, 这不仅会造成运行变慢的问题,更有可能导致用户输入的丢失。
如果原始数据源没有唯一标识符, 那么我们可以使用内置 hook - useId() 来生成唯一的 id。
import { useId } from 'react';
function Contact({ person }) {
const id = useId();
const { firstName, lastName } = person;
return (
<li key={id}>
<span>{ firstName }</span>
<span>{ lastName }</span>
</li>
);
}
function ContactList({ directory }) {
return (
<ul>
{directory.map((person) => (
<Contact key={person.id} person={person} />
))}
</ul>
);
}
key={index}
如果你没有显式添加 key, React 会默认使用 index 作为 key, 但 React 并不推荐这么做,因为当数组项的顺序在插入、删除或者重新排序等操作中会发生改变,此时把索引顺序index 用作 key 值会造成性能问题甚至可能会产生一些微妙且令人困惑的 bug, 比如您有 [A,B,C] 并删除 B ,这时候 索引 1 将代表 C 而不是 B, React 需要重新渲染所有元素,而不是仅仅删除该项。
从第一个列表中删除项目时,我们可以看到整个列表正在重新渲染,而在第二个列表中,原始列表保持不变,只是删除了目标元素。
Rendering - 渲染
讲了这么多, React 是怎么通过编写代码而使其在浏览器中显示 UI 呢, 这个过程被称为渲染(Rendering)。
React 重新渲染的步骤
- State 改变? -> 更新 VDOM
- React 进行 "diffs" -> Diff 算法:查找更新真实 DOM 所需的最少步骤数的算法, 不同类型的两个元素会产生不同的树, 比较更新的 VDOM 和先前的 VDOM
- "Reconciliation" with the DOM -> 协调:将虚拟 DOM 与真实 DOM 同步的过程
Vitrual DOM & DOM
Virtual DOM:它是真实 DOM 的轻量级表示,存储在内存中并且从不渲染
DOM: Document Object Model - 文档对象模型, 它表示文档的层次化结构
Diff & Reconciliation
当 React 应用程序中的 state 发生变化时,React 会进行虚拟 DOM 的 Diff 和Reconciliation,以确定需要更新哪些部分的 UI - 此处查看 源码
简单来说, diff 是指比较两棵虚拟 DOM 树(前一次渲染的树和当前渲染的树)之间的差异,找出发生变化的节点。React 会使用一种高效的算法来比较两棵树的结构和内容,以找出最小的变化集合,从而最小化 DOM 操作的次数
Reconciliation 则是负责找出变化的组件, 根据 Diff 找出的差异,更新实际的 DOM 树,以保持 UI 与应用程序状态的同步。在协调阶段(Reconciliation),组件复用的前提是必须满足三个条件: 同一层级下、同一类型、同一key值
如果你还想深入了解,那么这些材料值得推荐去阅读
Fiber Node
Fiber 是 React 中用于实现调和(Reconciliation)过程的新的架构。它是 React 16 中引入的重大改进,主要解决 React 在处理大型应用程序和复杂 UI 时可能遇到的性能问题。
| 没有fiber | 有fiber |
|---|---|
在之前的版本,Reconciliation 是不可中断的,React 会一次性遍历整个虚拟 DOM 树,进行比较和更新,如果虚拟 DOM 树比较大,会阻塞主线程,导致 UI 卡顿。 (JS 运行时间长,单位时间内 CSS 渲染时间少,渲染出的画面少,帧数低,出现卡顿)
假设遍历发生中断,但是找不到其父节点 —— 因为每个节点只有其子节点的指向。断点没有办法恢复,只能从头再来一遍
Fiber 架构通过引入一种可中断和恢复的调和过程,将之前的调和过程拆分成不同的小任务(生成 fiber 节点, 每个 fiber 节点拥有 3 个指针分别指向父节点 return, 子节点 child,兄弟节点 sibling),这三个指针用于描述组件树中节点之间的关系,即便中断, fiber 节点也可以记住当前位置并恢复之前的进度。React 还使用了一种称为协作式调度(Cooperative Scheduling)的算法来处理 Fiber 节点。该算法基于优先级和循环,使得 React 可以在多个渲染周期中分批处理更新,并根据更新的紧急程度来动态调整任务的执行顺序
总之,新的 fiber 架构使得 React 可以根据需要暂停和恢复任务的执行,以响应用户的交互或其他异步事件, Reconciliation 具有可中断性和恢复性,fiber 通过任务切片、时间切片的方式避免长时间阻塞主线程,尽管没有减少 JS 的运行时间,但在单位时间内获得更多渲染时间,一秒内可以获得更多动画帧,从而解决卡顿的性能的问题。
如果你还想深入了解,那么这些材料值得推荐去阅读
- 阿里三面:灵魂拷问——有react fiber,为什么不需要vue fiber呢? - 掘金
- React Fiber Architecture - Github
- inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react - Medium
React 重新渲染的原因
- State 改变
- 父组件重新渲染
- Context 改变
- hooks 改变
当组件的 props 改变时, 就会发生重新渲染, 这句话其本身是不正确的, 因为要改变 props , 父组件必须会重新渲染, 将导致子组件的重新渲染, 而无论其 props 如何变化。 - 强烈推荐阅读React re-renders guide: everything, all at once
有时候我们可能会做错事而导致 React 无限地重复渲染,这会导致应用程序崩溃。
Too many re-renders. React limits the number of renders to prevent an infinite loop
import React, { useState } from 'react';
function RenderCountExceedExample() {
const [count, setCount] = useState(0);
// 每次组件渲染时都会执行setCount, state 改变导致组件的重新渲染,无限渲染循环
setCount(count + 1);
return (
<p>Count: {count}</p>
);
}
export default RenderCountExceedExample;
Event Handling - 事件处理
React 需要通过事件去处理用户的交互, 将在响应交互时触发, 如
- 鼠标点击、移动,键盘事件
- 如点击、悬停、表单输入框获得焦点等
事件处理函数是执行副作用的最佳位置, 与渲染函数不同, 事件处理函数不需要是纯函数,因此它是用来更改某些值的绝佳位置。例如,更改输入框的值以响应键入,或者更改列表以响应按钮的触发
React 内部自定义了一套事件系统,有许多内置事件如 onClick, onChange, onSubmit, 也称为合成事件(SyntheticEvent), 是React提供的一种事件处理机制,它抽象了浏览器原生事件,提供了一些额外的特性和优化, 旨在磨平浏览器之间的差异。
使用方法: 需要先 1. 定义一个函数, 2. 将其作为 prop 传入 合适的 JSX 标签
必须传递事件处理函数,而非函数调用!
onClick={handleClick} ,不是 onClick={handleClick()}onClick= { () => handleClick(id) }传递参数onClick= { () => {e.stopPropagation(); handleClick(id); }}执行多个事件
State - 状态
State 是 React 内部管理数据的方式, 每个 state 可以看作是一张“快照(snapshot)”
为了管理 React 应用程序内部的数据, 我们需要使用 State. 如果只是普通的 JavaScript 变量, 它们不会导致应用程序重新渲染,而是使用 useState/useReducer, 如果需要更新 State 的值, 使用调用 setState 函数来更新该状态。
React 组件重新渲染的根本原因就是 State 的改变
// 返回一个数组, 第一项为 state, 第二项为更新该 state 的函数, 需要给定初始值
const [like, setLike] = useState<number>(0);
// 返回一个数组,第一项为 state, 第二项为 dispatch 函数,一个触发状态更新的函数
const [state, dispatch] = useReducer(reducer, initialArg, init?)
Controlled Components - 受控组件
受控组件是指表单元素的值由 React 组件的状态(state)来控制。这意味着每当用户输入内容时, 对应的状态会更新, 并且由 React 来管理输入框的值。如 <input> <select> <textearea> 等组件
React 官方推荐使用受控组件的形式
function ControlledInput(){
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e)=>setValue(e.target.value)}
/>
)
}
Hooks - 钩子
React hook 是 React 16.8 引入的新特性, 它可以让你在不编写 class 的情况下使用 state 和其他 React 特性, 它解决了 class 组件的一些问题, 使得函数组件也可以拥有状态和生命周期等特性, 没有 hook 之前, 函数组件几乎只能用来展示纯 UI。
hook 使得函数组件也可以拥有状态和生命周期等特性, 使得代码更加简洁, 逻辑更加清晰, 复用状态逻辑变得更加容易, 自定义 Hook 使得组件之间的逻辑复用变得更加容易
但是 hooks 使用存在以下限制:
- 只能在
React 函数顶层使用, 且不能在 循环, 条件或嵌套函数 中调用 Hook - 自定义 Hooks
必须以 use 开头, 这是 React 的规范, 以便于区分普通函数和自定义 Hook
目前可以将内置 hook 分为以下五种:
- state hook:
useState(), useReducer() - context hook:
useContext() - ref hook:
useRef() - effect hook:
useEffect() - performance hook:
useMemo(), useCallback()
其中最常用的便是 useState, useEffect, useRef
Purity - 纯度
纯函数是理论上一个 React 组件工作的方式。 为什么说理论上呢? 因为 React 并不是纯粹的函数式编程,它不是一个纯函数式编程框架, React 并没有完全遵循函数式编程的所有原则和范式, 比如你还可以在 React 中使用 let 等。 如果你对 函数式编程 感兴趣的可以自己探讨,本文不过多展开。
纯函数指的是一个函数相同的输入总是返回相同的输出
一个 React 纯组件 有以下两个特征
- 只返回 JSX
- 不改变渲染之前已经存在的东西(对象或变量)
import React from 'react';
// 纯函数组件
const PureFunctionComponent = ({ name }) => {
return (
<div>
<h1>Hello, {name}!</h1>
<p>This is a pure function component.</p>
</div>
);
}
export default PureFunctionComponent;
这个组件没有内部状态,也没有副作用,只是接受输入并输出界面。
它符合纯函数组件的定义,即给定相同的输入,它总是会产生相同的输出,不会影响外部状态或产生副作用。
这是一个函数不纯的例子:
函数相同的输入无法得到相同的输出,这个函数的输出是无法预测的, 可以说这个函数“不纯”
Strict Mode - 严格模式
严格模式用于辅助我们尽早地发现组件中的常见错误,减少项目上线产生的未知的 BUG 风险。
开启严格模式后,React 应用在开发环境会有以下这些行为:
- 组件将 重新渲染一次,以查找由于非纯渲染而引起的错误。
- 组件将 重新运行 Effect 一次,以查找由于缺少 Effect 清理而引起的错误。(useEffect 执行两次的缘由于此)
- 组件将被 检查是否使用了已弃用的 API
严格模式使用非常方便, 它是一个组件,只需要把整个 App 包裹在其中即可开启严格模式。
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
Effects - 副作用
我们需要在 React 应用程序之外做某事,比如可能需要和浏览器 API 对话,或者向服务器发出请求。 Effect 能让我们到达 React 应用程序之外的代码,我们称为“”副作用”
通常来说,我们都在事件处理程序(Event Handler)中运行副作用, 如果无法这么处理,我们则放在 useEffect 这个钩子里。比如下面这个例子就是执行副作用,操作 DOM 修改页面标题。
import React, { useState, useRef, useEffect } from "react";
function EffectsDemoNoDependency() {
const [title, setTitle] = useState("default title");
const titleRef = useRef();
useEffect(() => {
console.log("useEffect");
document.title = title;
});
const handleClick = () => setTitle(titleRef.current.value);
console.log("render");
return (
<div>
<input ref={titleRef} />
<button onClick={handleClick}>change title</button>
</div>
);
}
Refs - 引用
和 Effect 一样, 有时候我们想要跳出 React 并直接使用 DOM, Ref 就可以用来获取真实DOM对象的引用.可以通过 useRef 来创建 ref,并且通过 ref 属性来访问 DOM 元素, 以执行某些任务, 例如聚焦输入。 同样地,如果我想要持久化保存数据,但不需要该值的变化像 State 那样引起重新渲染,那么同样可以使用 useRef.
但需要谨记这两点:
- 不要滥用 ref
- 如果可以通过 prop 实现,那就不应该使用 ref
查看 useRef 实例代码
获取真实 DOM 对象的引用
import { useRef } from "react";
function MyComponent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return <input ref={inputRef} />;
}
持久化保存数据
import React, { useRef } from "react";
const PersistentValueWithRefExample = () => {
const savedDataRef = useRef(localStorage.getItem("savedData") || "");
const handleSubmit = (event) => {
event.preventDefault();
const inputValue = savedDataRef.current; // 通过 ref 对象获取值
localStorage.setItem("savedData", inputValue);
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
defaultValue={savedDataRef.current} // 使用 ref 对象中保存的值
onChange={(event) => {
savedDataRef.current = event.target.value;
}} // 更新 ref 对象中的值
/>
<button type="submit">保存</button>
</form>
<p>从本地存储中读取的数据:{localStorage.getItem("savedData")}</p>
</div>
);
};
export default PersistentValueWithRefExample;
Context - 上下文
Context 上下文是 React 除了通过 prop 数据逐级传递到各级组件外强大方法,如果你想要跨组件传递数据,那么 Context 可以做到这一点。
- 创建 Context
- 包裹在 Provider 组件内
- 把数据放在 Provider 的 value 里
- 通过 useContext 读取数据
import React, { createContext, useContext } from "react";
// 创建一个 Context
const MyContext = createContext();
// 父组件,包含 Provider
const ParentComponent = () => {
// 定义需要共享的数据
const sharedData = "Hello from Context!";
return (
// 使用 Provider 提供共享数据
<MyContext.Provider value={sharedData}>
<ChildComponent />
</MyContext.Provider>
);
};
// 子组件,使用 useContext 读取数据
const ChildComponent = () => {
// 使用 useContext 钩子读取 Provider 提供的值
const data = useContext(MyContext);
return <p>{data}</p>;
};
export default ParentComponent;
Portals - 传送门
Portal 可以让你把一个 React 组件移动到任何 HTML 元素中, 父组件样式, 在以下场景会很有用
- modals 模态框
- drop-down 下拉框
- tool tips 工具提示
使用方法:
- 使用
createPortal函数 - 传递组件
- 选择要传送到 HTMl 元素的位置
Suspense - 悬念
Suspense 是等待某个内容加载的组件,常用于在子组件完成加载前展示后备方案, 在获取数据需要时间时,可以改善用户体验,待数据到达之前“回退”到一个组件。
<Suspense fallback={<Loading />}>
<Component />
</Suspense>
suspense 还可以和 组件懒加载 一同工作
,当且仅当需要该组件时才加载
import React, { Suspense, lazy } from 'react';
// 使用 lazy 函数引入要懒加载的组件
const LazyComponent = lazy(() => import('./LazyComponent'));
const App = () => {
return (
<div>
<h1>Lazy Loading Example</h1>
{/* 使用 Suspense 组件包裹要懒加载的组件 */}
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
};
export default App;
Error Boundaies - 错误边界
ErrorBoundary (错误边界) 是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树, 相当于一个兜底的操作。
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<App />
</ErrorBoundary>
经典 React 面试题
相信你了解完上面的概念后,相比硬记硬背“八股文”,相信会让你对一些经典面试题也有更加深刻的理解,就一起来复习吧 ~
为什么 React 组件只能返回一个根元素? React 是如何解决?
点击查看答案
这个限制主要是因为在 JSX 中,每个组件的返回值会被解析成一个 JavaScript 表达式,JSX 会被转译成对 React.createElement 函数的调用,而这个函数只能接受一个根节点。如果一个组件返回多个根元素,React 将无法确定如何将它们包裹在一个根节点内,这会导致错误。
React 中解决这个问题的一种常见方法是使用 Fragment(React.Fragment 或简写形式 <>...</>),它可以作为一个容器,包裹多个子元素而不在最终渲染的 DOM 中引入额外的节点。
import React from 'react';
function MyComponent() {
return (
<React.Fragment>
<div>Element 1</div>
<div>Element 2</div>
</React.Fragment>
);
}
// 使用简写形式
function MyComponent() {
return (
<>
<div>Element 1</div>
<div>Element 2</div>
</>
);
}
这些常见的错误是由什么引起的?怎么解决?
常见 React 错误
JSX expressions must have one parent elementToo many re-renders. React limits the number of renders to prevent an infinite loopEach child in a list should have a unique “key” prop.Objects are not valid as a React child / Functions are not valid as a React childReact Hook useEffect has a missing dependency: 'XXX'. Either include it or remove the dependency arrayReact Hook "useXXX" is called conditionally. React Hooks must be called in the exact same order in every component render
点击查看答案
- 存在多个根标签, 需要封闭标签, 可以使用 Fragment
- 太多重复渲染,React 限制渲染的数量以防止无限循环, 请检查代码.
- 渲染列表没有添加 key 属性
- 对象作为 React 子元素无效/函数作为 React 子元素无效,不能将对象和函数渲染到 DOM 中,如果渲染了对象或函数,就会报上面的错误
- useEffect 缺少依赖项, 添加依赖项或者忽略 eslint
- Hooks 调用顺序错误, 使用在条件、循环语句中
什么是 JSX?Babel 如何转换 JSX 让浏览器识别的?
点击查看答案
JSX 是 JavaScript XML 的缩写, 是一种 JavaScript 的语法拓展,可以直接编写 HTML 的结构, 以直观和简洁的方式描述用户页面。
浏览器并不认识 JSX 代码,需要通过 Babel 编译转换为 JavaScript 代码。 Babel 将 JSX 语法转换为对 React.createElement() 函数的调用。转换后的代码只是普通的 JavaScript 的代码,浏览器可以识别和执行。
// JSX
function MyComponent() {
return (
<div>
<h1>Hello, world!</h1>
<p>This is a paragraph.</p>
</div>
);
}
// Babel 转译后的
function MyComponent() {
return React.createElement(
"div",
null,
React.createElement("h1", null, "Hello, world!"),
React.createElement("p", null, "This is a paragraph.")
);
}
React 渲染列表中 Keys 的作用?不加有什么影响?
点击查看答案
在 React 中, key 是一个特殊的 prop,用以标识列表中的组件。 key 帮助 React 识别哪些 item 已更改、添加或删除。 当重新渲染列表时, React 使用 key 来确定已添加或删除哪些元素,并且仅更新已更改的元素。
不加的话 React 为默认使用 index 作为 key, 但并不推荐这么做,因为这会存在以下这些问题
- 数组索引可以改变
- 造成不必要的性能开销,重新渲染所有子项
- 造成 UI 渲染错误,如果使用 index 作为 key,两个兄弟组件可能具有相同的 key
最佳实践:
- 使用唯一标识符作为 key
- 使用 useId() 内置 hook (原始数据源没有唯一标识符)
严格模式作用?为什么需要在开发环境中运行两次?
点击查看答案
React 严格模式 StrictMode 是一个用来突出显示应用程序中潜在问题的工具。它只在开发模式下运行,不会影响生产构建。严格模式有助于识别应用程序的不安全的生命周期方法,检测副作用,并使组件的行为更加可预测。
在开发环境中运行两次是为了检测不安全的生命周期方法和副作用。在第一次渲染时,会发出警告,第二次渲染时,会抛出错误。 比如 useEffect 执行两次,useEffect 执行两次
React hook 使用的限制条件?
点击查看答案
- 只能在最顶层使用,不能在循环、条件语句或嵌套函数中使用
- 只能在 React 函数组件或自定义 hook 中使用
React hook 的限制条件是为了确保 hook 的调用顺序一致以便正确使用。 hooks 底层是通过有序表(数组/链表)实现的,react 在刷新时会按照初次渲染的表顺序对数组中的元素进行比对和更新,如果不在函数顶层 React 就无法确认每次刷新(重新加载组件)时调用hooks的序列是完全相同的, 同时如果在循环、条件语句或嵌套函数中使用,可能会导致 hook 的调用顺序发生变化,从而导致组件状态的不一致。
React 重新渲染的条件?如何减少不必要的渲染次数(性能优化)?
点击查看答案
- state 改变
- 父组件重新渲染导致子组件重新渲染
- context 改变
- hooks 改变
React 组件减少渲染次数和性能优化手段
- 拆小组件, 这样即便组件重新渲染, 代价也是很小。
- 复用组件,
同一层级, 同一类型, 同一个 key 值, 尽量保证这三者的稳定性 - 减少组件的重新 render, 重新 render 会导致组件进入协调, 协调的核心就是 vdom diff, 非常耗时;如果能够减少协调, 复用旧的 fiber 节点, 会加快渲染的速度, 组件如果没有进入协调阶段, 就会进入 bailout 阶段, 意思就是这层组件退出更新
- 让组件进入
bailout阶段, 方法有以下这些:- 类组件: 使用
PureComponent, shouldComponentUpdate - 函数组件: 使用
React.memo, useMemo, useCallback, 避免改变 Context
- 类组件: 使用
React 受控组件和非受控组件
点击查看答案
受控组件和非受控组件 (只存在于表单元素)
- 受控组件就是表单元素的 value 通过 state(useState)来定义
- 非受控组件就是表单元素的 value 无法通过 state 获取,只能使用 ref(useRef)来获取
- 比如要获取 input 输入框的值,有 2 种方法
- 获取 input DOM
- 让 input 的 value 被管理起来,也就是通过 state 来定义
React 组件如何通信以及不同通信方式的特点
点击查看答案
- 父传子 - 通过 props 将数据从父组件传递给子组件
- 子传父 - 父组件将一个函数作为 prop 传递给子组件,在子组件内部调用该函数来传递数据给父组件
- context 传递 - 使用 Context 在组件树中传递数据,跨层级,避免 props 层层传递的麻烦
- state management (Redux, Zustand, Jotai, Mobx...) - 使用状态管理库来管理应用的状态,可以在任意组件中获取和修改状态
除了子传父比较绕点外, 其他都挺直观的,就不给代码实例了
import React, { useState } from 'react';
// 子组件
function Child({ onChildClick }) {
const handleClick = () => {
// 子组件点击事件触发时调用父组件传入的回调函数,并传递数据
onChildClick('Data from child component');
};
return (
<button onClick={handleClick}>Click me</button>
);
}
// 父组件
function Parent() {
const [dataFromChild, setDataFromChild] = useState('');
// 定义一个回调函数,用于接收子组件传递的数据
const handleChildClick = (data) => {
setDataFromChild(data); // 将数据存储在父组件的状态中
};
return (
<div>
<p>Data from child: {dataFromChild}</p>
{/* 将回调函数作为 props 传递给子组件 */}
<Child onChildClick={handleChildClick} />
</div>
);
}
export default Parent;
React 父组件如何调用子组件的方法?
点击查看答案
useImperativeHandleforwardRef
React Portal 有哪些使用场景?
点击查看答案
React Portal 提供了一种将子节点渲染到存在于父组件 DOM 结构之外的 DOM 节点的方法。这非常有用, 因为有时我们需要子组件从样式或层级结构上“跳出”其父组件。以下是一些常见的使用场景:
-
模态对话框(Modal): 这可能是 React Portal 最常见的用途。模态对话框通常需要覆盖应用程序的其他部分, 而且从样式和功能上看, 它们通常与其父组件没有太大的关联。使用 Portal 可以使模态对话框从其父组件“跳出”, 并能够覆盖应用程序的其他部分。
-
提示框(Tooltips):
-
下拉菜单(dropdown menu)
总的来说, 任何需要独立于父组件进行定位或渲染的组件, 或者需要“跳出”父组件的组件, 都可以考虑使用 React Portal
Context 使用方法 ?讲述 Context 原理?
点击查看答案
Context 提供了一种在组件之间共享数据的方法, 而不必通过组件树的逐层传递 props。Context 主要由两部分组成: Provider 和 Consumer。
- Provider: 用于提供共享的数据, 并且可以通过 value 属性传递数据。
- Consumer: 用于消费共享的数据, 并且可以通过嵌套函数的方式来消费数据。
useContext 是一个读取 Context 的 hook
// 创建一个 Context
const MyContext = React.createContext();
// 使用 Provider 提供共享的数据
<MyContext.Provider value={/* 共享的数据 */}>
<Child />
</MyContext.Provider>;
// 使用 Consumer 消费共享的数据
<MyContext.Consumer>
{(value) => /* 渲染共享的数据 */}
</MyContext.Consumer>;
// 使用 useContext 消费共享的数据
const value = useContext(MyContext);
useState 和 useRef 区别? useRef 作用?
点击查看答案
- useState 用于在函数组件中添加状态, 并且每次更新状态都会重新渲染组件。
- useRef 能让你引用一个不需要渲染的值, 也可以通过在 DOM 绑定 ref 来实现一些对 DOM 的直接操作。
// 用途一: 引用不需要触发重新渲染的值
import React, { useRef } from 'react';
function MyComponent() {
const valueRef = useRef(0); // 初始值为 0
const incrementValue = () => {
valueRef.current += 1; // 修改引用的值
console.log('Value:', valueRef.current); // 打印引用的值
};
return (
<div>
<button onClick={incrementValue}>Increment Value</button>
</div>
);
}
// 用途二:实现 DOM 的直接操作
import React, { useRef, useEffect } from 'react';
function MyComponent() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 在组件挂载后将焦点设置在输入框上
}, []);
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={() => console.log(inputRef.current.value)}>Log Input Value</button>
</div>
);
}
useState 和 useReducer 区别?为什么都返回一个数组而不是对象?
点击查看答案
useState 和 useReducer 都是 React 提供的用于管理组件状态的 hook。它们的主要区别在于:
- useState 是基于 State 的更新, 适用于简单的状态更新
- useReducer 是基于 Action 的更新, 适用于复杂的状态逻辑
useState 和 useReducer 都返回一个数组, 而不是对象, 是因为返回数组的开发体验更好。
- 数组的元素是按次序排列的,变量的取值由它的位置决定,数组解构时的变量可以是任意名称,不会影响它的取值,
- 对象解构的变量必须与属性同名,才能取到正确的值
React 的任务调度机制
点击查看答案
React 任务调度机制是基于 Fiber 架构实现的。Fiber 是 React 16 中引入的一种新的协调引擎, 它可以将任务拆分为小单元, 并且可以中断或者终止任务。
React 任务调度机制主要包括两个阶段: Render 阶段和 Commit 阶段。
- Render 阶段: 该阶段主要负责协调更新任务, 并且可以中断任务。在 Render 阶段, React 会找出所有需要更新的组件, 并且构建 Fiber 树。
- Commit 阶段: 该阶段主要负责将更新的结果渲染到页面上。在 Commit 阶段, React 会根据 Fiber 树中的 effect list, 依次执行 DOM 操作。
React 任务调度机制的主要目的是为了提高性能, 使得 React 应用可以更加流畅地运行。