React Hooks 使用介绍
Hook
是React16.8
新增的特性,可以说是扩展了函数式组件的各项功能。有了这个功能,函数式组件和类式组件在实现功能上就没有什么差异了,而且使用hook
后,能使代码更加的简洁,代码也更具复用性了。
useState
以一个计数器来介绍:
import React, {useState} from 'react'
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>当前计数{count}</h2>
<button onClick={(e) => setCount(count + 1)}>+1</button>
<button onClick={(e) => setCount(count - 1)}>-1</button>
</div>
);
}
上面的组件就实现了一个计数器,点击按钮就会加减,hook
使的函数组件有了自己的状态。**useState
就是一个hook
。**
参数:初始化值,不设置就为undefined
,也可以可以传入数组对象,另外还可以传入一个带有返回值的函数。
**返回值:数组,包含两个元素,一般我们简化书写会使用数组解构语法。
- 第一个元素使当前状态的值(第一次调用为初始化的值)
- 第二个元素为设置状态值的函数。比如,上面的例子
count
的初始值为0 ,可以通过setCount
来重新设置count
的值,而且也会触发组件的创新渲染。
我们可以在组件中多次使用这个hook
来创建多个变量。**
**另外我们多次调用setXxx
函数来修改对应状态的时候,其实也和类组件中的setState
基本一样:
//会被合并成一次执行
setCount(count+10);
setCount(count+10);
setCount(count+10);
setCount(count+10);
//不会被合并
setCount((prev) => prev + 10);
setCount((prev) => prev + 10);
setCount((prev) => prev + 10);
setCount((prev) => prev + 10);
setCount((prev) => prev + 10);
可以看到使用hook
是多么的清爽。比新的一年换上新内裤还爽~~。那么接下来介绍第二个hook
。
useEffect
函数组件自身没有生命周期这类函数,我们要实现这个功能就可以使用这个hook
现在演示一个需求,页面的title
总是显示counter
的数字。**
**首先使用类组件进行实现:
class ChangeTitle extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidMount() {
document.title = this.state.count;
}
componentDidUpdate() {
document.title = this.state.count;
}
render() {
return (
<div>
<h2>类组件当前标题计数:{this.state.count}</h2>
<button onClick={(e) => this.setState({ count: this.state.count + 1 })}>
+1
</button>
</div>
);
}
}
可以看到更改标题的操作有重复的部分。**
**函数式组件:
function HookChangetitle() {
const [count, setCount] = useState(0);
//模拟生命周期
useEffect(() => {
document.title = count;
});
return (
<div>
<h2>函数当前计数{count}</h2>
<button onClick={(e) => setCount(count + 1)}>+1</button>
<button onClick={(e) => setCount(count - 1)}>-1</button>
</div>
);
}
useEffect
这个hook
可以告知React
在渲染好组件后执行某些操作。该hook
要求传入一个回调函数,在React
执行完DOM
更新操作后就会回调这个函数。默认情况下,无论是第一次渲染还是之后的每次更新都会执行一次这个回调函数。
清除effect
我们需要在组件卸载的时候清除componentWillUnmount
中进行清除。而useEffect
为我们可以这样做,在我们传入的回调函数中,我么们可以指定一个返回值,返回一个回调函数。
这是useEffect
可选清除机制,每个useEffect
都可以返回一个清除函数。
useEffect(() => {
xxx
return () => {
console.log('清除了');
}
});
React
会在组件每次更新和卸载的时候执行清除操作。
而且useEffect
也可以多次调用,我们可以根据代码用途来分开调用,使得代码更加明朗。
useEffect性能优化
某些代码我们只希望在组件第一次挂载完毕后执行一次就行了,之后的更新就无需再执行。但是默认情况下useEffect
在第一次挂载和之后的更新都会执行回调。这多次的执行一定程度上会导致一些性能问题。
所以useEffect
可以传入第二个参数,第二个参数是一个数组,传入该useEffect
的依赖项。如果该useEffect
不依赖任何内容的话可以传入一个空数组。当传入空数组的时候,useEffect
中的回调函数就相当于componentDidMount
,回调函数返回的回调函数就相当于componentWillUnmount
。当然传入的依赖项如果改变了那就会重新执行回调函数和返回的回调函数。
useContext
没啥好说的,另类的获取context
的方式。
const ThemeContext = React.createContext();
function ContextHooksDemo() {
const theme = useContext(ThemeContext);
return <div>主题颜色{theme.color}</div>;
}
当最上层的Provider
更新的时候,该hook
会重新触发渲染并传递新的值。
useReducer
useReducer
仅仅是useState
的一种替代方案。在某些情况下,state
处理比较复杂的逻辑就可以使用这个hook
来进行拆分。
//处理逻辑
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;
}
}
function ReducerHookDemo() {
const [state, dispatch] = React.useReducer(counterReducer, { counter: 111 });
return (
<div>
{state.counter}
<button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
<button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
</div>
);
}
另外这里通过useReducer
创建的数据是不能共享的,所以不能代替Redux
,只是用法有点类似。
useCallback
这个hook
也是为了进行性能的优化。
函数有两个参数,第一个传入一个回调函数,第二个传入数组(依赖项)。这个hook
会返回一个函数的memoized
。
下面展示两个案例:
案 例一:使用useCallback
和不使用useCallback
定义一个函数是否会带来性能的优化。
function CallbackDemo01() {
const [count, setCount] = useState(0);
function increment() {
console.log("执行");
setCount(count + 1);
}
//下面使用usecallback不能带来性能优化,和上面的差不多
const increment2 = React.useCallback(
function () {
console.log("执行");
setCount(count + 1);
},
[count]
);
return (
<div>
<h2>CallbackDemo01</h2>
<h2>{count}</h2>
<button onClick={increment}>+1</button>
<button onClick={increment2}>useCallback优化+1</button>
</div>
);
}
单纯从上面这个案例看不出来什么性能优化,因为这两个函数在组件创建的时候都会重新定义,count
更新也会重新定义,所以结合下面这个案例来进行理解。
案例二:使用useCallback
和不使用useCallback
定义一个函数传递给子组件是否会带来性能的优化。
const Mybutton = React.memo(function (props) {
console.log("MyButton重新渲染:" + props.title);
return <button onClick={props.increment}>+1</button>;
});
function CallbackDemo02() {
console.log("callbackDemo02重新渲染");
const [count, setCount] = useState(0);
const [show, setShow] = useState(false);
const increment1 = () => {
console.log("执行increment1");
setCount(count + 1);
};
//当执行show切换时,父组件会重新渲染,子组件默认会处理。如果子组件使用memo进行包裹,那么子组件会对props进行浅层比较,相同就不需要重新渲染,但是这里两个函数在父组件重新渲染也会重新创建,所以传递给父组件的两次props是不同的,所以也会更新,
//但实际上函数依赖的count并没有发生变动,组件更新是没有必要的,所以使用usecallback就避免了这个问题
const increment2 = React.useCallback(() => {
console.log("执行increment2");
setCount(count + 1);
}, [count]);
return (
<div>
<h2>callbackdemo02{count}</h2>
<Mybutton title="btn1" increment={increment1}></Mybutton>
<Mybutton title="btn2" increment={increment2}></Mybutton>
<button onClick={(e) => setShow(!show)}>切换</button>
</div>
);
}
总之通常使用useCallback
的目的是不希望子组件进行多次渲染。另外要注意子组件一定要使用memo包裹,否则使用useCallback
是无效的。
useMemo
这个hook
也是为了性能优化。
useMemo
返回的也是一个memoized
,在依赖不变的情况下,多次定义返回的都是同一个值。
案例展示:**
**有时候我们定义一个函数,组件内其他的状态改变会引起这个组件的重新渲染,然后内部的函数就会重新在定义或者执行一次,如果函数的依赖状态没有改变,那这次的重新定义和执行是没有必要的。
function calcNumber(count) {
console.log("执行计算");
let total = 0;
for (let i = 1; i <= count; i++) {
total += i;
}
return total;
}
//useMemo 也是返回一个memoized
const MyInfo = React.memo((props) => {
console.log("My重新渲染");
return (
<h2>
名字:{props.info.name}年龄:{props.info.age}
</h2>
);
});
function MemoHookDemo01() {
const [count, setCount] = useState(10);
const [show, setShow] = useState(true);
const info = React.useMemo(() => {
return { name: "zhangsan", age: 18 };
}, []);
//计算1到count的和,当count不变时该函数不会重新执行
const total = React.useMemo(() => {
return calcNumber(count);
}, [count]);
return (
<div>
<h2>memoHook</h2>
<h2>计算的和为{total}</h2>
<button onClick={(e) => setCount(count + 1)}>+1</button>
<MyInfo info={info}></MyInfo>
<button onClick={(e) => setShow(!show)}>切换</button>
</div>
);
}
这个有点类似vue
中的computed
,当然原理完全不一样。
useRef
该hook
返回一个ref
对象,返回的ref
对象在整个生命周期保持不变。
常见有两种用法:
用法一:引入DOM元素,或者组件(需要为class
组件)。
用法二:保存一个数据整个对象在整个生命周期中可以保持不变。
function RefHookDemo01() {
//获取dom
const titleRef = React.useRef();
const inputRef = React.useRef();
function changeDom() {
titleRef.current.innerHTML = "hello refhook";
inputRef.current.focus();
}
//可以保存上一次的state值,因为countRef更新不会引起dom的更新,所以当界面更新完时count和countRef.current值一样,但实际dom中countRef.current展示的为上一次的值
const [count, setCount] = useState(0);
const countRef = React.useRef(count);
useEffect(() => {
countRef.current = count;
console.log(countRef.current);
}, [count]);
return (
<div>
<h2 ref={titleRef}>RefHookDemo01</h2>
<input ref={inputRef} />
<button onClick={(e) => changeDom()}>修改dom</button>
<MyInfo info={{ name: "nihao", age: 20 }}></MyInfo>
<h2>上一次count:{countRef.current}</h2>
<h2>这一次的count:{count}</h2>
<button onClick={(e) => setCount(count + 10)}>+10</button>
</div>
);
}
useImperativeHandle
这个hook
不是那么好理解,这里举一个使用例子:我们通过ref
要获取函数式组件中的dom
元素时,不能直接获取,需要ref
的转发,就是利用到forwardRef
这个API对函数式组件进行包裹,然后子组件拿到父组件的ref
绑定到某一个元素上,这样父组件就有这个元素的完全使用权了。但是我们只希望父组件能够操作这个元素某些属性,比如只能操作focus
这个事件。那么useImperativeHandle
这个hook
就是用来限制传递给父组件操作的。
该hook
有三个参数,第一个为父组件传递过来的ref
,第二个为回调函数,内部返回一个对象,对象中的方法就是暴露给父组件能操作的,第三个为依赖项`
const MyInput = React.forwardRef(function (props, ref) {
//有时候我们并不像向父组件暴露过多的属性
const inputRef = React.useRef();
React.useImperativeHandle(
ref,
() => ({
focus: () => {
inputRef.current.focus();
},
}),
[inputRef]
);
return <input ref={inputRef} />;
});
function ImperativeHookDemo() {
const inputRef = React.useRef();
return (
<div>
<MyInput ref={inputRef} />
<button onClick={(e) => inputRef.current.focus()}>获取焦点</button>
</div>
);
}
useLayoutEffect
这个hook
和useEffect
很相似,只有一点点区别,useEffect
会在组件内容更行到DOM后执行,不会阻塞DOM的更新。而useLayoutEffect
则是再组件内容更新到DOM上之前执行,会阻塞DOM更新。所以视情况来选择hook
。
function LayoutEffectHookDemo() {
const [count, setCount] = useState(10);
//这里使用useEffect可能会出现先从o再变为随机数的过程在网页上可能会闪一下,使用useLayoutEffect就不会
React.useLayoutEffect(() => {
if (count === 0) {
setCount(Math.random());
}
}, [count]);
return (
<div>
<h2>layoutEffect:{count}</h2>
<button onClick={(e) => setCount(0)}>改变</button>
</div>
);
}
以上的案例可以尝试体会下二者的区别。
当然还有一些hook
没有提到,大部分常用的hook
都已经介绍了,剩下的可以查看官网。
自定义hook
本质上自定义hook
只是一种函数代码逻辑的抽取,并不算React
特性,只是为了代码和逻辑的复用。
以下演示几个自定义hook
的案例。注意自定义的hook
函数的命名必须以use
开头。
函数创建和销毁时打印一些东西:
//可以提到一个单独的文件中
function useLoggingLife(name) {
React.useEffect(() => {
console.log(`${name}组件创建出来`);
return () => {
console.log(`${name}销毁了`);
};
}, [name]);
}
function CustomHookDemo01() {
useLoggingLife("CustomHookDemo01");
return (
<div>
<h2>CustomHookDemo01</h2>
<Home></Home>
<About></About>
</div>
);
}
context的共享:
import React from "react";
import { UserContext, TokenContext } from "./App";
export default function useContext() {
const user = React.useContext(UserContext);
const token = React.useContext(TokenContext);
return [user, token];
}
import React from "react";
import useContextHook from "./useContextHook";
export default function CustomContextShareHook() {
const [token, user] = useContextHook();
console.log(token, user);
return (
<div>
<h2>获取context</h2>
</div>
);
}
function App() {
return (
<div className="App">
<UserContext.Provider value={{ name: "zhangsan", age: 19 }}>
<TokenContext.Provider value="abckdfhafh">
<CustomContextShareHook></CustomContextShareHook>
</TokenContext.Provider>
</UserContext.Provider>
</div>
);
}
本地缓存:
import React from "react";
export default function useLocalStore(key) {
//获取
const [name, setName] = React.useState(() => {
const name = JSON.parse(window.localStorage.getItem(key));
return name;
});
//设置
React.useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(name));
}, [name]);
return [name, setName];
}
import React from "react";
import useLocalStore from './useLocalStore'
export default function LocalStore() {
const [name, setName] = useLocalStore('saber')
return (
<div>
<h2>获取设置name:{name}</h2>
<button onClick={(e) => setName("zhangsan")}>设置name</button>
</div>
);
}
//todo useState源码
//todo redux hooks