React-Hook(一)

150 阅读19分钟

什么是Hooks?和传统的函数式组件有什么区别?和类组件有什么区别?

  • Hook指的类似于useStateuseEffect这样的函数, Hooks是对这类函数的统称

  • 首先需要了解函数式组件与类组件的优缺点

  • 类组件可以定义自己的state,并且可以保存自己内部的状态

    • 函数式组件不能定义自己的状态的, 因为函数每次调用都会产生新的临时变量
  • 类组件有自己的生命周期 -- 而函数式组件没有

  • 类组件在状态改变是会重新执行render函数

    • 函数式组件时不会重新渲染的, 如果重新渲染, 整个函数会被重新执行, 相应的状态也会被重新赋值
  • 同时类组件也有自己的缺点

    • 随着业务的增多,类组件会变得越来越复杂
    • 复用其中的状态也会很艰难,有时需要通过一些高阶组件
  • Hooks可以让我们在不编写class的情况下使用state以及其他的React特性

    • Hook只能在函数组件中使用,不能在类组件
    • 通过Hook可以在函数式组件中 定义自己的状态 完成类似于class组件中的生命周期功能
  • 类组件实现计数器 image.png

  • hooks 实现计数器 image.png

常见的Hooks

Hooks使用规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

State Hook(主要)

  • useState 来自react,需要从react中导入,它是一个hook;
    • 参数:初始化值,如果不设置为undefined
    • 返回值:数组,包含两个元素;
      • 元素一:当前状态的值(第一调用为初始化值);
      • 元素二:设置状态值的函数;
const App = memo(() => {
  const [message, setMessage] = useState("Hello World")

  function changeMessage() {
    setMessage("你好啊, coder!")
  }

  return (
    <div>
      <h2>App: {message}</h2>
      <button onClick={changeMessage}>修改文本</button>
    </div>
  )
})
  • State Hook的API就是 useState,我们在前面已经进行了学习:
    • useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同
      • 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
    • useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为undefined)。 - useState的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。
    • developer.mozilla.org/zh-CN/docs/…

Effect Hook(主要)

Effect Hook 可以让你来完成一些类似于class中生命周期的功能;

  • 类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects)
  • 对于完成这些功能的Hook被称之为 Effect Hook

useEffect的解析

  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
  • useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数; * 默认情况下,无论是第一次渲染之后还是每次更新之后,都会执行这个 回调函数
const App = memo(() => {
  const [count, setCount] = useState(200)

  useEffect(() => {
    // 当前传入的回调函数会在组件被渲染完成后, 自动执行
    // 网络请求/DOM操作(修改标题)/事件监听
    document.title = count
  })

  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
    </div>
  )
})

清除Effect

  • class组件的编写过程中,某些副作用的代码,可以在componentWillUnmount中进行清除:
    • 比如我们事件总线或Redux中手动调用subscribe
    • 需要在componentWillUnmount有对应的取消订阅;
  • useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B

type EffectCallback = () => (void | (() => void | undefined));

const App = memo(() => {
  const [count, setCount] = useState(0)

  // 负责告知react, 在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1.监听事件
    // const unubscribe = store.subscribe(() => {
    // })
    // function foo() {
    // }
    // eventBus.on("why", foo)
    console.log("监听redux中数据变化, 监听eventBus中的why事件")

    // 返回值: 回调函数 => 组件被重新渲染或者组件卸载的时候执行
    return () => {
      console.log("取消监听redux中数据变化, 取消监听eventBus中的事件")
    }
  })

  return (
    <div>
      <button onClick={e => setCount(count+1)}>+1({count})</button>
    </div>
  )
})
  • 为什么要在 effect 中返回一个函数?
    • 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数
    • 如此可以将添加和移除订阅的逻辑放在一起
    • 它们都属于 effect 的一部分
  • React 何时清除 effect?
    • React 会在组件更新和卸载的时候执行清除操作
    • effect 在每次渲染的时候都会执行

使用多个Effect

  • 类组件

    • 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;
  • Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:

    • React 将按照 effect 声明的顺序依次调用组件中的 每一个 effect;
const App = memo(() => {
  const [count, setCount] = useState(0)

  // 负责告知react, 在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1.修改document的title
    console.log("修改title")
  })

  // 一个函数式组件中, 可以存在多个useEffect
  useEffect(() => {
    // 2.对redux中数据变化监听
    console.log("监听redux中的数据")
    return () => {
      // 取消redux中数据的监听
    }
  })

  useEffect(() => {
    // 3.监听eventBus中的why事件
    console.log("监听eventBus的why事件")
    return () => {
      // 取消eventBus中的why事件监听
    }
  })

  return (
    <div>
      <button onClick={e => setCount(count+1)}>+1({count})</button>
    </div>
  )
})

Effect性能优化

1.默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

  • 某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅、手动修改 DOM);
  • 另外,多次执行也会导致一定的性能问题
  1. 如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?
  • useEffect实际上有两个参数:
    • 参数一:执行的回调函数
    • 参数二:useEffect在哪些state发生变化时,才重新执行;(副作用确实需要依赖于某些值(比如,根据 props 或 state 的变化来决定是否需要执行副作用))
 const [count, setCount] = useState(0)
 const [message, setMessage] = useState("Hello World")

  useEffect(() => {
    console.log("修改title:", count)
  }, [count])

  useEffect(() => {
    console.log("监听redux中的数据")
    return () => {}
  }, [])
  1. 如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []
    • 那么这里的两个回调函数分别对应的就是componentDidMountcomponentWillUnmount生命周期函数了;
useEffect(() => {
  // 这部分代码在 componentDidMount 时执行
  console.log('Component Did Mount');

  return () => {
    // 这部分代码在 componentWillUnmount 时执行
    console.log('Component Will Unmount');
  }
}, []); // 传入空数组表示这个副作用不依赖于任何值,所以只在组件加载和卸载时执行

useContext

  • 类组件中使用共享的Context有两种方式:
    • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context
    • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;
  • 但是多个Context共享时的方式会存在大量的嵌套:
    • Context Hook允许我们通过Hook来直接获取某个Context的值
import { createContext } from "react";
const UserContext = createContext()
const ThemeContext = createContext()


export {
  UserContext,
  ThemeContext
}

const App = memo(() => {
  // 使用Context
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)

  return (
    <div>
      <h2>User: {user.name}-{user.level}</h2>
      <h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2>
    </div>
  )
})

  • 注意事项:
    • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值

useReducer

  1. useReducer仅仅是useState的一种替代方案:

    • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
    • 或者这次修改的state需要依赖之前的state时,也可以使用;
  2. 数据是不会共享的,它们只是使用了相同的counterReducer的函数而已。

// 定义一个reducer函数,它接收当前的state和一个action对象,根据action.type来更新state
function reducer(state, action) {
  switch(action.type) {
    case "increment":
      // 当action.type为increment时,将state.counter增加1
      return { ...state, counter: state.counter + 1 }
  
    default:
      // 当action.type不是上述任何一种情况时,返回当前的state
      return state
  }
}
// useReducer+Context => redux
// 定义一个App组件,使用memo函数包装,以避免不必要的重新渲染
const App = memo(() => {
  // 使用useReducer Hook初始化state并创建dispatch函数。传递给useReducer的第一个参数是reducer函数,第二个参数是初始state
  const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} })

  // 返回App组件的JSX结构
  return (
    <div>
      // 显示当前计数器的值
      <h2>当前计数: {state.counter}</h2>
      // 定义一个按钮,点击时dispatch一个type为increment的action
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
    </div>
  )
})

这里的 useReducer Hook 将 reducer 函数和一个初始状态作为参数,并返回一个包含当前状态和一个可以发送给 reducerdispatch 函数的数组。dispatch 函数可以接收一个 action 对象,并将其发送到 reducer 函数,从而触发状态更新。

useCallback

  1. useCallback实际的目的是为了进行性能的优化
    • 如何进行性能的优化呢?
      • useCallback会返回一个函数的 memoized(记忆的) 值
      • 依赖不变的情况下,多次定义的时候,返回的值是相同的
  2. 通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存
// 定义一个名为CoderHome的组件,使用memo进行优化,避免不必要的重新渲染
const CoderHome = memo(function(props) {
  // 从props中解构出increment函数
  const { increment } = props
  // 打印出"CoderHome被渲染"
  console.log("CoderHome被渲染")
  // 返回组件的JSX结构,其中包含一个点击时会调用increment函数的按钮
  return (
    <div>
      <button onClick={increment}>increment+1</button>
      {/* 省略了100个子组件 */}
    </div>
  )
})

// 定义一个名为App的组件,使用memo进行优化,避免不必要的重新渲染
const App = memo(function() {
  // 使用useState创建count和message状态,以及它们的更新函数setCount和setMessage
  const [count, setCount] = useState(0)
  const [message, setMessage] = useState("hello")

  // 使用useRef创建一个引用countRef,并将count的当前值赋给countRef.current
  const countRef = useRef()
  countRef.current = count
  // 使用useCallback创建increment函数,该函数更新count的值。由于useCallback的依赖数组为空,increment函数的引用在组件的整个生命周期内都不会改变
  const increment = useCallback(function foo() {
    console.log("increment")
    setCount(countRef.current + 1)
  }, [])

  // 返回组件的JSX结构,其中包含显示count和message值的元素,以及触发increment和setMessage函数的按钮
  return (
    <div>
      <h2>计数: {count}</h2>
      <button onClick={increment}>+1</button>
      <CoderHome increment={increment}/>
      <h2>message:{message}</h2>
      <button onClick={e => setMessage(Math.random())}>修改message</button>
    </div>

  1. useCallback的闭包陷阱?如何解决出现的闭包陷阱。 在 React 中,使用 useCallback 创建的函数会产生闭包。闭包会“记住”函数被创建时的环境,也就是说,它会捕获函数外部的变量。这在 React 中可能会引发问题,因为这意味着如果闭包内部使用了组件的状态或 props,那么这个函数就会“记住”创建它时的那个状态或 props,即使之后状态或 props 发生了变化。

例如,以下的代码会引发闭包陷阱:

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

const increment = useCallback(() => {
  setCount(count + 1);
}, []); // 这里的依赖数组为空

在上面的代码中,increment 函数是在组件的初始渲染时创建的,并且它捕获了初始渲染时 count 的值(即 0)。即使后续 setCount 被调用,count 的值发生了变化,increment 函数中的 count 值依然是 0,这就是闭包陷阱。

解决这个问题的一个方法是在依赖数组中包含所有在回调中使用的状态和 props,这样,每当这些依赖发生变化时,都会创建一个新的回调:

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

const increment = useCallback(() => {
  setCount(count + 1);
}, [count]); // 这里的依赖数组包含了 count

但是这仍然存在一个问题,那就是每当 count 的值发生变化,就会创建一个新的 increment 函数,这可能会引发不必要的重渲染。

另一个解决方案是使用 setState 函数的函数形式,这样就可以避免在回调中直接引用状态:

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

const increment = useCallback(() => {
  setCount(prevCount => prevCount + 1);
}, []); // 这里的依赖数组为空

在上面的代码中,setCount 函数的参数是一个接受前一个状态值作为参数的函数。这样,即使 increment 函数是在组件的初始渲染时创建的,也能正确地获取到最新的 count 值,从而避免了闭包陷阱。

最后,可以使用 useRef 在多次渲染之间持久化变量。这在需要在 useCallbackuseEffect 的依赖数组中引用变量,但又不想因为变量的变化而重新执行回调或副作用时,是非常有用的:

const countRef = useRef(count);
countRef.current = count;

const increment = useCallback(() => {
  setCount(countRef.current + 1);
}, []); // 这里的依赖数组为空

在上面的代码中,countRef 是一个引用,其 current 属性可以用来存储 count 的当前值。这样,即使 count 的值发生变化,increment 函数也能通过 countRef.current 获取到最新的 count 值,从而避免了闭包陷阱。

使用 useRef 的好处是,即使 ref 对象的 current 属性发生变化,也不会引起组件的重渲染。而且,由于 ref 的值在组件的整个生命周期内保持不变,因此可以在多次渲染之间持久化变量。这使得 useRef 成为了避免 useCallbackuseEffect 中的闭包陷阱的理想选择。

总的来说,useCallback 的闭包陷阱主要是因为函数捕获了创建它时的环境,导致即使状态或 props 发生了变化,函数内部还是引用的旧的状态或 props。要解决这个问题,可以在依赖数组中包含所有在回调中使用的状态和 props,或者使用 setState 函数的函数形式,或者使用 useRef 在多次渲染之间持久化变量。

useMemo

  1. useMemo实际的目的也是为了进行性能的优化。
    • 如何进行性能的优化呢?
      • useMemo返回的也是一个 memoized(记忆的) 值
      • 依赖不变的情况下,多次定义的时候,返回的值是相同的

这段代码中使用了 React 的 useMemouseCallback 钩子。现在为每一部分添加中文注释:

// 定义 App 组件
const App = memo(() => {
  // 使用 useState Hook 管理 count 状态
  const [count, setCount] = useState(0)

  // 直接调用函数进行计算
  const result1 = calcNumTotal(50)

  // 使用 useMemo Hook 进行优化,此计算不依赖任何值,只在首次渲染时计算
  const result2 = useMemo(() => {
    return calcNumTotal(50)
  }, [])

  // 使用 useMemo Hook,当 count 值改变时重新计算
  const result3 = useMemo(() => {
    return calcNumTotal(count*2)
  }, [count])

  // 定义一个空函数
  function fn() {}
  
  // useCallback 和 useMemo 对比,两者都可以返回固定的引用,但 useCallback 直接返回函数,useMemo 返回的是传入函数的运行结果
  const increment = useCallback(fn, [])
  const increment2 = useMemo(() => fn, [])

  // 使用 useMemo 对子组件渲染进行优化,创建 info 对象并利用 useMemo 优化,保证只在依赖的值改变时才重新创建新的对象
  const info = useMemo(() => ({name: "why", age: 18}), [])

  return (
    <div>
      <h2>计算结果: {result}</h2>
      <h2>计数器: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>

      // HelloWorld 组件会接收到两个 props:result 和 info
      <HelloWorld result={result} info={info} />
    </div>
  )
})

在这个例子中,使用 useMemo 优化了 calcNumTotal 函数的计算和 info 对象的创建,这样可以在值未发生改变时复用之前的计算结果和对象,避免了不必要的计算和渲染。

此外,useCallbackuseMemo 的区别也在代码中展示了出来,useCallback 直接返回传入的函数,而 useMemo 返回的是传入函数的运行结果。两者都能返回固定的引用,但具体选择使用哪个,需要根据具体的使用场景和需要优化的目标来确定。

useMemo和useCallback有什么区别?

useMemouseCallback 是 React 中两个用来优化性能的钩子(Hooks)。它们都接收一个函数和一个依赖数组作为参数,并且只有当依赖数组中的值发生改变时,才会重新执行传入的函数。然而,它们在使用上有一些关键的区别:

  • useMemo: 返回的是传入函数的运行结果。通常用于优化复杂的计算,或者在依赖的值改变时才重新创建的对象、数组等。

  • useCallback: 返回的是传入的函数本身。通常用于优化那些需要作为 props 传给子组件的函数,因为函数的引用变化会导致子组件重新渲染。

来看一下具体的例子:

import React, { useState, useMemo, useCallback } from 'react';

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

  // 使用 useMemo 进行复杂计算的优化
  const computedValue = useMemo(() => {
    let sum = 0;
    for(let i = 0; i <= count; i++) {
      sum += i;
    }
    return sum;
  }, [count]);

  // 使用 useCallback 优化函数的引用
  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <p>Sum: {computedValue}</p>
      <button onClick={increment}>Increment count</button>
    </div>
  );
}

export default App;

在这个例子中,我们使用 useMemo 是为了对复杂的计算或者创建对象、数组等进行优化,使得只有在依赖的值发生改变时,才重新计算或创建新的对象。useCallback 用于优化函数(增加计数器)的引用,这样每次组件渲染时都不会生成新的函数,防止子组件的重新渲染。

useRef

  1. useRef返回一个ref对象,返回的ref对象组件的整个生命周期保持不变
    • 最常用的ref是两种用法:
      • 用法一:引入DOM(或者组件,但是需要是class组件)元素;
// 使用memo包裹函数式组件,如果组件的props未发生变化,则会避免不必要的渲染
const App = memo(() => {
  // 使用useRef创建一个可以容纳DOM元素的引用对象
  const titleRef = useRef()
  const inputRef = useRef()
  
  // 创建一个函数,用于打印标题元素的引用,并将焦点放到输入框
  function showTitleDom() {
    // 打印标题元素的DOM节点
    console.log(titleRef.current)
    // 将焦点放到输入框
    inputRef.current.focus()
  }

  return (
    <div>
      // 将标题元素的引用关联到DOM元素
      <h2 ref={titleRef}>Hello World</h2>
      // 将输入框的引用关联到DOM元素
      <input type="text" ref={inputRef} />
      // 点击按钮时,调用showTitleDom函数
      <button onClick={showTitleDom}>查看title的dom</button>
    </div>
  )
})
  • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;
// 使用memo包裹函数式组件,如果组件的props未发生变化,则会避免不必要的渲染
const App = memo(() => {
  // 使用useState定义一个状态变量count以及其更新函数setCount,初始值为0
  const [count, setCount] = useState(0)
  // 使用useRef创建一个可以容纳任何JavaScript值的引用对象nameRef
  const nameRef = useRef()

  // 通过useRef创建一个引用countRef,用来保存count的值
  const countRef = useRef()
  // 将count的值赋给countRef的current属性
  countRef.current = count

  // 使用useCallback创建一个记忆化版本的increment函数,依赖项为空数组,这意味着无论组件何时渲染,函数引用始终不变
  const increment = useCallback(() => {
    // 更新count的值,这里使用的是countRef.current,以此解决闭包陷阱
    setCount(countRef.current + 1)
  }, [])

  return (
    <div>
      // 显示当前的count值
      <h2>Hello World: {count}</h2>
      // 点击按钮时,更新count值
      <button onClick={e => setCount(count+1)}>+1</button>
      // 点击按钮时,调用increment函数,更新count值
      <button onClick={increment}>+1</button>
    </div>
  )
})

这段代码主要展示了如何在函数式组件中使用 useState, useRefuseCallback 钩子。这里的 useRef 钩子被用于保存 count 的值,以便在 useCallback 中可以获取最新的 count 值,从而解决闭包陷阱问题。

useImperativeHandle

  1. ref和forwardRef结合使用:
    • 通过forwardRef可以将ref转发到子组件;
    • 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
  2. forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:
    • 直接暴露给父组件带来的问题是某些情况的不可控;
    • 父组件可以拿到DOM后进行任意的操作;
  3. 通过useImperativeHandle可以暴露固定的操作:

useImperativeHandle 是一个 React Hook,它允许你在使用 ref 时自定义暴露给父组件的实例值。通常,组件的整个实例(在类组件中是 class 实例,在函数组件中是 DOM 节点)都将暴露给父组件。但有时,你可能只希望暴露给父组件一些特定的属性或方法,而不是整个实例,这时 useImperativeHandle 就派上用场了。

*   通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;

*   所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;


* `useImperativeHandle` 需要配合 `forwardRef` 一起使用。第一个参数是通过 `forwardRef` 传入的 ref 属性,第二个参数是一个函数,返回值是一个对象,该对象的内容就是你想要暴露给父组件的属性或方法。这样,父组件在获取到 ref 之后,就能通过 ref.current 访问到这些属性和方法。
// 使用memo和forwardRef包裹函数式组件HelloWorld,如果组件的props未发生变化,会避免不必要的渲染,同时使得组件可以接收一个ref属性
const HelloWorld = memo(forwardRef((props, ref) => {

  // 使用useRef创建一个可以容纳任何JavaScript值的引用对象inputRef
  const inputRef = useRef()

  // 使用useImperativeHandle自定义暴露给父组件的实例值
  useImperativeHandle(ref, () => {
    return {
      // 定义focus方法,当调用时,使得input获得焦点
      focus() {
        console.log("focus")
        inputRef.current.focus()
      },
      // 定义setValue方法,当调用时,可以设置input的值
      setValue(value) {
        inputRef.current.value = value
      }
    }
  })

  // 渲染一个input元素,其ref属性被设置为inputRef
  return <input type="text" ref={inputRef}/>
}))

// 使用memo包裹函数式组件App
const App = memo(() => {
  // 使用useRef创建引用对象
  const titleRef = useRef()
  const inputRef = useRef()

  function handleDOM() {
    // 调用inputRef上的方法,使得input获得焦点,并设置input的值
    inputRef.current.focus()
    inputRef.current.setValue("哈哈哈")
  }

  return (
    <div>
      // 渲染一个h2元素,其ref属性被设置为titleRef
      <h2 ref={titleRef}>哈哈哈</h2>
      // 渲染HelloWorld组件,其ref属性被设置为inputRef
      <HelloWorld ref={inputRef}/>
      // 点击按钮时,执行handleDOM函数
      <button onClick={handleDOM}>DOM操作</button>
    </div>
  )
})

这段代码主要展示了如何在函数式组件中使用 useRef, forwardRefuseImperativeHandle 钩子。这里的 useImperativeHandle 钩子被用于在子组件中定义暴露给父组件的实例值。

useLayoutEffect

  1. useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
    • useEffect会在渲染的内容更新到DOM上后执行不会阻塞DOM的更新
    • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新
  2. 如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
image.png

这是一个使用 useStateuseLayoutEffect 的 React 组件。以下是各行代码的中文注释:

const App = memo(() => { // 使用React.memo包裹的App组件,如果props没有发生变化,就不会重新渲染
  const [count, setCount] = useState(100) // 使用useState Hook初始化一个状态变量count,初始值为100

  useLayoutEffect(() => { // 使用useLayoutEffect Hook,该函数在浏览器完成页面布局与绘制后,在更新DOM之前被调用。
    console.log("useLayoutEffect") 
    if (count === 0) { // 如果count的值为0
      setCount(Math.random() + 99) // 则重新设置count为一个1-100的随机数
    }
  })

  console.log("App render") // 在渲染组件时打印日志

  return (
    <div>
      <h2>count: {count}</h2> // 渲染当前count的值
      <button onClick={e => setCount(0)}>设置为0</button> // 渲染一个按钮,点击时将count的值设置为0
    </div>
  )
})