1 基础

1.1 引入

01.总结
    react.min.js - React 的核心库
    react-dom.min.js - 提供与 DOM 相关的功能
    babel.min.js - Babel 可以将 ES6 代码转为 ES5 代码

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js" ></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js" ></script>
      <script src="../../statics/react/zijie/babel.min.js" ></script>
    </head>
    <body>
    <div id="example"></div>
    </body>
    <script type="text/babel">
      // 简单的 React 组件
      function App() {
        return <h1>Hello, React!</h1>;
      }
      const root = ReactDOM.createRoot(document.getElementById("example"));
      // 渲染 React 组件到 DOM
      root.render(<App />);
    </script>
    </html>

1.2 JSX规则

00.总结1
    React 使用 JSX 来替代常规的 JavaScript。
    JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。
    我们不需要一定使用 JSX,但它有以下优点:
        JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化。
        它是类型安全的,在编译过程中就能发现错误。
        使用 JSX 编写模板更加简单快速。
    ---------------------------------------------------------------------------------------------------------
    const element = <h1 className="foo">Hello, world!</h1>;
    这种看起来可能有些奇怪的标签语法既不是字符串也不是 HTML。
    它被称为 JSX, 一种 JavaScript 的语法扩展。 我们推荐在 React 中使用 JSX 来描述用户界面。
    JSX 是在 JavaScript 内部实现的。
    我们知道元素是构成 React 应用的最小单位,JSX 就是用来声明 React 当中的元素。
    与浏览器的 DOM 元素不同,React 当中的元素事实上是普通的对象,React DOM 可以确保 浏览器 DOM 的数据内容与 React 元素保持一致。
    ---------------------------------------------------------------------------------------------------------
    要将 React 元素渲染到根 DOM 节点中,我们通过把它们都传递给 ReactDOM.render() 的方法来将其渲染到页面上:
    const element = <h1 className="foo">Hello, world</h1>;
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(element);

00.JSX规则
    1.只能返回一个根元素:如果你不想在标签中增加一个额外的 <div>,可以用 <> 和 </> 元素来代替
    2.标签必须闭合:JSX 要求标签必须正确闭合。像 <img> 这样的自闭合标签必须书写成 <img />,而像 <li>oranges 这样只有开始标签的元素必须带有闭合标签,需要改为 <li>oranges</li>
    3.使用驼峰式命名法给所有大部分属性命名:这就是为什么在 React 中,大部分 HTML 和 SVG 属性都用驼峰式命名法表示。例如,需要用 strokeWidth 代替 stroke-width。由于 class 是一个保留字,所以在 React 中需要用 className 来代替。这也是 DOM 属性中的命名

00.在JSX中通过大括号使用JavaScript
    如何使用引号传递字符串:当你想把一个字符串属性传递给 JSX 时,把它放到单引号或双引号中
    在 JSX 的大括号内引用 JavaScript 变量
    在 JSX 的大括号内调用 JavaScript 函数
    在 JSX 的大括号内使用 JavaScript 对象

01.代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <!-- 引入本地的 React、ReactDOM 和 Babel -->
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <!-- 根 DOM 节点,React 将在此节点内渲染内容 -->
    <div id="root"></div>

    <!-- JavaScript 代码,包括 JSX 示例和解释 -->
    <script type="text/babel">
      // JSX 基础示例:声明一个简单的元素
      function renderBasicJSX() {
        // 创建一个 JSX 元素
        const element = <h1 className="foo">Hello, world!</h1>;
        const root = ReactDOM.createRoot(document.getElementById("root"));
        root.render(element);
      }

      // 使用嵌套的 JSX 元素
      function renderNestedJSX() {
        const root = ReactDOM.createRoot(document.getElementById("root"));
        root.render(
          <div>
            <h1>菜鸟教程</h1>
            <h2>欢迎学习 React</h2>
            <p data-myattribute="somevalue">这是一个很不错的 JavaScript 库!</p>
          </div>
        );
      }

      // 在独立文件中引入 JSX 代码
      function renderFromFile() {
        // 在一个独立文件中编写 JSX,并在 HTML 文件中引入它
        // 文件内容示例:
        // const element = <h1 className="foo">Hello, world</h1>;
        // const root = ReactDOM.createRoot(document.getElementById("root"));
        // root.render(element);
        // 在 HTML 文件中通过 <script src="helloworld_react.js"><//script> 引入
      }

      // 在 JSX 中使用 JavaScript 表达式
      function renderWithJSExpression() {
      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(
      <div>
        <h1>{1 + 1}</h1>
      </div>
      );
      }

      // 使用三元运算符代替 if else 语句
      function renderConditionalJSX() {
      var i = 1;
      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(
      <div>
        <h1>{i === 1 ? 'True!' : 'False'}</h1>
      </div>
      );
      }

      // 为 JSX 元素添加样式
      function renderWithStyle() {
      var myStyle = {
        fontSize: 100,
        color: '#FF0000'
      };
      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(
        <h1 style={myStyle}>菜鸟教程</h1>
      );
      }

      // JSX 中添加注释
      function renderWithComments() {
      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(
        <div>
          <h1>菜鸟教程</h1>
          {/* 这是一个注释 */}
          <p>注释示例</p>
        </div>
      );
      }

      // 在 JSX 中插入数组
      function renderWithArray() {
      var arr = [
        <h1>菜鸟教程</h1>,
        <h2>学的不仅是技术,更是梦想!</h2>,
      ];
      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(
        <div>{arr}</div>
      );
      }

      // 通过引号传递字符串属性
      function renderWithStringProps() {
        const title = "React 入门";
        const root = ReactDOM.createRoot(document.getElementById("root"));
        root.render(
          <div>
            <h1 title="Hello React!">Hello, React!</h1> {/* 直接使用字符串 */}
            <h2 title={title}>欢迎学习 React</h2> {/* 使用 JavaScript 变量 */}
          </div>
        );
      }

      // 在 JSX 的大括号内引用 JavaScript 变量
      function renderWithJSVariable() {
        const name = "菜鸟教程";
        const root = ReactDOM.createRoot(document.getElementById("root"));
        root.render(
          <div>
            <h1>{name}</h1> {/* 在大括号内引用变量 */}
            <p>欢迎学习 React,{name} 为您提供丰富的教程资源。</p>
          </div>
        );
      }

      // 在 JSX 的大括号内调用 JavaScript 函数
      function renderWithJSFunction() {
        function formatName(user) {
          return user.firstName + ' ' + user.lastName;
        }

        const user = {
          firstName: '菜鸟',
          lastName: '教程'
        };

        const root = ReactDOM.createRoot(document.getElementById("root"));
        root.render(
          <div>
            <h1>你好, {formatName(user)}!</h1> {/* 调用函数 */}
          </div>
        );
      }

      // 在 JSX 的大括号内使用 JavaScript 对象
      function renderWithJSObject() {
        const myStyle = {
          fontSize: 20,
          color: '#FF0000'
        };

        const root = ReactDOM.createRoot(document.getElementById("root"));
        root.render(
          <div>
            <h1 style={myStyle}>使用 JavaScript 对象来设置样式</h1> {/* 使用对象 */}
          </div>
        );
      }

      // 默认调用一个示例方法
      renderBasicJSX();
      // 你可以取消上面的调用,选择调用其他方法来学习不同的 JSX 功能
      // renderNestedJSX();
      // renderWithJSExpression();
      // renderConditionalJSX();
      // renderWithStyle();
      // renderWithComments();
      // renderWithArray();

      // renderWithStringProps();
      // renderWithJSVariable();
      // renderWithJSFunction();
      // renderWithJSObject();
    </script>
    </body>
    </html>

1.3 Refs响应

01.总结
    React 支持一种非常特殊的属性 Ref ,你可以用来绑定到 render() 输出的任何组件上。
    这个特殊的属性允许你引用 render() 返回的相应的支撑实例( backing instance ),这样就可以确保在任何时间总是拿到正确的实例。
    在 React 中,Refs(引用)提供了一种访问 DOM 元素或组件实例的方法。使用 Refs 可以直接操作 DOM 元素或获取子组件实例,适用于处理焦点、文本选择、媒体播放、触发强制动画或集成第三方 DOM 库等场景。
    ---------------------------------------------------------------------------------------------------------
    使用方法:
    使用 React.createRef 或 useRef 来创建和访问 refs。
    React.createRef 通常用于类组件,而 useRef 是一个 Hook,通常用于函数组件。
    此外,在事件处理函数中绑定 this 也可以通过类属性语法来避免每次渲染时都创建一个新的函数。
    ---------------------------------------------------------------------------------------------------------
    创建 Ref:在类组件的构造函数中使用 React.createRef 创建 ref 对象,并将其赋值给组件实例的一个属性。
    绑定 Ref:在 render 方法中,将 ref 对象绑定到需要引用的 DOM 元素上。
    访问 Ref:在组件的其它方法中,通过 this.myInputRef.current 访问绑定的 DOM 元素,可以操作该元素的属性和方法。

02.总结
    创建 Ref:在类组件和函数组件中使用 React.createRef 和 useRef。
    绑定 Ref:将 Ref 绑定到 DOM 元素或子组件。
    访问 Ref:通过 Ref 操作 DOM 元素或子组件实例。
    回调 Refs:通过回调函数创建 Ref(不推荐,但了解其用法)。
    ---------------------------------------------------------------------------------------------------------
    MyComponentClass:
        使用 React.createRef 创建 Ref。
        在 componentDidMount 中自动聚焦输入框。
        通过按钮点击事件改变输入框的背景颜色。
    MyComponentFunction:
        使用 useRef Hook 创建 Ref。
        点击按钮时自动聚焦输入框。
    ParentComponent 和 ChildComponent:
        ChildComponent 使用 Ref 访问子组件的实例方法。
        ParentComponent 使用 Ref 访问 ChildComponent 的方法来改变子组件的行为。

03.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <script type="text/babel">

    // 使用 React.createRef 创建 Ref 并访问 DOM 元素
    class MyComponentClass extends React.Component {
      constructor(props) {
        super(props);
        this.myRef = React.createRef();
      }

      componentDidMount() {
        // 在组件挂载后,聚焦输入框
        this.myRef.current.focus();
      }

      handleClick = () => {
        this.myRef.current.style.backgroundColor = 'yellow';
      };

      render() {
        return (
          <div>
            <input type="text" ref={this.myRef} />
            <button onClick={this.handleClick}>Change Background</button>
          </div>
        );
      }
    }

    // 使用 useRef Hook 创建 Ref 并访问 DOM 元素
    function MyComponentFunction() {
      const inputRef = React.useRef(null);

      const handleClick = () => {
        inputRef.current.focus();
      };

      return (
        <div>
          <input type="text" ref={inputRef} />
          <button onClick={handleClick}>Focus Input</button>
        </div>
      );
    }

    // 使用 Refs 访问子组件实例
    class ChildComponent extends React.Component {
      constructor(props) {
        super(props);
        this.inputRef = React.createRef();
      }

      focusInput = () => {
        this.inputRef.current.focus();
      };

      render() {
        return <input type="text" ref={this.inputRef} />;
      }
    }

    class ParentComponent extends React.Component {
      constructor(props) {
        super(props);
        this.childRef = React.createRef();
      }

      handleClick = () => {
        this.childRef.current.focusInput();
      };

      render() {
        return (
          <div>
            <ChildComponent ref={this.childRef} />
            <button onClick={this.handleClick}>Focus Child Input</button>
          </div>
        );
      }
    }

    // 渲染所有组件
    ReactDOM.createRoot(document.getElementById('root')).render(
      <div>
        <h1>React Refs 示例</h1>
        <h2>类组件中的 Ref</h2>
        <MyComponentClass />
        <h2>函数组件中的 Ref</h2>
        <MyComponentFunction />
        <h2>父子组件中的 Ref</h2>
        <ParentComponent />
      </div>
    );

    </script>
    <div id="root"></div>
    </body>
    </html>

1.4 Hooks组件

01.什么是 React Hooks?
    React Hooks 是一组函数,允许你在不编写类组件的情况下使用状态、生命周期方法和其他 React 特性。主要的 Hooks 包括 useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef 和 useImperativeHandle 等。

02.主要的 React Hooks
    a.useState 是最基本的 Hook,允许在函数组件中添加局部状态。
        import React, { useState } from 'react';

        function Counter() {
          const [count, setCount] = useState(0);

          return (
            <div>
              <p>You clicked {count} times</p>
              <button onClick={() => setCount(count + 1)}>Click me</button>
            </div>
          );
        }
    b.useEffect 允许在函数组件中执行副作用操作,如数据获取、订阅等。类似于类组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 这些生命周期方法的结合。
        import React, { useState, useEffect } from 'react';

        function Timer() {
          const [seconds, setSeconds] = useState(0);

          useEffect(() => {
            const interval = setInterval(() => {
              setSeconds(seconds => seconds + 1);
            }, 1000);

            return () => clearInterval(interval);
          }, []); // 依赖数组为空,仅在组件挂载和卸载时执行

          return <p>Timer: {seconds} seconds</p>;
        }
    c.useContext 用于在组件中访问 React Context 数据,而不需要通过每层组件传递 props。
        import React, { useContext } from 'react';

        const MyContext = React.createContext();

        function MyComponent() {
          const value = useContext(MyContext);
          return <div>{value}</div>;
        }
    d.useReducer 适用于状态逻辑较复杂的情况,类似于 Redux 的 reducer。
        import React, { useReducer } from 'react';

        function reducer(state, action) {
          switch (action.type) {
            case 'increment':
              return { count: state.count + 1 };
            case 'decrement':
              return { count: state.count - 1 };
            default:
              throw new Error();
          }
        }

        function Counter() {
          const [state, dispatch] = useReducer(reducer, { count: 0 });

          return (
            <div>
              <p>Count: {state.count}</p>
              <button onClick={() => dispatch({ type: 'increment' })}>+</button>
              <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
            </div>
          );
        }
    e.useCallback 返回一个 memoized 版本的回调函数,防止不必要的重新渲染。
        import React, { useState, useCallback } from 'react';

        function MyComponent() {
          const [count, setCount] = useState(0);

          const increment = useCallback(() => {
            setCount(c => c + 1);
          }, []); // 依赖数组为空,意味着此回调在初始渲染时会创建一次

          return <button onClick={increment}>Increment</button>;
        }
    f.useMemo 用于记住计算结果,避免每次渲染都重复计算。
        import React, { useMemo } from 'react';

        function MyComponent({ a, b }) {
          const result = useMemo(() => a + b, [a, b]);
          return <div>{result}</div>;
        }
    g.useRef 创建一个可在整个组件生命周期内保持不变的引用对象。
        import React, { useRef } from 'react';

        function TextInputWithFocusButton() {
          const inputEl = useRef(null);
          const onButtonClick = () => {
            inputEl.current.focus();
          };

          return (
            <>
              <input ref={inputEl} type="text" />
              <button onClick={onButtonClick}>Focus the input</button>
            </>
          );
        }

03.使用 React Hooks 的好处
    简化代码逻辑:通过 Hooks,可以在不编写类组件的情况下实现状态管理和生命周期方法,减少了代码复杂度。
    提升代码复用性:可以将逻辑提取到可重用的 Hooks 中,减少重复代码。
    更好的性能优化:使用 useEffect, useCallback, useMemo 等 Hooks 可以精确控制副作用和性能消耗。

04.注意事项
    仅在顶层使用 Hooks:不要在循环、条件或嵌套函数中调用 Hook,确保 Hooks 在每次渲染时都以相同的顺序调用。
    使用 ESLint 插件:React 官方提供了 eslint-plugin-react-hooks 插件来帮助你正确使用 Hooks。

05.综合实例
    import React, { useState, useEffect } from 'react';

    function Example() {
      const [count, setCount] = useState(0);

      useEffect(() => {
        document.title = `You clicked ${count} times`;
      }, [count]); // 仅在 count 发生变化时更新标题

      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>Click me</button>
        </div>
      );
    }

    export default Example;
    这个例子展示了如何使用 useState 管理状态,使用 useEffect 实现副作用(如更新页面标题)。
    通过使用 Hooks,可以更加简洁地实现组件功能,同时提高代码的可维护性和可复用性。

1.5 Ajax请求

01.总结
    FetchExample 组件: 使用 fetch API 发起请求,并在组件加载时获取数据。
    AxiosExample 组件: 使用 axios 库发起请求,axios 是一个流行的 HTTP 客户端库。
    JqueryExample 组件: 使用 jQuery 发起请求,演示如何与旧版技术结合使用 React。
    ReactDOM.createRoot: 用于渲染 React 组件到页面中。
    jQuery 和 axios: 在页面底部引入了 jQuery 和 axios 库,以便在 React 组件中使用。

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <div id="root"></div>
    <script type="text/babel">
      // 使用 fetch API 进行 AJAX 请求的 React 组件
      const FetchExample = () => {
        const [data, setData] = React.useState(null);
        const [loading, setLoading] = React.useState(true);

        React.useEffect(() => {
          const fetchData = async () => {
            try {
              const response = await fetch('https://api.example.com/data');
              const result = await response.json();
              setData(result);
              setLoading(false);
            } catch (error) {
              console.error('Error fetching data:', error);
            }
          };

          fetchData();
        }, []);

        if (loading) {
          return <div>Loading...</div>;
        }

        return (
          <div>
            <h1>Data from API:</h1>
            <pre>{JSON.stringify(data, null, 2)}</pre>
          </div>
        );
      };

      // 使用 axios 进行 AJAX 请求的 React 组件
      const AxiosExample = () => {
        const [data, setData] = React.useState(null);
        const [loading, setLoading] = React.useState(true);

        React.useEffect(() => {
          const fetchData = async () => {
            try {
              const response = await axios.get('https://api.example.com/data');
              setData(response.data);
              setLoading(false);
            } catch (error) {
              console.error('Error fetching data:', error);
            }
          };

          fetchData();
        }, []);

        if (loading) {
          return <div>Loading...</div>;
        }

        return (
          <div>
            <h1>Data from API:</h1>
            <pre>{JSON.stringify(data, null, 2)}</pre>
          </div>
        );
      };

      // 使用 jQuery 进行 AJAX 请求的 React 组件
      class JqueryExample extends React.Component {
        constructor(props) {
          super(props);
          this.state = { username: '', lastGistUrl: '' };
        }

        componentDidMount() {
          this.serverRequest = $.get(this.props.source, (result) => {
            const lastGist = result[0];
            this.setState({
              username: lastGist.owner.login,
              lastGistUrl: lastGist.html_url
            });
          });
        }

        componentWillUnmount() {
          this.serverRequest.abort();
        }

        render() {
          return (
            <div>
              {this.state.username} 用户最新的 Gist 共享地址:
              <a href={this.state.lastGistUrl}>{this.state.lastGistUrl}</a>
            </div>
          );
        }
      }

      // 渲染组件
      ReactDOM.createRoot(document.getElementById('root')).render(
        <div>
          <h2>Fetch Example:</h2>
          <FetchExample />
          <h2>Axios Example:</h2>
          <AxiosExample />
          <h2>jQuery Example:</h2>
          <JqueryExample source="https://api.github.com/users/octocat/gists" />
        </div>
      );
    </script>
    <!-- 引入 jQuery 库 -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- 引入 axios 库 -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    </body>
    </html>

1.6 Router路由

01.总结
    1.引入 React 和 React Router: 使用 <script> 标签引入本地的 React 和 ReactDOM,以及 React Router 的生产版本。
    2.基础路由组件: 定义了 Home, About, Contact 组件。
    3.嵌套路由: 定义了 Dashboard, Profile, Settings 组件,并在 Dashboard 组件中使用了 Outlet 来嵌套其他路由。
    4.动态路由: 定义了 User 组件来处理动态路由参数。
    5.404 页面: 使用 Navigate 组件来实现 404 页面重定向。
    6.主应用组件: 使用 Router, Routes, Route, 和 Link 设置路由,并在 Routes 中定义所有的路由路径和组件。

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/react-router-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <div id="root"></div>
    <script type="text/babel">
      const { BrowserRouter: Router, Routes, Route, Link, Navigate, Outlet, useParams } = ReactRouterDOM;

      // 创建基础路由组件
      const Home = () => <h2>Home</h2>;
      const About = () => <h2>About</h2>;
      const Contact = () => <h2>Contact</h2>;

      // 嵌套路由组件
      const Dashboard = () => (
        <div>
          <h2>Dashboard</h2>
          <nav>
            <ul>
              <li><Link to="profile">Profile</Link></li>
              <li><Link to="settings">Settings</Link></li>
            </ul>
          </nav>
          <Outlet />
        </div>
      );

      const Profile = () => <h2>Profile</h2>;
      const Settings = () => <h2>Settings</h2>;

      // 动态路由组件
      const User = () => {
        const { userId } = useParams();
        return <h2>User ID: {userId}</h2>;
      };

      // 404 页面
      const NotFound = () => <h2>404 Page Not Found</h2>;

      // 主应用组件
      const App = () => {
        return (
          <Router>
            <div>
              <nav>
                <ul>
                  <li><Link to="/">Home</Link></li>
                  <li><Link to="/about">About</Link></li>
                  <li><Link to="/contact">Contact</Link></li>
                  <li><Link to="/dashboard">Dashboard</Link></li>
                  <li><Link to="/user/1">User 1</Link></li>
                  <li><Link to="/user/2">User 2</Link></li>
                </ul>
              </nav>
              <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/about" element={<About />} />
                <Route path="/contact" element={<Contact />} />
                <Route path="/dashboard" element={<Dashboard />}>
                  <Route path="profile" element={<Profile />} />
                  <Route path="settings" element={<Settings />} />
                </Route>
                <Route path="/user/:userId" element={<User />} />
                <Route path="*" element={<Navigate to="/" replace />} />
              </Routes>
            </div>
          </Router>
        );
      };

      // 渲染应用
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(<App />);
    </script>
    </body>
    </html>

2 核心

2.1 条件判断

01.总结
    if 语句:适合在 render 方法或函数组件的返回值中使用来决定渲染内容。
    三元运算符:适合在 JSX 中进行简洁的条件渲染。
    逻辑与 (&&) 运算符:适合在 JSX 中条件渲染,当条件为 true 时渲染元素。
    switch 语句:适合处理多个条件,进行不同内容的渲染。

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <div id="root"></div>

    <script type="text/babel">

      // if 语句示例
      class IfExample extends React.Component {
        render() {
          const isLoggedIn = this.props.isLoggedIn;
          let content;

          if (isLoggedIn) {
            content = <h1>Welcome back!</h1>;
          } else {
            content = <h1>Please sign up.</h1>;
          }

          return (
            <div>
              <h2>If Example:</h2>
              {content}
            </div>
          );
        }
      }

      // 三元运算符示例
      const TernaryExample = (props) => {
        const isLoggedIn = props.isLoggedIn;
        return (
          <div>
            <h2>Ternary Example:</h2>
            {isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please sign up.</h1>}
          </div>
        );
      };

      // 逻辑与 (&&) 运算符示例
      const AndExample = (props) => {
        const isLoggedIn = props.isLoggedIn;
        return (
          <div>
            <h2>Logical AND Example:</h2>
            {isLoggedIn && <h1>Welcome back!</h1>}
            {!isLoggedIn && <h1>Please sign up.</h1>}
          </div>
        );
      };

      // switch 语句示例
      class SwitchExample extends React.Component {
        render() {
          const userRole = this.props.userRole;
          let content;

          switch (userRole) {
            case 'admin':
              content = <h1>Welcome, Admin!</h1>;
              break;
            case 'user':
              content = <h1>Welcome, User!</h1>;
              break;
            case 'guest':
              content = <h1>Welcome, Guest!</h1>;
              break;
            default:
              content = <h1>Who are you?</h1>;
          }

          return (
            <div>
              <h2>Switch Example:</h2>
              {content}
            </div>
          );
        }
      }

      // 渲染组件
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(
        <div>
          <IfExample isLoggedIn={true} />
          <TernaryExample isLoggedIn={true} />
          <AndExample isLoggedIn={false} />
          <SwitchExample userRole="admin" />
        </div>
      );

    </script>
    </body>
    </html>

2.2 元素渲染

01.总结
    说明:
    App 组件:最简单的 React 组件,显示 "Hello, React!"。
    renderElement 方法:渲染一个简单的元素 <h1>Hello, world!</h1>。
    tick 方法:每秒钟更新一次页面,显示当前时间。
    Clock 组件:封装了一个显示时间的组件,并使用 setInterval 每秒更新一次。
    ---------------------------------------------------------------------------------------------------------
    你可以取消注释 renderElement() 或 startClock() 方法调用,以分别演示不同的渲染功能。
    默认情况下,页面加载时会执行 App 组件的渲染。

02.代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <!-- 引入本地的 React、ReactDOM 和 Babel -->
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <!-- 根 DOM 节点,React 将在此节点内渲染内容 -->
    <div id="example"></div>

    <!-- JavaScript 代码,包括 React 组件定义和渲染逻辑 -->
    <script type="text/babel">
      // 简单的 React 组件
      function App() {
        return <h1>Hello, React!</h1>;
      }

      // 将元素渲染到 DOM 中
      function renderElement() {
        // 创建一个 React 元素
        const element = <h1>Hello, world!</h1>;

        // 获取 DOM 容器并创建根节点
        const root = ReactDOM.createRoot(document.getElementById("example"));

        // 渲染 React 元素到 DOM 中的根节点
        root.render(element);
      }

      // 更新渲染元素,显示当前时间
      function tick() {
        // 创建一个 React 元素,包含当前时间
        const element = (
          <div>
            <h1>Hello, world!</h1>
            <h2>现在是 {new Date().toLocaleTimeString()}.</h2>
          </div>
        );

        // 获取 DOM 容器并创建根节点
        const root = ReactDOM.createRoot(document.getElementById("example"));

        // 渲染 React 元素到 DOM 中的根节点
        root.render(element);
      }

      // 定时器:每秒更新一次页面,显示当前时间
      setInterval(tick, 1000);

      // 封装成一个 React 组件的时钟
      class Clock extends React.Component {
        render() {
          return (
            <div>
              <h1>Hello, world!</h1>
              <h2>现在是 {this.props.date.toLocaleTimeString()}.</h2>
            </div>
          );
        }
      }

      // 定时器:每秒更新一次页面,显示当前时间(使用组件)
      function startClock() {
        const root = ReactDOM.createRoot(document.getElementById("example"));
        setInterval(() => {
          root.render(<Clock date={new Date()} />);
        }, 1000);
      }

      // 初始化页面时,调用简单的 App 组件渲染
      const root = ReactDOM.createRoot(document.getElementById("example"));
      root.render(<App />);

      // 调用其他方法进行不同的演示
      // renderElement(); // 渲染简单元素
      // startClock(); // 渲染时钟组件
    </script>
    </body>
    </html>

2.3 列表渲染

01.总结
    RenderList 组件:使用 map() 方法遍历数组并生成列表项,将数据渲染为 HTML 元素。
    FilteredList 组件:使用 filter() 方法筛选出特定条件的数组项(例如,职业为“化学家”的人),并使用 map() 将筛选后的数据渲染为列表。
    KeyedList 组件:确保每个列表项都有唯一的 key 属性,以便 React 能够高效地管理和更新列表项。

02.代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="UTF-8" />
      <title>React 列表渲染示例</title>
      <!-- 引入本地的 React、ReactDOM 和 Babel -->
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <div id="root"></div>

    <script type="text/babel">
      // 概览: 使用 React 渲染简单的 JSX 元素
      function App() {
        return (
          <div>
            <h1>Hello React!</h1>
            <RenderList />
            <FilteredList />
            <KeyedList />
          </div>
        );
      }

      // 从数组中渲染数据: 使用 map() 方法生成列表项
      function RenderList() {
        const people = [
          '凯瑟琳·约翰逊: 数学家',
          '马里奥·莫利纳: 化学家',
          '穆罕默德·阿卜杜勒·萨拉姆: 物理学家',
          '珀西·莱温·朱利亚: 化学家',
          '苏布拉马尼扬·钱德拉塞卡: 天体物理学家'
        ];

        // 使用 map() 方法遍历数组并生成列表项
        const listItems = people.map((person, index) =>
          <li key={index}>{person}</li>
        );

        return (
          <div>
            <h2>从数组中渲染数据</h2>
            <ul>{listItems}</ul>
          </div>
        );
      }

      // 对数组项进行过滤: 使用 filter() 筛选特定条件的项并渲染
      function FilteredList() {
        const people = [
          { id: 1, name: '凯瑟琳·约翰逊', profession: '数学家' },
          { id: 2, name: '马里奥·莫利纳', profession: '化学家' },
          { id: 3, name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家' },
          { id: 4, name: '珀西·莱温·朱利亚', profession: '化学家' },
          { id: 5, name: '苏布拉马尼扬·钱德拉塞卡', profession: '天体物理学家' }
        ];

        // 使用 filter() 方法筛选出职业为化学家的项
        const chemists = people.filter(person => person.profession === '化学家');

        // 使用 map() 方法遍历筛选后的数组并生成列表项
        const listItems = chemists.map(person =>
          <li key={person.id}>{person.name}: {person.profession}</li>
        );

        return (
          <div>
            <h2>对数组项进行过滤</h2>
            <ul>{listItems}</ul>
          </div>
        );
      }

      // 用 key 保持列表项的顺序: 使用唯一的 key 属性
      function KeyedList() {
        const people = [
          { id: 1, name: '凯瑟琳·约翰逊', profession: '数学家' },
          { id: 2, name: '马里奥·莫利纳', profession: '化学家' },
          { id: 3, name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家' },
          { id: 4, name: '珀西·莱温·朱利亚', profession: '化学家' },
          { id: 5, name: '苏布拉马尼扬·钱德拉塞卡', profession: '天体物理学家' }
        ];

        // 使用 map() 方法生成列表项,并为每个项指定一个唯一的 key 属性
        const listItems = people.map(person =>
          <li key={person.id}>{person.name}: {person.profession}</li>
        );

        return (
          <div>
            <h2>用 key 保持列表项的顺序</h2>
            <ul>{listItems}</ul>
          </div>
        );
      }

      // 渲染主组件
      ReactDOM.render(<App />, document.getElementById('root'));
    </script>
    </body>
    </html>

2.4 条件渲染

01.总结
    条件返回 JSX:Item 组件根据 isPacked 属性返回不同的 JSX。
    选择性地返回 null:ItemWithNull 组件在 isPacked 为 true 时返回 null,不渲染任何内容。
    选择性地包含 JSX:ItemWithConditionalJSX 组件使用 && 运算符在满足条件时添加勾选符号。
    三目运算符(?:):ItemWithTernaryOperator 组件使用三目运算符进行条件渲染。
    选择性地将 JSX 赋值给变量:ItemWithVariable 组件通过变量 itemContent 控制渲染内容。

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <div id="root"></div>

    <script type="text/babel">

      // 条件返回 JSX
      function Item({ name, isPacked }) {
        if (isPacked) {
          return <li className="item">{name} ✔</li>;
        }
        return <li className="item">{name}</li>;
      }

      // 选择性地返回 null
      function ItemWithNull({ name, isPacked }) {
        if (isPacked) {
          return null;
        }
        return <li className="item">{name}</li>;
      }

      // 选择性地包含 JSX
      function ItemWithConditionalJSX({ name, isPacked }) {
        return (
          <li className="item">
            {name} {isPacked && '✔'}
          </li>
        );
      }

      // 三目运算符(?:)
      function ItemWithTernaryOperator({ name, isPacked }) {
        return (
          <li className="item">
            {isPacked ? <del>{name} ✔</del> : name}
          </li>
        );
      }

      // 选择性地将 JSX 赋值给变量
      function ItemWithVariable({ name, isPacked }) {
        let itemContent = name;
        if (isPacked) {
          itemContent = <del>{name} ✔</del>;
        }
        return <li className="item">{itemContent}</li>;
      }

      function PackingList() {
        return (
          <section>
            <h1>Sally Ride 的行李清单</h1>
            <ul>
              <Item name="宇航服" isPacked={true} />
              <Item name="带金箔的头盔" isPacked={true} />
              <Item name="Tam 的照片" isPacked={false} />
            </ul>
            <h2>选择性地返回 null</h2>
            <ul>
              <ItemWithNull name="宇航服" isPacked={true} />
              <ItemWithNull name="带金箔的头盔" isPacked={true} />
              <ItemWithNull name="Tam 的照片" isPacked={false} />
            </ul>
            <h2>选择性地包含 JSX</h2>
            <ul>
              <ItemWithConditionalJSX name="宇航服" isPacked={true} />
              <ItemWithConditionalJSX name="带金箔的头盔" isPacked={true} />
              <ItemWithConditionalJSX name="Tam 的照片" isPacked={false} />
            </ul>
            <h2>三目运算符(?:)</h2>
            <ul>
              <ItemWithTernaryOperator name="宇航服" isPacked={true} />
              <ItemWithTernaryOperator name="带金箔的头盔" isPacked={true} />
              <ItemWithTernaryOperator name="Tam 的照片" isPacked={false} />
            </ul>
            <h2>选择性地将 JSX 赋值给变量</h2>
            <ul>
              <ItemWithVariable name="宇航服" isPacked={true} />
              <ItemWithVariable name="带金箔的头盔" isPacked={true} />
              <ItemWithVariable name="Tam 的照片" isPacked={false} />
            </ul>
          </section>
        );
      }

      // 渲染组件
      ReactDOM.render(<PackingList />, document.getElementById('root'));

    </script>
    </body>
    </html>

2.5 表单与事件

01.总结
    引入 React 和 Babel: 引入 React、ReactDOM 和 Babel 脚本。
    示例1: 演示如何在组件中处理输入框的变化。
    示例2: 演示如何在父组件和子组件中共享表单状态。
    示例3: 演示如何处理 select 下拉菜单。
    示例4: 演示如何处理多个表单输入,包括复选框和数字输入框。
    示例5: 演示如何通过点击按钮处理事件。
    示例6: 演示如何从子组件中更新父组件的状态。

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <div id="root"></div>

    <script type="text/babel">
      // 示例 1: 基本的表单处理
      class HelloMessage extends React.Component {
        constructor(props) {
          super(props);
          this.state = { value: 'Hello Runoob!' };
          this.handleChange = this.handleChange.bind(this);
        }

        handleChange(event) {
          this.setState({ value: event.target.value });
        }

        render() {
          const value = this.state.value;
          return (
            <div>
              <input type="text" value={value} onChange={this.handleChange} />
              <h4>{value}</h4>
            </div>
          );
        }
      }

      // 示例 2: 在子组件中使用表单
      class Content extends React.Component {
        render() {
          return (
            <div>
              <input type="text" value={this.props.myDataProp} onChange={this.props.updateStateProp} />
              <h4>{this.props.myDataProp}</h4>
            </div>
          );
        }
      }

      class HelloMessageWithContent extends React.Component {
        constructor(props) {
          super(props);
          this.state = { value: 'Hello Runoob!' };
          this.handleChange = this.handleChange.bind(this);
        }

        handleChange(event) {
          this.setState({ value: event.target.value });
        }

        render() {
          const value = this.state.value;
          return (
            <div>
              <Content myDataProp={value} updateStateProp={this.handleChange} />
            </div>
          );
        }
      }

      // 示例 3: Select 下拉菜单
      class FlavorForm extends React.Component {
        constructor(props) {
          super(props);
          this.state = { value: 'coconut' };

          this.handleChange = this.handleChange.bind(this);
          this.handleSubmit = this.handleSubmit.bind(this);
        }

        handleChange(event) {
          this.setState({ value: event.target.value });
        }

        handleSubmit(event) {
          alert('Your favorite flavor is: ' + this.state.value);
          event.preventDefault();
        }

        render() {
          return (
            <form onSubmit={this.handleSubmit}>
              <label>
                选择您最喜欢的网站
                <select value={this.state.value} onChange={this.handleChange}>
                  <option value="gg">Google</option>
                  <option value="rn">Runoob</option>
                  <option value="tb">Taobao</option>
                  <option value="fb">Facebook</option>
                </select>
              </label>
              <input type="submit" value="提交" />
            </form>
          );
        }
      }

      // 示例 4: 处理多个表单输入
      class Reservation extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            isGoing: true,
            numberOfGuests: 2
          };

          this.handleInputChange = this.handleInputChange.bind(this);
        }

        handleInputChange(event) {
          const target = event.target;
          const value = target.type === 'checkbox' ? target.checked : target.value;
          const name = target.name;

          this.setState({
            [name]: value
          });
        }

        render() {
          return (
            <form>
              <label>
                是否离开:
                <input
                  name="isGoing"
                  type="checkbox"
                  checked={this.state.isGoing}
                  onChange={this.handleInputChange} />
              </label>
              <br />
              <label>
                访客数:
                <input
                  name="numberOfGuests"
                  type="number"
                  value={this.state.numberOfGuests}
                  onChange={this.handleInputChange} />
              </label>
            </form>
          );
        }
      }

      // 示例 5: 事件处理
      class HelloMessageWithButton extends React.Component {
        constructor(props) {
          super(props);
          this.state = { value: 'Hello Runoob!' };
          this.handleChange = this.handleChange.bind(this);
        }

        handleChange() {
          this.setState({ value: '菜鸟教程' });
        }

        render() {
          const value = this.state.value;
          return (
            <div>
              <button onClick={this.handleChange}>点我</button>
              <h4>{value}</h4>
            </div>
          );
        }
      }

      // 示例 6: 从子组件中更新父组件的 state
      class ContentWithButton extends React.Component {
        render() {
          return (
            <div>
              <button onClick={this.props.updateStateProp}>点我</button>
              <h4>{this.props.myDataProp}</h4>
            </div>
          );
        }
      }

      class HelloMessageWithContentAndButton extends React.Component {
        constructor(props) {
          super(props);
          this.state = { value: 'Hello Runoob!' };
          this.handleChange = this.handleChange.bind(this);
        }

        handleChange() {
          this.setState({ value: '菜鸟教程' });
        }

        render() {
          const value = this.state.value;
          return (
            <div>
              <ContentWithButton myDataProp={value} updateStateProp={this.handleChange} />
            </div>
          );
        }
      }

      // 渲染示例
      ReactDOM.createRoot(document.getElementById('root')).render(
        <div>
          <h2>示例 1: 基本的表单处理</h2>
          <HelloMessage />
          <h2>示例 2: 在子组件中使用表单</h2>
          <HelloMessageWithContent />
          <h2>示例 3: Select 下拉菜单</h2>
          <FlavorForm />
          <h2>示例 4: 处理多个表单输入</h2>
          <Reservation />
          <h2>示例 5: 事件处理</h2>
          <HelloMessageWithButton />
          <h2>示例 6: 从子组件中更新父组件的 state</h2>
          <HelloMessageWithContentAndButton />
        </div>
      );
    </script>
    </body>
    </html>

2.6 事件处理

01.总结
    引入 React 和 Babel: 引入 React、ReactDOM 和 Babel 脚本,以支持 JSX 和 React 渲染。
    示例1: ActionLink 组件展示了如何处理链接点击事件并阻止默认行为。
    示例2: Toggle 组件展示了如何处理按钮点击事件来切换状态,并在 handleClick 方法中绑定 this。
    示例3: LoggingButton 使用属性初始化器语法来绑定 this。
    示例4: LoggingButtonWithArrowFunction 使用箭头函数来绑定 this。
    示例5: DeleteButton 组件展示了如何向事件处理函数传递额外的参数。
    示例6: Popper 组件展示了如何使用 bind() 方法传递参数,并且事件对象 e 必须放在参数列表的最后。

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>React Events Handling</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <div id="root"></div>

    <script type="text/babel">
      // 示例 1: 基本事件处理
      function ActionLink() {
        function handleClick(e) {
          e.preventDefault();
          console.log('链接被点击');
        }

        return (
          <a href="#" onClick={handleClick}>
            点我
          </a>
        );
      }

      // 示例 2: Toggle 组件
      class Toggle extends React.Component {
        constructor(props) {
          super(props);
          this.state = { isToggleOn: true };
          this.handleClick = this.handleClick.bind(this);
        }

        handleClick() {
          this.setState(prevState => ({
            isToggleOn: !prevState.isToggleOn
          }));
        }

        render() {
          return (
            <button onClick={this.handleClick}>
              {this.state.isToggleOn ? 'ON' : 'OFF'}
            </button>
          );
        }
      }

      // 示例 3: 使用属性初始化器语法绑定 this
      class LoggingButton extends React.Component {
        handleClick = () => {
          console.log('this is:', this);
        }

        render() {
          return (
            <button onClick={this.handleClick}>
              Click me
            </button>
          );
        }
      }

      // 示例 4: 使用箭头函数绑定 this
      class LoggingButtonWithArrowFunction extends React.Component {
        handleClick() {
          console.log('this is:', this);
        }

        render() {
          return (
            <button onClick={(e) => this.handleClick(e)}>
              Click me
            </button>
          );
        }
      }

      // 示例 5: 向事件处理程序传递参数
      class DeleteButton extends React.Component {
        deleteRow(id, e) {
          e.preventDefault();
          console.log(`Delete row with id: ${id}`);
        }

        render() {
          const id = 1; // 示例 id
          return (
            <button onClick={(e) => this.deleteRow(id, e)}>
              Delete Row
            </button>
          );
        }
      }

      // 示例 6: 使用 bind() 传递参数
      class Popper extends React.Component {
        constructor() {
          super();
          this.state = { name: 'Hello world!' };
        }

        preventPop(name, e) {
          e.preventDefault();
          alert(name);
        }

        render() {
          return (
            <div>
              <p>hello</p>
              {/* 通过 bind() 方法传递参数 */}
              <a href="https://reactjs.org" onClick={this.preventPop.bind(this, this.state.name)}>
                Click
              </a>
            </div>
          );
        }
      }

      // 渲染所有示例
      ReactDOM.createRoot(document.getElementById('root')).render(
        <div>
          <h2>示例 1: 基本事件处理</h2>
          <ActionLink />
          <h2>示例 2: Toggle 组件</h2>
          <Toggle />
          <h2>示例 3: 使用属性初始化器语法绑定 this</h2>
          <LoggingButton />
          <h2>示例 4: 使用箭头函数绑定 this</h2>
          <LoggingButtonWithArrowFunction />
          <h2>示例 5: 向事件处理程序传递参数</h2>
          <DeleteButton />
          <h2>示例 6: 使用 bind() 传递参数</h2>
          <Popper />
        </div>
      );
    </script>
    </body>
    </html>

3 组件

3.1 定义

01.总结
    组件:Profile 组件用于显示一个科学家的头像。
    使用组件:Gallery 组件展示了多个 Profile 组件,并将它们组织到一个 <section> 标签中。
    渲染:通过 ReactDOM.render 方法,将 Gallery 组件渲染到页面上的 root 节点中。

02.代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
      <script type="text/babel">
        // 1. 组件:UI构成要素
        // 在 Web 开发中,我们使用 HTML 标签来创建页面结构。在 React 中,我们使用组件来构建 UI。

        // 2. 定义组件
        // 第一阶段:定义组件
        // 组件是 JavaScript 函数,它们返回 JSX(JavaScript XML)。JSX 让你在 JavaScript 中写 HTML。

        // 第二阶段:导出组件
        // 使用 `export default` 将组件导出,以便其他文件可以导入和使用它。

        function Profile() {
          return (
            <img
              src="https://i.imgur.com/MK3eW3Am.jpg"
              alt="Katherine Johnson"
            />
          );
        }

        // 第三阶段:添加标签
        // 组件可以包含其他 HTML 标签,并返回它们。

        // 使用组件
        // 你可以在其他组件中使用之前定义的组件。例如,下面的 Gallery 组件使用了多个 Profile 组件。

        function Gallery() {
          return (
            <section>
              <h1>了不起的科学家</h1>
              <Profile />
              <Profile />
              <Profile />
            </section>
          );
        }

        // 渲染组件
        // 使用 ReactDOM.render() 方法将 Gallery 组件渲染到页面上的 DOM 节点中。
        ReactDOM.render(<Gallery />, document.getElementById('root'));
      </script>
      <!-- 定义一个根节点 -->
      <div id="root"></div>
    </body>
    </html>

3.2 分类

01.总结
    函数组件
    类组件
    复合组件

02.代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <script type="text/babel">
      // 1. 函数组件示例
      // 定义一个函数组件 Welcome
      function Welcome(props) {
        return <h1>Hello, {props.name}!</h1>;
      }

      // 2. 类组件示例
      // 使用 ES6 类语法定义一个类组件 Welcome
      class WelcomeClass extends React.Component {
        render() {
          return <h1>Hello, {this.props.name}!</h1>;
        }
      }

      // 3. 复合组件示例
      // 定义多个小组件并组合成一个大组件 App
      function Name(props) {
        return <h1>网站名称:{props.name}</h1>;
      }

      function Url(props) {
        return <h1>网站地址:{props.url}</h1>;
      }

      function Nickname(props) {
        return <h1>网站小名:{props.nickname}</h1>;
      }

      function App() {
        return (
          <div>
            <Name name="菜鸟教程" />
            <Url url="http://www.runoob.com" />
            <Nickname nickname="Runoob" />
          </div>
        );
      }

      // 4. 渲染函数组件 Welcome
      function RenderWelcome() {
        return <Welcome name="World" />;
      }

      // 5. 渲染类组件 WelcomeClass
      function RenderWelcomeClass() {
        return <WelcomeClass name="World" />;
      }

      // 6. 渲染复合组件 App
      function RenderApp() {
        return <App />;
      }

      // 选择需要渲染的组件,这里选择渲染 App 组件
      const element = <RenderApp />;
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(element);
    </script>

    <!-- 定义一个根节点 -->
    <div id="root"></div>
    </body>
    </html>

3.3 State

00.总结
    state 和 props 主要的区别在于 props 是不可变的,而 state 可以根据与用户交互来改变。
    这就是为什么有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 props 来传递数据。

01.总结
    1.函数组件示例:Welcome 组件接收 props 并返回一个包含欢迎信息的 h1 元素。
    2.类组件示例:Counter 组件使用类组件语法,包含 state 和一个按钮来增加计数值。
    3.带状态的函数组件示例:Clock 组件使用 React Hooks (useState 和 useEffect) 来管理时间的状态,并每秒更新。
    4.多个组件示例:FormattedDate 组件接收 date 属性并格式化显示时间。App 组件包含了多个子组件的使用,包括带状态的类组件和函数组件。它还演示了如何在类组件中管理状态和生命周期方法(componentDidMount 和 componentWillUnmount)。

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
    </head>
    <body>
    <div id="root"></div>
    <script type="text/babel">

      // 函数组件示例
      function Welcome(props) {
        return <h1>Hello, {props.name}!</h1>;
      }

      // 类组件示例
      class Counter extends React.Component {
        constructor(props) {
          super(props);
          this.state = { count: 0 }; // 初始化状态
        }

        increment = () => {
          this.setState({ count: this.state.count + 1 }); // 更新状态
        }

        render() {
          return (
            <div>
              <p>Count: {this.state.count}</p>
              <button onClick={this.increment}>Increment</button>
            </div>
          );
        }
      }

      // 使用函数组件
      function HelloMessage(props) {
        return <h1>Hello World!</h1>;
      }

      // 使用带状态的函数组件
      function Clock() {
        const [date, setDate] = React.useState(new Date());

        React.useEffect(() => {
          const timerID = setInterval(() => setDate(new Date()), 1000);
          return () => clearInterval(timerID);
        }, []);

        return (
          <div>
            <h1>Hello, world!</h1>
            <h2>现在是 {date.toLocaleTimeString()}.</h2>
          </div>
        );
      }

      // 使用多个组件的示例
      function FormattedDate(props) {
        return <h2>现在是 {props.date.toLocaleTimeString()}.</h2>;
      }

      class App extends React.Component {
        constructor(props) {
          super(props);
          this.state = { date: new Date() };
        }

        componentDidMount() {
          this.timerID = setInterval(() => this.tick(), 1000);
        }

        componentWillUnmount() {
          clearInterval(this.timerID);
        }

        tick() {
          this.setState({ date: new Date() });
        }

        render() {
          return (
            <div>
              <h1>Hello, world!</h1>
              <FormattedDate date={this.state.date} />
              <Welcome name="World" />
              <Counter />
              <Clock />
            </div>
          );
        }
      }

      // 渲染 App 组件到 DOM
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(<App />);

    </script>
    </body>
    </html>

3.4 Props

00.总结
    state 和 props 主要的区别在于 props 是不可变的,而 state 可以根据与用户交互来改变。
    这就是为什么有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 props 来传递数据。

01.总结
    1.使用 Props: HelloMessage 组件接收 name 属性并显示它。
    2.默认 Props: HelloMessageWithDefault 组件通过 defaultProps 提供了默认值。
    3.多个 Props: UserCard 组件展示了如何处理多个 props。
    4.PropTypes 验证: Greeting 组件使用 propTypes 验证属性类型。
    5.传递回调函数: ParentComponent 和 ChildComponent 组件展示了如何将回调函数作为 props 传递。
    6.解构 Props: GreetingWithDestructuring 组件展示了如何在函数组件中解构 props。
    7.组合使用 state 和 props: WebSite 组件展示了如何将 state 传递到子组件。
    8.Props 验证: MyComponent 使用 propTypes 验证器来检查各种类型的 props。

02.代码
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>Hello React!</title>
      <script src="../../statics/react/zijie/react.production.min.js"></script>
      <script src="../../statics/react/zijie/react-dom.production.min.js"></script>
      <script src="../../statics/react/zijie/babel.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/[email protected]/prop-types.min.js"></script>
    </head>
    <body>
    <div id="root"></div>
    <script type="text/babel">

      // 使用 Props
      function HelloMessage(props) {
        return <h1>Hello {props.name}!</h1>;
      }

      const HelloElement = <HelloMessage name="Runoob" />;

      // 默认 Props
      class HelloMessageWithDefault extends React.Component {
        render() {
          return <h1>Hello, {this.props.name}</h1>;
        }
      }
      HelloMessageWithDefault.defaultProps = {
        name: 'Runoob'
      };

      // 多个 Props
      const UserCard = (props) => {
        return (
          <div>
            <h2>{props.name}</h2>
            <p>Age: {props.age}</p>
            <p>Location: {props.location}</p>
          </div>
        );
      };

      const App = () => {
        return (
          <div>
            <UserCard name="Alice" age={25} location="New York" />
          </div>
        );
      };

      // PropTypes 验证
      const PropTypes = window.PropTypes;

      class Greeting extends React.Component {
        render() {
          return <h1>Hello, {this.props.name}!</h1>;
        }
      }

      Greeting.propTypes = {
        name: PropTypes.string.isRequired
      };

      // 传递回调函数作为 Props
      class ParentComponent extends React.Component {
        constructor(props) {
          super(props);
          this.state = { message: '' };
        }

        handleMessage = (msg) => {
          this.setState({ message: msg });
        };

        render() {
          return (
            <div>
              <ChildComponent onMessage={this.handleMessage} />
              <p>Message from Child: {this.state.message}</p>
            </div>
          );
        }
      }

      const ChildComponent = (props) => {
        const sendMessage = () => {
          props.onMessage('Hello from Child!');
        };

        return (
          <div>
            <button onClick={sendMessage}>Send Message</button>
          </div>
        );
      };

      // 解构 Props
      const GreetingWithDestructuring = ({ name }) => {
        return <h1>Hello, {name}!</h1>;
      };

      // 组合使用 state 和 props
      class WebSite extends React.Component {
        constructor() {
          super();
          this.state = {
            name: "菜鸟教程",
            site: "https://www.runoob.com"
          };
        }
        render() {
          return (
            <div>
              <Name name={this.state.name} />
              <Link site={this.state.site} />
            </div>
          );
        }
      }

      class Name extends React.Component {
        render() {
          return <h1>{this.props.name}</h1>;
        }
      }

      class Link extends React.Component {
        render() {
          return <a href={this.props.site}>{this.props.site}</a>;
        }
      }

      // Props 验证
      class MyComponent extends React.Component {
        static propTypes = {
          title: PropTypes.string.isRequired, // 必须是字符串且必需
          age: PropTypes.number,              // 可选的数字
          isAdmin: PropTypes.bool,            // 可选的布尔值
          user: PropTypes.shape({             // 必须是具有特定形状的对象
            name: PropTypes.string,
            email: PropTypes.string
          }),
          items: PropTypes.arrayOf(PropTypes.string), // 必须是字符串数组
          callback: PropTypes.func,           // 可选的函数
          children: PropTypes.node,           // 可选的可以渲染的内容
          options: PropTypes.oneOf(['option1', 'option2']), // 必须是特定值之一
        };

        render() {
          return (
            <div>
              <h1>{this.props.title}</h1>
              {this.props.age && <p>Age: {this.props.age}</p>}
              {this.props.isAdmin && <p>Admin</p>}
              {this.props.user && (
                <div>
                  <p>Name: {this.props.user.name}</p>
                  <p>Email: {this.props.user.email}</p>
                </div>
              )}
              {this.props.items && (
                <ul>
                  {this.props.items.map((item, index) => (
                    <li key={index}>{item}</li>
                  ))}
                </ul>
              )}
              {this.props.callback && (
                <button onClick={this.props.callback}>Click me</button>
              )}
              {this.props.children}
              {this.props.options && <p>Option: {this.props.options}</p>}
            </div>
          );
        }
      }

      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(
        <div>
          <HelloElement />
          <HelloMessageWithDefault />
          <App />
          <Greeting name="Alice" />
          <ParentComponent />
          <GreetingWithDestructuring name="Alice" />
          <WebSite />
          <MyComponent
            title="Hello, World!"
            age={30}
            isAdmin={true}
            user={{ name: "John Doe", email: "[email protected]" }}
            items={['Item 1', 'Item 2', 'Item 3']}
            callback={() => alert('Button clicked!')}
            options="option1"
          >
            <p>This is a child element</p>
          </MyComponent>
        </div>
      );

    </script>
    </body>
    </html>

3.5 生命周期

00.总结
    a.生命周期方法
        React 组件的生命周期分为三个主要阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。
        a.挂载阶段
            constructor(props): 构造函数,用于初始化组件状态或绑定方法。
            static getDerivedStateFromProps(props, state): 在调用 render 之前执行,用于更新状态。
            componentDidMount(): 组件挂载后执行,可以在这里进行 DOM 操作或数据请求。
        b.更新阶段
            static getDerivedStateFromProps(props, state): 与挂载阶段相同,用于更新状态。
            shouldComponentUpdate(nextProps, nextState): 返回布尔值,决定组件是否需要重新渲染。
            render(): 渲染组件的 UI。
            getSnapshotBeforeUpdate(prevProps, prevState): 在 DOM 更新前调用,用于捕获信息(如滚动位置)。
            componentDidUpdate(prevProps, prevState, snapshot): 在组件更新后调用。
        c.卸载阶段
            componentWillUnmount(): 组件即将卸载时调用,用于清理资源(如定时器、事件监听等)。
    b.状态管理
        状态是组件内部的数据,通过 this.state 来定义和管理。可以使用 setState 方法更新状态。
    c.属性传递
        组件可以通过 this.props 访问传递给组件的属性,并可以使用 PropTypes 进行类型检查。
    d.事件处理
        通过事件处理函数处理用户交互。需要使用 .bind(this) 或箭头函数来确保 this 指向正确。
    e.条件渲染
        通过条件语句控制组件的渲染。
    f.列表渲染
        通过数组的 map 方法渲染列表。
    g.受控组件
        通过状态控制表单元素的值。
    h.setState 方法
        setState 是 React 中用于更新组件状态的方法。

01.生命周期方法
    React 组件的生命周期分为三个主要阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。
    01.挂载阶段
        constructor(props): 构造函数,用于初始化组件状态或绑定方法。
        static getDerivedStateFromProps(props, state): 在调用 render 之前执行,用于更新状态。
        componentDidMount(): 组件挂载后执行,可以在这里进行 DOM 操作或数据请求。
        示例:
        class MyComponent extends React.Component {
          constructor(props) {
            super(props);
            this.state = { count: 0 };
          }

          static getDerivedStateFromProps(nextProps, prevState) {
            if (nextProps.reset) {
              return { count: 0 };
            }
            return null;
          }

          componentDidMount() {
            console.log('Component mounted');
          }

          render() {
            return <div>{this.state.count}</div>;
          }
        }
    02.更新阶段
        static getDerivedStateFromProps(props, state): 与挂载阶段相同,用于更新状态。
        shouldComponentUpdate(nextProps, nextState): 返回布尔值,决定组件是否需要重新渲染。
        render(): 渲染组件的 UI。
        getSnapshotBeforeUpdate(prevProps, prevState): 在 DOM 更新前调用,用于捕获信息(如滚动位置)。
        componentDidUpdate(prevProps, prevState, snapshot): 在组件更新后调用。
        示例:
        class MyComponent extends React.Component {
          shouldComponentUpdate(nextProps, nextState) {
            return nextState.count !== this.state.count;
          }

          getSnapshotBeforeUpdate(prevProps, prevState) {
            return { scrollPosition: window.scrollY };
          }

          componentDidUpdate(prevProps, prevState, snapshot) {
            if (snapshot) {
              window.scrollTo(0, snapshot.scrollPosition);
            }
            console.log('Component updated');
          }

          render() {
            return <div>{this.state.count}</div>;
          }
        }
    03.卸载阶段
        componentWillUnmount(): 组件即将卸载时调用,用于清理资源(如定时器、事件监听等)。
        示例:
        class MyComponent extends React.Component {
          componentWillUnmount() {
            console.log('Component will unmount');
          }

          render() {
            return <div>{this.state.count}</div>;
          }
        }

02.状态管理
    状态是组件内部的数据,通过 this.state 来定义和管理。可以使用 setState 方法更新状态。
    示例:
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
      }

      increment = () => {
        this.setState((prevState) => ({ count: prevState.count + 1 }));
      };

      render() {
        return (
          <div>
            <p>Count: {this.state.count}</p>
            <button onClick={this.increment}>Increment</button>
          </div>
        );
      }
    }

03.属性传递
    组件可以通过 this.props 访问传递给组件的属性,并可以使用 PropTypes 进行类型检查。
    示例:
    import PropTypes from 'prop-types';

    class MyComponent extends React.Component {
      render() {
        return <div>{this.props.title}</div>;
      }
    }

    MyComponent.propTypes = {
      title: PropTypes.string.isRequired,
    };

    // 使用组件并传递属性
    <MyComponent title="Hello, World!" />

04.事件处理
    通过事件处理函数处理用户交互。需要使用 .bind(this) 或箭头函数来确保 this 指向正确。
    示例:
    class MyComponent extends React.Component {
      handleClick = () => {
        console.log('Button clicked');
      };

      render() {
        return <button onClick={this.handleClick}>Click me</button>;
      }
    }

05.条件渲染
    通过条件语句控制组件的渲染。
    示例:
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { isVisible: true };
      }

      toggleVisibility = () => {
        this.setState((prevState) => ({ isVisible: !prevState.isVisible }));
      };

      render() {
        return (
          <div>
            {this.state.isVisible && <p>This is visible</p>}
            <button onClick={this.toggleVisibility}>Toggle</button>
          </div>
        );
      }
    }

06.列表渲染
    通过数组的 map 方法渲染列表。
    示例:
    class MyComponent extends React.Component {
      render() {
        const items = ['Item 1', 'Item 2', 'Item 3'];
        return (
          <ul>
            {items.map((item, index) => (
              <li key={index}>{item}</li>
            ))}
          </ul>
        );
      }
    }

07.受控组件
    通过状态控制表单元素的值。
    示例:
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { value: '' };
      }

      handleChange = (event) => {
        this.setState({ value: event.target.value });
      };

      handleSubmit = (event) => {
        event.preventDefault();
        console.log('Submitted value:', this.state.value);
      };

      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <input type="text" value={this.state.value} onChange={this.handleChange} />
            <button type="submit">Submit</button>
          </form>
        );
      }
    }

08.setState 方法
    setState 是 React 中用于更新组件状态的方法。
    语法:
    this.setState({ key: value }, callback);
    示例:
    class Counter extends React.Component {
      constructor(props) {
        super(props);
        this.state = { clickCount: 0 };
      }

      handleClick = () => {
        this.setState((state) => ({ clickCount: state.clickCount + 1 }));
      };

      render() {
        return <h2 onClick={this.handleClick}>点我!点击次数为: {this.state.clickCount}</h2>;
      }
    }

09.生命周期示例
    以下是一个显示当前时间的 React 组件示例,它每秒更新一次:
    示例:
    class Clock extends React.Component {
      constructor(props) {
        super(props);
        this.state = { date: new Date() };
      }

      componentDidMount() {
        this.timerID = setInterval(() => this.tick(), 1000);
      }

      componentWillUnmount() {
        clearInterval(this.timerID);
      }

      tick() {
        this.setState({ date: new Date() });
      }

      render() {
        return (
          <div>
            <h1>Hello, World!</h1>
            <h2>现在时间是:{this.state.date.toLocaleTimeString()}.</h2>
          </div>
        );
      }
    }

4 交互

4.1 响应事件

00.总结
    React 允许你向 JSX 中添加事件处理程序。事件处理程序是你自己的函数,它将在用户交互时被触发,如点击、悬停、焦点在表单输入框上等等。
    <button> 等内置组件只支持内置浏览器事件,如 onClick。但是,你也可以创建你自己的组件,并给它们的事件处理程序 props 指定你喜欢的任何特定于应用的名称。

01.添加事件处理函数
    你可以在组件内定义事件处理函数,并将它传递给 JSX 元素的事件属性。例如,下面的代码展示了如何在按钮点击时弹出消息:
    export default function Button() {
      function handleClick() {
        alert('你点击了我!');
      }

      return (
        <button onClick={handleClick}>
          点我
        </button>
      );
    }

02.使用内联事件处理函数
    你也可以直接在 JSX 中使用内联函数,尤其是在函数体较短的情况下:
    <button onClick={() => alert('你点击了我!')}>
      点我
    </button>

03.传递函数而不是调用函数
    注意不要在传递函数时加上括号,否则会导致函数在渲染时立即执行,而不是等待事件触发时执行。
    正确示例:
    <button onClick={handleClick}>
      点我
    </button>
    错误示例:
    <button onClick={handleClick()}>
      点我
    </button>

04.在事件处理函数中读取 props
    因为事件处理函数在组件内部定义,所以它们可以访问组件的 props。下面的例子展示了如何根据传入的 message prop 弹出不同的消息:
    function AlertButton({ message, children }) {
      return (
        <button onClick={() => alert(message)}>
          {children}
        </button>
      );
    }
    export default function Toolbar() {
      return (
        <div>
          <AlertButton message="正在播放!">播放电影</AlertButton>
          <AlertButton message="正在上传!">上传图片</AlertButton>
        </div>
      );
    }

05.将事件处理函数作为 props 传递
    事件处理函数可以从父组件传递到子组件中,以便父组件控制子组件的行为:
    function Button({ onClick, children }) {
      return (
        <button onClick={onClick}>
          {children}
        </button>
      );
    }

    function PlayButton({ movieName }) {
      function handlePlayClick() {
        alert(`正在播放 ${movieName}!`);
      }

      return (
        <Button onClick={handlePlayClick}>
          播放 "{movieName}"
        </Button>
      );
    }

    export default function Toolbar() {
      return (
        <div>
          <PlayButton movieName="魔女宅急便" />
        </div>
      );
    }

06.事件传播和阻止传播
    事件默认会向上冒泡,即从子组件传播到父组件。你可以使用 e.stopPropagation() 阻止这一传播:
    function Button({ onClick, children }) {
      return (
        <button onClick={e => {
          e.stopPropagation();
          onClick();
        }}>
          {children}
        </button>
      );
    }

    export default function Toolbar() {
      return (
        <div className="Toolbar" onClick={() => alert('你点击了 toolbar !')}>
          <Button onClick={() => alert('正在播放!')}>播放电影</Button>
        </div>
      );
    }

07.阻止默认行为
    某些事件(如表单提交)有默认的浏览器行为,可以使用 e.preventDefault() 来阻止:
    export default function Signup() {
      return (
        <form onSubmit={e => {
          e.preventDefault();
          alert('提交表单!');
        }}>
          <input />
          <button>发送</button>
        </form>
      );
    }

4.2 state组件记忆

00.总结
    使用 useState 为组件添加状态。
    useState 返回当前状态值和更新它的函数。
    state 是组件私有的,实例之间的 state 相互独立。

01.如何使用 useState 添加 state 变量
    首先,你需要从 React 中导入 useState:
    import { useState } from 'react';
    ---------------------------------------------------------------------------------------------------------
    然后,声明一个 state 变量,并为其设置一个初始值:
    const [index, setIndex] = useState(0);
    在这个例子中,index 是当前的状态值,而 setIndex 是用于更新该值的函数。useState(0) 的参数 0 是 index 的初始值。

02.更新 state 并触发重新渲染
    当你希望更新 state 并重新渲染组件时,调用 setIndex 函数并传入新的值即可。例如,在按钮点击时更新 index:
    function handleClick() {
      setIndex(index + 1);
    }

03.示例代码
    以下是一个使用 useState Hook 的完整示例,用于在点击“Next”按钮时切换当前雕塑:
    import { useState } from 'react';
    import { sculptureList } from './data.js';

    export default function Gallery() {
      const [index, setIndex] = useState(0);
      const [showMore, setShowMore] = useState(false);

      function handleNextClick() {
        setIndex((index + 1) % sculptureList.length);
      }

      function handleMoreClick() {
        setShowMore(!showMore);
      }

      let sculpture = sculptureList[index];
      return (
        <>
          <button onClick={handleNextClick}>
            Next
          </button>
          <h2>
            <i>{sculpture.name} </i>
            by {sculpture.artist}
          </h2>
          <h3>
            ({index + 1} of {sculptureList.length})
          </h3>
          <button onClick={handleMoreClick}>
            {showMore ? 'Hide' : 'Show'} details
          </button>
          {showMore && <p>{sculpture.description}</p>}
          <img
            src={sculpture.url}
            alt={sculpture.alt}
          />
        </>
      );
    }

04.深入理解 useState
    每次你调用 useState 时,React 都会记住 state 变量的当前值,并在组件重新渲染时保留它。
    useState 返回的数组包含当前的 state 值以及更新它的函数。

05.使用多个 state 变量
    在一个组件中,你可以声明多个 state 变量来处理不同的数据。
    例如,除了 index,你还可以使用 showMore 来控制是否显示详细信息。

06.组件的 state 是私有且隔离的
    state 是私有的,每个组件实例都有自己独立的 state。
    如果同一个组件在页面上渲染多次,每个实例的 state 是相互独立的,不会互相影响。

4.3 渲染和提交

00.总结
    浏览器绘制:在 React 更新 DOM 之后,浏览器会重新绘制屏幕,这一步被称为“绘制”(painting)。这个过程涉及将更新后的 DOM 结构显示在用户的屏幕上。
    每次 React 应用进行屏幕更新时,都会经历以下步骤:
    触发渲染:通过初次渲染或状态更新来触发。
    渲染组件:通过调用组件函数生成新的 UI 结构。
    提交到 DOM:React 只会更新发生变化的部分。

01.触发渲染
    渲染的触发通常有两种情况:
    初次渲染:当应用启动时,React 会进行初次渲染。此时,根组件会被渲染并插入到指定的 DOM 节点中。
    状态更新引发的重渲染:一旦组件被初次渲染,如果其状态或属性发生变化,就会触发重新渲染。React 会自动将渲染请求加入队列,并在适当的时候执行。

02.React 渲染组件
    当渲染被触发后,React 会调用组件的函数来计算需要显示的内容。这时,React 会递归地调用各个子组件的渲染函数,直到生成完整的 JSX 结构。需要注意的是:
    渲染过程应该是纯计算:即对于相同的输入,组件应该始终返回相同的输出,且不应在渲染过程中修改任何外部状态或产生副作用。
    递归渲染:如果组件 A 渲染组件 B,组件 B 又渲染组件 C,那么 React 会依次渲染 A、B 和 C,直到所有组件都被渲染完成。

03.React 将更改提交到 DOM
    完成组件的渲染后,React 会根据渲染结果对 DOM 进行更新:
    初次渲染:React 会将所有生成的 DOM 节点插入到指定的位置,通常是通过 appendChild 操作。
    重渲染:React 会计算新旧渲染结果之间的差异,并只对发生变化的部分进行最小化更新。这种优化机制确保了 DOM 操作的高效性。

4.4 state如同一张快照

00.总结
    在 React 中,state 变量的表现类似于一个时间点的快照。
    虽然它看起来像普通的 JavaScript 变量,但在设置 state 时,实际上是向 React 发送了请求,
    要求它在下一次渲染时使用更新后的 state 值。这种特性使得 React 能够高效地管理组件的状态变化和界面的更新。
    ---------------------------------------------------------------------------------------------------------
    设置 state 会触发一次新的渲染。
    state 存储在组件之外,每次渲染都会获得一张当前的 state 快照。
    每次渲染都有独立的事件处理函数,它们只看到当次渲染时的 state 值。

01.设置 state 会触发渲染
    在 React 中,界面不会直接响应用户的输入,而是通过更新 state 来触发渲染,进而更新 UI。
    例如,在一个简单的表单中,当用户点击“发送”按钮时,调用 setIsSent(true),
    这会通知 React 重新渲染组件,以显示消息已发送的状态。

02.渲染会及时生成一张快照
    “渲染” 是指 React 调用你的组件函数,并返回一个包含组件当前状态的 JSX。
    这个 JSX 就像是一个 UI 的快照,展示了特定时间点的界面状态。
    重要的是,state 在渲染过程中是固定的,不会在当前渲染中发生变化。

03.示例:state 的更新和快照特性
    考虑以下场景,点击按钮时连续调用了三次 setNumber(number + 1),你可能会认为计数器会增加三次,
    但实际上只增加了一次。这是因为 setNumber 只是为下一次渲染设置了新的 state 值,
    而在当前渲染中,number 的值是固定的,不会因为 setNumber 的调用而改变。
    ---------------------------------------------------------------------------------------------------------
    <button onClick={() => {
      setNumber(number + 1);
      setNumber(number + 1);
      setNumber(number + 1);
    }}>+3</button>
    在这个例子中,number 在这次渲染中始终是 0,所以 setNumber(number + 1) 实际上三次都是 setNumber(1),最终 number 的值只增加了一次。

04.state 的快照特性
    即使事件处理函数是异步的,state 在一次渲染中也是固定的。例如,在下面的例子中,点击按钮后弹出的提示框显示的 number 值是当前渲染时的快照,而不是更新后的值。
    ---------------------------------------------------------------------------------------------------------
    <button onClick={() => {
      setNumber(number + 5);
      setTimeout(() => {
        alert(number);
      }, 3000);
    }}>+5</button>
    即使 setNumber 更新了 state,但在这次渲染的事件处理函数中,number 的值仍然是原来的值。

05.事件处理函数中的 state
    在 React 中,事件处理函数绑定的是当次渲染的 state。
    这意味着无论在渲染后 state 发生了什么变化,事件处理函数中的 state 值不会改变。
    想要在事件处理函数中获取最新的 state,可以通过状态更新函数来实现。

4.5 把一系列state更新加入队列

00.总结
    把一系列 state 更新加入队列
    设置组件 state 会把一次重新渲染加入队列。但有时你可能会希望在下次渲染加入队列之前对 state 的值执行多次操作。
    为此,了解 React 如何批量更新 state 会很有帮助。
    ---------------------------------------------------------------------------------------------------------
    React 会对 state 更新进行批处理
    在下面的示例中,你可能会认为点击 “+3” 按钮会使计数器递增三次,因为它调用了 setNumber(number + 1) 三次:
    ---------------------------------------------------------------------------------------------------------
    在下次渲染前多次更新同一个 state
    这是一个不常见的用例,但是如果你想在下次渲染之前多次更新同一个 state,
    你可以像 setNumber(n => n + 1) 这样传入一个根据队列中的前一个 state 计算下一个 state 的 函数,
    而不是像 setNumber(number + 1) 这样传入 下一个 state 值。
    这是一种告诉 React “用 state 值做某事”而不是仅仅替换它的方法。
    ---------------------------------------------------------------------------------------------------------
    总结:
    1.设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。
    2.React 会在事件处理函数执行完成之后处理 state 更新。这被称为批处理。
    3.要在一个事件中多次更新某些 state,你可以使用 setNumber(n => n + 1) 更新函数。

01.问题
    a.问题原因
        在这个例子中,handleClick 函数中的 setPending 和 setCompleted 更新依赖于旧的 pending 和 completed 状态值。
        这会导致状态更新之间的竞争条件,尤其是在处理异步操作(如 await delay(3000))时。
        如果你快速多次点击按钮,前一次更新的状态值可能会与后一次更新的状态值产生冲突,导致状态更新不准确。
    b.解决方案
        要解决这个问题,需要确保状态更新是基于前一个状态值进行的。
        你可以通过将 setState 函数中的值改为更新函数来实现这一点。
        更新函数接收前一个状态值作为参数,从而确保每次更新都能正确反映状态的变化。
        -----------------------------------------------------------------------------------------------------
        修改后的代码
        import { useState } from 'react';

        export default function RequestTracker() {
          const [pending, setPending] = useState(0);
          const [completed, setCompleted] = useState(0);

          async function handleClick() {
            setPending(p => p + 1);
            await delay(3000);
            setPending(p => p - 1);
            setCompleted(c => c + 1);
          }

          return (
            <>
              <h3>
                等待:{pending}
              </h3>
              <h3>
                完成:{completed}
              </h3>
              <button onClick={handleClick}>
                购买
              </button>
            </>
          );
        }

        function delay(ms) {
          return new Promise(resolve => {
            setTimeout(resolve, ms);
          });
        }
    c.关键点
        使用 setPending(p => p + 1) 和 setCompleted(c => c + 1),确保状态更新是基于前一个状态值进行的,避免了状态更新之间的竞争条件。
        即使在异步操作中,状态更新也能保持正确的顺序和逻辑。
        这样修改之后,pending 和 completed 计数器将能够正确地反映多次点击的状态更新。

4.6 更新state中的对象

00.总结
    更新 React state 中的对象
    在 React 中,更新 state 中的对象需要特别小心,因为直接修改 state 对象不会触发组件的重新渲染。
    为了保持不可变性(immutability),你应该创建对象的一个新副本并用它来更新 state。
    ---------------------------------------------------------------------------------------------------------
    将 React state 中的对象视为不可变的,避免直接修改。
    使用对象展开语法或 Immer 库来创建新对象并更新 state。
    正确处理对象的不可变性可以确保 React 正确检测变化并触发重新渲染。

01.不可变性与 mutation
    不可变性意味着对象一旦创建就不能修改。
    直接修改对象会引发 mutation,而 React 依赖 state 的不可变性来检测变化并触发重新渲染。
    因此,必须避免直接修改存放在 state 中的对象。

02.更新对象的方法
    a.直接修改对象:这会破坏不可变性,不推荐使用。
        position.x = e.clientX;
        position.y = e.clientY;
    b.正确方式:创建对象的新副本并更新 state。
        setPosition({
          x: e.clientX,
          y: e.clientY
        });

03.使用对象展开语法
    当你只需要更新对象中的某些字段时,使用对象展开语法可以避免手动复制每个属性。
    setPerson({
      ...person, // 复制所有字段
      firstName: e.target.value // 更新 firstName 字段
    });

04.更新嵌套对象
    如果对象是嵌套的,使用多次展开语法来处理不同层级。
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: 'New Delhi'
      }
    });

05.使用 Immer 简化更新
    对于深层嵌套对象,重复展开语法可能会显得繁琐。Immer 库提供了更简单的方式来更新嵌套对象。
    import { useImmer } from 'use-immer';

    function MyComponent() {
      const [person, updatePerson] = useImmer(initialPerson);

      updatePerson(draft => {
        draft.artwork.city = 'Lagos';
      });
    }

4.7 更新state中的数组

00.总结
    你可以把数组放入 state 中,但你不应该直接修改它。
    不要直接修改数组,而是创建它的一份 新的 拷贝,然后使用新的数组来更新它的状态。
    你可以使用 [...arr, newItem] 这样的数组展开语法来向数组中添加元素。
    你可以使用 filter() 和 map() 来创建一个经过过滤或者变换的数组。
    你可以使用 Immer 来保持代码简洁。

01.向数组中添加元素
    为了避免直接修改原数组,你可以使用数组展开语法 (...) 创建一个新数组,然后将新元素添加进去。例如:
    setArtists([
      ...artists,  // 展开原数组的元素
      { id: nextId++, name: name }  // 添加新的元素
    ]);
    或者如果你想将新元素添加到数组的开头,可以这样做:
    setArtists([
      { id: nextId++, name: name },
      ...artists  // 展开原数组的元素
    ]);

02.从数组中删除元素
    使用 filter 方法生成一个不包含特定元素的新数组。例如,假设你要删除某个 ID 的元素:
    setArtists(artists.filter(artist => artist.id !== idToRemove));

03.替换数组中的元素
    使用 map 方法可以创建一个新数组,并在满足条件的情况下替换特定元素。例如:
    setArtists(artists.map(artist => {
      if (artist.id === idToReplace) {
        return { ...artist, name: newName };  // 返回一个新的对象
      } else {
        return artist;  // 保持原来的对象不变
      }
    }));

04.插入元素到数组特定位置
    结合使用 slice 和数组展开语法来插入元素。例如,向索引为 1 的位置插入新元素:
    const insertAt = 1;
    setArtists([
      ...artists.slice(0, insertAt),  // 插入点之前的元素
      { id: nextId++, name: name },  // 新的元素
      ...artists.slice(insertAt)  // 插入点之后的元素
    ]);

05.翻转或排序数组
    因为 reverse 和 sort 方法会直接修改数组,你可以先创建一个数组的副本,然后对其进行操作。例如:
    const reversedArtists = [...artists].reverse();
    setArtists(reversedArtists);

06.更新数组内部的对象
    如果数组中包含对象,且你需要更新某个对象的某个属性,你需要小心处理避免直接修改对象。可以使用 map 和对象展开语法:
    setMyList(myList.map(artwork => {
      if (artwork.id === artworkId) {
        return { ...artwork, seen: nextSeen };  // 返回更新后的新对象
      } else {
        return artwork;  // 保持原来的对象不变
      }
    }));

5 状态

5.1 用State响应输入

01.声明式与命令式 UI 编程的比较
    命令式编程:在命令式编程中,你明确告诉计算机每一步如何去做。例如,当用户提交表单时,你需要编写详细的指令来逐步完成这些任务,如禁用输入框、显示加载动画、处理错误信息等。这种方式的代码较为冗长,且在管理复杂的 UI 状态时容易出现错误。
    声明式编程:声明式编程则不同,你只需要声明 UI 应该处于什么状态,React 会根据这些声明自动管理 UI 的更新。你像设计师一样思考组件的各种状态,React 会处理这些状态的具体实现。这种方式让你可以专注于定义不同的视图状态,而无需关心如何一步一步地实现这些状态的变化。

02.React 中的状态管理
    在React 中,状态(state)用于表示组件的不同视图状态。以下是管理状态的几个步骤:
    ---------------------------------------------------------------------------------------------------------
    步骤 1:定位组件中的不同视图状态
    首先,你需要识别组件可能具有的所有不同视图状态。例如,对于一个表单,你可能会有以下状态:
    空状态(empty):表单和提交按钮都处于初始状态。
    输入中(typing):用户正在输入,提交按钮处于可用状态。
    提交中(submitting):表单提交中,加载动画出现。
    成功(success):提交成功,显示成功信息。
    错误(error):提交失败,显示错误信息。
    ---------------------------------------------------------------------------------------------------------
    步骤 2:确定触发状态变化的原因
    你需要确定哪些用户操作或计算机事件会触发状态的变化。例如:
    用户输入文本时,状态从“空状态”变为“输入中”。
    用户点击提交按钮时,状态变为“提交中”。
    网络请求成功或失败后,状态变为“成功”或“错误”。
    ---------------------------------------------------------------------------------------------------------
    步骤 3:通过 useState 表示内存中的状态
    在 React 中,你使用 useState 来管理状态。每个状态变量只存储一部分信息。你可以将多个状态合并成一个更有意义的状态。例如:
    const [answer, setAnswer] = useState('');
    const [error, setError] = useState(null);
    const [status, setStatus] = useState('typing'); // 状态可以是 'typing', 'submitting' 或 'success'
    ---------------------------------------------------------------------------------------------------------
    步骤 4:删除任何不必要的状态变量
    为了避免重复和复杂性,尽量减少状态变量的数量。例如,你可以通过一个状态变量 status 来代替多个布尔值状态变量。这样可以减少状态管理的复杂性并避免潜在的错误。
    ---------------------------------------------------------------------------------------------------------
    步骤 5:连接事件处理函数以设置状态
    最后,创建事件处理函数来更新状态。例如,当用户提交表单时,你会调用 handleSubmit 函数来更新状态:
    async function handleSubmit(e) {
      e.preventDefault();
      setStatus('submitting');
      try {
        await submitForm(answer);
        setStatus('success');
      } catch (err) {
        setStatus('typing');
        setError(err);
      }
    }

03.总结
    使用声明式编程,你声明你想要的 UI 状态,而 React 会处理如何实现这些状态的更新。
    这样可以简化状态管理,减少代码的复杂性,并使代码更易于维护。
    在 React 中,管理状态主要包括识别视图状态、确定触发条件、使用 useState 管理状态、简化状态变量以及连接事件处理函数。

5.2 选择State结构

00.总结
    合并关联的状态:如果多个状态总是一起更新,将它们合并成一个状态变量。
    避免矛盾的状态:避免创建状态之间存在逻辑矛盾的情况。
    避免冗余的状态:不要在状态中保存可以从其他状态或属性中计算出来的信息。
    避免重复的状态:避免在多个状态变量或嵌套对象中重复存储相同的数据。
    避免深度嵌套的状态:避免使用深度嵌套的状态结构,尽量使用扁平化的状态结构。

01.合并关联的状态
    原则:如果多个状态总是一起更新,将它们合并成一个状态变量。
    ---------------------------------------------------------------------------------------------------------
    示例:
    不推荐:
    const [x, setX] = useState(0);
    const [y, setY] = useState(0);
    推荐:
    const [position, setPosition] = useState({ x: 0, y: 0 });
    解释:如果两个状态变量(如 x 和 y)总是一起变化,合并它们可以确保它们始终保持同步。例如,在一个移动光标的组件中,position 作为一个对象包含 x 和 y,可以更方便地更新这两个值,并且保证它们总是一致的。

02.避免矛盾的状态
    原则:避免创建状态之间存在逻辑矛盾的情况。
    ---------------------------------------------------------------------------------------------------------
    示例:
    不推荐:
    const [isSending, setIsSending] = useState(false);
    const [isSent, setIsSent] = useState(false);
    推荐:
    const [status, setStatus] = useState('typing');
    解释:状态 isSending 和 isSent 之间存在矛盾,最好的做法是使用一个统一的 status 状态变量来表示组件的不同状态,这样可以避免状态之间的不一致性。

03.避免冗余的状态
    原则:不要在状态中保存可以从其他状态或属性中计算出来的信息。
    ---------------------------------------------------------------------------------------------------------
    示例:
    不推荐:
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [fullName, setFullName] = useState('');
    推荐:
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const fullName = `${firstName} ${lastName}`;
    解释:fullName 可以从 firstName 和 lastName 中计算得出,因此不需要单独存储 fullName。这样可以减少状态的冗余和维护复杂性。

04.避免重复的状态
    原则:避免在多个状态变量或嵌套对象中重复存储相同的数据。
    ---------------------------------------------------------------------------------------------------------
    示例:
    不推荐:
    const [items, setItems] = useState(initialItems);
    const [selectedItem, setSelectedItem] = useState(items[0]);
    推荐:
    const [items, setItems] = useState(initialItems);
    const [selectedId, setSelectedId] = useState(items[0].id);
    const selectedItem = items.find(item => item.id === selectedId);
    解释:将 selectedItem 替换为 selectedId,然后通过 items 查找选中的项目,这样可以避免重复存储相同的信息,并简化状态更新逻辑。

05.避免深度嵌套的状态
    原则:避免使用深度嵌套的状态结构,尽量使用扁平化的状态结构。
    ---------------------------------------------------------------------------------------------------------
    示例:
    不推荐:
    const [travelPlan, setTravelPlan] = useState({
      id: 0,
      title: '(Root)',
      childPlaces: [
        {
          id: 1,
          title: 'Earth',
          childPlaces: [
            {
              id: 2,
              title: 'Africa',
              childPlaces: [...]
            }
          ]
        }
      ]
    });
    推荐:
    const [plan, setPlan] = useState({
      0: { id: 0, title: '(Root)', childIds: [1, 2] },
      1: { id: 1, title: 'Earth', childIds: [2] },
      2: { id: 2, title: 'Africa', childIds: [] }
    });
    解释:深度嵌套的状态更新复杂,容易出错。通过扁平化状态结构(如将子节点的 ID 存储在数组中),可以简化更新逻辑,并避免深度复制对象的问题。

5.3 在组件间共享状态

00.总结
    状态提升:将状态移到公共父组件中,以便子组件能够通过 props 接收状态和事件处理函数。
    受控组件与非受控组件:受控组件的状态由父组件控制,非受控组件的状态由自身控制。通过状态提升,可以将多个受控组件的状态集中在一个地方。

01.概念简介
    在 React 中,当你希望多个组件的状态始终同步时,可以将状态从这些组件中提取到它们的共同父组件中。这种方法被称为“状态提升”。这样,父组件可以控制所有子组件的状态并将其通过 props 传递给子组件,从而确保组件之间状态的一致性。

02.示例场景
    假设你有一个 Accordion 组件,它渲染了两个独立的 Panel 组件。每个 Panel 组件都有一个 isActive 属性,控制其内容是否可见。
    ---------------------------------------------------------------------------------------------------------
    初始代码
    import { useState } from 'react';

    function Panel({ title, children }) {
      const [isActive, setIsActive] = useState(false);
      return (
        <section className="panel">
          <h3>{title}</h3>
          {isActive ? (
            <p>{children}</p>
          ) : (
            <button onClick={() => setIsActive(true)}>
              显示
            </button>
          )}
        </section>
      );
    }

    export default function Accordion() {
      return (
        <>
          <h2>哈萨克斯坦,阿拉木图</h2>
          <Panel title="关于">
            阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
          </Panel>
          <Panel title="词源">
            这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
          </Panel>
        </>
      );
    }
    在上述代码中,每个 Panel 组件有独立的 isActive 状态。这意味着点击一个面板的按钮不会影响另一个面板的状态。
    ---------------------------------------------------------------------------------------------------------
    步骤 1: 从子组件中移除状态
    为了让 Accordion 组件可以控制两个 Panel 的状态,我们需要将 isActive 的状态从 Panel 组件中移除。这样,Accordion 就可以管理 isActive 的状态。
    修改后的 Panel 组件:
    function Panel({ title, children, isActive, onShow }) {
      return (
        <section className="panel">
          <h3>{title}</h3>
          {isActive ? (
            <p>{children}</p>
          ) : (
            <button onClick={onShow}>
              显示
            </button>
          )}
        </section>
      );
    }
    ---------------------------------------------------------------------------------------------------------
    步骤 2: 从公共父组件传递硬编码数据
    现在 Accordion 组件需要控制哪个面板处于活动状态。我们可以将 isActive 作为 props 传递给 Panel 组件。
    修改后的 Accordion 组件:
    import { useState } from 'react';

    export default function Accordion() {
      const [activeIndex, setActiveIndex] = useState(0);

      return (
        <>
          <h2>哈萨克斯坦,阿拉木图</h2>
          <Panel
            title="关于"
            isActive={activeIndex === 0}
            onShow={() => setActiveIndex(0)}
          >
            阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
          </Panel>
          <Panel
            title="词源"
            isActive={activeIndex === 1}
            onShow={() => setActiveIndex(1)}
          >
            这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
          </Panel>
        </>
      );
    }
    ---------------------------------------------------------------------------------------------------------
    步骤 3: 添加事件处理函数
    我们需要在 Panel 组件中添加 onShow 事件处理函数,这样点击按钮时可以更新 Accordion 组件中的 activeIndex。
    最终的 Panel 组件代码:
    function Panel({ title, children, isActive, onShow }) {
      return (
        <section className="panel">
          <h3>{title}</h3>
          {isActive ? (
            <p>{children}</p>
          ) : (
            <button onClick={onShow}>
              显示
            </button>
          )}
        </section>
      );
    }

5.4 对state进行保留和重置

00.总结
    React 保留状态的关键在于组件在 UI 树中的位置。
    通过修改组件位置或使用 key 属性,可以控制组件的状态是否被重置。
    确保组件函数定义在最顶层,避免因重新定义组件导致状态丢失。

01.状态保留和重置的基本概念
    React 根据组件在渲染树中的位置来跟踪组件的 state。如果组件在渲染树中的位置保持不变,React 会保留该组件的状态。否则,组件的状态可能会被重置。

02.状态保留的规则
    位置不变:如果同一个组件始终渲染在相同的位置,React 会保留其状态。例如,当在 App 组件中渲染两个相同的 <Counter /> 组件时,每个组件都会拥有自己的 state,它们互不影响。
    组件位置和键:组件的状态也与其在渲染树中的位置有关。如果你在相同位置渲染不同的组件或更改组件的类型,React 会丢弃之前的状态。例如,当你切换不同的组件或在同一位置渲染不同类型的组件时,React 会重新初始化这些组件的状态。

03.如何强制 React 重置组件的状态
    修改组件位置:将组件渲染在不同的位置,可以确保组件的状态在每次渲染时都被重置。例如,在 Scoreboard 组件中,将 <Counter /> 组件渲染在不同的位置可以使状态被重置。
    使用 key 属性:可以通过为组件指定唯一的 key 属性来强制 React 重新渲染组件。每次组件的 key 改变时,React 会将其视为全新的组件,并重置其状态。例如,你可以为每个 <Counter /> 组件使用唯一的 key,使其状态在每次重新渲染时被重置。

04.实际例子
    a.状态保留
        import { useState } from 'react';

        export default function App() {
          return (
            <div>
              <Counter />
              <Counter />
            </div>
          );
        }

        function Counter() {
          const [score, setScore] = useState(0);
          const [hover, setHover] = useState(false);

          let className = 'counter';
          if (hover) {
            className += ' hover';
          }

          return (
            <div
              className={className}
              onPointerEnter={() => setHover(true)}
              onPointerLeave={() => setHover(false)}
            >
              <h1>{score}</h1>
              <button onClick={() => setScore(score + 1)}>
                加一
              </button>
            </div>
          );
        }
        在这个例子中,两个 <Counter /> 组件在 UI 树中占据不同的位置,React 会分别保存它们的状态。
    b.状态重置
        import { useState } from 'react';

        export default function App() {
          const [showB, setShowB] = useState(true);
          return (
            <div>
              <Counter />
              {showB && <Counter />}
              <label>
                <input
                  type="checkbox"
                  checked={showB}
                  onChange={e => {
                    setShowB(e.target.checked)
                  }}
                />
                渲染第二个计数器
              </label>
            </div>
          );
        }

        function Counter() {
          const [score, setScore] = useState(0);
          const [hover, setHover] = useState(false);

          let className = 'counter';
          if (hover) {
            className += ' hover';
          }

          return (
            <div
              className={className}
              onPointerEnter={() => setHover(true)}
              onPointerLeave={() => setHover(false)}
            >
              <h1>{score}</h1>
              <button onClick={() => setScore(score + 1)}>
                加一
              </button>
            </div>
          );
        }
        在这个例子中,点击复选框切换渲染状态会导致组件的状态被重置,因为 <Counter /> 在被移除和重新添加时,其状态被销毁并重新初始化。

5.5 迁移状态逻辑至Reducer中

01.理解 Reducer 函数
    reducer 函数是处理状态更新的核心。它接收两个参数:
    state:当前的状态。
    action:描述状态变化的对象。
    reducer 函数的目的是根据 action 对象更新 state 并返回新的状态。它应保持纯粹,即相同的输入应返回相同的输出,不应有副作用。

02.将 useState 迁移到 useReducer
    步骤 1: 修改事件处理函数以 dispatch action
    当前状态更新逻辑是直接使用 setTasks。使用 reducer 时,需要将状态更新逻辑转为 dispatch action。每个事件处理函数将创建一个 action 对象并通过 dispatch 发送它。
    例如,将 handleAddTask 修改为:
    function handleAddTask(text) {
      dispatch({
        type: 'added',
        id: nextId++,
        text: text,
      });
    }
    ---------------------------------------------------------------------------------------------------------
    步骤 2: 编写 reducer 函数
    reducer 函数接受当前状态和 action 对象,并返回更新后的状态。例如:
    function tasksReducer(tasks, action) {
      switch (action.type) {
        case 'added':
          return [
            ...tasks,
            {
              id: action.id,
              text: action.text,
              done: false,
            },
          ];
        case 'changed':
          return tasks.map(t => t.id === action.task.id ? action.task : t);
        case 'deleted':
          return tasks.filter(t => t.id !== action.id);
        default:
          throw new Error('Unknown action: ' + action.type);
      }
    }
    ---------------------------------------------------------------------------------------------------------
    步骤 3: 使用 useReducer 替代 useState
    在组件中,使用 useReducer 替代 useState,并传递 reducer 函数和初始状态:
    import { useReducer } from 'react';
    import AddTask from './AddTask';
    import TaskList from './TaskList';
    import tasksReducer from './tasksReducer';

    export default function TaskApp() {
      const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

      // 事件处理函数
      function handleAddTask(text) {
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        });
      }

      function handleChangeTask(task) {
        dispatch({
          type: 'changed',
          task: task,
        });
      }

      function handleDeleteTask(taskId) {
        dispatch({
          type: 'deleted',
          id: taskId,
        });
      }

      return (
        <>
          <h1>布拉格的行程安排</h1>
          <AddTask onAddTask={handleAddTask} />
          <TaskList
            tasks={tasks}
            onChangeTask={handleChangeTask}
            onDeleteTask={handleDeleteTask}
          />
        </>
      );
    }

03.优势和注意事项
    优势:
        代码清晰:将状态更新逻辑集中到 reducer 函数中,提高了代码的可读性。
        易于调试:可以方便地跟踪 action 和状态变化。
        可测试性:reducer 函数可以独立于组件进行测试。
    注意事项:
        纯粹性:reducer 函数应为纯函数,不应包含副作用。
        代码量:虽然 useReducer 可能会增加一些代码量,但它有助于管理复杂的状态逻辑。
    个人偏好:
        使用 useState 还是 useReducer 取决于组件的复杂性和个人喜好。
        对于简单的状态管理,useState 可能更方便;对于复杂的状态逻辑,useReducer 更加合适。

04.使用 Immer 简化 Reducer
    如果需要在 reducer 中修改状态对象,可以使用 Immer 库。
    Immer 允许你以更直观的方式修改状态,并自动处理不可变数据的更新。
    ---------------------------------------------------------------------------------------------------------
    安装 Immer:
    npm install immer use-immer
    ---------------------------------------------------------------------------------------------------------
    使用 useImmerReducer 替代 useReducer:
    import { useImmerReducer } from 'use-immer';
    import produce from 'immer';

    function tasksReducer(draft, action) {
      switch (action.type) {
        case 'added':
          draft.push({
            id: action.id,
            text: action.text,
            done: false,
          });
          break;
        case 'changed':
          const index = draft.findIndex(t => t.id === action.task.id);
          if (index !== -1) {
            draft[index] = action.task;
          }
          break;
        case 'deleted':
          return draft.filter(t => t.id !== action.id);
        default:
          throw new Error('Unknown action: ' + action.type);
      }
    }

    export default function TaskApp() {
      const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);

      // 事件处理函数与之前相同

      return (
        <>
          {/* 组件 JSX */}
        </>
      );
    }
    使用 Immer 使得更新状态更加简洁,避免了直接修改不可变数据的复杂性。

5.6 使用Context深层传递参数

00.使用 Context 深层传递参数
    在 React 中,通常会通过 props 从父组件传递信息到子组件。
    然而,当组件树变得很深或者多个组件需要相同的数据时,逐级传递 props 会变得很麻烦。
    这时候,Context 提供了一种更简洁的方式来解决这个问题。

01.Prop 逐级透传
    props 是将数据从父组件传递到子组件的常用方法。
    然而,若组件树层级很深,或者需要在多个组件中共享相同的数据,逐级传递 props 会显得繁琐。
    例如,当一个组件层级较多时,你需要将 props 逐层传递到最终需要的组件中,这种做法既冗长又不便。
    ---------------------------------------------------------------------------------------------------------
    状态提升:
    状态提升是将状态从子组件提升到父组件,然后通过 props 将状态传递到所有需要它的组件。
    这种方法在简单情况下有效,但对于复杂组件树则可能会导致 props 逐级透传的问题。

02.Context:传递 props 的另一种方法
    Context 允许我们在组件树中深层次地传递数据,而不需要逐级传递 props。你可以通过以下步骤实现:
    创建 Context: 使用 createContext 创建一个上下文对象,并导出它。
    使用 Context: 在需要数据的组件中,使用 useContext Hook 来读取上下文的值。
    提供 Context: 在组件中使用 Context.Provider 来提供数据。
    ---------------------------------------------------------------------------------------------------------
    示例
    假设你有一个 Heading 组件和 Section 组件,你希望在 Section 组件中设置标题级别,并使其子组件中的 Heading 组件自动获取这个级别。以下是实现的步骤:
    ---------------------------------------------------------------------------------------------------------
    Step 1: 创建 Context
    // LevelContext.js
    import { createContext } from 'react';

    export const LevelContext = createContext(1);
    ---------------------------------------------------------------------------------------------------------
    Step 2: 使用 Context
    // Heading.js
    import { useContext } from 'react';
    import { LevelContext } from './LevelContext';

    export default function Heading({ children }) {
      const level = useContext(LevelContext);
      return React.createElement(`h${level}`, null, children);
    }
    ---------------------------------------------------------------------------------------------------------
    Step 3: 提供 Context
    // Section.js
    import { LevelContext } from './LevelContext';

    export default function Section({ level, children }) {
      return (
        <section className="section">
          <LevelContext.Provider value={level}>
            {children}
          </LevelContext.Provider>
        </section>
      );
    }
    ---------------------------------------------------------------------------------------------------------
    最终实现
    // App.js
    import Heading from './Heading';
    import Section from './Section';

    export default function Page() {
      return (
        <Section level={1}>
          <Heading>主标题</Heading>
          <Section level={2}>
            <Heading>副标题</Heading>
            <Section level={3}>
              <Heading>子标题</Heading>
              <Section level={4}>
                <Heading>子子标题</Heading>
              </Section>
            </Section>
          </Section>
        </Section>
      );
    }
    ---------------------------------------------------------------------------------------------------------
    注意事项
    Context 的穿透: Context 会穿过组件树中任何层级的组件。即使有中间层级的组件,useContext 也会从最近的 Context.Provider 获取值。
    避免过度使用 Context: 如果只是为了传递少量数据或 props,考虑使用 props 传递或者将 JSX 作为 children 传递,Context 应用于更复杂的数据传递场景,例如主题、当前账户、路由等。
    Context 和 CSS 的类似性: Context 的工作方式类似于 CSS 的属性继承。组件可以从上层获取 Context 值,而不是手动传递。
    ---------------------------------------------------------------------------------------------------------
    总结
    使用 Context 可以让组件树中的子组件方便地访问和共享数据,避免了逐层传递 props 的麻烦。
    创建、使用和提供 Context 的基本步骤分别是创建一个 Context 对象、在需要使用的组件中读取 Context、
    在父组件中提供 Context 值。对于复杂的数据共享场景,Context 是一种有效的解决方案,
    但在简单的情况下,仍然可以通过 props 传递来实现。

5.7 使用Reducer和Context拓展你的应用

00.总结
    Reducer 和 Context 结合:通过创建和提供两个 Context(一个用于 state,另一个用于 dispatch 函数),可以在组件树中更方便地管理和访问状态。
    避免 Props 传递:利用 Context,可以避免在深层组件中通过 props 传递状态和函数。
    整理代码:将所有相关的 context 和 reducer 逻辑移动到一个单独的文件中,并使用自定义 Hook 进一步简化组件的逻辑。

01.使用Reducer和Context拓展你的应用
    Reducer 和 Context 是 React 中非常强大的工具,能够帮助你管理复杂应用的状态。当你的应用变得更复杂,
    简单地通过 props 传递状态和事件处理函数可能变得不可行。
    这时,Reducer 和 Context 的组合可以帮助你更好地管理状态。

02.结合使用 Reducer 和 Context
    在你使用 Reducer 的应用中,状态更新逻辑通常由 Reducer 管理。
    在组件树中,状态和 dispatch 函数通常仅在顶层组件中可用。
    为了让其他组件也能访问这些状态和函数,你通常需要通过 props 显式传递它们。
    但如果你的应用有很多层组件,传递这些 props 会变得麻烦。这时,可以利用 Context 来简化传递过程。

03.过程
    a.首先,你需要创建两个 Context:
        TasksContext:提供当前的任务列表(tasks)。
        TasksDispatchContext:提供 dispatch 函数,以便组件可以派发 actions。
        你可以将这两个 Context 声明在一个单独的文件中,例如 TasksContext.js:
        import { createContext } from 'react';

        export const TasksContext = createContext(null);
        export const TasksDispatchContext = createContext(null);
    b.将 state 和 dispatch 函数放入 Context
        在顶层组件中(如 TaskApp),你需要将 tasks 和 dispatch 分别提供给 TasksContext 和 TasksDispatchContext:
        import { useReducer } from 'react';
        import AddTask from './AddTask.js';
        import TaskList from './TaskList.js';
        import { TasksContext, TasksDispatchContext } from './TasksContext.js';

        export default function TaskApp() {
          const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

          return (
            <TasksContext.Provider value={tasks}>
              <TasksDispatchContext.Provider value={dispatch}>
                <h1>Day off in Kyoto</h1>
                <AddTask />
                <TaskList />
              </TasksDispatchContext.Provider>
            </TasksContext.Provider>
          );
        }
    c.在组件树中的任何地方使用 Context
        现在,任何需要访问 tasks 或 dispatch 的组件都可以直接从 Context 中读取它们,而不需要通过 props 传递。例如:
        import { useContext } from 'react';
        import { TasksContext, TasksDispatchContext } from './TasksContext.js';

        export default function TaskList() {
          const tasks = useContext(TasksContext);
          return (
            <ul>
              {tasks.map(task => (
                <li key={task.id}>
                  <Task task={task} />
                </li>
              ))}
            </ul>
          );
        }
        -----------------------------------------------------------------------------------------------------
        import { useState, useContext } from 'react';
        import { TasksDispatchContext } from './TasksContext.js';

        export default function AddTask() {
          const [text, setText] = useState('');
          const dispatch = useContext(TasksDispatchContext);

          return (
            <button onClick={() => {
              setText('');
              dispatch({
                type: 'added',
                id: nextId++,
                text: text,
              });
            }}>Add</button>
          );
        }
    d.将相关逻辑迁移到一个文件中
        为了进一步整理代码,可以将 reducer 和 context 逻辑移动到一个文件中。
        例如,你可以在 TasksContext.js 中创建一个 TasksProvider 组件,它会管理 tasks 的状态,并提供上下文给组件树:
        import { useReducer, createContext } from 'react';

        const tasksReducer = (state, action) => {
          // Reducer 函数逻辑
        };

        const initialTasks = []; // 初始任务数据

        export const TasksContext = createContext(null);
        export const TasksDispatchContext = createContext(null);

        export function TasksProvider({ children }) {
          const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

          return (
            <TasksContext.Provider value={tasks}>
              <TasksDispatchContext.Provider value={dispatch}>
                {children}
              </TasksDispatchContext.Provider>
            </TasksContext.Provider>
          );
        }
        -----------------------------------------------------------------------------------------------------
        然后在 TaskApp 组件中使用 TasksProvider:
        import AddTask from './AddTask.js';
        import TaskList from './TaskList.js';
        import { TasksProvider } from './TasksContext.js';

        export default function TaskApp() {
          return (
            <TasksProvider>
              <h1>Day off in Kyoto</h1>
              <AddTask />
              <TaskList />
            </TasksProvider>
          );
        }
        -----------------------------------------------------------------------------------------------------
        自定义 Hook
        你还可以创建自定义 Hook 来简化 context 的使用:
        import { useContext } from 'react';
        import { TasksContext, TasksDispatchContext } from './TasksContext.js';

        export function useTasks() {
          return useContext(TasksContext);
        }

        export function useTasksDispatch() {
          return useContext(TasksDispatchContext);
        }
        -----------------------------------------------------------------------------------------------------
        然后在组件中使用这些 Hook:
        import { useTasks, useTasksDispatch } from './TasksContext.js';

        export default function TaskList() {
          const tasks = useTasks();
          return (
            <ul>
              {tasks.map(task => (
                <li key={task.id}>
                  <Task task={task} />
                </li>
              ))}
            </ul>
          );
        }

        function Task({ task }) {
          const [isEditing, setIsEditing] = useState(false);
          const dispatch = useTasksDispatch();
          let taskContent;
          if (isEditing) {
            taskContent = (
              <>
                <input
                  value={task.text}
                  onChange={e => {
                    dispatch({
                      type: 'changed',
                      task: {
                        ...task,
                        text: e.target.value
                      }
                    });
                  }} />
                <button onClick={() => setIsEditing(false)}>Save</button>
              </>
            );
          } else {
            taskContent = <span>{task.text}</span>;
          }
          return (
            <div>
              {taskContent}
              <button onClick={() => setIsEditing(true)}>Edit</button>
            </div>
          );
        }

6 脱围

6.1 使用ref引用值

00.总结
    a.说明
        ref 是 React 中用于访问组件或 DOM 元素的一个工具,通常在你需要存储一些不会影响渲染的值时使用。
        以下是关于如何使用 ref 以及它与 state 的不同之处的详细说明:
    b.对比
        使用 ref 主要用于存储不需要影响渲染的值。
        与 state 不同,ref 的修改不会触发重新渲染。
        ref 常用于访问 DOM 元素或存储只在事件处理器中使用的值。

01.如何使用 ref
    a.创建 ref
        使用 useRef Hook 来创建一个 ref 对象。它接收一个初始值并返回一个对象,其中包含一个名为 current 的属性,该属性可以被读取和修改。
        import { useRef } from 'react';

        function MyComponent() {
          const myRef = useRef(null);  // 创建一个 ref 对象

          // 使用 ref 访问或修改 DOM 元素
          return <div ref={myRef}>Hello World</div>;
        }
    b.更新 ref 的值
        ref 的 current 属性可以直接修改。注意,这种修改不会触发组件的重新渲染。
        import { useRef } from 'react';

        function Counter() {
          const countRef = useRef(0);

          function handleClick() {
            countRef.current += 1;  // 修改 ref 的值
            alert(`点击次数:${countRef.current}`);
          }

          return <button onClick={handleClick}>点击我</button>;
        }
    c.ref 与 state 的不同
        触发渲染
            state: 修改 state 会触发组件的重新渲染。
            ref: 修改 ref 的 current 属性不会触发重新渲染。它主要用于存储不需要渲染的值。
        更新方式
            state: 你需要使用 setState 函数来更新 state。这种方式确保 React 能正确地管理和同步状态。
            ref: 你可以直接修改 ref.current。这种方式更直接,但要小心,因为 React 不会知道 ref 的变化。
        适用场景
            state: 用于存储和管理需要在组件渲染中展示的值。
            ref: 用于存储不需要影响渲染的值,如定时器 ID、DOM 元素引用等。

02.使用 ref 的示例
    a.结合使用 state 和 ref 来实现一个秒表:
        import { useState, useRef } from 'react';

        function Stopwatch() {
          const [startTime, setStartTime] = useState(null);
          const [now, setNow] = useState(null);
          const intervalRef = useRef(null);

          function handleStart() {
            setStartTime(Date.now());
            setNow(Date.now());

            clearInterval(intervalRef.current);
            intervalRef.current = setInterval(() => {
              setNow(Date.now());
            }, 10);
          }

          function handleStop() {
            clearInterval(intervalRef.current);
          }

          let secondsPassed = 0;
          if (startTime && now) {
            secondsPassed = (now - startTime) / 1000;
          }

          return (
            <>
              <h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
              <button onClick={handleStart}>开始</button>
              <button onClick={handleStop}>停止</button>
            </>
          );
        }
    b.注意事项
        避免在渲染过程中读取或写入 ref.current: 在渲染期间直接访问或修改 ref.current 可能导致组件行为不稳定。 ref 更适合用来存储和访问不需要影响渲染的值。
        ref 与 DOM 操作: ref 常用于访问 DOM 元素,例如控制焦点等。你可以通过将 ref 赋值给 JSX 元素的 ref 属性来访问相应的 DOM 元素。
        import { useRef } from 'react';

        function FocusInput() {
          const inputRef = useRef(null);

          function handleClick() {
            inputRef.current.focus();  // 使输入框获得焦点
          }

          return (
            <>
              <input ref={inputRef} type="text" />
              <button onClick={handleClick}>聚焦输入框</button>
            </>
          );
        }

6.2 使用ref操作DOM

00.总结
    a.说明
        在 React 中,通常你不需要直接操作 DOM,因为 React 会自动处理更新。
        然而,有时你可能需要访问由 React 管理的 DOM 元素,比如使一个元素获得焦点、滚动到它或测量它的尺寸。
        在这些情况下,你可以使用 ref。

01.如何获取指向节点的 ref
    a.导入 useRef Hook:
        import { useRef } from 'react';
    b.创建 ref:
        const myRef = useRef(null);
    c.将 ref 传递给 DOM 节点:
        <div ref={myRef}>...</div>
        -----------------------------------------------------------------------------------------------------
        useRef 返回一个对象,包含一个 current 属性,初始值为 null。
        当 React 创建 DOM 节点时,它会将该节点引用存入 current 属性中。
        你可以通过 myRef.current 访问这个 DOM 节点。
        -----------------------------------------------------------------------------------------------------
        // 使用 DOM API
        myRef.current.scrollIntoView();

02.示例:使文本输入框获得焦点
    import { useRef } from 'react';

    export default function Form() {
      const inputRef = useRef(null);

      function handleClick() {
        inputRef.current.focus();
      }

      return (
        <>
          <input ref={inputRef} />
          <button onClick={handleClick}>聚焦输入框</button>
        </>
      );
    }

03.示例:滚动至一个元素
    import { useRef } from 'react';

    export default function CatFriends() {
      const firstCatRef = useRef(null);
      const secondCatRef = useRef(null);
      const thirdCatRef = useRef(null);

      function handleScrollToFirstCat() {
        firstCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
      }

      function handleScrollToSecondCat() {
        secondCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
      }

      function handleScrollToThirdCat() {
        thirdCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
      }

      return (
        <>
          <nav>
            <button onClick={handleScrollToFirstCat}>First Cat</button>
            <button onClick={handleScrollToSecondCat}>Second Cat</button>
            <button onClick={handleScrollToThirdCat}>Third Cat</button>
          </nav>
          <div ref={firstCatRef}>First Cat</div>
          <div ref={secondCatRef}>Second Cat</div>
          <div ref={thirdCatRef}>Third Cat</div>
        </>
      );
    }

04.访问另一个组件的 DOM 节点
    默认情况下,React 不允许将 ref 传递给自定义组件,因为这些组件通常不是直接渲染 DOM 元素。
    若需要使自定义组件的 DOM 节点可被访问,你可以使用 forwardRef API。
    import { forwardRef, useRef } from 'react';

    const MyInput = forwardRef((props, ref) => {
      return <input {...props} ref={ref} />;
    });

    export default function Form() {
      const inputRef = useRef(null);

      function handleClick() {
        inputRef.current.focus();
      }

      return (
        <>
          <MyInput ref={inputRef} />
          <button onClick={handleClick}>聚焦输入框</button>
        </>
      );
    }

05.使用 ref 的最佳实践
    避免直接修改由 React 管理的 DOM:手动修改 DOM 可能会导致 React 的渲染与实际显示不一致。
    仅在必要时使用 ref:例如,用于聚焦、滚动或测量 DOM 元素。避免在渲染过程中读取或写入 ref.current,因为这会导致不稳定的组件行为。
    使用 forwardRef 处理子组件:如果自定义组件需要将 ref 传递给其内部的 DOM 节点,使用 forwardRef。

6.3 使用Effect同步

00.总结
    a.说明1
        Effect 是处理副作用的重要工具,通过正确声明、设置依赖项和添加清理函数,可以有效管理组件的生命周期,
        并确保你的应用运行稳定。在开发环境中注意处理重复挂载,确保 Effect 在多次挂载时能够正常工作。
    b.说明2
        Effect 是一种机制,用于在 React 组件渲染后执行副作用操作。
        这些副作用操作通常涉及与外部系统的交互,比如网络请求、操作 DOM、订阅事件等。Effect 与事件处理器不同,
        因为它们不依赖于特定的用户操作,而是与组件的生命周期相关。

01.Effect 与事件的区别
    事件处理程序 是直接响应用户操作的函数,如点击按钮或输入文本。它们用于处理用户交互并更新状态。
    Effect 是在组件渲染后执行的代码,用于执行那些不直接影响渲染输出的操作,比如数据获取或 DOM 操作。Effect 运行在屏幕更新后,而事件处理程序是在事件发生时运行的。

02.如何声明 Effect
    在 React 中,使用 useEffect Hook 来声明 Effect。useEffect 接受两个参数:
    Effect 函数:在每次渲染后执行的函数。
    依赖数组(可选):一个数组,包含 Effect 所依赖的状态或属性,如果这些依赖发生变化,Effect 会重新运行。如果依赖数组为空,则 Effect 仅在组件挂载时执行一次。
    ---------------------------------------------------------------------------------------------------------
    import { useEffect, useRef } from 'react';

    function MyComponent({ someProp }) {
      useEffect(() => {
        // 在每次渲染后执行的副作用
        console.log('Component rendered or someProp changed');
      }, [someProp]); // 依赖数组
    }

03.避免不必要的 Effect 执行
    在某些情况下,你可能不希望每次渲染后都执行 Effect。可以通过依赖数组来优化 Effect 的执行:
    无依赖数组:Effect 会在每次渲染后执行。
    空数组:Effect 只在组件挂载时执行一次。
    包含依赖项的数组:Effect 会在依赖项变化时执行。
    ---------------------------------------------------------------------------------------------------------
    useEffect(() => {
      // 只在组件挂载时执行一次
      console.log('Component mounted');
    }, []);

04.处理开发环境中的重复挂载
    在开发环境中,React 会故意重复挂载组件,以帮助你找到潜在的副作用问题。你应该确保你的 Effect 能正确处理重复挂载的情况。
    为了解决这个问题,可以在 Effect 中返回一个清理函数,这样 React 就会在每次重新执行 Effect 之前,或者在组件卸载时,调用这个清理函数来清理之前的副作用。
    useEffect(() => {
      const connection = createConnection();
      connection.connect();

      return () => {
        connection.disconnect(); // 清理副作用
      };
    }, []);

05.示例
    控制非 React 组件:
    import { useEffect, useRef } from 'react';

    function VideoPlayer({ src, isPlaying }) {
      const ref = useRef(null);

      useEffect(() => {
        if (isPlaying) {
          ref.current.play();
        } else {
          ref.current.pause();
        }
      }, [isPlaying]);

      return <video ref={ref} src={src} loop playsInline />;
    }
    ---------------------------------------------------------------------------------------------------------
    订阅事件:
    useEffect(() => {
      function handleScroll(e) {
        console.log(window.scrollX, window.scrollY);
      }
      window.addEventListener('scroll', handleScroll);

      return () => window.removeEventListener('scroll', handleScroll);
    }, []);
    ---------------------------------------------------------------------------------------------------------
    触发动画:
    useEffect(() => {
      const node = ref.current;
      node.style.opacity = 1; // 触发动画
      return () => {
        node.style.opacity = 0; // 清理动画
      };
    }, []);