基础

语法基础与规范

  1. JSX注意事项。

    • JSX必须有一个根节点,如果没有,可以用幽灵节点<></>代替。
    • 所有标签必须闭合,成对闭合或者自闭和都可以。
    • JSX采用小驼峰命名法,对于class要转化成classNamefor需要转化成htmlFor,以防止js关键字冲突。
    • JSX支持换行,如果需要换行需要加上(),防止bug出现。
    • 注释在模板中的书写方式:{/* 苏苏苏 */}
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      import "./app.css";
      const showTitle = true;
      function App() {
        return (
          <div className="App">
            {/* 苏苏苏 */}
            <div className={showTitle ? "title" : ""}>this is a div</div>
          </div>
        );
      }
      export default App;
  2. jsx的列表渲染

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 来个列表
    const songs = [
      { id: 1, name: "痴心绝对" },
      { id: 2, name: "像我这样的人" },
      { id: 3, name: "南山南" },
    ];
    
    function App() {
      return (
        <div className="App">
          <ul>
            {songs.map((item, index) => (
              <li key={index}>{item.name}</li>
            ))}
          </ul>
        </div>
      );
    }
    
    export default App;
  3. React函数组件

    • 函数组件首字母必须大写
    • 函数组件必须有返回值,若不需要渲染内容则返回null
    • 使用函数组件可以自闭和也可以成对闭合。
  4. 注意在React中的样式绑定只能使用驼峰规则,这点和Vue不一样,vue可以使用kebab-cased命名方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 这种写法才是正确的
    export function TodoList() {
      return (
        <ul
          style={{
            backgroundColor: "black",
            color: "pink",
          }}
        >
          <li>Improve the videophone</li>
          <li>Prepare aeronautics lectures</li>
          <li>Work on the alcohol-fuelled engine</li>
        </ul>
      );
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <script setup>
    import { ref, reactive } from "vue";
    // react 不能使用这样的方式,这样的方式会发出警告
    const styleObject = reactive({
      color: "red",
      "font-size": "20px",
    });
    </script>
    
    <template>
      <input v-model="msg" :style="styleObject" />
    </template>
  5. React的纯函数概念。

    • 一个组件必须是纯粹的,就意味着:
      • 只负责自己的任务。 它不会更改在该函数调用前就已存在的对象或变量。
      • 输入相同,则输出相同。 给定相同的输入,组件应该总是返回相同的 JSX
    • 渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。
    • 你不应该改变任何用于组件渲染的输入。这包括 propsstatecontext。通过 “设置” state 来更新界面,而不要改变预先存在的对象。
    • 努力在你返回的 JSX 中表达你的组件逻辑。当你需要“改变事物”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用 useEffect
    • 编写纯函数需要一些练习,但它充分释放了 React 范式的能力。

状态(state)

  1. useState特征

    1. 每个渲染(以及其中的函数)始终“看到”的是 React 提供给这个 渲染的 state 快照。
    2. 过去创建的事件处理函数拥有的是创建它们的那次渲染中的 state 值。
    3. setState推入队列后遍历

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      export default function Counter() {
        const [number, setNumber] = useState(0);
      
        return (
          <>
            <h1>{number}</h1>
            <button
              onClick={() => {
                setNumber((n) => n + 1);
                setNumber((n) => n + 1);
                setNumber((n) => n + 1);
              }}
            >
              +3
            </button>
          </>
        );
      }

      当你在下次渲染期间调用 useState 时,React 会遍历队列。之前的 number state 的值是 0,所以这就是 React 作为参数 n 传递给第一个更新函数的值。然后 React 会获取你上一个更新函数的返回值,并将其作为 n 传递给下一个更新函数,以此类推…

      注意永远不要直接修改state!!!

  2. Immer简化react对象的修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    import { useImmer } from "use-immer";
    
    export default function Form() {
      const [person, updatePerson] = useImmer({
        name: "Niki de Saint Phalle",
        artwork: {
          title: "Blue Nana",
          city: "Hamburg",
          image: "https://i.imgur.com/Sd1AgUOm.jpg",
        },
      });
    
      function handleNameChange(e) {
        updatePerson((draft) => {
          draft.name = e.target.value;
        });
      }
    
      function handleTitleChange(e) {
        updatePerson((draft) => {
          draft.artwork.title = e.target.value;
        });
      }
    
      function handleCityChange(e) {
        updatePerson((draft) => {
          draft.artwork.city = e.target.value;
        });
      }
    
      function handleImageChange(e) {
        updatePerson((draft) => {
          draft.artwork.image = e.target.value;
        });
      }
    
      return (
        <>
          <label>
            Name:
            <input value={person.name} onChange={handleNameChange} />
          </label>
          <label>
            Title:
            <input value={person.artwork.title} onChange={handleTitleChange} />
          </label>
          <label>
            City:
            <input value={person.artwork.city} onChange={handleCityChange} />
          </label>
          <label>
            Image:
            <input value={person.artwork.image} onChange={handleImageChange} />
          </label>
          <p>
            <i>{person.artwork.title}</i>
            {" by "}
            {person.name}
            <br />
            (located in {person.artwork.city})
          </p>
          <img src={person.artwork.image} alt={person.artwork.title} />
        </>
      );
    }
  3. 使用Immer编写数组更简单的逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    import { useState } from "react";
    import { useImmer } from "use-immer";
    
    let nextId = 3;
    const initialList = [
      { id: 0, title: "Big Bellies", seen: false },
      { id: 1, title: "Lunar Landscape", seen: false },
      { id: 2, title: "Terracotta Army", seen: true },
    ];
    
    export default function BucketList() {
      const [myList, updateMyList] = useImmer(initialList);
      const [yourList, updateYourList] = useImmer(initialList);
    
      function handleToggleMyList(id, nextSeen) {
        updateMyList((draft) => {
          const artwork = draft.find((a) => a.id === id);
          artwork.seen = nextSeen;
        });
      }
    
      function handleToggleYourList(artworkId, nextSeen) {
        updateYourList((draft) => {
          const artwork = draft.find((a) => a.id === artworkId);
          artwork.seen = nextSeen;
        });
      }
    
      return (
        <>
          <h1>艺术愿望清单</h1>
          <h2>我想看的艺术清单:</h2>
          <ItemList artworks={myList} onToggle={handleToggleMyList} />
          <h2>你想看的艺术清单:</h2>
          <ItemList artworks={yourList} onToggle={handleToggleYourList} />
        </>
      );
    }
    
    function ItemList({ artworks, onToggle }) {
      return (
        <ul>
          {artworks.map((artwork) => (
            <li key={artwork.id}>
              <label>
                <input
                  type="checkbox"
                  checked={artwork.seen}
                  onChange={(e) => {
                    onToggle(artwork.id, e.target.checked);
                  }}
                />
                {artwork.title}
              </label>
            </li>
          ))}
        </ul>
      );
    }
  4. State 不依赖于特定的函数调用或在代码中的位置,它的作用域“只限于”屏幕上的某块特定区域。你渲染了两个 <Gallery /> 组件,所以它们的 state 是分别存储的。
    还要注意 Page 组件“不知道”关于 Gallery state 的任何信息,甚至不知道它是否有任何 state。与 props 不同,state 完全私有于声明它的组件。父组件无法更改它。这使你可以向任何组件添加或删除 state,而不会影响其他组件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import Gallery from "./Gallery.js";
    
    export default function Page() {
      return (
        <div className="Page">
          <Gallery />
          <Gallery />
        </div>
      );
    }
  5. 不要在state中镜像props
    由于state只会在第一次渲染期间初始化,后续props发生更新后,这样的代码不会发生更新。

    1
    2
    3
    function Message({ messageColor }) {
      const [color, setColor] = useState(messageColor);
    }

    只有当你 想要 忽略特定 props 属性的所有更新时,将 props “镜像”到 state 才有意义。按照惯例,prop 名称以 initialdefault 开头,以阐明该 prop 的新值将被忽略:

    1
    2
    3
    4
    5
    function Message({ initialColor }) {
      // 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。
      // 对于 `initialColor` 属性的进一步更改将被忽略。
      const [color, setColor] = useState(initialColor);
    }
  6. React中的Hook只能在组件或者自己的Hook的顶层调用,而不能嵌套调用。原因为Hooks的执行需要严格按照其调用顺序,Hooks的调用顺序是一致的,从而避免了状态的混乱或者副作用的错误。
  7. 注意React中的setState方法中的更改只会在下一次渲染时才会生效。
  8. useState内部原理的简单实现。

    • index.js

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      let componentHooks = [];
      let currentHookIndex = 0;
      
      // useState 在 React 中是如何工作的(简化版)
      function useState(initialState) {
        let pair = componentHooks[currentHookIndex];
        if (pair) {
          // 这不是第一次渲染
          // 所以 state pair 已经存在
          // 将其返回并为下一次 hook 的调用做准备
          currentHookIndex++;
          return pair;
        }
      
        // 这是我们第一次进行渲染
        // 所以新建一个 state pair 然后存储它
        pair = [initialState, setState];
      
        function setState(nextState) {
          // 当用户发起 state 的变更,
          // 把新的值放入 pair 中
          pair[0] = nextState;
          updateDOM();
        }
      
        // 存储这个 pair 用于将来的渲染
        // 并且为下一次 hook 的调用做准备
        componentHooks[currentHookIndex] = pair;
        currentHookIndex++;
        return pair;
      }
      
      function Gallery() {
        // 每次调用 useState() 都会得到新的 pair
        const [index, setIndex] = useState(0);
        const [showMore, setShowMore] = useState(false);
      
        function handleNextClick() {
          setIndex(index + 1);
        }
      
        function handleMoreClick() {
          setShowMore(!showMore);
        }
      
        let sculpture = sculptureList[index];
        // 这个例子没有使用 React,所以
        // 返回一个对象而不是 JSX
        return {
          onNextClick: handleNextClick,
          onMoreClick: handleMoreClick,
          header: `${sculpture.name} by ${sculpture.artist}`,
          counter: `${index + 1} of ${sculptureList.length}`,
          more: `${showMore ? "Hide" : "Show"} details`,
          description: showMore ? sculpture.description : null,
          imageSrc: sculpture.url,
          imageAlt: sculpture.alt,
        };
      }
      
      function updateDOM() {
        // 在渲染组件之前
        // 重置当前 Hook 的下标
        currentHookIndex = 0;
        let output = Gallery();
      
        // 更新 DOM 以匹配输出结果
        // 这部分工作由 React 为你完成
        nextButton.onclick = output.onNextClick;
        header.textContent = output.header;
        moreButton.onclick = output.onMoreClick;
        moreButton.textContent = output.more;
        image.src = output.imageSrc;
        image.alt = output.imageAlt;
        if (output.description !== null) {
          description.textContent = output.description;
          description.style.display = "";
        } else {
          description.style.display = "none";
        }
      }
      
      let nextButton = document.getElementById("nextButton");
      let header = document.getElementById("header");
      let moreButton = document.getElementById("moreButton");
      let description = document.getElementById("description");
      let image = document.getElementById("image");
      let sculptureList = [
        {
          name: "Homenaje a la Neurocirugía",
          artist: "Marta Colvin Andrade",
          description:
            "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
          url: "https://i.imgur.com/Mx7dA2Y.jpg",
          alt: "A bronze statue of two crossed hands delicately holding a human brain in their fingertips.",
        },
        {
          name: "Floralis Genérica",
          artist: "Eduardo Catalano",
          description:
            "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.",
          url: "https://i.imgur.com/ZF6s192m.jpg",
          alt: "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.",
        },
        {
          name: "Eternal Presence",
          artist: "John Woodrow Wilson",
          description:
            'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."',
          url: "https://i.imgur.com/aTtVpES.jpg",
          alt: "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.",
        },
        {
          name: "Moai",
          artist: "Unknown Artist",
          description:
            "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
          url: "https://i.imgur.com/RCwLEoQm.jpg",
          alt: "Three monumental stone busts with the heads that are disproportionately large with somber faces.",
        },
        {
          name: "Blue Nana",
          artist: "Niki de Saint Phalle",
          description:
            "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
          url: "https://i.imgur.com/Sd1AgUOm.jpg",
          alt: "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.",
        },
        {
          name: "Ultimate Form",
          artist: "Barbara Hepworth",
          description:
            "This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.",
          url: "https://i.imgur.com/2heNQDcm.jpg",
          alt: "A tall sculpture made of three elements stacked on each other reminding of a human figure.",
        },
        {
          name: "Cavaliere",
          artist: "Lamidi Olonade Fakeye",
          description:
            "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
          url: "https://i.imgur.com/wIdGuZwm.png",
          alt: "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.",
        },
        {
          name: "Big Bellies",
          artist: "Alina Szapocznikow",
          description:
            "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
          url: "https://i.imgur.com/AlHTAdDm.jpg",
          alt: "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.",
        },
        {
          name: "Terracotta Army",
          artist: "Unknown Artist",
          description:
            "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
          url: "https://i.imgur.com/HMFmH6m.jpg",
          alt: "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.",
        },
        {
          name: "Lunar Landscape",
          artist: "Louise Nevelson",
          description:
            "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
          url: "https://i.imgur.com/rN7hY6om.jpg",
          alt: "A black matte sculpture where the individual elements are initially indistinguishable.",
        },
        {
          name: "Aureole",
          artist: "Ranjani Shettar",
          description:
            'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."',
          url: "https://i.imgur.com/okTpbHhm.jpg",
          alt: "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.",
        },
        {
          name: "Hippos",
          artist: "Taipei Zoo",
          description:
            "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
          url: "https://i.imgur.com/6o5Vuyu.jpg",
          alt: "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.",
        },
      ];
      
      // 使 UI 匹配当前 state
      updateDOM();
    • index.html

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <button id="nextButton">Next</button>
      <h3 id="header"></h3>
      <button id="moreButton"></button>
      <p id="description"></p>
      <img id="image" />
      
      <style>
        * {
          box-sizing: border-box;
        }
        body {
          font-family: sans-serif;
          margin: 20px;
          padding: 0;
        }
        button {
          display: block;
          margin-bottom: 10px;
        }
      </style>

    相同位置的相同组件会使得 state 被保留下来:记住 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!
    重置 state 有两种方法:

    • 在不同的位置渲染组件
    • 添加唯一的 key
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    import { useState } from "react";
    
    export default function App() {
      const [isFancy, setIsFancy] = useState(false);
      if (isFancy) {
        return (
          <div>
            <Counter isFancy={true} />
            <label>
              <input
                type="checkbox"
                checked={isFancy}
                onChange={(e) => {
                  setIsFancy(e.target.checked);
                }}
              />
              使用好看的样式
            </label>
          </div>
        );
      }
      return (
        <div>
          <Counter isFancy={false} />
          <label>
            <input
              type="checkbox"
              checked={isFancy}
              onChange={(e) => {
                setIsFancy(e.target.checked);
              }}
            />
            使用好看的样式
          </label>
        </div>
      );
    }
    
    function Counter({ isFancy }) {
      const [score, setScore] = useState(0);
      const [hover, setHover] = useState(false);
    
      let className = "counter";
      if (hover) {
        className += " hover";
      }
      if (isFancy) {
        className += " fancy";
      }
    
      return (
        <div
          className={className}
          onPointerEnter={() => setHover(true)}
          onPointerLeave={() => setHover(false)}
        >
          <h1>{score}</h1>
          <button onClick={() => setScore(score + 1)}>加一</button>
        </div>
      );
    }

Props

  1. React中使用props传递参数

    对于React中的函数组件,props相当于一个整体对象,这里是将其解构赋值出来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    function Avatar({ person, size = 20 }) {
      return (
        <img
          className="avatar"
          src={getImageUrl(person)}
          alt={person.name}
          width={size}
          height={size}
        />
      );
    }
    
    export default function Profile() {
      return (
        <div>
          <Avatar
            size={100}
            person={{
              name: "Katsuko Saruhashi",
              imageId: "YfeOqp2",
            }}
          />
          <Avatar
            size={80}
            person={{
              name: "Aklilu Lemma",
              imageId: "OKS67lh",
            }}
          />
          <Avatar
            size={50}
            person={{
              name: "Lin Lanying",
              imageId: "1bX5QH6",
            }}
          />
        </div>
      );
    }

    React中的默认参数与js中函数的默认参数一致,但位置随意。

  2. React中通过扩展运算符传递参数

    1
    2
    3
    4
    5
    6
    7
    function Profile(props) {
      return (
        <div className="card">
          <Avatar {...props} />
        </div>
      );
    }

    你可以使用 <Avatar {...props} /> JSX 展开语法转发所有 props,但不要过度使用它!

  3. React中的列表渲染

    1
    const listItems = people.map((person, key) => <li key={key}>{person}</li>);
  4. 对于ReactVue中的组件中的keykey会作为组件中的一个特殊的属性而不会作为props传入。

  5. Reactpropschildren属性如果有多个同级的JSX元素传递过来,则children是数组的形式。

注意事项

  1. React使用Fragment标签来充当幽灵标签,用来添加key值,这时候不能使用<></>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import { Fragment } from "react";
    
    const poem = {
      lines: [
        "I write, erase, rewrite",
        "Erase again, and then",
        "A poppy blooms.",
      ],
    };
    
    export default function Poem() {
      return (
        <article>
          {poem.lines.map((line, i) => (
            <Fragment key={i}>
              {i > 0 && <hr />}
              <p>{line}</p>
            </Fragment>
          ))}
        </article>
      );
    }

    React只能渲染字符串,数字,布尔值,nullundefined,数组或者React元素

  2. React中绑定一个事件的捕获模式,即在事件名称末尾添加Capture

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
      /* 每个事件分三个阶段传播:
    
      1. 它向下传播,调用所有的 onClickCapture 处理函数。
      2. 它执行被点击元素的 onClick 处理函数。
      3. 它向上传播,调用所有的 onClick 处理函数。
      捕获事件对于路由或数据分析之类的代码很有用,但你可能不会在应用程序代码中使用它们。 */
    }
    <div
      onClickCapture={() => {
        /* 这会首先执行 */
      }}
    >
      <button onClick={(e) => e.stopPropagation()} />
      <button onClick={(e) => e.stopPropagation()} />
    </div>;
  3. React如果需要触发组件的re-render,除了自身的setState方法,如果父组件的变量作为props传递给子组件,那么如果该变量发生变化也会引起子组件的re-render

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    import { useState } from "react";
    import { foods, filterItems } from "./data.js";
    
    export default function FilterableList() {
      const [query, setQuery] = useState("");
      const results = filterItems(foods, query);
    
      function handleChange(e) {
        setQuery(e.target.value);
      }
    
      return (
        <>
          <SearchBar query={query} onChange={handleChange} />
          <hr />
          <List items={results} />
        </>
      );
    }
    
    function SearchBar({ query, onChange }) {
      return (
        <label>
          搜索: <input value={query} onChange={onChange} />
        </label>
      );
    }
    
    function List({ items }) {
      return (
        <table>
          <tbody>
            {items.map((food) => (
              <tr key={food.id}>
                <td>{food.name}</td>
                <td>{food.description}</td>
              </tr>
            ))}
          </tbody>
        </table>
      );
    }
  4. 井字棋游戏示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    import { useState } from "react";
    
    function Square({ value, onSquareClick }) {
      return (
        <button className="square" onClick={onSquareClick}>
          {value}
        </button>
      );
    }
    
    function Board({ xIsNext, squares, onPlay }) {
      function handleClick(i) {
        if (calculateWinner(squares) || squares[i]) {
          return;
        }
        const nextSquares = squares.slice();
        if (xIsNext) {
          nextSquares[i] = "X";
        } else {
          nextSquares[i] = "O";
        }
        onPlay(nextSquares);
      }
    
      const winner = calculateWinner(squares);
      let status;
      if (winner) {
        status = "Winner: " + winner;
      } else {
        status = "Next player: " + (xIsNext ? "X" : "O");
      }
    
      return (
        <>
          <div className="status">{status}</div>
          <div className="board-row">
            <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
            <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
            <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
          </div>
          <div className="board-row">
            <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
            <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
            <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
          </div>
          <div className="board-row">
            <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
            <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
            <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
          </div>
        </>
      );
    }
    
    export default function Game() {
      const [history, setHistory] = useState([Array(9).fill(null)]);
      const [currentMove, setCurrentMove] = useState(0);
      const xIsNext = currentMove % 2 === 0;
      const currentSquares = history[currentMove];
    
      function handlePlay(nextSquares) {
        const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
        setHistory(nextHistory);
        setCurrentMove(nextHistory.length - 1);
      }
    
      function jumpTo(nextMove) {
        setCurrentMove(nextMove);
      }
    
      const moves = history.map((squares, move) => {
        let description;
        if (move > 0) {
          description = "Go to move #" + move;
        } else {
          description = "Go to game start";
        }
        return (
          <li key={move}>
            <button onClick={() => jumpTo(move)}>{description}</button>
          </li>
        );
      });
    
      return (
        <div className="game">
          <div className="game-board">
            <Board
              xIsNext={xIsNext}
              squares={currentSquares}
              onPlay={handlePlay}
            />
          </div>
          <div className="game-info">
            <ol>{moves}</ol>
          </div>
        </div>
      );
    }
    
    function calculateWinner(squares) {
      const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
      ];
      for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (
          squares[a] &&
          squares[a] === squares[b] &&
          squares[a] === squares[c]
        ) {
          return squares[a];
        }
      }
      return null;
    }

React Router

  1. 创建React Router的基本过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import * as React from "react";
    import * as ReactDOM from "react-dom/client";
    import { createBrowserRouter, RouterProvider } from "react-router-dom";
    import "./index.css";
    
    const router = createBrowserRouter([
      {
        path: "/",
        element: <div>Hello world!</div>,
      },
    ]);
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <React.StrictMode>
        <RouterProvider router={router} />
      </React.StrictMode>
    );
  2. React中子路由的创建,使用<Outlet />渲染路由相当于vue中的<RouterView />

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    const router = createBrowserRouter([
      {
        path: "/",
        element: <Root />,
        errorElement: <ErrorPage />,
        children: [
          {
            path: "contacts/:contactId",
            element: <Contact />,
          },
        ],
      },
    ]);
    
    import { Outlet } from "react-router-dom";
    
    export default function Root() {
      return (
        <>
          {/* all the other elements */}
          <div id="detail">
            <Outlet />
          </div>
        </>
      );
    }
  3. React Router中通过向路由配置的loader配置项传递一个
    具有返回值的函数,然后通过useLoaderData函数进行数据的获取。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /* other imports */
    import Root, { loader as rootLoader } from "./routes/root";
    
    const router = createBrowserRouter([
      {
        path: "/",
        element: <Root />,
        errorElement: <ErrorPage />,
        loader: rootLoader,
        children: [
          {
            path: "contacts/:contactId",
            element: <Contact />,
          },
        ],
      },
    ]);
    // other file
    import { useLoaderData } from "react-router-dom";
    const { contacts } = useLoaderData();
  4. React Router会截取页面中表单提交的行为,并将其转发到目前路由的action当中。

    • edit.tsx

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      import { Form, useLoaderData, redirect } from "react-router-dom";
      import { updateContact } from "../contacts";
      
      export async function action({ request, params }) {
        const formData = await request.formData();
        const updates = Object.fromEntries(formData);
        await updateContact(params.contactId, updates);
        return redirect(`/contacts/${params.contactId}`);
      }
      
      /* existing code */
    • main.tsx

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      /* existing code */
      import EditContact, { action as editAction } from "./routes/edit";
      
      const router = createBrowserRouter([
        {
          path: "/",
          element: <Root />,
          errorElement: <ErrorPage />,
          loader: rootLoader,
          action: rootAction,
          children: [
            {
              path: "contacts/:contactId",
              element: <Contact />,
              loader: contactLoader,
            },
            {
              path: "contacts/:contactId/edit",
              element: <EditContact />,
              loader: contactLoader,
              action: editAction,
            },
          ],
        },
      ]);
      
      /* existing code */
  5. 使用React Router中的JSX Routes

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import {
      createRoutesFromElements,
      createBrowserRouter,
      Route,
    } from "react-router-dom";
    
    const router = createBrowserRouter(
      createRoutesFromElements(
        <Route
          path="/"
          element={<Root />}
          loader={rootLoader}
          action={rootAction}
          errorElement={<ErrorPage />}
        >
          <Route errorElement={<ErrorPage />}>
            <Route index element={<Index />} />
            <Route
              path="contacts/:contactId"
              element={<Contact />}
              loader={contactLoader}
              action={contactAction}
            />
            <Route
              path="contacts/:contactId/edit"
              element={<EditContact />}
              loader={contactLoader}
              action={editAction}
            />
            <Route path="contacts/:contactId/destroy" action={destroyAction} />
          </Route>
        </Route>
      )
    );
  6. React Router集中式路由配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    import { BrowserRouter, Routes, Route, useRoutes } from "react-router-dom";
    
    import Layout from "./pages/Layout";
    import Board from "./pages/Board";
    import Article from "./pages/Article";
    import NotFound from "./pages/NotFound";
    
    // 1. 准备一个路由数组 数组中定义所有的路由对应关系
    const routesList = [
      {
        path: "/",
        element: <Layout />,
        children: [
          {
            element: <Board />,
            index: true, // index设置为true 变成默认的二级路由
          },
          {
            path: "article",
            element: <Article />,
          },
        ],
      },
      // 增加n个路由对应关系
      {
        path: "*",
        element: <NotFound />,
      },
    ];
    
    // 2. 使用useRoutes方法传入routesList生成Routes组件
    function WrapperRoutes() {
      let element = useRoutes(routesList);
      return element;
    }
    
    function App() {
      return (
        <div className="App">
          <BrowserRouter>
            {/* 3. 替换之前的Routes组件 */}
            <WrapperRoutes />
          </BrowserRouter>
        </div>
      );
    }
    
    export default App;
  7. react routerjsx方式的嵌套路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 引入必要的内置组件
    import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
    
    // 准备俩个路由组件
    
    const Home = () => <div>this is home</div>;
    const About = () => <div>this is about</div>;
    
    function App() {
      return (
        <div className="App">
          {/* 按照规则配置路由 */}
          <BrowserRouter>
            <Link to="/">首页</Link>
            <Link to="/about">关于</Link>
            <Routes>
              <Route path="/" element={<Home />}></Route>
              <Route path="/about" element={<About />}></Route>
            </Routes>
          </BrowserRouter>
        </div>
      );
    }
    
    export default App;
  8. React路由懒加载以及回调显示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    import { Routes, Route } from "react-router-dom";
    import { HistoryRouter, history } from "./utils/history";
    import { AuthRoute } from "./components/AuthRoute";
    
    // 导入必要组件
    import { lazy, Suspense } from "react";
    // 按需导入路由组件
    const Login = lazy(() => import("./pages/Login"));
    const Layout = lazy(() => import("./pages/Layout"));
    const Home = lazy(() => import("./pages/Home"));
    const Article = lazy(() => import("./pages/Article"));
    const Publish = lazy(() => import("./pages/Publish"));
    
    function App() {
      return (
        <HistoryRouter history={history}>
          <Suspense
            fallback={
              <div
                style={{
                  textAlign: "center",
                  marginTop: 200,
                }}
              >
                loading...
              </div>
            }
          >
            <Routes>
              {/* 需要鉴权的路由 */}
              <Route
                path="/"
                element={
                  <AuthRoute>
                    <Layout />
                  </AuthRoute>
                }
              >
                {/* 二级路由默认页面 */}
                <Route index element={<Home />} />
                <Route path="article" element={<Article />} />
                <Route path="publish" element={<Publish />} />
              </Route>
              {/* 不需要鉴权的路由 */}
              <Route path="/login" element={<Login />} />
            </Routes>
          </Suspense>
        </HistoryRouter>
      );
    }
    
    export default App;
  9. vite配置路径别名的方案。

    1. 安装@types/node库,便于后续文件的引用。
    2. 打开vite.config.ts进行路径别名的配置,使vite能够识别@

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      import { fileURLToPath, URL } from "node:url";
      
      import { defineConfig } from "vite";
      import react from "@vitejs/plugin-react-swc";
      
      // https://vitejs.dev/config/
      export default defineConfig({
        plugins: [react()],
        resolve: {
          alias: {
            "@": fileURLToPath(new URL("./src", import.meta.url)),
          },
        },
      });
    3. tsconfig.json中进行配置,使得vscode能使用@进行智能提示。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      {
        "compilerOptions": {
          "composite": true,
          "baseUrl": ".",
          "paths": {
            "@/*": ["./src/*"]
          }
        }
      }

Mobx

  1. mobx模块化的基本用法。

    • 定义单独的store

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      // login.ts
      import { makeAutoObservable } from "mobx";
      import request from "@/utils/request";
      import { clearToken, getToken, setToken } from "@/utils/token";
      
      class LoginStore {
        token = getToken() || "";
        constructor() {
          makeAutoObservable(this);
        }
        login = async (data: { mobile: string; code: string }) => {
          const res = await request.post<any, LoginResData>(
            "/authorizations",
            data
          );
          this.token = res.data.token;
          setToken(res.data.token);
        };
        logout = async () => {
          this.token = "";
          clearToken();
        };
      }
      
      export default LoginStore;
      
      // user.ts
      import { makeAutoObservable } from "mobx";
      import request from "@/utils/request";
      
      class UserStore {
        userInfo = {};
        constructor() {
          makeAutoObservable(this);
        }
        getUserInfo = async () => {
          const res = await request.get("/user/profile");
          this.userInfo = res.data;
        };
      }
      export default UserStore;
    • index.ts中统一进行注入

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      import React from "react";
      import LoginStore from "./modules/login";
      import UserStore from "./modules/user";
      
      class RootStore {
        loginStore: LoginStore;
        userStore: UserStore;
        constructor() {
          this.loginStore = new LoginStore();
          this.userStore = new UserStore();
        }
      }
      const StoresContext = React.createContext(new RootStore());
      export const useStore = () => React.useContext(StoresContext);
    • 使用store

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      import { Layout as AntLayout, Menu, Popconfirm, message } from "antd";
      import {
        HomeOutlined,
        DiffOutlined,
        EditOutlined,
        LogoutOutlined,
      } from "@ant-design/icons";
      import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
      import { observer } from "mobx-react-lite";
      import "./index.scss";
      import { useEffect } from "react";
      import { useStore } from "@/stores";
      
      const { Header, Sider } = AntLayout;
      const Layout = observer(() => {
        const location = useLocation();
        const selectedKey = location.pathname;
        // load userinfo
        const { userStore, loginStore } = useStore();
        useEffect(() => {
          try {
            userStore.getUserInfo();
          } catch (error: any) {
            message.error(error.response?.data?.message || "获取用户信息失败");
          }
        }, [userStore]);
        // logout function
        const navigate = useNavigate();
        const handleLogout = () => {
          loginStore.logout();
          navigate("/login");
          message.success("退出登录成功");
        };
        return (
          <AntLayout>
            <Header className="header">
              <div className="logo" />
              <div className="user-info">
                <span className="user-name">user.name</span>
                <span className="user-logout">
                  <Popconfirm
                    title="是否确认退出?"
                    okText="退出"
                    cancelText="取消"
                    onConfirm={handleLogout}
                  >
                    <LogoutOutlined /> 退出
                  </Popconfirm>
                </span>
              </div>
            </Header>
            <AntLayout>
              <Sider width={200} className="site-layout-background">
                <Menu
                  mode="inline"
                  theme="dark"
                  selectedKeys={[selectedKey]}
                  style={{ height: "100%", borderRight: 0 }}
                  items={[
                    {
                      icon: <HomeOutlined />,
                      key: "/",
                      label: <Link to="/">数据概览</Link>,
                    },
                    {
                      icon: <DiffOutlined />,
                      key: "/article",
                      label: <Link to="/article">内容管理</Link>,
                    },
                    {
                      icon: <EditOutlined />,
                      key: "/publish",
                      label: <Link to="/publish">发布文章</Link>,
                    },
                  ]}
                ></Menu>
              </Sider>
              <AntLayout className="layout-content" style={{ padding: 20 }}>
                <Outlet />
              </AntLayout>
            </AntLayout>
          </AntLayout>
        );
      });
      export default Layout;

组件通信

  1. 父传子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    import React from "react";
    
    // 函数式子组件
    function FSon(props) {
      console.log(props);
      return (
        <div>
          子组件1
          {props.msg}
        </div>
      );
    }
    
    // 类子组件
    class CSon extends React.Component {
      render() {
        return (
          <div>
            子组件2
            {this.props.msg}
          </div>
        );
      }
    }
    // 父组件
    class App extends React.Component {
      state = {
        message: "this is message",
      };
      render() {
        return (
          <div>
            <div>父组件</div>
            <FSon msg={this.state.message} />
            <CSon msg={this.state.message} />
          </div>
        );
      }
    }
    
    export default App;
  2. 子传父

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    import React from "react";
    
    // 子组件
    function Son(props) {
      function handleClick() {
        // 调用父组件传递过来的回调函数 并注入参数
        props.changeMsg("this is newMessage");
      }
      return (
        <div>
          {props.msg}
          <button onClick={handleClick}>change</button>
        </div>
      );
    }
    
    class App extends React.Component {
      state = {
        message: "this is message",
      };
      // 提供回调函数
      changeMessage = (newMsg) => {
        console.log("子组件传过来的数据:", newMsg);
        this.setState({
          message: newMsg,
        });
      };
      render() {
        return (
          <div>
            <div>父组件</div>
            <Son
              msg={this.state.message}
              // 传递给子组件
              changeMsg={this.changeMessage}
            />
          </div>
        );
      }
    }
    
    export default App;
  3. 兄弟组件通信,核心思路为变量提升。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    import React from "react";
    
    // 子组件
    function Son(props) {
      function handleClick() {
        // 调用父组件传递过来的回调函数 并注入参数
        props.changeMsg("this is newMessage");
      }
      return (
        <div>
          {props.msg}
          <button onClick={handleClick}>change</button>
        </div>
      );
    }
    
    class App extends React.Component {
      state = {
        message: "this is message",
      };
      // 提供回调函数
      changeMessage = (newMsg) => {
        console.log("子组件传过来的数据:", newMsg);
        this.setState({
          message: newMsg,
        });
      };
      render() {
        return (
          <div>
            <div>父组件</div>
            <Son
              msg={this.state.message}
              // 传递给子组件
              changeMsg={this.changeMessage}
            />
          </div>
        );
      }
    }
    
    export default App;
  4. 跨组件间通信

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import React, { createContext } from "react";
    
    // 1. 创建Context对象
    const { Provider, Consumer } = createContext();
    
    // 3. 消费数据
    function ComC() {
      return <Consumer>{(value) => <div>{value}</div>}</Consumer>;
    }
    
    function ComA() {
      return <ComC />;
    }
    
    // 2. 提供数据
    class App extends React.Component {
      state = {
        message: "this is message",
      };
      render() {
        return (
          <Provider value={this.state.message}>
            <div className="app">
              <ComA />
            </div>
          </Provider>
        );
      }
    }
    
    export default App;
  5. 小案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    import React from "react";
    
    // 子组件
    function ListItem(props) {
      const { name, price, info, id, delHandler } = props;
      return (
        <div>
          <h3>{name}</h3>
          <p>{price}</p>
          <p>{info}</p>
          <button onClick={() => delHandler(id)}>删除</button>
        </div>
      );
    }
    
    // 父组件
    class App extends React.Component {
      state = {
        list: [
          {
            id: 1,
            name: "超级好吃的棒棒糖",
            price: 18.8,
            info: "开业大酬宾,全场8折",
          },
          {
            id: 2,
            name: "超级好吃的大鸡腿",
            price: 34.2,
            info: "开业大酬宾,全场8折",
          },
          {
            id: 3,
            name: "超级无敌的冰激凌",
            price: 14.2,
            info: "开业大酬宾,全场8折",
          },
        ],
      };
    
      delHandler = (id) => {
        this.setState({
          list: this.state.list.filter((item) => item.id !== id),
        });
      };
    
      render() {
        return (
          <>
            {this.state.list.map((item) => (
              <ListItem key={item.id} {...item} delHandler={this.delHandler} />
            ))}
          </>
        );
      }
    }
    
    export default App;

Hooks

useState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useState } from "react";

function App() {
  // 参数:状态初始值比如,传入 0 表示该状态的初始值为 0
  // 返回值:数组,包含两个值:1 状态值(state) 2 修改该状态的函数(setState)
  const [count, setCount] = useState(0);
  return (
    <button
      onClick={() => {
        setCount(count + 1);
      }}
    >
      {count}
    </button>
  );
}
export default App;
  1. 如果就是初始化一个普通的数据 直接使用 useState(普通数据) 即可
  2. 如果要初始化的数据无法直接得到需要通过计算才能获取到,使用useState(()=>{})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useState } from "react";

function Counter(props) {
  const [count, setCount] = useState(() => {
    return props.count;
  });
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </div>
  );
}

function App() {
  return (
    <>
      <Counter count={10} />
      <Counter count={20} />
    </>
  );
}

export default App;

useEffect

  1. 不添加依赖项

    组件首次渲染执行一次,以及不管是哪个状态更改引起组件更新时都会重新执行

    • 组件初始渲染
    • 组件更新 (不管是哪个状态引起的更新)
    1
    2
    3
    useEffect(() => {
      console.log("副作用执行了");
    });
  2. 添加空数组

    组件只在首次渲染时执行一次

    1
    2
    3
    useEffect(() => {
      console.log("副作用执行了");
    }, []);
  3. 添加特定依赖项

    副作用函数在首次渲染时执行,在依赖项发生变化时重新执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    function App() {
      const [count, setCount] = useState(0);
      const [name, setName] = useState("zs");
    
      useEffect(() => {
        console.log("副作用执行了");
      }, [count]);
    
      return (
        <>
          <button
            onClick={() => {
              setCount(count + 1);
            }}
          >
            {count}
          </button>
          <button
            onClick={() => {
              setName("cp");
            }}
          >
            {name}
          </button>
        </>
      );
    }

    useEffect 回调函数中用到的数据(比如,count)就是依赖数据,就应该出现在依赖项数组中,如果不添加依赖项就会报错。

    1. 如果想要清理副作用 可以在副作用函数中的末尾 return 一个新的函数,在新的函数中编写清理副作用的逻辑

      1. 组件卸载时自动执行
      2. 组件更新时,下一个 useEffect 副作用函数执行之前自动执行

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        import { useEffect, useState } from "react";
        
        const App = () => {
          const [count, setCount] = useState(0);
          useEffect(() => {
            const timerId = setInterval(() => {
              setCount(count + 1);
            }, 1000);
            return () => {
              // 用来清理副作用的事情
              clearInterval(timerId);
            };
          }, [count]);
          return <div>{count}</div>;
        };
        
        export default App;
    2. 异步的正确写法

      1
      2
      3
      4
      5
      6
      useEffect(() => {
        async function fetchData() {
          const res = await axios.get("http://geek.itheima.net/v1_0/channels");
          console.log(res);
        }
      }, []);

useRef

  1. 获取一个在重新渲染中保存,但改变其值不会触发页面重新渲染的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { useRef } from "react";
    
    export default function Counter() {
      let ref = useRef(0);
    
      function handleClick() {
        ref.current = ref.current + 1;
        alert("你点击了 " + ref.current + " 次!");
      }
    
      return <button onClick={handleClick}>点击我!</button>;
    }
  2. 获取DOM实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    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}>Tom</button>
            <button onClick={handleScrollToSecondCat}>Maru</button>
            <button onClick={handleScrollToThirdCat}>Jellylorum</button>
          </nav>
          <div>
            <ul>
              <li>
                <img
                  src="https://placekitten.com/g/200/200"
                  alt="Tom"
                  ref={firstCatRef}
                />
              </li>
              <li>
                <img
                  src="https://placekitten.com/g/300/200"
                  alt="Maru"
                  ref={secondCatRef}
                />
              </li>
              <li>
                <img
                  src="https://placekitten.com/g/250/200"
                  alt="Jellylorum"
                  ref={thirdCatRef}
                />
              </li>
            </ul>
          </div>
        </>
      );
    }
  3. 通过 Ref 函数参数的形式管理 Ref 列表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    import { useRef } from "react";
    
    export default function CatFriends() {
      const itemsRef = useRef(null);
    
      function scrollToId(itemId) {
        const map = getMap();
        const node = map.get(itemId);
        node.scrollIntoView({
          behavior: "smooth",
          block: "nearest",
          inline: "center",
        });
      }
    
      function getMap() {
        if (!itemsRef.current) {
          // 首次运行时初始化 Map。
          itemsRef.current = new Map();
        }
        return itemsRef.current;
      }
    
      return (
        <>
          <nav>
            <button onClick={() => scrollToId(0)}>Tom</button>
            <button onClick={() => scrollToId(5)}>Maru</button>
            <button onClick={() => scrollToId(9)}>Jellylorum</button>
          </nav>
          <div>
            <ul>
              {catList.map((cat) => (
                <li
                  key={cat.id}
                  ref={(node) => {
                    const map = getMap();
                    if (node) {
                      map.set(cat.id, node);
                    } else {
                      map.delete(cat.id);
                    }
                  }}
                >
                  <img src={cat.imageUrl} alt={"Cat #" + cat.id} />
                </li>
              ))}
            </ul>
          </div>
        </>
      );
    }
    
    const catList = [];
    for (let i = 0; i < 10; i++) {
      catList.push({
        id: i,
        imageUrl: "https://placekitten.com/250/200?image=" + i,
      });
    }
  4. 子组件通过 forwardRef 选择是否暴露 DOM 元素
    它是这样工作的:

    1. <MyInput ref={inputRef} /> 告诉 React 将对应的 DOM 节点放入 inputRef.current 中。但是,这取决于 MyInput 组件是否允许这种行为, 默认情况下是不允许的。
    2. MyInput 组件是使用 forwardRef 声明的。 这让从上面接收的 inputRef 作为第二个参数 ref 传入组件,第一个参数是 props
    3. MyInput 组件将自己接收到的 ref 传递给它内部的 <input>

    现在,单击按钮聚焦输入框起作用了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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>
        </>
      );
    }
  5. 通过命令句柄(useImperativeHandle)暴露一部分的 API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 通过命令句柄暴露一部分 API
    import { forwardRef, useRef, useImperativeHandle } from "react";
    
    const MyInput = forwardRef((props, ref) => {
      const realInputRef = useRef(null);
      useImperativeHandle(ref, () => ({
        // 只暴露 focus,没有别的
        focus() {
          realInputRef.current.focus();
        },
      }));
      return <input {...props} ref={realInputRef} />;
    });
    
    export default function Form() {
      const inputRef = useRef(null);
    
      function handleClick() {
        inputRef.current.focus();
      }
    
      return (
        <>
          <MyInput ref={inputRef} />
          <button onClick={handleClick}>聚焦输入框</button>
        </>
      );
    }
  6. flushSync 同步更新 DOM:与 Vue 中的 nextTick 的用途类似,都是用来在 DOM 更改后进行某些操作,但Vue是将该操作放在nextTick的回调队列里面执行,包裹的是目的执行操作,而React是让setState同步更新 DOM。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    import { useState, useRef } from "react";
    import { flushSync } from "react-dom";
    
    export default function TodoList() {
      const listRef = useRef(null);
      const [text, setText] = useState("");
      const [todos, setTodos] = useState(initialTodos);
    
      function handleAdd() {
        const newTodo = { id: nextId++, text: text };
        flushSync(() => {
          setText("");
          setTodos([...todos, newTodo]);
        });
        listRef.current.lastChild.scrollIntoView({
          behavior: "smooth",
          block: "nearest",
        });
      }
    
      return (
        <>
          <button onClick={handleAdd}>添加</button>
          <input value={text} onChange={(e) => setText(e.target.value)} />
          <ul ref={listRef}>
            {todos.map((todo) => (
              <li key={todo.id}>{todo.text}</li>
            ))}
          </ul>
        </>
      );
    }
    
    let nextId = 0;
    let initialTodos = [];
    for (let i = 0; i < 20; i++) {
      initialTodos.push({
        id: nextId++,
        text: "待办 #" + (i + 1),
      });
    }

    函数组件由于没有实例,不能使用 ref 获取,如果想获取组件实例,必须是类组件

useReducer

  • App.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    import { useReducer } from "react";
    import AddTask from "./AddTask.js";
    import TaskList from "./TaskList.js";
    import tasksReducer from "./tasksReducer.js";
    
    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}
          />
        </>
      );
    }
    
    let nextId = 3;
    const initialTasks = [
      { id: 0, text: "参观卡夫卡博物馆", done: true },
      { id: 1, text: "看木偶戏", done: false },
      { id: 2, text: "打卡列侬墙", done: false },
    ];
  • tasksReducer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    export default 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) => {
            if (t.id === action.task.id) {
              return action.task;
            } else {
              return t;
            }
          });
        }
        case "deleted": {
          return tasks.filter((t) => t.id !== action.id);
        }
        default: {
          throw Error("未知 action:" + action.type);
        }
      }
    }

useContext

  1. 常规使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import { createContext, useContext } from "react";
    // 创建Context对象,可以添加默认值
    const Context = createContext();
    
    function Foo() {
      return (
        <div>
          Foo <Bar />
        </div>
      );
    }
    
    function Bar() {
      // 底层组件通过useContext函数获取数据
      const name = useContext(Context);
      return <div>Bar {name}</div>;
    }
    
    function App() {
      return (
        // 顶层组件通过Provider 提供数据
        <Context.Provider value={"this is name"}>
          <div>
            <Foo />
          </div>
        </Context.Provider>
      );
    }
    
    export default App;
  2. 在相同的组件中提供并使用 context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    // App.js
    
    import Heading from "./Heading.js";
    import Section from "./Section.js";
    
    export default function Page() {
      return (
        <Section>
          <Heading>主标题</Heading>
          <Section>
            <Heading>副标题</Heading>
            <Heading>副标题</Heading>
            <Heading>副标题</Heading>
            <Section>
              <Heading>子标题</Heading>
              <Heading>子标题</Heading>
              <Heading>子标题</Heading>
              <Section>
                <Heading>子子标题</Heading>
                <Heading>子子标题</Heading>
                <Heading>子子标题</Heading>
              </Section>
            </Section>
          </Section>
        </Section>
      );
    }
    
    // Section.js
    import { useContext } from 'react';
    import { LevelContext } from './LevelContext.js';
    
    export default function Section({ children }) {
      const level = useContext(LevelContext);
      return (
        <section className="section">
          <LevelContext.Provider value={level + 1}>
            {children}
          </LevelContext.Provider>
        </section>
      );
    }
    
    // Heading.js
    import { useContext } from 'react';
    import { LevelContext } from './LevelContext.js';
    
    export default function Heading({ children }) {
      const level = useContext(LevelContext);
      switch (level) {
        case 0:
          throw Error('Heading 必须在 Section 内部!');
        case 1:
          return <h1>{children}</h1>;
        case 2:
          return <h2>{children}</h2>;
        case 3:
          return <h3>{children}</h3>;
        case 4:
          return <h4>{children}</h4>;
        case 5:
          return <h5>{children}</h5>;
        case 6:
          return <h6>{children}</h6>;
        default:
          throw Error('未知的 level:' + level);
      }
    }
    
    
    // LevelContext.js
    import { createContext } from 'react';
    // 默认值
    export const LevelContext = createContext(1);
  3. 使用 ContextReducer 封装一个组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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>
      );
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    import { createContext, useContext, useReducer } from "react";
    
    const TasksContext = createContext(null);
    
    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>
      );
    }
    
    export function useTasks() {
      return useContext(TasksContext);
    }
    
    export function useTasksDispatch() {
      return useContext(TasksDispatchContext);
    }
    
    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) => {
            if (t.id === action.task.id) {
              return action.task;
            } else {
              return t;
            }
          });
        }
        case "deleted": {
          return tasks.filter((t) => t.id !== action.id);
        }
        default: {
          throw Error("Unknown action: " + action.type);
        }
      }
    }
    
    const initialTasks = [
      { id: 0, text: "Philosopher’s Path", done: true },
      { id: 1, text: "Visit the temple", done: false },
      { id: 2, text: "Drink matcha", done: false },
    ];
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import { useState } from "react";
    import { useTasksDispatch } from "./TasksContext.js";
    
    export default function AddTask() {
      const [text, setText] = useState("");
      const dispatch = useTasksDispatch();
      return (
        <>
          <input
            placeholder="Add task"
            value={text}
            onChange={(e) => setText(e.target.value)}
          />
          <button
            onClick={() => {
              setText("");
              dispatch({
                type: "added",
                id: nextId++,
                text: text,
              });
            }}
          >
            Add
          </button>
        </>
      );
    }
    
    let nextId = 3;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    import { useState } from "react";
    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 = (
          <>
            {task.text}
            <button onClick={() => setIsEditing(true)}>Edit</button>
          </>
        );
      }
      return (
        <label>
          <input
            type="checkbox"
            checked={task.done}
            onChange={(e) => {
              dispatch({
                type: "changed",
                task: {
                  ...task,
                  done: e.target.checked,
                },
              });
            }}
          />
          {taskContent}
          <button
            onClick={() => {
              dispatch({
                type: "deleted",
                id: task.id,
              });
            }}
          >
            Delete
          </button>
        </label>
      );
    }

useCallback

React 中的 useCallback 是一个用于缓存函数定义的 Hook。它的作用是避免在每次重新渲染时都创建新的函数,从而提高性能和避免不必要的渲染。

useCallback 的使用方法如下:

  • useCallback 接收两个参数:回调函数和依赖项数组。回调函数是需要缓存的函数,依赖项数组用于确定何时需要重新创建函数实例。
  • 当依赖项数组中的任何一个值发生变化时,useCallback 将返回一个新的函数实例,否则它将返回之前缓存的函数实例。
  • useCallback 返回的函数不会被 React 调用,而是由你自己决定何时以及是否调用它。
  • useCallback 必须在组件的顶层或自定义 Hook 中调用,不能在循环或条件语句中调用。

useCallback 的使用场景有以下几种:

  • 当你需要将一个函数作为 prop 传递给子组件时,你可以使用 useCallback 来避免子组件因为父组件的重新渲染而不必要地重新渲染。例如,下面的代码展示了一个使用 useCallback 的计数器组件,它将一个函数作为 prop 传递给子组件 Button,用来增加计数器的值。由于使用了 useCallbackButton 组件只会在 count 发生变化时才重新渲染,而不会因为父组件的其他 state 变化而重新渲染。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import React, { useState, useCallback } from "react";
    
    function Button({ onClick, children }) {
      console.log("Button 组件重新渲染");
      return <button onClick={onClick}>{children}</button>;
    }
    
    export default function Counter() {
      const [count, setCount] = useState(0);
      const [name, setName] = useState("");
    
      const handleIncrement = useCallback(() => {
        setCount((prevCount) => prevCount + 1);
      }, []);
    
      const handleNameChange = (event) => {
        setName(event.target.value);
      };
    
      return (
        <div>
          <p>计数器的值:{count}</p>
          <Button onClick={handleIncrement}>增加</Button>
          <p>姓名:{name}</p>
          <input type="text" value={name} onChange={handleNameChange} />
        </div>
      );
    }
  • 当你需要在一个 memoized 的回调函数中更新 state 时,你可以使用 useCallback 来保证回调函数的引用不变,从而避免触发无限循环。例如,下面的代码展示了一个使用 useCallback 的自定义 Hook,用来获取和设置 localStorage 中的数据。由于使用了 useCallbacksetStoredValue 函数的引用不会在每次重新渲染时改变,从而避免在 useEffect 中触发无限循环。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import { useState, useEffect, useCallback } from "react";
    
    function useLocalStorage(key, initialValue) {
      // 获取 localStorage 中的数据
      const [storedValue, setStoredValue] = useState(() => {
        try {
          const item = window.localStorage.getItem(key);
          return item ? JSON.parse(item) : initialValue;
        } catch (error) {
          console.error(error);
          return initialValue;
        }
      });
    
      // 设置 localStorage 中的数据
      const setValue = useCallback(
        (value) => {
          try {
            const valueToStore =
              value instanceof Function ? value(storedValue) : value;
            setStoredValue(valueToStore);
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
          } catch (error) {
            console.error(error);
          }
        },
        [key, storedValue]
      );
    
      return [storedValue, setValue];
    }
  • 当你需要在一个 useEffect 中使用一个函数时,你可以使用 useCallback 来避免因为函数的重新创建而导致 useEffect 的频繁执行。例如,下面的代码展示了一个使用 useCallback 的组件,用来在组件挂载时获取用户的位置信息,并在用户位置发生变化时更新 state。由于使用了 useCallbackgetLocation 函数的引用不会在每次重新渲染时改变,从而避免在 useEffect 中触发不必要的执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import React, { useState, useEffect, useCallback } from "react";
    
    export default function Location() {
      const [location, setLocation] = useState({});
    
      // 获取用户的位置信息
      const getLocation = useCallback(() => {
        navigator.geolocation.getCurrentPosition(
          (position) => {
            setLocation({
              latitude: position.coords.latitude,
              longitude: position.coords.longitude,
            });
          },
          (error) => {
            console.error(error);
          }
        );
      }, []);
    
      // 在组件挂载时获取用户的位置信息
      useEffect(() => {
        getLocation();
      }, [getLocation]);
    
      return (
        <div>
          <p>你的位置是:</p>
          <p>纬度:{location.latitude}</p>
          <p>经度:{location.longitude}</p>
        </div>
      );
    }

useMemo

useMemo 是一个 React 钩子,它允许你记住函数调用的结果,并在依赖关系发生变化时重新计算它。它可以通过防止不必要的重渲染来优化 React 应用程序的性能。

useMemo 的基本语法是:

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

其中,第一个参数是一个函数,它返回需要被记住的值。第二个参数是一个数组,它指定了哪些变量会影响这个值的计算。如果这个数组为空,那么 useMemo 只会在组件第一次渲染时执行一次。如果这个数组包含了一些变量,那么 useMemo 只会在这些变量发生变化时重新执行。如果这个数组包含了所有的状态和属性,那么 useMemo 的行为就和普通的函数一样,每次渲染都会执行一次。

useMemo 的使用场景有以下几种:

  • 当组件中有一些复杂的计算逻辑,而这些计算逻辑的结果又不会频繁地变化时,可以使用 useMemo 来缓存这些结果,避免每次渲染都重新计算,提高性能。
  • 当组件中有一些子组件,而这些子组件的属性是由父组件的状态或属性计算而来时,可以使用 useMemo 来缓存这些属性,避免父组件的状态或属性发生变化时,导致子组件不必要的重渲染。
  • 当组件中有一些引用类型的状态或属性,如对象或数组时,可以使用 useMemo 来缓存这些状态或属性,避免每次渲染都创建新的引用,导致组件的浅比较失效,引起不必要的重渲染。

下面是一个使用 useMemo 的例子,它展示了如何使用 useMemo 来缓存一个复杂的计算结果,并将其作为子组件的属性传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React, { useState, useMemo } from "react";

// 一个子组件,接收一个数字作为属性,并显示它
function Child(props) {
  console.log("Child component rendered");
  return <div>{props.number}</div>;
}

// 一个父组件,有一个数字状态,一个按钮,和一个子组件
function Parent() {
  // 定义一个数字状态,初始值为0
  const [number, setNumber] = useState(0);

  // 定义一个函数,用来计算一个复杂的值,这里简单地模拟为一个循环
  function computeExpensiveValue(num) {
    console.log("computeExpensiveValue called");
    let i = 0;
    while (i < 1000000000) i++;
    return num * 2;
  }

  // 使用useMemo来缓存计算结果,只有当number发生变化时,才重新计算
  const memoizedValue = useMemo(() => computeExpensiveValue(number), [number]);

  // 定义一个函数,用来更新数字状态,每次加1
  function handleClick() {
    setNumber(number + 1);
  }

  // 返回一个JSX元素,包含一个按钮,一个子组件,和一个显示useMemo返回值的div
  return (
    <div>
      <button onClick={handleClick}>Click</button>
      <Child number={number} />
      <div>{memoizedValue}</div>
    </div>
  );
}

export default Parent;

在这个例子中,当点击按钮时,数字状态会加 1,父组件会重新渲染,但是由于使用了 useMemo,只有当数字状态发生变化时,才会重新调用computeExpensiveValue 函数,否则会直接返回缓存的值。这样就避免了每次渲染都执行一个复杂的计算,提高了性能。同时,由于子组件的属性没有变化,所以子组件也不会重新渲染,这也提高了性能。