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 值
解决方法:
- 将
count添加到 useEffect 的依赖数组中 - 使用
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 |
| 全局 | Redux、Event Bus |
高阶组件
接受一个或多个组件作为参数并且返回一个组件,本质也就是一个函数
高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
受控组件和非受控组件
- 受控组件:指表单元素的值由 React 的状态(state)控制,而不是由 DOM 自身管理
- 非受控组件:一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态
受控 vs 非受控:
- 受控:
value+onChange;状态驱动 UI - 非受控:
defaultValue用户输入由 DOM 自行维护,读取时用 ref
性能优化
useMemo:缓存计算结果useCallback:缓存回调函数,避免在每次渲染时创建新的函数实例React.memo:一个高阶组件,用于缓存组件的渲染结果,避免在 props 未变化时重新渲染路由懒加载:动态导入(dynamic import)将路由组件拆分为单独的代码块,按需加载。可以减少初始加载的代码量,提升页面加载速度
Redux
View -> Action -> Reducer -> State -> View
- View:用户在界面(View)上触发一个事件(如点击按钮)
- Action:事件触发一个 action,并通过 store.dispatch(action) 分发
- Reducer:store 调用 reducer,传入当前的 state 和 action,生成一个新的 state
- State:store 更新 state,并通知所有订阅了 store 的组件
- View:组件根据新的 state 重新渲染界面
Redux 的数据是怎么跟组件关联起来的
- 通过
<Provider>将根组件<App>包裹起来,将 Redux store 传递给所有子组件 - 然后组件中就可以使用
useSelectorHook 从 store 中提取状态数据(也可以使用 connect 高阶组件将 Redux state 映射到组件 props) useDispatch:获取 dispatch 函数来分发 Action