描述UI
- React 组件是一段可以 使用标签进行扩展 的 JavaScript 函数。
- React 组件是常规的 JavaScript 函数,但 组件的名称必须以大写字母开头
- 同一文件中,有且仅有一个默认导出,但可以有多个具名导出!
- 为什么需要一个根标签
- JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。
- 切勿将数字放在 && 左侧.左侧为0则会渲染为0
- key 需要满足的条件
- key 值在兄弟节点之间必须是唯一的。 不过不要求全局唯一,在不同的数组中可以使用相同的 key。
- key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 key。
- 为什么需要key
- React 里需要 key 和文件夹里的文件需要有文件名的道理是类似的。一个精心选择的 key 值所能提供的信息远远不止于这个元素在数组中的位置。即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中一直认得它。
- 请不要在运行过程中动态地产生 key,像是 key={Math.random()} 这种方式。这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建。这不仅会造成运行变慢的问题,更有可能导致用户输入的丢失。
- 纯函数的基本定义:
- 只负责自己的任务。 它不会更改在该函数调用前就已存在的对象或变量。
- 输入相同,输出也相同。 在输入相同的情况下,对纯函数来说应总是返回相同的结果。
- React 假设你编写的所有组件都是纯函数。React 的渲染过程必须自始至终是纯粹的。组件应该只返回它们的 JSX,而不改变在渲染前,就已存在的任何对象或变量 — 这将会使它们变得不纯粹!
- React 提供了 “严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件。
- 副作用:包括更新屏幕、启动动画、更改数据等,它们被称为 副作用。它们是 “额外” 发生的事情,与渲染过程无关。
- 在 React 中,副作用通常属于 事件处理程序。事件处理程序是 React 在你执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在你的组件 内部 定义的,它们也不会在渲染期间运行! 因此事件处理程序无需是纯函数。
- 如果你用尽一切办法,仍无法为副作用找到合适的事件处理程序,你还可以调用组件中的
useEffect
方法将其附加到返回的 JSX 中。这会告诉 React 在渲染结束后执行它。然而,这种方法应该是你最后的手段。
添加交互
-
e.stopPropagation()
阻止触发绑定在外层标签上的事件处理函数。 -
e.preventDefault()
阻止少数事件的默认浏览器行为。
事件处理函数是执行副作用的最佳位置,数据的改变使用state存储
-
为什么需要useState
- 局部变量无法在多次渲染中持久保存。当 React 再次渲染这个组件时,它会从头开始渲染——不会考虑之前对局部变量的任何更改。
- 更改局部变量不会触发渲染。 React 没有意识到它需要使用新数据再次渲染组件。
Hooks ——以
use
开头的函数——只能在组件或自定义 Hook 的最顶层调用。如果你渲染同一个组件两次,每个副本都会有完全隔离的 state
-
React 把更改提交到 DOM 上
-
对于初次渲染, React 会使用
appendChild()
DOM API 将其创建的所有 DOM 节点放在屏幕上。 - 对于重渲染, React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。
-
对于初次渲染, React 会使用
React 仅在渲染之间存在差异时才会更改 DOM 节点
在渲染完成并且 React 更新 DOM 之后,浏览器就会重新绘制屏幕
-
在一个 React 应用中一次屏幕更新都会发生以下三个步骤:
- 触发
- 组件的 初次渲染。
- 组件(或者其祖先之一)的 状态发生了改变
- 渲染。在您触发渲染后,React 会调用您的组件来确定要在屏幕上显示的内容。“渲染中” 即 React 在调用您的组件。
- 在进行初次渲染时, React 会调用根组件。
- 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。
- 提交。在渲染(调用)您的组件之后,React 将会修改 DOM。
-
对于初次渲染, React 会使用
appendChild()
DOM API 将其创建的所有 DOM 节点放在屏幕上。 - 对于重渲染, React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。
设置 state 会触发渲染(当你调用 useState 时)
-
渲染会及时生成一张快照
- “正在渲染” 就意味着 React 正在调用你的组件——一个函数。你从该函数返回的 JSX 就像是 UI 的一张及时的快照。它的 props、事件处理函数和内部变量都是 根据当前渲染时的 state 被计算出来的。
- 返回的 UI “快照”是可交互的。它其中包括类似事件处理函数的逻辑,这些逻辑用于指定如何对输入作出响应。React 随后会更新屏幕来匹配这张快照,并绑定事件处理函数。因此,按下按钮就会触发你 JSX 中的点击事件处理函数。
- state 实际上“活”在 React 本身中——就像被摆在一个架子上!——位于你的函数之外。当 React 调用你的组件时,它会为特定的那一次渲染提供一张 state 快照。你的组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的!
设置 state 只会为下一次渲染变更 state 的值。
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。
React 会使 state 的值始终”固定“在一次渲染的各个事件处理函数内部。 你无需担心代码运行时 state 是否发生了变化。
变量和事件处理函数不会在重渲染中“存活”。每个渲染都有自己的事件处理函数。
每个渲染(以及其中的函数)始终“看到”的是 React 提供给这个 渲染的 state 快照。
过去创建的事件处理函数拥有的是创建它们的那次渲染中的 state 值。
React 会等到事件处理函数中的 所有 代码都运行完毕再处理你的 state 更新。
在下次渲染前多次更新同一个 state,需要传入一个更新函数,如setNumber(n => n + 1),而不是像 setNumber(number + 1) 这样传入 下一个 state 值
设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。
React 会在事件处理函数执行完成之后处理 state 更新。这被称为批处理。
要在一个事件中多次更新某些 state,你可以使用 setNumber(n => n + 1) 更新函数。
把所有存放在 state 中的 JavaScript 对象都视为只读的,为了真正地 触发一次重新渲染,你需要创建一个新对象并把它传递给 state 的设置函数
不要直接修改一个对象,而要为它创建一个 新 版本,并通过把 state 设置成这个新版本来触发重新渲染。
你可以使用这样的 {...obj, something: 'newValue'} 对象展开语法来创建对象的拷贝。
状态管理
- 只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。
- 相同位置的相同组件会使得 state 被保留下来
- 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!
- 当你在相同位置渲染不同的组件时,组件的整个子树都会被重置
- 永远要将组件定义在最上层(函数组件外)并且不要把它们的定义嵌套起来,否则每次渲染出的都是不同的组件,状态会丢失
- 在相同位置重置 state
- 将组件渲染在不同的位置。同一个组件在第一个位置与第二个位置交替展示时会重置state
- 使用 key 来重置 state。即使两个 <Counter /> 会出现在 JSX 中的同一个位置,它们也不会共享 state
- 使用key后,React 将重新创建 DOM 元素,而不是复用它们。
- 为被移除的组件保留 state
- 用 CSS 把其他组件隐藏起来。这些组件就不会从树中被移除了,所以它们的内部 state 会被保留下来。这种解决方法对于简单 UI 非常有效。但如果要隐藏的树形结构很大且包含了大量的 DOM 节点,那么性能就会变得很差。
- 你可以进行状态提升并在父组件中保存每个收件人的草稿消息。这是最常见的解决方法
- 你也可以使用localStorage存储草稿信息
- 为多个 不同位置的 相同组件 指定key可以保留state
- 把 useState 转化为 useReducer:
- 通过事件处理函数 dispatch actions(派发action对象);
- 编写一个 reducer 函数,它接受传入的 state 和一个 action对象(通常包含type字段),并返回一个新的 state;
- 使用 useReducer 替换 useState;
- reducer 必须是一个纯函数——它应该只计算下一个状态。而不应该 “做” 其它事情,包括向用户显示消息。这应该在事件处理程序中处理。(为了便于捕获这样的错误,React 会在严格模式下多次调用你的 reducer。
- Context 可以让父节点,甚至是很远的父节点都可以为其内部的整个组件树提供数据。
- 通过 export const MyContext = createContext(defaultValue) 创建并导出 context。
- 在无论层级多深的任何子组件中,把 context 传递给 useContext(MyContext) Hook 来读取它。
- 在父组件中把 children 包在 <MyContext.Provider value={...}> 中来提供 context。
- 在 React 中,覆盖来自上层的某些 context 的唯一方法是将子组件包裹到一个提供不同值的 context provider 中。
- 不同的 React context 不会覆盖彼此。你通过 createContext() 创建的每个 context 都和其他 context 完全分离,只有使用和提供 那个特定的 context 的组件才会联系在一起。一个组件可以轻松地使用或者提供许多不同的 context。
应急方案
- 当你希望组件“记住”某些信息,但又不想让这些信息触发新的渲染时,你可以使用 ref 。
- 当一条信息用于渲染时,将它保存在 state 中。当一条信息仅被事件处理器需要,并且更改它不需要重新渲染时,使用 ref 可能会更高效。
- 何时使用ref
- 存储timeout ID
- 存储和操作DOM 元素
- 存储不需要被用来计算 JSX 的其他对象。
- 不要在渲染过程中读取或写入 ref.current,ref 本身是一个普通的 JavaScript 对象
- 给未知长度的列表中每项绑定ref
- 访问另一个组件的 DOM 节点
- 一个组件可以指定将它的 ref “转发”给一个子组件,子组件是使用 forwardRef 声明的。 这让从父组件接收的ref作为第二个参数传入组件,第一个参数是 props
- 使用ref后,父组件可以调用子组件dom节点的所有方法,使用命令句柄(useImperativeHandle)可以只暴露一部分 API
- 通常,你将从事件处理器访问 refs。
- 你可以强制 React 同步更新(“刷新”)DOM(添加一个元素后滚动至该元素)。 为此,从 react-dom 导入 flushSync 并将 state 更新包裹 到 flushSync 调用中
- useEffect 会把它包裹的代码放到屏幕更新渲染之后执行
-为什么依赖数组中可以省略 ref 或者 set函数?- 这是因为
ref
具有 稳定 的标识:React 保证 每轮渲染中调用useRef
所产生的引用对象时,获取到的对象引用总是相同的,即获取到的对象引用永远不会改变,所以它不会导致重新运行 Effect。因此,依赖数组中是否包含它并不重要。当然也可以包括它 -
useState
返回的set
函数 也有稳定的标识符,所以也可以把它从依赖数组中忽略掉
- 这是因为
- React 总是在执行下一轮渲染的 Effect 之前清理(执行effect中return出的清理函数)上一轮渲染的 Effect。
- 如果一个值可以基于现有的 props 或 state 计算得出,不要把它作为一个 state,而是在渲染期间直接计算这个值
- 你可以使用
useMemo
Hook 缓存(或者说 记忆(memoize))一个昂贵的计算(用该hook包裹一个耗时的函数,以避免每次渲染期间的重新计算)。 - 当 props 变化时重置所有 state,应当给该组件设置key属性,key变化,react会自动重置该组件
- 当 props 变化时重置部分 state
- 在渲染期间更新组件时,React 会丢弃已经返回的 JSX 并立即尝试重新渲染。为了避免非常缓慢的级联重试,React 只允许在渲染期间更新 同一 组件的状态。如果你在渲染期间更新另一个组件的状态,你会看到一条报错信息。
- 如果某些逻辑必须在 每次应用加载时执行一次,而不是在 每次组件挂载时执行一次,可以添加一个顶层变量(写在组件函数之外)来记录它是否已经执行过了
- 订阅外部 store,useSyncExternalStore
- 数据请求中,为了修复竞态条件下返回值set错误的问题,你需要添加一个 清理函数 来忽略较早的返回结果
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
- 每个 Effect 表示一个独立的同步过程。抵制将与 Effect 无关的逻辑添加到已经编写的 Effect 中(代码中的每个 Effect 应该代表一个独立的同步过程),仅仅因为 这些逻辑 需要与 Effect 同时运行(应重新写一个effect函数来同步 这些逻辑,它的的好处是,删除一个 Effect 不会影响另一个 Effect 的逻辑)。
- 组件内部的所有值(包括 props、state 和组件体内的变量)都是响应式的。任何响应式值都可以在重新渲染时发生变化,所以需要将响应式值包括在 Effect 的依赖项中。
- 避免将对象和函数作为依赖项。如果在渲染过程中创建对象和函数,然后在 Effect 中读取它们,它们将在每次渲染时都不同。这将导致 Effect 每次都重新同步
- 避免禁用检查工具。为了不违反规则,修复代码总是值得的!
useEffect(() => {
// ...
// 🔴 避免这样禁用静态代码分析工具:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);
- 使用
useEffectEvent
(实验性的功能)这个特殊的 Hook 从 Effect 中提取非响应式逻辑。Effect Event。它是 Effect 逻辑的一部分,但是其行为更像事件处理函数。它内部的逻辑不是响应式的,不需要添加到effect的依赖中,Effect Event 让你在 Effect 响应性和不应是响应式的代码间“打破链条”, Effect Event 读取的是最新的 props 和 state 。 - 事件处理函数内部的逻辑是非响应式的(用户的特定操作,才会执行)。
- Effect 内部的逻辑是响应式的(切换聊天室,需要自动断开之前的,连接最新的)。
- 你可以将非响应式逻辑从 Effect 移到 Effect Event 中。
- 只在 Effect 内部调用 Effect Event。
- 不要将 Effect Event 传给其他组件或者 Hook。
- 注意,你不能“选择” Effect 的依赖。每个被 Effect 所使用的响应式值,必须在依赖中声明。依赖是由 Effect 的代码决定的。每个 Effect 应该代表一个独立的同步过程,不应该同步两个不同的、不相关的东西,应将他们拆分在不同的effect中
- 是否在读取一些状态来计算下一个状态?使用state的更新函数写法
- 你想读取一个值而不对其变化做出“反应”吗?或者来自 props 的事件处理程序的处理,将非响应式逻辑(props的事件处理程序)移至 Effect Event 中(非响应式逻辑应该在事件中)
- 尽量避免对象和函数依赖。将它们移到组件外(常量对象或者函数)、effect外(在effect外对对象或者函数解构,使effect依赖解构出来的基本类型的值)、 Effect 内(在effect中创建对象或者函数)
- 自定义 Hook 共享的只是状态逻辑而不是状态本身。对 Hook 的每个调用完全独立于对同一个 Hook 的其他调用
- 不要创建像 useMount 这样的自定义 Hook。保持目标具体化。
api
- 即使一个组件被记忆化(memo)了,当它自身的状态发生变化时,它仍然会重新渲染
- 即使组件已被记忆化(memo),当其使用的 context 发生变化时,它仍将重新渲染