hooks

本文详细介绍了React Hooks中的useState、useEffect、useRef和useCallback,探讨了如何处理副作用、避免不必要的重渲染,并提供了一些最佳实践。包括使用useEffect进行定时任务管理、依赖项变更的处理、自定义Hooks的封装以及优化组件性能的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

https://juejin.im/post/5ceb36dd51882530be7b1585

一.

对于class component :
  • 首先state是Immutable的, setState后一定会生成一个全新的state引用。
  • 但是class component是通过this.state的方式读取的,这导致每次代码执行都会拿到最新的state引用。
对于Function component:
  • useState产生的数据也是immutable的,通过数组第二个参数set一个新值后, 原来的值会在下次渲染时形成一个新的引用。
  • 但由于对state的读取没有通过this.的方式,使得每次setTimeout都读取了当时渲染时闭包环境里的数据,虽然state跟着最新的渲染变了,但旧的渲染里,状态依然是旧值。
function Counter() {
   const [count, setCount] = useState();
   const log = () => {
   	setCount(count+1);
   	setTimeout(() => {
   		console.log(count);
   	}, 3000);
   };
   return (
   	<div>
   		<p>you clickeed {count} times</p>
   		<button onclick={log}>click me</button>
   	</div>
   );
}

打印 0、1、2

二.
useRef: 通过useRef创建的对象,其值只有一份,而且在所有Rerender之间共享。
所以我们对count.current赋值或读取,读到的永远是其最新的值,而与渲染闭包无关。

function Counter() {
	const count = useRef(0);
	const log = () => {
		count.current++;
		setTimeout(() => {
			console.log(count.current);
		}, 3000);
	};
	return (
		<div>
			<p>you clicked {count.current} times</p>
			<button onClick={log}>click me</button>
		</div>
	);
}

答案: 3、3、3

三.
useEffect: 使用useRef会改变原始值,为了不改变原始值且达到相同效果,可以使用useEffect。useEffect是处理副作用的,其执行时机是在每次渲染完毕后,实际是在真实dom操作完成后。他的第二个参数是一个数组定义了依赖, 只要所有依赖项不变, useEffect就不会执行, 如果传入[], 则只在初始化时执行一次。 因此我们可以新建一个useRef的值给setTimeout用,其他部分用原始的state count。

function Counter() {
	const [count, setCount] = useState(0);
	const currentCount = useRef(count);
	 
	useEffect(() => {
		currentCount.current = count;
	}, [count]);
	
	const log = () => {
		setCount(count + 1);
		setTimeout(() => {
			console.log(currentCount.current);
		}, 3000);
	}
	return (
		<div>
			<p>you clicked {count.current} times</p>
			<button onClick={log}>click me</button>
		</div>
	);
};

四.
封装下useRef作为一个自定义的hooks
自定义hooks: 以use开头且返回非jsx元素。

function useCurrentValue(value) {
	const ref = useRef(0);
	useEffect(() => {
		ref.current = value;
	}, [value]);
	return ref;
};

function Counter() {
	const [count, setCount] = useState(0);
	const currentCount = useCurrentValue(count);
 
	const log = () => {
		setCount(count + 1);
		setTimeout(() => {
			console.log(currentCount.current);
		}, 3000);
	}
	return (
		<div>
			<p>you clicked {count.current} times</p>
			<button onClick={log}>click me</button>
		</div>
	);
};

五.
useEffect可以返回一个函数,这个函数是在useEffect下次调用前调用的(比如第二次调用useEffect的时候会先执行return函数)

function Counter() {
	const [count, setCount] = useState(0);

	useEffect(() => {
		const id = setInterval(() => {
			setCount(count + 1);
		}, 1000);
		return () => clearInterval(id);
	}, [count]);
	return <h1>{count}</h1>;
}

答案: 123 (每次重新渲染如果count变化就先执行clearInterval, 然后重新实例化一个计时器)

六.
setCount的第二种用法是传入一个函数,参数是最新的count值。
所以如果不想每次都实例化一个新的计时器可以传入回调函数

function Counter() {
	const [count, setCount] = useState(0);
	
	useEffect(() => {
		const id = setInterval(() => {
			setCount(c => c+1);
		}, 1000);
		return () => clearInterval(id);
	}, []);
	return <h1>{count}</h1>;
}

这样就做到了

  • 对依赖诚实(useEffect里的回调方法用到什么state, 就在[]里监控什么state)
  • 依赖项是[] , 就只会执行一次useEffect

七.
useReducer: 就是把一堆state放在一起,可以替换 在一个setXXState的回调函数里面想用除了XX以外别的State的情况

function Counter() {
	const [state, dispatch] = useReducer(reducer, initialState);
	const {count, step} = state;
	useEffect(() => {
		const id = setInterval(() => {
			dispatch({type: 'tick'});		
		},1000);
		return () => clearInterval(id);
	}, [dispatch]);
	
	return <h1>{count}</h1>
};
function reducer(state, action) {
	switch (action.type) {
		case 'tick':
			return {
				...state,
				count: state.count + state.step
			};
	}
}

对于依赖dispatch,他的引用永远不会变,因此可以忽略他的影响。

八.
为了避免遗漏依赖,尽量将useEffect里用到的函数写在useEffect的内部。
如果想要抽到外部,可以使用useCallback.
把抽出去的函数用useCallback打包一下(打包时需要传入依赖项)得到一个新的函数,这个新的函数要作为useEffect的依赖项放入[]中,然后就可以在useEffect的回调函数利用了。

function Counter() {
	const {count, setCount} = useState(0);
	const fetchUrl = useCallback(() => {
		return 'https://ssssss' + count;
	}, [count]);
	useEffect(()=>{
		const url = fetchUrl();
	}, [fetchUrl]);
	return <h1>{count}</h1>
}

useEffect对业务的抽象:

  • 参数变化,列表变化:依赖项是查询参数,那么只要查询参数变化了,列表就会自动取数刷新。注意我们将取数据时机从触发端改成了接收端。
  • 当列表更新后,重新注册一遍拖拽响应事件:依赖参数是列表。只要列表变化,拖拽响应就会重新初始化。
  • 只要数据流某个数据变化,标题就同步修改

九.
可以利用自定义hooks,将一个功能抽到整个组件的外部
但是如果抽象出的hooks重新执行成本很高,依赖又不是中心功能所需要的(比如只是打日志), 可以通过将依赖转化成ref

十.
把依赖绑在ref上,这样依赖不会变,却能每次都拿到新值: 也就是多加一个useEffect把传进来的state绑在ref上,useCallback的依赖使用ref。 但是官方还是推荐使用reducer

十一.
render优化可以使用memo(props => {useEffect…; return (//…)}); 等价于pureComponent。
useMemo可做局部的PureRender

return useMemo(() => {
	//...
},[])

十二.
可以使用Context做批量透传

const store = createContext(null);
function Parent() {
	const [count, setCount] = useState(0);
	const [step, setStep] = useState(0);
	const fetchData = useFetch(count, step);
	return (
		<Store.Provider value={{setCount, setStep, fetchData}}>
			<Child />
		</Store.Provider>
	)
}
const Child = useMemo( props => {
	const dispatch = useContext(Store);
	function onClick(){
		dispatch({
			type: 'countInc'
		});
	}
	return (//...);
});

也可以将state通过Provider注入,但会有性能问题,需要注意:
如果有多个子组件使用了state, 那么任意一个set某state的操作都会让使用state的子组件rerender。这是因为即使使用了memo包裹子组件,memo也只能挡在外层,而通过useContext的数据注入发生在函数内部,会绕过memo,所以当出发dispatch导致state变化时,所有使用了state的组件内部都会强制重新刷新。要想对渲染次数优化,可以用useMemo, useMemo的第一个参数是原子组件返回的jsx,第二个参数是jsx依赖项的数组。
如果使用useContext而没有用props, 就可以使用useMemo代替memo做性能优化。

十三.
异步操作: 写在useEffect里。可封装成一个自定义hook:

const useDataApi = (initialUrl, initialData) => {
	const [url, setUrl] = useState(initialUrl);
	
	const [state, dispatch] = useReducer(dataFetchReducer, {
		isLoading: false,
		isError: false,
		data: initialData
	});
	useEffect(() => {
		let didCancel = false;

		const fetchData  = async () => {
			dispatch({type: 'FETCH_INIT});
			try {
				const result = await axios(url);
				if(!didCancel) {
					dispatch({type: 'FETCH_SUCCESS', payload: result.data});
				}
			} catch (error) {
				if(didCancel) {
					dispatch({type: 'FETCH_FAILURE'});
				}
			}
		};
		fetchData();
		return () => {
			didCancel = true;
		};
	}, [url]);
	const doFetch = url => setUrl(url);
	return { ...state, doFetch };
}

function App() {
	const { data, isLoading, isError } = useDataApi("http://v", {
		showLog: true
	});
	useEffect(() => {
		dispatch({
			type: 'updateLoading',
			data,
			isLoading,
			isError
		});
	}, [dispatch, data, isLoading, isError]);
}

十四.
Child.defaultProps = {};

十五.
给子组件传对象的时候,为了避免对象的引用不断变化,可以将对象挂在ref上: const scheme = useRef({b: 1});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值