React核心概念 core-concepts
CRA - Create React App
CRA 使用 webpack 作为打包工具,和 vite 是竞品。
vite既是打包工具,又是构建工具。
Create React App 曾使用最多的脚手架,React 官方现已经不推荐
官网https://create-react-app.dev/
创建项目
PS:直接使用 typescript
## 使用 npx
npx create-react-app my-app --template typescript
## 使用 npm
npm init react-app my-app --template typescript
## 使用 yarn
yarn create react-app my-app --template typescript
JSX
X在编程里一般代表扩展的意思
jsx即,js的扩展,可以在 JS 中写模板(类似 HTML 语法)
Rxjs 即对 响应数据的扩展 js
JSX 是 React 提供的可以在 JavaScript 中编写类似 HTML 的代码的语法糖,它可以让开发者更方便的描述 UI 的结构。
JSX 不能直接在浏览器中运行,需要通过 Babel 等工具将 JSX 转换为 JavaScript 代码。
状态变量 state
const [A,setA] = useState(0)
其中 A称为 状态变量
通过 React 提供的 Hook API:useState()
可以用来操作组件中的状态。 useState()
接受一个初始状态值,返回一个数组,数组的第一个元素是当前状态值,第二个元素是一个函数,用来更新状态值。
- State 变量用于保存渲染间的数据
state 是 React 中的组件内部状态,当 state 发生变化时,会触发组件的重新渲染。
状态更新函数可以接受一个函数作为参数,函数的参数是上一个状态值,返回值是新的状态值。
使用展开运算符:...pre
,可以更新状态对象的部分属性,并保持其他属性不变。
特别注意对象或数组类型, 不能直接修改,推荐使用immer.js
state是React实现数据驱动UI的核心。
当state值发生变化时,组件会尝试重新渲染,因此,函数会重新执行一次。
函数重新执行后,此时,state中的数据已经是变化后的结果,因此渲染到UI的结果也会发生改变。
state与UI的变化息息相关,在实践中,还有很多其他的数据与UI变化无关。他们不应该放在state中来管理,而应该想其他办法。
组件外部数据:props传递的,content中的
组件内部数据:state中的
注意state的变化,是异步的。
因此,你不能在改变state后,立刻使用它,你无法拿到状态的最新的值。而到下一个事件循环周期执行时,状态才是最新的。
set函数
例如setSomething(nextState)
useState
返回的set
函数允许你将 state 更新为不同的值并触发重新渲染- 你可以直接传递新状态,也可以传递一个根据先前状态来计算新状态的函数
- 如果你将函数作为
nextState
传递,它将被视为更新函数
state 更新函数
setMessages(msgs => [...msgs, receivedMessage]);
- 它必须是纯函数,只接受待定的 state 作为其唯一参数,并应返回下一个状态。
- React 将把你的更新函数放入队列中并重新渲染组件。
- 在下一次渲染期间,React 将通过把队列中所有更新函数应用于先前的状态来计算下一个状态
- 它获取 待定状态 并从中计算 下一个状态
初始化函数
React 只在初次渲染时保存初始状态,后续渲染时将其忽略。
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
尽管 createInitialTodos()
的结果仅用于初始渲染,但你仍然在每次渲染时调用此函数。如果它创建大数组或执行昂贵的计算,这可能会浪费资源。
为了解决这个问题,你可以将它 作为初始化函数传递给 useState
:
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
请注意,你传递的是 createInitialTodos
函数本身,而不是 createInitialTodos()
调用该函数的结果。如果将函数传递给 useState
,React 仅在初始化期间调用它。
React 在开发模式下可能会调用你的 初始化函数 两次,以验证它们是否是 纯函数。
我尝试将 state 设置为一个函数,但它却被调用了](https://zh-hans.react.dev/reference/react/useState#im-trying-to-set-state-to-a-function-but-it-gets-called-instead "Link for 我尝试将 state 设置为一个函数,但它却被调用了 ")
你不能像这样把函数放入状态:
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
因为你传递了一个函数,React 认为 someFunction
是一个 初始化函数, 而 someOtherFunction
是一个 更新函数 ,于是它尝试调用它们并存储结果。 要实际 存储 一个函数,你必须在两种情况下在它们之前加上 () =>
。然后 React 将存储你传递的函数。
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}
原始值
响应性值
- 可能因组件重新渲染而改变的值
- 响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数
非响应性的值
组件外的值,它不会因为重新渲染而改变,所以它不是依赖
props
props 是 React 中提供的将数据从父组件传递到子组件的机制,是父子组件通信的一种方式。
在 React 中 props 是只读的,子组件不能修改 props,以保证数据的单向流动。
在子组件中,可以使用解构赋值的方式获取 props 中的数据。
除了传递数据,还可以通过 props 传递函数,实现父子组件之间的通信。
在子组件中,可以使用剩余参数的方式获取除了指定的 props 之外的其他 props。
在子组件中,可以给 props 设置默认值,当父组件没有传递该 props 时,使用默认值。
children
children
是一个特殊的 prop,它允许将组件作为数据传递到其他组件。
props.children
可以是任意的 JSX 元素,包括原始值、React 元素、函数、组件等。
function Page({
header,
content,
footer,
}: {
header: JSX.Element;
content: JSX.Element;
footer: JSX.Element;
}) {
return (
<div>
{header}
{content}
{footer}
</div>
);
}
export default function ChildrenMultiple() {
return (
<Page
header={<h1>页头</h1>}
content={<div>页面内容</div>}
footer={<h1>页尾</h1>}
>
</Page>
);
}
事件处理程序
对交互的直接反应,而非间接
setup 函数
useEffect(setup, dependencies?)
- 处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。
- 当组件被添加到 DOM 的时候,React 将运行 setup 函数。
- 在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。
- 在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。
可选 dependencies
依赖项列表
setup
代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。- 依赖项列表的元素数量必须是固定的,并且必须像
[dep1, dep2, dep3]
这样内联编写。 - React 将使用
Object.is
来比较每个依赖项和它先前的值。 - 如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数
ref对象 - state的孪生兄弟
useRef - react中 useState的孪生兄弟
useRef
返回一个具有单个 current
属性 的 ref 对象,并初始化为你提供的 初始值。
- 在后续的渲染中,
useRef
将返回相同的对象。你可以改变它的current
属性来存储信息,并在之后读取它。
当该需要持久化的数据不会跟UI的变化产生关系时,我们需要使用useRef
渲染期间
函数组件中,非hooks内,非事件处理函数内 包括 函数组件的大部分,return 中的jsx
记忆化与记忆函数
箭头函数体
我调用的useMemo
应该返回一个对象,但返回了undefined
这段代码不起作用:
// 🔴 你不能像这样 `() => {` 在箭头函数中直接返回一个对象
const searchOptions = useMemo(() => {
matchMode: 'whole-word',
text: text
}, [text]);
在 JavaScript 中,() => {
是箭头函数体的开始标志,因此 {
大括号不是对象的一部分。这就是它不返回对象并导致错误的原因。你可以通过添加像 ({
与 })
这样的括号来修复它:
// 这行得通,但很容易有人再次破坏
const searchOptions = useMemo(() => ({
matchMode: 'whole-word',
text: text
}), [text]);
然而,这仍然令人困惑,而且对于某些人来说,通过移除括号来破坏它太容易了。
为避免此错误,请显式编写 return
语句:
// ✅ 这有效并且是明确的
const searchOptions = useMemo(() => {
return {
matchMode: 'whole-word',
text: text
};
}, [text]);
reducer
用于更新 state 的纯函数。 参数为 state 和 action,返回值是更新后的 state。 state 与 action 可以是任意合法值。
dispatch
函数
- 用于更新 state 并触发组件的重新渲染
- 它需要传入一个 action 作为参数
dispatch
函数没有返回值
action
- 用户执行的操作。可以是任意类型的值。
- 通常来说 action 是一个对象,其中
type
属性标识类型,其它属性携带额外信息。
纯函数
- 只有组件、初始化函数和 reducer 函数需要是纯函数。
- 事件处理函数不需要实现为纯函数,并且 React 永远不会调用事件函数两次。
- React 假设编写的每个组件都是纯函数。这意味着编写的 React 组件在给定相同的输入(props、state 和 context)时必须始终返回相同的 JSX。
- 如果一个函数是纯函数,运行两次不会改变其行为,因为纯函数每次都会产生相同的结果。 然而,如果一个函数是非纯函数(例如,它会修改接收到的数据),运行两次通常会产生明显的差异(这就是它是非纯函数的原因!)。这有助于及早发现并修复错误。
标记为转换效果
- 使用 useTransition()
- startTransition()包裹切换
render
函数
- 组件的渲染函数。React 会调用该函数并传入父组件传递的 props 和
ref
。 - 返回的 JSX 将作为组件的输出。
错误边界
错误边界用于 React 中捕获子组件树中发生的 JavaScript 错误,并输出这些错误,防止整个应用崩溃。
错误边界可以捕获渲染期间、生命周期方法以及其下整个树的构造函数中发生的错误。
受控组件
受控组件:值同步到state,并且使用value属性
受控组件是指表单元素的值由 React 组件的 state 控制的表单元素。
受控组件的表单中的输入值由 React 组件的 state 控制,通过 onChange()
事件来更新 state。
非受控组件:值不同步到state,使用defaultValue属性
非受控组件是指表单元素的值不受 React 控制,而是由 DOM 元素本身来控制的组件。
非受控组件的表单中的输入值不受 React 控制,通过useRef()
保存 DOM 元素的引用,并通过current
属性来获取 DOM 元素的值。
在 HTML 中,表单元素(如<input>
、 <textarea>
和 <select>
)通常自己维护 state,并根据用户输入进行更新。
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。
渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
对于受控组件来说,输入的值始终由 React 的 state 驱动。你也可以将 value 传递给其他 UI 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码。
然而当有多个输入框,或者多个这种组件时,如果想同时获取到全部的值就必须每个都要编写事件处理函数,这会让代码看着很臃肿,所以为了解决这种情况,出现了非受控组件。
这个时候我们更推荐使用非受控组件。
非受控组件是呈现表单元素并在 DOM 本身中更新其值的组件。可以使用 ref 而不是为每个状态更新编写事件处理程序,以便从 DOM 访问表单值。
总体而言,非受控组件可以在必要时使用或比受控组件更有效,否则,尽可能使用受控组件
若要使用非受控制的组件,可以使用 ref 直接访问 DOM 值。对于受控组件,我们将表单数据值存储在 React 组件的状态属性中。
在了解了“非受控制”和“受控”组件之后,很明显,“受控”组件绝对推荐使用“而不是”非受控制“组件,但”非受控制“组件有时也是必不可少的,因此,了解两者是件好事。
默认值」
在 React 的渲染生命周期中,DOM 中的值将被表单元素上的 value 属性覆盖。
通过使用不受控制的组件,您可能希望 React 设置初始值,但保持后续更新不变。
在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value。
在一个组件已经挂载之后去更新 defaultValue 属性的值,不会造成 DOM 上值的任何更新。
同样,<input type="checkbox">
和 <input type="radio">
支持 defaultChecked,<select>
和 <textarea>
支持 defaultValue。
文件输入标记
<input type="file" ref={this.fileInput} />;
元素始终是不受控制的组件,因为它的值不能以编程方式设置,而只能由用户设置。例如<input type="file" />
FormData
FormData 是内置的表单对象,用于创建表单数据对象。
FormData 对象包含表单中的数据,Object.fromEntries(formData.entries())
将 FormData 对象转换为普通的 JavaScript 对象。
状态提升 Lifting State Up
状态提升是一种在 React 中处理共享状态的方法。 当多个组件需要共享相同的数据时,可以将该数据提升到这些组件的最近共同祖先中。 这样,这些组件就可以通过 props 来访问和修改这个共享的数据。
单向数据流
任何一个组件的内部数据state发生变化,都会影响组件树中低于它的组件。
想象一个水流,自上而下单向流动,在某个位置添加一个颜料,只会影响下游。
单向数据流特点如下:
- Single Source of Truth:单一 Store,类似前端数据库
- Every state change must dispatch action:每次 State 更改一定有对应的 Action
- Reducer must be pure function:Reducer 操作数据时必须返回新的数据,而不能对元数据进行突变
- Every component state is a slice of Store:即某个组件的状态是 Store 这个全局状态的一个切片
闭包
在函数组件中,如果我们在回调函数中使用了state的值,那么闭包就会产生。
闭包在函数创建时产生,他会缓存创建时的state的值,它是一种正常现象。
注意,不是在函数执行时产生的闭包。
# React 树
portal 中的事件传播遵循 React 树而不是 DOM 树。
例如点击<div onClick>
内部的 portal,将触发onClick
处理程序。
如果这会导致意外的问题,请在 portal 内部停止事件传播,或将 portal 移动到 React 树中的上层
re-render
React 做的主要事情是保持UI与应用程序状态同步。
它用来做这件事的工具叫做“re-render”。基于当前应用程序状态,每次重新呈现都是应用程序UI在给定时刻应该是什么样子的快照。
我们可以把它想象成一堆照片,每一张照片都记录了给定每个状态变量的特定值时事物的样子。
每次“re-render”都会根据当前状态在脑海中生成 DOM 应该是什么样子的图像。但实际上它是一堆JS对象,被称为“「virtual DOM」”。
我们不直接告诉 React 需要更改哪些 DOM 节点。
相反,我们根据当前状态告诉React UI应该是什么样子。
通过重新渲染,React 创建一个新的快照,它可以通过比较快照找出需要更改的内容,就像玩“寻找差异”游戏一样。
初学React的感受
- 与vue相比,react使用jsx,将html标签与js相混合,结构灵活。而vue使用模板语法,具有明显的template,script,css,结构标准化。
在vue中,使用指令 v-if、v-for 等进行条件控制。 在react中,使用JavaScript控制结构如if结构、map结构。
组件嵌套, vue中使用slot插槽,react中使用props.children
- 渲染diff算法不同,react默认会将整个组件进行重新渲染,类似于canvas,运用不好可能容易有性能问题。 Object.is 进行比较是否有变化。依赖收集需要开发者自己指明,否则容易陷入闭包陷阱。
vue会自动进行细粒度的比较,自动依赖收集,上手简单。
修改数据,react中不应该直接修改useState创建的数据,而应该拷贝一份副本,否则认为是mutation突变。 vue中采用双向数据绑定,可以直接对数据进行修改,框架本身会根据依赖收集自动处理,使得数据的更新更直观。
响应式,react中,没有类似vue的指令,需要自己实现,父组件定义子组件中需要触发的方法,单向数据流体现的更明显。
react的组件化更明显,因为hooks只能在顶层调用,并且循环中不可以使用,需要把循环项抽成子组件,才能使用useCallback
React中对于状态,需要理解的十分透彻才行。在渲染期间中,由数据间关系可以计算出来的,可以放到渲染期间useMemo中,类似于vue中的计算属性。 不要放到useEffect,类似于vue中的watch。
vue中可以使用ref调用子组件的方法,但是react中对单向数据流的要求更高,推荐由父组件中定义,通过props传递给子组件
vue就像是精装房,家具什么的都给你准备好了,官方提供了比较完整的解决方案
React就像是毛坯房,需要你自己处理,如果你很懂装修之类的,可能会比精装房还好。
React有一个非常庞大的生态系统,开发者需要根据需求选择合适的库进行搭配。
感觉react是 开发中发现哪种场景不满足,就可以考虑写一个hook,弱化了生命周期的概念
React推崇函数式编程(纯函数),数据不可变以及单向数据流。 函数式编程最大的好处是其稳定性(无副作用)和可测试性(输入相同,输出一定相同) 所以通常大家说的React适合大型应用,根本原因还是在于其函数式编程。