hook

是 React 16.8 的新增特性,可以在不编写 class 的情况下使用 state 以及其他的 React 特性

为什么引入 hook:

  • 之前相同的逻辑需要在不同的生命周期里写,非常分散不利于维护
  • 逻辑复用之前主要是使用高阶组件实现,但是嵌套过多会形成嵌套地狱的问题
  • 以前,函数组件也被称为无状态的组件,只负责渲染的一些工作。现在有了 hooks 就拥有了类组件的特性,例如组件内的状态、生命周期

常见的 hook:

useState:声明组件内状态

useEffect:执行一些带有副作用的操作

  • useEffect(() => {}):相当于 componentDidMount + componentDidUpdate(会在页面渲染之后执行,即挂载后和每次更新后都执行)
  • useEffect(() => {}, []):相当于 componentDidMount(组件首次挂载后执行,仅在组件挂载时执行一次)
  • useEffect(() => {}, [xxx]):如果想跳过对 useEffect 的调用,只需要传入第二个参数依赖项(相当于类组件中 componentDidUpdate
  • 回调函数中可以返回一个清除函数,相当于类组件中 componentWillUnmount 生命周期函数

useMemo:它接受一个函数和一个依赖数组。用于缓存计算结果,避免在每次渲染时都重新计算,只有当依赖项发生改变时才会重新计算

useCallback:它接收一个函数和一个依赖数组。用于缓存回调函数,避免在每次渲染时都创建新的回调,只有当依赖项发生改变时才会重新创建

useEffect 和 useLayoutEffect 的区别

  • useEffect: 在浏览器完成绘制(即 DOM 更新并渲染到屏幕)之后异步执行,不阻塞渲染,适合大多数副作用操作。
  • useLayoutEffect: 在 DOM 更新之后,但在浏览器绘制之前同步执行,阻塞渲染,适合需要在绘制前同步完成的副作用操作。

useEffect 闭包问题

function Counter() {
  const [count, setCount] = useState(0)
 
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count) // 每次打印的都是初始值 0
    }, 1000)
 
    return () => clearInterval(timer)
  }, []) // 依赖数组为空,effect 只运行一次
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}
  • useEffect 只在组件挂载时运行一次
  • setInterval 的回调函数形成了一个闭包,捕获了初始的 count 值(即 0)
  • 即使 count 状态更新了,setInterval 中的回调函数仍然访问的是旧的 count 值

解决方法:

  1. count 添加到 useEffect 的依赖数组中
  2. 使用 useRef 保存最新值,useEffect 访问 ref 值

fiber

为什么要有 fiber?

以前 DOM diff 时是同步、递归且不可中断的,如果组件树过大时会长时间占用主线程导致卡顿

原理

Fiber 采用双向链表存储组件树,每个组件对应一个 Fiber 节点
Fiber 节点:是一个包含组件信息和状态的对象,它代表了组件树中的一个节点
React 将组件树遍历拆解为多个 Fiber 工作单元,通过循环(而非递归)逐个处理。可以中断/恢复,防止长时间占用主线程

为何 Hooks 不能放在条件或循环之内?

  • 每个函数组件有一个 “Hook 链表”,组件中所有 Hook 的状态(如 useState 的初始值、useEffect 的依赖项)都会按调用顺序依次存入链表
  • 在同一组件的每次渲染中,Hooks 都依托于一个稳定的调用顺序。
  • 每次组件重新渲染(如 props 变化、state 状态更新)时,React 会从头遍历 Hook 链表,严格按照代码中 Hook 的调用顺序读取 / 更新状态。

如放在条件语句里,那有时候有这个 hook,有时候没有,会导致 hook 链表中状态错位,且链表长度前后不一致,产生报错

React hooks: not magic, just arrays | by Rudi Yardley | Medium

组件通信方式

类别方式
父子props 回调函数 ref
跨层级Context
全局ReduxEvent Bus

高阶组件

接受一个或多个组件作为参数并且返回一个组件,本质也就是一个函数

高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

受控组件和非受控组件

  • 受控组件:指表单元素的值由 React 的状态(state)控制,而不是由 DOM 自身管理
  • 非受控组件:一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态

受控 vs 非受控:

  • 受控:value + onChange;状态驱动 UI
  • 非受控:defaultValue 用户输入由 DOM 自行维护,读取时用 ref

性能优化

  • useMemo:缓存计算结果
  • useCallback:缓存回调函数,避免在每次渲染时创建新的函数实例
  • React.memo:一个高阶组件,用于缓存组件的渲染结果,避免在 props 未变化时重新渲染
  • 路由懒加载:动态导入(dynamic import)将路由组件拆分为单独的代码块,按需加载。可以减少初始加载的代码量,提升页面加载速度

Redux

View -> Action -> Reducer -> State -> View

  1. View:用户在界面(View)上触发一个事件(如点击按钮)
  2. Action:事件触发一个 action,并通过 store.dispatch(action) 分发
  3. Reducer:store 调用 reducer,传入当前的 state 和 action,生成一个新的 state
  4. State:store 更新 state,并通知所有订阅了 store 的组件
  5. View:组件根据新的 state 重新渲染界面

Redux 的数据是怎么跟组件关联起来的

  1. 通过 <Provider> 将根组件 <App> 包裹起来,将 Redux store 传递给所有子组件
  2. 然后组件中就可以使用 useSelector Hook 从 store 中提取状态数据(也可以使用 connect 高阶组件将 Redux state 映射到组件 props)
  3. useDispatch:获取 dispatch 函数来分发 Action

参考

【React精讲】面试时被反复拷打?别担心!字节大佬一个视频带你速通React Hooks_哔哩哔哩_bilibili