为什么使用Hook不用Class
-
在组件之间复用状态逻辑很难
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。
具体将在自定义 Hook 中对此展开更多讨论。
-
复杂组件变得难以理解
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。
为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
我们将在使用 Effect Hook 中对此展开更多讨论。
-
难以理解的 class
除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
类组件和函数式组件的区别
- 在React中,你可以通过两种方式来创建组件:类组件(Class Components)和函数式组件(Functional Components)。随着React Hooks的引入,函数式组件的功能得到了极大的增强,使得它们能够执行几乎所有类组件能够做的事情。以下是类组件和函数式组件之间的一些主要区别:
- 类组件
语法:类组件使用ES6的类语法定义。
状态管理:类组件可以使用this.state和this.setState来管理状态。
生命周期方法:类组件可以使用生命周期方法(如componentDidMount、componentDidUpdate和componentWillUnmount)来执行代码。
引用(Refs):在类组件中,你可以使用React.createRef来创建引用,并通过this.refs.refName来访问它们。
this关键字:类组件中经常需要绑定事件处理程序或传递回调函数时使用this。
简洁性:class 定义类相对function定义的执行起来比较臃肿,复杂度要高;export default class App extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() {} increment = () => { this.setState({ count: this.state.count + 1 }); }; render() { return <div onClick={this.increment}>{this.state.count}</div>; } }
- 函数式组件
语法:函数式组件使用普通的JavaScript函数或箭头函数来定义。
Hooks:React 16.8引入了Hooks,使得函数式组件能够使用状态和其他React特性,如useState、useEffect。
没有this关键字:函数式组件不使用this关键字,一切都通过函数的参数传递。
简洁性:通常情况下,函数式组件比类组件更加简洁且易于理解。import React, { useState, useEffect } from "react"; function MyComponent(props) { const [count, setCount] = useState(0); useEffect(() => { // 类似生命周期 componentDidMount, componentDidUpdate, componentWillUnmount }, []); //依赖数组,数组变化,重新执行 const increment = () => { setCount(count + 1); }; return <div onClick={increment}>{count}</div>; }
- 类组件
Hook简介
- Hook简介
- React Hooks 自 React 16.8 版本引入以来,已成为在函数组件中使用状态和其他React特性的标准方式。
- 在 React 18 版本之前,基础的 Hooks 包括了以下几种:
useState: 使你能够在函数组件中添加和管理状态。
useEffect: 使你能够执行副作用操作,如数据获取、订阅或手动更改 DOM。
useContext: 使你能够访问 React 的上下文(Context)API,从而在组件树中无需直接传递 props 就可以共享值。
useReducer: 用于处理组件状态逻辑,尤其是复杂组件的状态逻辑,提供了一个类似 Redux 的reducer 函数。
useCallback: 返回一个记忆化的回调函数,该回调函数仅在其依赖项改变时才会更新。
useMemo: 返回一个记忆化的值,该值仅在依赖项改变时才会重新计算。
useRef: 返回一个可变的 ref 对象,其.current属性被初始化为传入的参数(initialValue),返回的对象将在组件的整个生命周期内保持不变。
useImperativeHandle: 自定义使用 ref 时公开给父组件的实例值。
useLayoutEffect: 与 useEffect 相似,useLayoutEffect会先于useEffect执行。useLayoutEffect的执行时机紧随DOM更新之后同步调用,可以用于读取 DOM 布局并同步触发重渲染,而useEffect则等到浏览器完成渲染之后才会执行。
useDebugValue: 用于在 React 开发者工具中显示自定义的 Hook 标签,方便调试。 - React 18 版本引入了许多新特性,但核心的 Hooks API 并没有显著改变。
- react-v18
- React 18 引入的主要变化集中在并发特性上,如并发渲染(Concurrent Rendering),以及与之相关的新API如 useTransition 和 useDeferredValue。新的并发特性可以帮助开发者控制渲染的优先级,提高大型应用的性能和用户体验。例如:
useId: 是一个用于生成唯一ID的钩子,它在服务器端渲染和客户端渲染之间保持一致,从而避免水合时的不匹配问题。它主要用于为DOM元素生成稳定、唯一的ID,例如
Hooks
基础 Hook
useState
useEffect
useContext
额外的 Hook
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
useDeferredValue [v18.0]
useTransition [v18.0]
useId [v18.0]
useSyncExternalStore [v18.0]
useInsertionEffect [v18.0]
useState
- useState 的更新不是异步的,而是由于 React 的批处理和更新调度机制导致的
// useState:
function App(){
// 会把useState传递的第一个参数赋值给state
// 当改变state后,视图会更新;
// 每一次更改数据,那么就会重新渲染组件,同时赋默认值的操作也会执行,只不过如果后面state已经有值,
// 不再执行这个函数
// let a =1
let [state,setState]=useState(function(){
// 初始值执行一次,后面更新不再执行;
// console.log(99);
return {
m:0,
n:0
}
})
return <div>
{state.m}分
{state.n}
<button onClick={()=>{
// 这个方法会将原来的值直接覆盖;
setState({
...state,
n:200
})
}}>+</button>
</div>
}
// function App(){
// // 会把useState传递的第一个参数赋值给state
// // 当改变state后,视图会更新;
// // 每一次更改数据,那么就会重新渲染组件,同时赋默认值的操作也会执行,只不过
// console.log(100);
// let [m,setStateM] = useState(0);
// let [n,setStateN] = useState(0);
// return <div>
// {m}分
// {n}
// <button onClick={()=>{
// // 这个方法会将原来的值直接覆盖;
// setStateM(100);
// setStateN(200);
// }}>+</button>
// </div>
// }
// function App(){
// // 会把useState传递的第一个参数赋值给state
// // 当改变state后,视图会更新;
// let [state,setState] = useState({
// m:0,
// n:0
// });
// return <div>
// {state.m}分
// {state.n}
// <button onClick={()=>{
// setState({
// ...state,
// n:100
// })
// }}>+</button>
// </div>
// }
// function App(){
// let [num,changeNum] = useState(0);// [这就是定义状态的hook];
// // [状态,改变状态的方法]
// return <div>
// {num}
// <button onClick={()=>{
// changeNum(num+1)
// }}>+</button>
// </div>
// }
useEffect
- useEffect 跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。
- useEffect 第一个参数-副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,使用副作用函数来订阅事件,并通过取消订阅来进行取消订阅。
function App(){
let [state,setState]=useState(function(){
return {
m:0,
n:0
}
})
// 在设置钩子函数
// useEffect:接收一个回调函数(副作用函数);这个函数不是立即运行的;设置函数的生命周期:在第一次解析挂载结构会默认一次,相当于componentDidMount ;当后期数据更新,数据更新,相当于componentDidUpdate;副作用函数如果返回一个函数,React 会在组件销毁时执行返回的函数,然后在后续渲染时重新执行副作用函数,相当于componentWillUnmount;
useEffect(()=>{
// 设置依赖项,只有n状态发生改变,才会执行这个钩子函数;
console.log("ok1");
},[state.n]);
useEffect(()=>{
console.log("ok");
},[]);
// console.log("hello");
return <div>
{state.m}分
{state.n}
<button onClick={()=>{
setState({
...state,
m:200
})
}}>+</button>
</div>
}
useRef
function App(){
let spanRef = useRef();// {current:undefined}
console.log(spanRef);// 获取元素;
return <div>
<span ref={spanRef}>0</span>
<button onClick={()=>{
// 给哪个元素,那么这个对象就把这个元素赋值给对选哪个current属性
//console.log(spanRef)
spanRef.current.innerHTML++;
}}>+</button>
</div>
}
export default App;
useReducer
- Reducer 可以整合组件的状态更新逻辑.
import React,{useState,useEffect,useRef,useReducer} from "react";// {useState,Component}
function reducer(state,action){
// console.log(state)
state = JSON.parse(JSON.stringify(state));
switch(action.type){
case "CHANGE_N":
state.n++;
break;
}
return state;
}
// class : this.props
function App(props){
//console.log(this); this 指向undefined;
console.log(props);// props可以来接收行间的属性;这就是属性;
let [{n,m},dispatch]=useReducer(reducer,{n:0,m:0});
// dispatch派发动作让reducer元素,reducer运行,更改state;
return <div>
{n}<br></br>{m}
<button onClick={(ev)=>{
// console.log(ev);// 事件对象
dispatch({type:"CHANGE_N"})
}}>+N</button>
<button>+M</button>
</div>
}
export default App;
APIs
- createContext
- forwardRef
- lazy
- memo
- startTransition
- createPortal
- flushSync
常见问题
useEffect 和 useLaoutEffect 区别
在React中,useEffect和useLayoutEffect都是用于在函数组件中执行副作用的钩子。它们之间的主要区别在于执行时机和场景。
useEffect是在组件渲染到屏幕上之后执行的,是异步的。它适用于数据获取、订阅或者手动修改React之外的DOM等场景。
useLayoutEffect是在DOM更新之后,浏览器绘制之前同步执行的。它适用于那些需要同步读取DOM布局,以确保持久性布局更新场景,比如避免布局抖动。
useLayoutEffect会先于useEffect执行。useLayoutEffect的执行时机紧随DOM更新之后,而useEffect则等到浏览器完成渲染之后才会执行。
- 使用场景举例:
如果你需要在浏览器绘制之前同步执行一些操作,比如根据DOM尺寸计算新的布局,应该使用useLayoutEffect。
如果你需要进行数据获取、订阅或者在不关心它们对DOM布局影响的情况下修改DOM,应该使用useEffect。
由于useLayoutEffect会阻塞浏览器渲染,所以应当尽量优先使用useEffect以避免不必要的性能问题。
延迟调用会存在作用域不一致
在延迟调用的场景下,一定会存在作用域不一致
使用 setTimeout、setInterval、Promise.then 等
useEffect 的卸载函数
函数组件的state是单独存的,没有保存在当前这个函数的闭包里。
给useEffect的deps传个空数组,它依赖的所有state值,都是它创建那一刻的,不会再更新,所以一直是初始状态
- 利用函数参数oldState 直接取 let oldState = hookStates[currentIndex];
- 通过 useRef 来保证任何时候访问的 countRef.current 都是最新的,以解决作用域不一致问题
import { useEffect, useState, useRef } from "react";
export default function Test() {
const [count, setCount] = useState(1);
// const countRef = useRef(1);
useEffect(() => {
let timer = setInterval(() => {
//1
setCount((count) => ++count);
// 2
// countRef.current= ++countRef.current;
// setCount(countRef.current)
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return <div>number:{count}</div>;
}
---------------------------------------------------
const [count, setCount] = useState(0);
// 通过 ref 来储存最新的 count
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
useState 适当合并(Form表单)
const [formData, setFormData] = useState<any>({
inputGroupSelect: 'RPC',
inputGroupInput: '',
productName: '',
submittedBy: '',
rangePicker: [],
promoType: '',
status: '',
});
//通过 ref 来储存最新的 formData
const formDataRef = useRef(formData);
formDataRef.current = formData;
const { inputGroupSelect, inputGroupInput, productName, submittedBy, rangePicker, promoType, status } = formData;
function search() {
//点击搜索触发方法
const formData = formDataRef.current;
console.log('form: ', formData);
}
function setForm(key: string, value: any) {
//useState action 可以传函数
setFormData((prevState) => {
return { ...prevState, [key]: value };
});
}
const formBaseData = [{
onChange: (value) => {
setForm('submittedBy', value);
},
onClick: search
}];
Class组件 迁移到 Hook组件
生命周期方法要如何对应到 Hook?
constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。
getDerivedStateFromProps:改为 在渲染时 安排一次更新。
shouldComponentUpdate: React.memo.
render:这是函数组件体本身。
componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。
getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法.
列表滚动位置不对
- 在 React 中,state 更新是排队进行的。
- 强制 React 同步更新(“刷新”)DOM。 为此,从 react-dom 导入 flushSync 并将 state 更新包裹 到 flushSync 调用中,用 flushSync 同步更新 state
function handleAdd() { const newTodo = { id: nextId++, text: text }; flushSync(() => { setText(''); setTodos([ ...todos, newTodo]); }); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
子组件props改变默认值不刷新
- 数据源dataSourceProp是从父组件传递到子组件的属性,并且在子组件内部被用作状态的初始值。
- 需要注意的是,父组件传递的dataSourceProp只会在组件第一次渲染时设置初始状态。之后,即使父组件传递的dataSourceProp发生变化,也不会更新子组件的状态,除非你在子组件内部显式处理这种变化,比如使用useEffect钩子。
function (props){ const { dataSourceProp} = props; const [,]=usestate(dataSourceProp) }