Skip to content

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 代码。

标记语法(markup syntax) JSX

状态变量 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 更新函数

updater function

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 在开发模式下可能会调用你的 初始化函数 两次,以验证它们是否是 纯函数

你不能像这样把函数放入状态:

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 元素、函数、组件等。

tsx
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。

文件输入标记

tsx
<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的感受

  1. 与vue相比,react使用jsx,将html标签与js相混合,结构灵活。而vue使用模板语法,具有明显的template,script,css,结构标准化。

在vue中,使用指令 v-if、v-for 等进行条件控制。 在react中,使用JavaScript控制结构如if结构、map结构。

组件嵌套, vue中使用slot插槽,react中使用props.children

  1. 渲染diff算法不同,react默认会将整个组件进行重新渲染,类似于canvas,运用不好可能容易有性能问题。 Object.is 进行比较是否有变化。依赖收集需要开发者自己指明,否则容易陷入闭包陷阱。

vue会自动进行细粒度的比较,自动依赖收集,上手简单。

  1. 修改数据,react中不应该直接修改useState创建的数据,而应该拷贝一份副本,否则认为是mutation突变。 vue中采用双向数据绑定,可以直接对数据进行修改,框架本身会根据依赖收集自动处理,使得数据的更新更直观。

  2. 响应式,react中,没有类似vue的指令,需要自己实现,父组件定义子组件中需要触发的方法,单向数据流体现的更明显。

react的组件化更明显,因为hooks只能在顶层调用,并且循环中不可以使用,需要把循环项抽成子组件,才能使用useCallback

  1. React中对于状态,需要理解的十分透彻才行。在渲染期间中,由数据间关系可以计算出来的,可以放到渲染期间useMemo中,类似于vue中的计算属性。 不要放到useEffect,类似于vue中的watch。

  2. vue中可以使用ref调用子组件的方法,但是react中对单向数据流的要求更高,推荐由父组件中定义,通过props传递给子组件

  3. vue就像是精装房,家具什么的都给你准备好了,官方提供了比较完整的解决方案

React就像是毛坯房,需要你自己处理,如果你很懂装修之类的,可能会比精装房还好。

React有一个非常庞大的生态系统,开发者需要根据需求选择合适的库进行搭配。

感觉react是 开发中发现哪种场景不满足,就可以考虑写一个hook,弱化了生命周期的概念

React推崇函数式编程(纯函数),数据不可变以及单向数据流。 函数式编程最大的好处是其稳定性(无副作用)和可测试性(输入相同,输出一定相同) 所以通常大家说的React适合大型应用,根本原因还是在于其函数式编程。

你知道 React 和 Vue 的区别?

Contributors

作者:Long Mo
字数统计:5k 字
阅读时长:16 分钟
Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs