一、useState的应用 状态管理
function App() { const [name, setName] = useState("yhl"); const onChange = () => { setName('wxe') console.log('name', name)//yhl } return ( <div className="App"> {name} <button onClick={onChange}>更改名字</button> </div> );}export default App;
以上为useState的用法,[]表示解构运算,里面第一个是name的值,第二个值setName是改变name的方法,useState括号里的是name的默认值。
注意:
(1)如果状态是一个对象或者数组的话,必须传入传入完整的部分,因为他是替换老状态返回新状态。也就是useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果
(2)useStata是异步回调。也就是对数据更新后,不能立刻得到最新的数据;
解决办法:配合useEffect使用;
useEffect(() => { console.log('name2', name)//wxe }, [name])
创建新的变量保存最新的数据
const onChange = () => { const newName = 'wxe' setName(newName) console.log('name1', newName) }
(3)在多个useState()调用中,渲染之间的调用顺序必须相同。
仅从React 函数调用 Hook:必须仅在函数组件或自定义钩子内部调用useState()。不能在循环,条件,嵌套函数等中调用useState()。
(4)使用回调函数更新数据
const [a, setA] = useState({ c: 0 });
<button onClick={() => { setA((a) => { return { c: a.c + 1 } }) }}>加1</button>
(5)useState存入的值是引用类型,可使用深拷贝解决;
(6)useState如果保存引用数据,useEffect检测不到变化;
const textObj = {name:'dx'}
const [useState1, setUseState1] = useState(textObj)
/** usestate的操作不要放在函数的最外层,这里只是简单的代码展示,你可以将set操作放在某个函数里面 */
setUseState1((oldUseState1) => {
oldUseState1.age = 18
return oldUseState1
useEffect(() => {
console.log(useState1)
},[useState1])
//结果是没有任何反应
const textObj = {name:'dx'}
const [useState1, setUseState1] = useState(textObj)
/** usestate的操作不要放在函数的最外层,这里只是简单的代码展示,你可以将set操作放在某个函数里面 */
setUseState1((oldUseState1) => {
oldUseState1.age = 18
/** 返回一个新的对象,useEffectc才能检测得到 */
return {...oldUseState1}
useEffect(() => {
console.log(useState1) // {name: "dx", age: 18}
},[useState1])
(7)useState无法保存一个函数,在useState中,函数会自动调用,并且保存函数返回的值,而不能保存函数本身。建议使用useCallback
参考链接:blog.csdn.net/glorydx/art…
useState 源码中的链表实现
import React from 'react';
import ReactDOM from 'react-dom';
let firstWorkInProgressHook = {memoizedState: null, next: null};
let workInProgressHook;
function useState(initState) {
let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};
function setState(newState) {
currentHook.memoizedState = newState;
render();
}
// 这就是为什么 useState 书写顺序很重要的原因
// 假如某个 useState 没有执行,会导致指针移动出错,数据存取出错
if (workInProgressHook.next) {
// 这里只有组件刷新的时候,才会进入
// 根据书写顺序来取对应的值
// console.log(workInProgressHook);
workInProgressHook = workInProgressHook.next;
} else {
// 只有在组件初始化加载时,才会进入
// 根据书写顺序,存储对应的数据
// 将 firstWorkInProgressHook 变成一个链表结构
workInProgressHook.next = currentHook;
// 将 workInProgressHook 指向 {memoizedState: initState, next: null}
workInProgressHook = currentHook;
// console.log(firstWorkInProgressHook);
}
return [currentHook.memoizedState, setState];
}
function Counter() {
// 每次组件重新渲染的时候,这里的 useState 都会重新执行
const [name, setName] = useState('计数器');
const [number, setNumber] = useState(0);
return (
<>
<p>{name}:{number}</p>
<button onClick={() => setName('新计数器' + Date.now())}>新计数器</button>
<button onClick={() => setNumber(number + 1)}>+</button>
</>
)
}
function render() {
// 每次重新渲染的时候,都将 workInProgressHook 指向 firstWorkInProgressHook
workInProgressHook = firstWorkInProgressHook;
ReactDOM.render(<Counter/>, document.getElementById('root'));
}
render();
二、useEffect
利用useEffect可以在函数组件中执行副作用操作。特指那些没有发生在数据向视图转换过程中的逻辑。如:ajax请求、原生dom元素的访问、绑定/解绑事件、添加订阅、设置定时器、本地持久化缓存、记录日志等;
react组件中有两种常见副作用操作:需要清除的和不需要清除的。
无需清除的。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。
useEffect就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它与class组件中的生命周期函数componentDidMount 、componentDidUpdata和componentWillUnmount具有相同的用途,只不过被合成了一个api。
useEffect接受一个函数,该函数会在组件渲染到屏幕之后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容。
import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
const [number,setNumber] = useState(0);
// useEffect里面的这个函数会在第一次渲染之后和更新完成后执行
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
document.title = `你点击了${number}次`;
});
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
</>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'));
需要清除的副作用。
副作用函数可以通过返回一个函数来指定如何清除副作用。为防止内存泄漏,清除函数会在组件卸载前执行。如果组件多次渲染,则在执行下一个effect之前,上一个effect就已经被清除。
useEffect(() => { let timer = setInterval(() => { console.log(num) setNum((num) => num + 1) }, 2000)
// useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
// useEffect 在执行副作用函数之前,会先调用上一次返回的函数
// 如果要清除副作用,要么返回一个清除副作用的函数
return () => { console.log('卸载') clearInterval(timer) } })
或者
// useEffect(() => { // let timer = setInterval(() => { // console.log(num) // setNum((num) => num + 1) // }, 2000) // }, []) //要么在这里传入一个空的依赖项数组,这样就不会去重复执行 return ( <div className="App"> <p>{num}</p> </div> ); return ( <div className="App"> <p>{num}</p> </div> );
effect进行性能优化
effect第二个参数是依赖数组项,控制着useEffect的进行。
-
如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
-
如果只想运行一次的effect(组件挂载和卸载时执行),可以传递一个空数组,作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
-
不带第二个参数,每次组件重新渲染或者更新都执行清理或者执行effect,可能会导致性能问题,比如两次渲染的数据完全一样。
useEffect(() => { console.log('name2', name)//wxe }, [name])// 数组表示 effect 依赖的变量,只有当这个变量发生改变之后才会重新执行 efffect 函数
useLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
useEffect 和 useLayoutEffect 的区别?
useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。
useLayoutEffect 在渲染时是同步执行,其执行时机与 componentDidMount,componentDidUpdate 一致。useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行,在绘制之前执行的,会阻塞页面的绘制。
学习链接:useLayoutEffect和useEffect两者区别
三、useRef
类组件获取dom元素用的是字符串、回调函数和React.createRef();函数组件使用useRef。useRef返回一个可变的ref对象,其current属性被初始化为传入的参数(initialValue)。
const refContainer = useRef(initialValue)
-
useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref)
const divRef = useRef<HTMLDivElement | null>(null);//ts接口定义 console.log(divRef.current?.style) return (
);{num}
组件生命周期期间,useRef指向的对象都是一直存在的; 每次渲染时,useRef都指向同一个引用的对象;总结起来就是:useRef生成的对象,在组件生命周期期间内存地址都是不变的。
forwardRef
因为函数组件没有实例,所以无法像类组件一样接受ref属性;
forwardRef可以在父组件中操作子组件的ref对象;forwardRef可以将父组件的ref对象转发到子组件中的dom元素上;子组件接受props和ref作为参数。
import React, { useState, useEffect, useRef } from "react";const Child = React.forwardRef((props: any, ref: any) => { console.log('ref', ref) return ( <input type="text" ref={ref} /> )})function Parent() { let [number, setNumber] = useState<number>(0); // 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空 // 只有当它被赋给某个元素的 ref 属性时,才会有值 // 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了 // 那么父组件就可以操作子组件中的某个元素 // 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的 // 所以就需要用到 forwardRef 进行转发 const inputRef = useRef<HTMLDivElement | null>(null);//{current:''} function getFocus() { console.log('inputRef', inputRef) inputRef.current?.focus(); } return ( <> <Child ref={inputRef} /> <button onClick={() => setNumber(number + 1)}>+</button> <button onClick={getFocus}>获得焦点</button> </> )}export default Parent;
四、优化
useCallback:接受一个回调函数参数和一个依赖数组项(子组件依赖父组件的状态,即子组件会使用到父组件的值)。useCallback会返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
应用场景
通常在将一个组件中的函数,传递给子元素进行回调使用时,使用useCallback对函数进行处理;
1.useCallback会返回一个函数的memoized(记忆的)值
2.在依赖不变的情况下,多次定义的时候,返回的值是相同的
总结如下:
1、父组件传给子组件的是函数,即使父组件改变的是A值,正常情况下也会触发渲染;2、第二项加空数组时虽然不触发重新渲染,但是函数中的state状态值是初始值;
3、当没有第二项值,会触发重复渲染,函数中的state状态值是最新值;
5、当加上useCallback是第二项是依赖项,只有当依赖项发生改变时,子组件才会触发渲染。6、利用ref,当依赖项发生改变时,子组件可以不用渲染,函数内部也可以获取最新状态值。
useMemo:
用来缓存数据,当 组件内部某一个渲染的数据,需要通过计算而来,这个计算是依赖与特定的state、props数据,我们就用useMemo来缓存这个数据,以至于我们在修改她们没有依赖的数据源的情况下,多次调用这个计算函数,浪费计算资源。
把创建函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo 接收一个函数和依赖数组,当数组中依赖项变化的时候,这个函数就会执行,返回新的值。
import React, { FC, memo, useState, useMemo, useCallback } from "react"
interface Props { [key: string]: any}const Children:
FC<Props> = memo((props) => { console.log('props', props) return (
<button onClick={props.onClick}>{props.data.number}</button> )})
let oldData: any,
oldAddClick: any;export default function Counter2() {
console.log('Counter render');
const [name, setName] = useState('计数器');
const [number, setNumber] = useState(0); // 父组件更新时,这里的变量和函数每次都会重新创建,那么子组件接受到的属性每次都会认为是新的 // 所以子组件也会随之更新,这时候可以用到 useMemo
// 有没有后面的依赖项数组很重要,否则还是会重新渲染 // 如果后面的依赖项数组没有值的话,即使父组件的 number 值改变了,子组件也不会去更新
//const data = useMemo(()=>({number}),[]);
const data = useMemo(() => ({ number }), [number]);
console.log('data', data)
console.log('data===oldData ', data === oldData);
oldData = data; // 有没有后面的依赖项数组很重要,否则还是会重新渲染
const addClick = useCallback(() => { setNumber(number + 1); }, [number]);
console.log('addClick', addClick)
console.log('addClick===oldAddClick ', addClick === oldAddClick);
oldAddClick = addClick; return ( <>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} /> {number} <Children data={data} onClick={addClick} /> </> )}
五、useReducer
在 hooks 中提供了的 useReducer 功能,可以增强 ReducerDemo 函数提供类似 Redux 的功能。它和redux中的reducer很像。
-
useState 的替代方案,它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
-
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。
let initialState = 0; // 如果你希望初始状态是一个{number:0} // 可以在第三个参数中传递一个这样的函数 ()=>({number:initialState}) // 这个函数是一个惰性初始化函数,可以用来进行复杂的计算,然后返回最终的 initialState const [state, dispatch] = useReducer(reducer, initialState, init);
import React, { FC, useReducer } from "react"export default function Counter() { const initialState = 0; function reducer(state: any, action: any) { switch (action.type) { case 'increment': return { number: state.number + 1 }; case 'decrement': return { number: state.number - 1 }; default: throw new Error(); } } function init(initialState: any) { return { number: initialState }; } const [state, dispatch] = useReducer(reducer, initialState, init) return ( <> Count: {state.number} <button onClick={() => dispatch({ type: 'increment' })}>+ <button onClick={() => dispatch({ type: 'decrement' })}>- </> )}
import React, { FC, useReducer } from "react"export default function Counter() { // 第一个参数:应用的初始化 const initialState = { number: 0 }; // 第二个参数:state的reducer处理函数 function reducer(state: any, action: any) { switch (action.type) { case 'increment': return { number: state.number + 1 }; case 'decrement': return { number: state.number - 1 }; default: throw new Error(); } } // 返回值:最新的state和dispatch函数 const [state, dispatch] = useReducer(reducer, initialState) return ( <> Count: {state.number} <button onClick={() => dispatch({ type: 'increment' })}>+ <button onClick={() => dispatch({ type: 'decrement' })}>- </> )}
其他链接:blog.csdn.net/qq_22833925…
六、自定义hooks
useRize.jsx
import React, { useState, useEffect, useCallback } from "react"import React, { useState, useEffect, useCallback } from "react"const Resize = () => { const [size, setSize] = useState({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }) const onResize = useCallback(() => { setSize({ height: document.documentElement.clientHeight, width: document.documentElement.clientWidth }) }) useEffect(() => { window.addEventListener('resize', onResize); return () => { window.addEventListener('resize', onResize); } }, [onResize]) return size;}export default Resize
// 自定义useCount的Hooksfunction useCount() { const [count, setCount] = useState(0); return { count, setCount };}const App = () => { const { count, setCount } = useCount() const size = Resize() return ( <div> {size.width} {/* {count} <button onClick={() => setCount(count + 1)}>add</button> */} </div> )