title: Hooks基础使用
order: 4
常用的usestate、useEffect就不讲解了
Hooks基础使用
1.函数组件和类组件的区别
类组件的性能消耗比较大,因为类组件需要创建类组件的实例,而且不能销毁。
函数式组件性能消耗小,因为函数式组件不需要创建实例,渲染的时候就执行一下,得到返回的react元素后就直接把中间量全部都销毁。
函数组件和类组件当然是有区别的,而且函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。为了提高性能,尽量使用函数组件。
2.useContext():共享状态钩子
如想实现Test
和Message
组件通信
<div className="App">
<Test></Test>
<Message></Message>
</div>
第一步、外部建立一个组件
import { createContext } from 'react'
const NavContext= createContext<any>({});
export default NavContext;
第二步、在要共享状态组件外包裹
import AppContext from './AppContext'
...
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Test></Test>
<Message></Message>
</div>
</AppContext.Provider>
第三步,在组件里使用数据
import React,{useContext} from 'react'
import AppContext from './AppContext'
export default function Test() {
const { username } = useContext(AppContext);
return (
<div>
{username}
</div>
)
}
3.useReducer():action 钩子
useReducer
不是redux
的某个替代品。useReducer
仅仅是useState
的一种替代方案:
在某些场景下,如果state
的处理逻辑比较复杂,我们可以通过useReducer
来对其进行拆分;
或者这次修改的state
需要依赖之前的state
时,也可以使用;
单独创建一个reducer/counter.js文件:
export function counterReducer(state, action) {
switch(action.type) {
case "increment":
return {...state, counter: state.counter + 1}
case "decrement":
return {...state, counter: state.counter - 1}
default:
return state;
}
}
home.js
import React, { useReducer } from 'react'
import { counterReducer } from '../reducer/counter'
export default function Home() {
const [state, dispatch] = useReducer(counterReducer, {counter: 100});
return (
<div>
<h2>当前计数: {state.counter}</h2>
<button onClick={e => dispatch({type: "increment"})}>+1</button>
<button onClick={e => dispatch({type: "decrement"})}>-1</button>
</div>
)
}
创建另外一个profile.js也使用这个reducer函数,是否会进行数据的共享:
import React, { useReducer } from 'react'
import { counterReducer } from '../reducer/counter'
export default function Profile() {
const [state, dispatch] = useReducer(counterReducer, {counter: 0});
return (
<div>
<h2>当前计数: {state.counter}</h2>
<button onClick={e => dispatch({type: "increment"})}>+1</button>
<button onClick={e => dispatch({type: "decrement"})}>-1</button>
</div>
)
}
数据是不会共享的,它们只是使用了相同的counterReducer的函数而已。
所以,useReducer只是useState的一种替代品,并不能替代Redux。
4.useRef():保存引用
使用场景,如保存定时器ID
、卸载时删除定时器、获取dom
用法一:使用ref保存上一次的某一个值
- useRef可以想象成在ref对象中保存了一个.current的可变盒子;
- useRef在组件重新渲染时,返回的依然是之前的ref对象,但是current是可以修改的;
import React, { useEffect,useRef } from 'react';
...
let intervalHandle = useRef<any>();
let setTimeoutHandle = useRef<any>();
...
setTimeoutHandle.current = setTimeout(() => {
appHistory.push('/login');
clearInterval(intervalHandle.current);
}, 5000);
intervalHandle.current = setInterval(() => {
clearInterval(intervalHandle.current);//注意这个地方也要clear
setSecond(v => {
return v > 0 ? v - 1 : 0
}
)
}, 1000);
...
useEffect(() => {
return () => {
clearTimeout(setTimeoutHandle.current);
clearInterval(intervalHandle.current);
};
}, []);//清除定时器
用法二:引用DOM
import React, { useRef } from 'react';
export default function RefHookDemo() {
const inputRef = useRef();
const titleRef = useRef();
const handleOperating = () => {
titleRef.current.innerHTML = "我是coderwhy";
inputRef.current.focus();
}
return (
<div>
<input type="text" ref={inputRef}/>
<h2 ref={titleRef}>默认内容</h2>
<button onClick={e => handleOperating()}>操作</button>
</div>
)
}
5.useCallback
useCallback实际的目的是为了进行性能的优化。
- useCallback会返回一个函数的 memoized(记忆的) 值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b]
);
我们来看下面一段很有趣的代码:
-
increment1在每次函数组件重新渲染时,会返回相同的值;
-
increment2每次定义的都是不同的值;
-
问题:是否increment1会比increment2更加节省性能呢?
-
- 事实上,经过一些测试,并没有更加节省内存,因为useCallback中还是会传入一个函数作为参数;
- 所以并不存在increment2每次创建新的函数,而increment1不需要创建新的函数这种性能优化;
-
那么,为什么说useCallback是为了进行性能优化呢?
import React, { memo, useState, useCallback } from 'react'
export default function CallbackHookDemo() {
const [count, setCount] = useState(0);
const increment1 = useCallback(function increment() {
setCount(count + 1);
}, []);
const increment2 = function() {
setCount(count + 1);
}
return (
<div>
<h2>当前计数: {count}</h2>
<button onClick={increment1}>+1</button>
<button onClick={increment2}>+1</button>
</div>
)
}
我们来对上面的代码进行改进:
- 在下面的代码中,我们将回调函数传递给了子组件,在子组件中会进行调用;
- 在发生点击时,我们会发现接受increment1的子组件不会重新渲染,但是接受increment2的子组件会重新渲染;
- 所以useCallback最主要用于性能渲染的地方应该是和memo结合起来,决定子组件是否需要重新渲染;
import React, { memo, useState, useCallback } from 'react';
const CounterIncrement = memo((props) => {
console.log("CounterIncrment被渲染:", props.name);
return <button onClick={props.increment}>+1</button>
})
export default function CallbackHookDemo() {
const [count, setCount] = useState(0);
const increment1 = useCallback(function increment() {
setCount(count + 1);
}, []);
const increment2 = function() {
setCount(count + 1);
}
return (
<div>
<h2>当前计数: {count}</h2>
{/* <button onClick={increment1}>+1</button>
<button onClick={increment2}>+1</button> */}
<CounterIncrement increment={increment1} name="increment1"/>
<CounterIncrement increment={increment2} name="increment2"/>
</div>
)
}
网上使用场景:
使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props
;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return count;
}, [count]);
return <div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
}
6.useMemo
useMemo实际的目的也是为了进行性能的优化。
如何进行性能的优化呢?
- useMemo返回的也是一个 memoized(记忆的) 值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
我们来看一个案例:
- 无论我们点击了是
+1
还是切换
案例都会重新计算一次; - 事实上,我们只是希望在count发生变化时重新计算;
import React, { useState, useMemo } from 'react';
function calcNum(count) {
let total = 0;
for (let i = 0; i < count; i++) {
total += i;
}
console.log("计算一遍");
return total
}
export default function MemoHookDemo() {
const [count, setCount] = useState(10);
const [isLogin, setIsLogin] = useState(true);
const total = calcNum(count);
return (
<div>
<h2>数字和: {total}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
{isLogin && <h2>Coderwhy</h2>}
<button onClick={e => setIsLogin(!isLogin)}>切换</button>
</div>
)
}
这个时候,我们可以使用useMemo来进行性能的优化:
import React, { useState, useMemo } from 'react';
function calcNum(count) {
let total = 0;
for (let i = 0; i < count; i++) {
total += i;
}
console.log("计算一遍");
return total
}
export default function MemoHookDemo() {
const [count, setCount] = useState(10);
const [isLogin, setIsLogin] = useState(true);
const total = useMemo(() => {
return calcNum(count);
}, [count]);
return (
<div>
<h2>数字和: {total}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
{isLogin && <h2>Coderwhy</h2>}
<button onClick={e => setIsLogin(!isLogin)}>切换</button>
</div>
)
}
当然,useMemo也可以用于子组件的性能优化:
- ShowCounter子组件依赖的是一个基本数据类型,所以在比较的时候只要值不变,那么就不会重新渲染;
- ShowInfo接收的是一个对象,每次都会定义一个新的对象,所以我们需要通过useMemo来对其进行优化;
import React, { useState, useMemo, memo } from 'react';
function calcNum(count) {
let total = 0;
for (let i = 0; i < count; i++) {
total += i;
}
console.log("计算一遍");
return total
}
const ShowCounter = memo((props) => {
console.log("重新渲染");
return <h1>Counter: {props.total}</h1>
})
const ShowInfo = memo((props) => {
console.log("ShowInfo重新渲染");
return <h1>信息: {props.info.name}</h1>
})
export default function MemoHookDemo() {
const [count, setCount] = useState(10);
const [isLogin, setIsLogin] = useState(true);
const total = useMemo(() => {
return calcNum(count);
}, [count]);
const info = useMemo(() => {
return {name: "why"}
}, [])
return (
<div>
<h2>数字和: {total}</h2>
<ShowCounter total={total} />
<ShowInfo info={info}/>
<button onClick={e => setCount(count + 1)}>+1</button>
{isLogin && <h2>Coderwhy</h2>}
<button onClick={e => setIsLogin(!isLogin)}>切换</button>
</div>
)
}
7.useImperativeHandle
useImperativeHandle并不是特别好理解,我们一点点来学习。
我们先来回顾一下ref和forwardRef结合使用:
- 通过forwardRef可以将ref转发到子组件;
- 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
import React, { useRef, forwardRef } from 'react';
const HYInput = forwardRef(function (props, ref) {
return <input type="text" ref={ref}/>
})
export default function ForwardDemo() {
const inputRef = useRef();
return (
<div>
<HYInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>聚焦</button>
</div>
)
}
上面的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:
- 直接暴露给父组件带来的问题是某些情况的不可控;
- 父组件可以拿到DOM后进行任意的操作;
- 但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作;
通过useImperativeHandle可以只暴露固定的操作:
- 通过useImperativeHandle的Hook,将
传入的ref
和useImperativeHandle第二个参数返回的对象
绑定到了一起; - 所以在父组件中,使用
inputRef.current
时,实际上使用的是返回的对象
; - 比如我调用了
focus函数
,甚至可以调用printHello函数
;
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
const HYInput = forwardRef(function (props, ref) {
// 创建组件内部的ref
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
printHello: () => {
console.log("Hello World")
}
}))
// 这里绑定的是组件内部的inputRef
return <input type="text" ref={inputRef}/>
})
export default function ImperativeHandleHookForwardDemo() {
const inputRef = useRef();
return (
<div>
<HYInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>聚焦</button>
<button onClick={e => inputRef.current.printHello()}>Hello World</button>
</div>
)
}
8.useLayoutEffect
同vue中nextTick
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
- useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
- useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
我们来看下面的一段代码:
- 这段代码在开发中会发生闪烁的现象;
- 因为我们先将count设置为了0,那么DOM会被更新,并且会执行一次useEffect中的回调函数;
- 在useEffect中我们发现count为0,又执行一次setCount操作,那么DOM会再次被更新,并且useEffect又会被执行一次;
import React, { useEffect, useState, useLayoutEffect } from 'react';
export default function EffectHookDemo() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count === 0) {
setCount(Math.random()*200)
}
}, [count]);
return (
<div>
<h2>当前数字: {count}</h2>
<button onClick={e => setCount(0)}>随机数</button>
</div>
)
}
事实上,我们上面的操作的目的是在count被设置为0时,随机另外一个数字:
- 如果我们使用useLayoutEffect,那么会等到useLayoutEffect代码执行完毕后,再进行DOM的更新;
import React, { useEffect, useState, useLayoutEffect } from 'react';
export default function EffectHookDemo() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
if (count === 0) {
setCount(Math.random()*200)
}
}, [count]);
return (
<div>
<h2>当前数字: {count}</h2>
<button onClick={e => setCount(0)}>随机数</button>
</div>
)
}
9.自定义hooks
Hooks
代码还可以封装起来,变成一个自定义的 Hook
,便于共享。
const usePerson = (personId) => {
const [loading, setLoading] = useState(true);
const [person, setPerson] = useState({});
useEffect(() => {
setLoading(true);
fetch(`https://swapi.co/api/people/${personId}/`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(false);
});
}, [personId]);
return [loading, person];
};
usePerson()
就是一个自定义的 Hook
Person
组件就改用这个新的钩子,引入封装的逻辑
const Person = ({ personId }) => {
const [loading, person] = usePerson(personId);
if (loading === true) {
return <p>Loading ...</p>;
}
return (
<div>
<p>You're viewing: {person.name}</p>
<p>Height: {person.height}</p>
<p>Mass: {person.mass}</p>
</div>
);
};
二.父触发子事件
1.父组件
const uploadFile = () => {
childRef.current.onSubmit();
};
...
<UploadC ref={childRef}/>
//通过属性ref,想要获取通过childRef这个ref存放子组件实例
//子组件接收这个ref,然后给它身上绑定一些想要属性(也就是useImperativeHandle的第二个参数返回值)
2.子组件
//通过useImperativeHandle可以只暴露固定的操作
import { memo, forwardRef, useImperativeHandle } from 'react'
export default memo(forwardRef(function index(props, ref) {
useImperativeHandle(ref, () => {
return {
onSubmit,
};
});
const onSubmit = () => {
console.log('object')
};
return (
<div>
</div>
)
}))
三.更多概念
1.🧡useMemo和useCallback区别和场景
共同作用:
仅仅 依赖数据 发生变化, 才会重新计算结果,也就是起到缓存的作用。
两者区别:
useMemo
计算结果是return
回来的值
, 主要用于 缓存计算结果的值 ,应用场景如: 需要 计算的状态
2.useCallbauseCallback
计算结果是函数
, 主要用于缓存函数
,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
注意: 不要滥用会造成性能浪费,react中减少render就能提高性能,所以这个仅仅只针对缓存能减少重复渲染时使用和缓存计算结果。
总结:比较消耗性能的再用这两
2.hooks的闭包
useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用ref来访问。