(react知识记录一)react 18中hook函数超级详解
前言
在之前学习和写react项目过程中没有完整的记下react中hook函数的使用,今天又特地的去查看官网、翻阅资料,怕以后遗忘特此在这里记录一下。备注:这里使用到的组件都是函数组件,不是类组件。
正文
useState(常用)
使用:
let [number,setNumber] = useState(0)
如上所示,useState通过传入一个初始值,返回一个数组,这个数组的第一项是设置的当前组件的值,第二项是修改这个值的方法。如上,如果你直接修改这个值(number=1),就不会触发函数组件重新渲染,但这个状态值的确是改了的。想改变值并且触发更新,那就需要使用setNumber进行修改值,这样修改值会自动让组件重新渲染。
值的注意的是,在使用useState的时候,useState内置了一部分优化,它会比较你传入的值在更新前和更新后是否一样,比如你通过setNumber修改的number两次的值都是1,那么该函数组件并不会重新渲染,这个过程是浅比较的。正因为这样当你的状态值是一个引用数据类型的话,比如一个数组,如果你直接使用push、pop这样改变数组内的元素,那同样不会触发组件的重渲染,因为useState内部通过浅比较判断更新前和更新后的引用地址是同一个。
let [arr,setArr] = useState([0])
//这样不会触发
arr.push(1)
setArr(arr)
//这样会触发更新,因为引用地址变了
setArr([...arr,1])
还值得注意的是,一定要记住,函数组件每次重新渲染都是会重新执行这个函数组件的,也就是每次更新组件都会产生一个全新的“私有上下文”。执行下一段代码你最后得到的number的值始终是1,而不是我们想要的10,因为里面的number的值始终都是我们第一次执行函数组件产生的闭包里的number。
let [number,setNumber] = useState(0)
for(let i = 0;i < 10;i++){
setNumber(number+1)
}
如果想实现让number变为10,可以这样实现,每次调用setNumber时其实不会立即去重新执行函数组件,我认为它会进入一个类似栈的结构里,当所有的setNumber入栈后统一去执行setNumber,而每一次执行setNumber后会将本次修改的值传给下一个setNumber,所以prev拿到的值就是最新的number值:
setNumber(prev => {
// prev:存储上一次的状态值
console.log(prev);
return prev + 1; //返回的信息是我们要修改的状态值
});
useEffect(常用)
useEffect相当于充当着周期函数的职能,如下所示:
useEffect(()=>{
console.log("useEffect") //等价于 componentDidUpdate,在组件挂载完成后执行
})
useEffect(()=>{
console.log("useEffect") //等价于 componentDidMount,在组件每次更新执行
},[])
useEffect(()=>{
return ()=>{
console.log("useEffect") //等价于 componentWillUnmount,在组件将要销毁的时候执行
}
})
但有所不同的是,useEffect的第二项参数可以传入依赖项,当依赖项改变的时候再执行传入的callback,这有点vue中watch的味道了。
useLayoutEffect
useLayoutEffect与useEffect类似,但不同之处在于执行的阶段,在这里我把视图更新步骤分为以下四步。
- 第一步:基于babel-preset-react-app把JSX编译为createElement格式
- 第二步:把createElement执行,创建出virtualDOM
- 第三步:基于root.render方法把virtualDOM变为真实DOM对象「DOM-DIFF」
- 第四步:浏览器渲染和绘制真实DOM对象
在这里设置这样一个使用场景,我们通过一个点击div来回切换背景色,在运行下面代码后可以发现,每次点击最终都会变成红色,但变成红色过程中会存在闪烁,一瞬间变成绿色再变成红色。而如果将useEffect替换成useLayoutEffect就不会出现闪烁这种情况。
function App() {
let [flag, setFlag] = useState(false);
useEffect(() => {
if (!flag) {
setFlag(true);
}
}, [flag]);
return <div
style={{
backgroundColor: flag ? 'red' : 'green'
}}
onClick={() => {
setFlag(false);
}}>
{+flag}
</div>;
};
出现这样的原因是因为useLayoutEffect和useEffect执行的时间不同。在函数组件执行的时候,遇到useEffect或者useLayoutEffect并不会第一时间执行,一般情况下会按照代码从上往下的将他们放入一个effect链表当中,等全部放完后再统一依照先后进入链表的顺序执行。useEffect会在上面说的视图更新步骤中的第四步中同时执行的,这里,它相当于异步函数,所有执行的时候不会阻止第四步中浏览器的绘制。而对于useLayoutEffect却是在第三步和第四步之间执行的,并且useLayoutEffect执行中会阻塞第四步操作,因为它相当于同步执行函数,需要等它完全执行完成后,才会进入第四步(如果useLayoutEffect执行过程中产生了修改dom或者改变了视图依赖项的操作会重新从第一步执行)。所以这就是为什么使用useLayoutEffect不会产生闪烁,因为useLayoutEffect会等自身的callback执行完成后再去通知视图更新。
useRef(常用)
在函数组件中,可以基于useRef
获取DOM元素
function App() {
const [num, setNum] = useState(0);
const btnBox = useRef(null);
useEffect(() => {
console.log(btnBox.current);
}, [num]);
return (
<div>
<span>{num}</span>
<button ref={btnBox} onClick={() => setNum(num + 1)}>
按钮
</button>
</div>
);
}
值得注意的是,createRef也可以在函数组件中创建ref,但是与useRef唯一不同是,createRef会在函数组件每次更新的时候都会创建一个ref,而useRef在第一次创建ref后,后面函数组件更新后获取的ref都指向第一次函数组件产生的ref,并不会每次函数组件更新重新创建ref。所以在函数组件中使用useRef性能方面会好一些。
useImperativeHandle
useImperativeHandle是获取函数子组件内部状态或者方法的hook,使用方法:
const Child = forwardRef(function Child(props, ref) {
let [text, setText] = useState("child");
const submit = () => {
console.log("submit")
};
useImperativeHandle(ref, () => {
return {
text,
submit,
};
});
return (
<div className="child-box">
<span>Child</span>
</div>
);
});
const App = function Demo() {
let x = useRef(null);
useEffect(() => {
console.log(x.current);
}, []);
return (
<div className="demo">
App
<Child ref={x} />
</div>
);
};
useMemo(常用)
useMemo具备“计算缓存”,类似于vue里面的computed。在其依赖项没有改变的情况下,传给useMemo的callback就不会重新执行,如下所示,当number1或者number2的值改变的时候,sum才会改变,而改变x的值,不会改变sum,也不会重新执行useMemo的callback。
const App = function Demo() {
let [number1, setNumber1] = useState(10),
[number2, setNumber2] = useState(5),
[x, setX] = useState(0);
let sum = useMemo(() => {
console.log("change sum")
return number1 + number2
}, [number1, number2]);
return <div >
<div >
<p>number1:{number1}</p>
<p>number2:{number2}</p>
<p>sum:{sum}</p>
<p>x:{x}</p>
</div>
<div className="footer">
<Button type="primary" onClick={() => setNumber1(number1 + 1)}>number1增加</Button>
<Button type="primary" danger onClick={() => setNumber2(number2 + 1)}>number2增加</Button>
<Button onClick={() => setX(x + 1)}>x改变</Button>
</div>
</div>;
};
useMemo还有一个妙用就是可以让避免子组件无意义的重渲染。每次更新父组件的时候,子组件依赖的内容是否被修改都会触发子组件的重新渲染,这时可以用useMemo来实现当子组件依赖于父组件的某个依赖改变时再重新渲染,用法如下:
const Child1 = function Child1(props) {
const { number1 } = props;
console.log("Child1 change");
return <div>Child1:{number1}</div>;
};
const Child2 = function Child2(props) {
const { number2 } = props;
console.log("Child2 change");
return <div>Child2:{number2}</div>;
};
const App = function Demo() {
let [number1, setNumber1] = useState(10),
[number2, setNumber2] = useState(5),
[x, setX] = useState(0);
return (
<div>
{useMemo(() => {
return (
<>
<Child1 number1={number1} />
</>
);
}, [number1])}
<Child2 number2={number2} />
<div className="footer">
<Button type="primary" onClick={() => setNumber1(number1 + 1)}>
number1增加
</Button>
<Button type="primary" danger onClick={() => setNumber2(number2 + 1)}>
number2增加
</Button>
<Button onClick={() => setX(x + 1)}>x改变</Button>
</div>
</div>
);
};
当修改number1的时候才会触发Child1的更新,改变number2就不会触发。这里也给出一个React提供的高阶组件memo的实现,可以达到上面同样的效果:
const Child1 = React.memo(
function Child1(props) {
const { user } = props;
console.log("Child1 change");
return <div>Child1:{user.name}</div>;
}
)
值得注意的是,React.memo和useMemo中的依赖项,都是将更新前的依赖和更新后的依赖进行浅比较的,这意味着如果传入的依赖项是引用类型且没改变其引用地址,只是改变里面属性的话,是不会触发useMemo或者memo的。
useCallback(常用)
useCallback可以缓存一个函数,在其依赖项不变的情况下,useCallback返回的函数的引用地址不会产生变化
const handle = useCallback(() => {
//一部分函数逻辑
console.log("handle")
}, []);
useCallback相当于useMemo的语法糖,也就是说useCallback能实现的,useMemo也能实现,useMemo实现上述代码如下:
const handle = useMemo(()=> {
return ()=>{
//一部分函数逻辑
console.log("handle")
}
},[])
- 子组件没有从父组件传入的props或者传入的props仅仅为简单数值类型使用memo即可。
⚠️注意:这个高阶组件在function component以及class component都可以使用哦
- 子组件有从父组件传来的方法时,在使用memo的同时,使用useCallback包裹该方法,传入方法需要更新的依赖值。
- 子组件有从父组件传来的对象和数组等值时,在使用memo的同时,使用useMemo以方法形式返回该对象,传入需要更新的依赖值。
useContext
因为在React中,数据的传递是单向的,父组件通过属性将数据传递给子组件,子组件从props获取被传递的数据,但是,当组件嵌套的多了后,这样一层层传递数据就显得很繁琐了,所以React提供了一个Context Api,使用Context
可以避免的组件的层层props
嵌套的问题,使用如下:
const ThemeContext = React.createContext();
const App = function App() {
let [number,setNumber] = useState(0)
const changeNumber = (n)=>{
setNumber(n)
}
return <ThemeContext.Provider
value={{
number,
changeNumber
}}>
<div >
<Child />
</div>
</ThemeContext.Provider>;
};
const Child = function Child() {
let { number, changeNumber } = useContext(ThemeContext);
return <div className="main">
<p>child:{number}</p>
<Button onClick={()=>{changeNumber(++number)}} >点击</Button>
</div>;
};
useReducer
useReducer相当于是对useState的升级处理,useReducer的思想采用了redux中对状态的管理,每次改变一个状态的时候通过派发一个action去通知reducer去更新状态,但一般使用useReducer的场景是一个组件中含有大量的状态、大量的修改状态的逻辑,才会选择采用useReducer进行管理,使用如下:
const initialState = {
num: 0
};
const reducer = function reducer(state, action) {
state = { ...state };
switch (action.type) {
case 'plus':
state.num++;
break;
case 'minus':
state.num--;
break;
default:
}
return state;
};
const A1 = function A1() {
//state:获取的状态 dispatch:负责派发action通知reducer更新数据
//reducer:状态管理者,根据传入的action来进行不同的数据操作 initialState:初始化状态值
let [state, dispatch] = useReducer(reducer, initialState);
return <div className="box">
<span>{state.num}</span>
<br />
<button onClick={() => {
dispatch({ type: 'plus' });
}}>增加</button>
<button onClick={() => {
dispatch({ type: 'minus' });
}}>减少</button>
</div>;
};
自定义Hook
使用自定义hook可以将某些组件逻辑提取到可重用的函数中,或者单独抽取某个功能到另一个文件,显得主文件不那么臃肿,易读。如下实现一个简单的自定义hook,在useState中,如果创建了一个对象类型的状态,如果只是修改对象中某个属性,如果这样写会导致name属性丢失掉,因为setObj会将传入的对象进行全部替换更新,而不是部分替换更新:
这里实现一个usePartState来可以让对象状态进行部分更新:
const usePartState = function usePartState(initial) {
let [state, setState] = useState(initial);
const setPartState = (partState) => {
setState({
...state,
...partState
});
};
return [state, setPartState];
};
function App() {
let [state, setState] = usePartState({
x: 10,
y: 20
});
return <div>
<span>{state.x}</span>
<span>{state.y}</span>
<button onClick={() => {
setState({
x: state.x + 1
});
}}>处理x</button>
</div>;
};
注意的是,如果是自定义hook,最好以use进行开头,因为采用use开头,react可以识别到useXXX是一个hook函数,会帮忙检查一些使用上的错误。
转载自:https://juejin.cn/post/7221047453963812920