文章目录
1.Hook的优势
Hooks的概念是React 16.8的新增特性,他可以让我们在不编写class的组件的情况下还能使用state以及其他的一些React特性。Vue3的Composition API也采用了Hook的写法来填补Options API 不够灵活的开发方式。
首先我们可以先来回顾一下class组件相对于函数时组件有什么优势:
- 可以定义自己的state,用来保存自己的内部状态,但是函数式组件不可以,函数时组件在每次调用时会产生新的临时变量
- class组件有自己的生命周期,我们可以在生命周期中完成我们想要操作的业务,比如DOM操作或者网络请求等。但是在没有学习hooks之前,函数式组件是没有生命周期这个概念的
- class组件可以在状态改变时只会重新执行render函数以及我们相应的生命周期函数。而函数式组件在重新渲染时,整个函数都会被执行
因此在hooks出现之前,我们开发中常用的其实是类组件,但是class也有不少问题,我们可以列举出来:
- 复杂组件的难以理解
- 难以理解的class中的this指向
- 组件复用很难
而Hook的出现就解决了这些问题。
简单总结一下hooks:
- 它可以是我们在不编写class组件的情况下来进行使用state以及生命周期函数等
- 可以延伸出非常多的用法来让我们前面所提到的问题得以解决
1.1 Hook与类组件进行对比
我们可以通过一个计数器案例来对比一些class组件和结合hooks的函数式组件。
class组件实现:
import React, { PureComponent } from 'react'
export default class ClassCounter extends PureComponent {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
render() {
return (
<div>
<p>{this.state.counter}</p>
<button onClick={() => { this.add() }}>+</button>
<button onClick={() => { this.sub() }}>-</button>
<hr />
</div>
)
}
add() {
this.setState({
counter:this.state.counter+1
})
}
sub() {
this.setState({
counter:this.state.counter-1
})
}
}
函数时组件实现:
import React, { memo, useState } from 'react'
export default memo(function HooksCounter() {
let [counter, setcounter] = useState(10);
return (
<div>
Hook实现计数器功能
<p>{counter}</p>
<button onClick={e => setcounter(counter + 1)}>+</button>
<button onClick={e => setcounter(counter - 1)}>-</button>
<hr />
</div>
)
})
我们会发现上面的代码差异非常大,函数时组件结合hook的代码明显非常简单,并且不用考虑this指向的问题。
我们可以分析一下上面的代码,首先使用usestate函数是一个由react对象导出的hook,一般有一个参数是用来初始化值的,也可以传入一个function。返回值是一个数组,第一个元素是当前状态的值,第二个元素是用来设置当前状态的函数。可以看出我们上面的代码是使用es6解构赋值的语法进行编写的从而取出两个元素。接着在button的点击事件中调用setCounter从而设置新值。
1.2 总结
Hooks实际上就是JavaScript的函数,这个函数可以帮助你钩入React的state以及生命周期等特性。
使用时他们会有两个额外的规定:
- 只能在函数的最外层调用Hook。不要再循环、条件判断或者子函数中调用
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,后面学习)。
2. State Hook
State Hook的api就是usestate。
usestate会帮助我们定义一个state变量,usestate是一种新的方法,它与class里面的this.state提供的功能完全相同,一般来说,在函数退出变量就会消失。
usestate接收一个唯一的参数,在组件第一次被调用的时候用来做初始值。返回一个数组,可以通过解构的方式来进行拿出我们的状态以及设置状态的函数。
当然我们也可以在一个组件中定义多组变量和复杂变量(数组、对象)
这是我们分别操作数字、数组、对象的案例:
import React, { memo, useState } from 'react'
export default memo(function HooksCounter() {
let [counter, setcounter] = useState(()=>10);
const [student, setstudent] = useState(['张三', '李四', '王五'])
const [studentinfo, setstudentinfo] = useState([{ name: '张三', age: 12 }, { name: '李四', age: 5 }, { name: '王五', age: 10 }])
function setbtn() {
setstudent([...student, '123'])
}
function addage(index) {
let newinfoarray=[...studentinfo]
newinfoarray[index].age++
setstudentinfo(newinfoarray)
}
return (
<div>
<p>{counter}</p>
<button onClick={e => setcounter(counter + 1)}>+</button>
<button onClick={e => setcounter(counter - 1)}>-</button>
<hr />
<ul>
{
student.map((index) => {
return (
<li key={index}>{index}</li>
)
})
}
</ul>
<button onClick={e => setbtn()}>加姓名</button>
<hr />
<ul>
{
studentinfo.map((item,index) => {
return (
<li key={item.name}>
<p>{item.name}</p>
<p>{item.age}</p>
<button onClick={e=>addage(index)}>age+1</button>
</li>
)
})
}
</ul>
</div>
)
})
3. Effect Hook
这个hook主要是类似与处理网路请求相关的功能。实际上类似于网络请求、手动更新DOM、事件监听这些都是React更新DOM的一些副作用 ,因此对于完成这些功能的Hooks被称之为Effect Hook。
Effect基本使用:
假设有如下需求:页面的title总是显示counter的数字
import React, { memo, useEffect, useState } from 'react'
export default memo(function UseHooksChange() {
let [counter,setCounnter]=useState(0);
useEffect(() => {
document.title=counter
})
return (
<div>
<div>
<p>{counter}</p>
<button onClick={()=>{setCounnter(counter++)}}>+1</button>
</div>
</div>
)
})
上面就是我们使用useEffect这个api来完成这个需求,但是如果我们使用的是class组件的话,我们就需要同时在componentDidMount
和componentDidUpdate
中同时进行设置才可以完成这个需求。
我们可以解析一下这个函数:
- 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
- useEffect要求我们传入一个
回调函数
,在React执行完更新DOM操作之后,就会回调这个函数
; - 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个
回调函数
;
3.1 effect与componentWillUnmount
在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:
比如我们之前的事件总线或Redux中手动调用subscribe,都需要在componentWillUnmount有对应的取消订阅;
Effect Hook通过什么方式来模拟componentWillUnmount呢?
useEffect传入的回调函数A本身
可以有一个返回值,这个返回值是另外一个回调函数B
:
类型如下:
type EffectCallback = () => (void | (() => void | undefined));
使用方式如下:
useEffect(() => {
document.title = `当前计数: ${count}`;
console.log("每次DOM更新时会回调");
return () => {
console.log("DOM被移除时会回调");
}
})
为什么要在effect中返回一个函数?
这是effect可选的清除机制,每个effect都可以返回一个清除函数;如此可以将添加和移除订阅的逻辑放在一起,他们都属于effect的一部分
React何时清除effect?
- React 会在组件更新和卸载的时候执行清除操作;
- 正如之前学到的,effect 在每次渲染的时候都会执行;
3.2 使用多个effect
使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:
比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;
使用Effect Hook,我们可以将它们分离到不同的useEffect中:
useEffect(() => {
console.log('修改counter相关的dom')
}, [counter])
useEffect(() => {
document.title = counter
console.log('订阅事件');
}, [counter])
useEffect(() => {
document.title = counter
console.log('网络请求');
}, [])
Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:
- React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;
3.3 effect的性能优化
默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
- 某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅);
- 另外,多次执行也会导致一定的性能问题;
为了解决这个问题,其实useEffect实际上有两个参数:
- 参数一:执行的回调函数;
- 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
我们可以看一下一下这个案例:
在这个案例中,我们修改show的值,是不会让useEffect重新被执行的
因为我们规定了第二个参数为count,因此这个函数只有子count改变的时候才会执行,所以我们点击切换按钮并不会引发这个函数的执行。
import React, { useState, useEffect } from 'react';
export default function EffectPerformance() {
const [count, setCount] = useState(0);
const [show, setShow] = useState(true);
useEffect(() => {
console.log("修改DOM");
}, [count])
return (
<div>
<h2>当前计数: {count}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
<button onClick={e => setShow(!show)}>切换</button>
</div>
)
}
如果我们一个函数不像希望依赖任何内容时,也就是说只执行一次的话,那么我们可以为其传递一个空的数组[]:
那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了
useEffect(() => {
console.log("监听事件");
return () => {
console.log("取消监听");
}
}, [])
4. Context Hook
在之前的开发中,我们要在组件中使用共享的Context有两种方式:
- 类组件可以通过
类名.contextType = MyContext
方式,在类中获取context; - 多个Context或者在函数式组件中通过
MyContext.Consumer
方式共享context;
但是多个Context共享时的方式会存在大量的嵌套:
- Context Hook允许我们通过Hook来直接获取某个Context的值;
const value = useContext(MyContext);
在App.js中使用Context:
import React, { createContext } from 'react';
import ContextHook from './04_useContext使用/01_ContextHook';
export const UserContext = createContext();
export const ThemeContext = createContext();
export default function App() {
return (
<div>
<UserContext.Provider value={{name: "why", age: 18}}>
<ThemeContext.Provider value={{color: "red", fontSize: "20px"}}>
<ContextHook/>
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
在对应的函数式组件中使用Context Hook:
import React, { useContext } from 'react'
import { UserContext, ThemeContext } from '../App'
export default function ContextHook() {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
console.log(user);
console.log(theme);
return (
<div>
ContextHook
</div>
)
}
注意事项:当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext
provider 的 context value
值。
5. Reducer Hook
这里的reducer并不是redux中的reducer。
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>
)
}
注意:useReducer只是useState的一种替代品,并不能替代Redux。并不能做到数据共享
7.Usecallback
useCallback实际的目的是为了进行性能的优化。
如何进行性能的优化呢?
- useCallback会返回一个函数的 memoized(记忆的) 值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
实际上在没有父类向子类传递这种操作时,是用不到我们的useCallback的。就像以下的一个栗子:
import React, { memo, useCallback, useState } from 'react'
let AoButtoon=memo((props) => {
console.log('渲染了Ao组件', props.flag);
return (
<div>
<button onClick={props.increment}>我是一个BUTTON组件</button>
</div>
)
})
export default function UseRightCallBack() {
const [counter, setcounter] = useState(0)
const [isshow, setisshow] = useState(true)
let increment1 = () => {
console.log('执行点击事件+1');
setcounter(counter + 1)
}
let increment2 = useCallback(
() => {
console.log('执行callback');
setcounter(counter + 1)
},
[counter],
)
return (
<div>
<p>{counter}</p>
<AoButtoon flag='普通' increment={increment1}></AoButtoon>
<AoButtoon flag='回调的方式' increment={increment2}></AoButtoon>
<button onClick={() => { setisshow(!isshow) }}>隐藏显示button组件</button>
</div>
)
}
在发生点击时,我们会发现接受increment2的子组件不会重新渲染,但是接受increment1的子组件会重新渲染;
usecallback最主要用于性能渲染的地方应该是和memo结合起来,决定子组件是否需要重新渲染;
8.UseMemo
seMemo实际的目的也是为了进行性能的优化。
如何进行性能的优化呢?
- useMemo返回的也是一个 memoized(记忆的) 值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
我们可以来看一个栗子:
- 无论我们点击了是
+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也可以用于子组件的性能优化:也就是说它不仅可以返回一个基本类型的数据,也可以返回一个引用 类型的数据。
9.useRef
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。
最常用的ref是两种用法:
- 用法一:引入DOM(或者组件,但是需要是class组件)元素;
- 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;
**用法一:**操作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>
)
}
用法二:
- useRef可以想象成在ref对象中保存了一个.current的可变盒子;
- useRef在组件重新渲染时,返回的依然是之前的ref对象,但是current是可以修改的;
下面代码是显示我们改变counter前和改变dom后的数值:大概流程就是在每一次生命周期函数执行前也就是渲染时,此时的countRef.current就是我们上一次的值,而count就是我们当前的值,而在每次生命周期初期对current设置为当前值,以此类推,有点类似于链表的解构。
import React, { useState, useEffect, useRef } from 'react';
let preValue = 0;
export default function RefHookDemo02() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
return (
<div>
<h2>前一次的值: {countRef.current}</h2>
<h2>这一次的值: {count}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
</div>
)
}
10.useImperativeHandle
这个hook的作用实际上就是为了解决一个问题,就是,当我们父组件获取子组件的DOM时,防止我们父组件直接对DOM进行操作,而是将操作子组件DOM的一些方法写在了子元素本身,然后将这些操作事件的函数暴露给父组件,从而解决父组件直接操作子组件的问题。实际上也是做了一层优化。
我们先来回顾一下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>
)
}
11.useLayoutEffect
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的更新;
useLayoutEffect(() => {
if (count === 0) {
setCount(Math.random()*200)
}
}, [count]);
useEffect和useLayoutEffect对比
12.自定义hooks
自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。
我们通常将一些复用性较高的hook操作可以按照其特性封装为不同的函数,只是它与普通函数唯一的区别就是我们这里的函数名要use,剩下的和我们普通的函数时没有区别的。
我们这里有一个案例:
获取context:
因此我们可以自定义hook函数:
import { useContext } from "react";
import { Token, UserInfo } from "../App";
function useUserContent() {
let a=useContext(UserInfo)
let b=useContext(Token)
return [a,b]
}
export default useUserContent
然后直接在用到的地方使用即可
import React, { memo, useContext } from 'react'
import useUserContent from '../hook/use-context';
export default memo(function UseSelfHook() {
let [user,token]=useUserContent()
console.log(user,token);
return (
<div>
</div>
)
})
当然这种方式的使用场景非常多,非常灵活,也不是很复杂,因此在实际开发中使用的频率时比较高的。