一、hook基本概念
react组件中分为两种:一种是类组件,一种是函数组件
类组件:
- 通过class类组件进行开发
- 内部有内部state状态
- 拥有生命周期函数
函数组件:
- 通过函数来定义组件
- 内部没有state内部状态
- 没有生命周期
二、hook的由来
随着react版本的更迭,到了react16.8版本的时候,新增了hook特性,让函数组件内部也可以拥有内部状态,以及生命周期。没有this指向。
类组件:
class App extends React.Component {
constructor(props) {
super(props)
}
click = () => {
}
render() {
return <div onClick={this.click}>类组件</div>
}
}
函数组件:
function App(props) {
function click() {
}
return <div onClick={click}>函数组件</div>
}
为什么会有hook函数?
- 从类组件的开发的难易程度来说,类组件要求储备的知识量更大,而函数组件要求更低,如果函数组件拥有了内部状态那么就可以使用函数组件开发任何组件。
- 类组件开发基于面向对象的方式进行开发,类与类之间形成了一种强依赖,继承的方式有时候不是最好的解决方案,从代码拆分的难易程度来说,类组件相比之下不好拆分。
- 语法上来说,函数组件更加的简洁
hook的使用规则
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState
和 useEffect
调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在下面会有更深入的解释。)
只在 React 函数中调用 Hook
**不要在普通的 JavaScript 函数中调用 Hook。**你可以:
- ✅ 在 React 的函数组件中调用 Hook
- ✅ 在自定义 Hook 中调用其他 Hook (我们将会在下一页 中学习这个。)
遵循此规则,确保组件的状态逻辑在代码中清晰可见。
注意:不要在类组件中使用hook函数,要报错。
三、useState函数
函数组件外部数据
import PropTypes from 'prop-types'
function App (props) {
return <div></div>
}
// 设置默认值
App.defaultProps = {
}
// 设置外部数据的数据类型
App.propTypes = {
}
export default App;
useState
函数组件内部定义内部状态的hook函数。
初始化数据
基本数据
1)引入
import React, { useState } from 'react'
2)在函数组件顶成初始化数据
import React, { useState } from 'react'
export default function App() {
let [count, setCount] = useState(0);
}
useState返回的是一个数组,第一个值是一个内部状态数据,第二个值是一个函数,用于修改内部状态数据。
useState参数就是初始化的数据。
3)修改数据
function changeCount() {
setCount(count + 1);
}
引用数据
1)定义数据
let [student, setStudent] = useState({
username: '阿旺',
age: 80
});
2)修改数据
setStudent('蔡徐坤');
直接传递一个字符串,会导致数据全部丢失
setStudent({
username: '蔡徐坤'
});
这个方式用户名还在,但是其他属性丢失,因为这里的对象会覆盖原来整个对象。
正确方式:
将其他属性全部补上,通过解构扩展的方式。
setStudent({
...student,
username: '蔡徐坤'
});
传递函数的方式
setStudent((prevState) => {
return {
...prevState,
username: '蔡徐坤'
}
})
注意:useState更新数据时,没有第二个参数。
四、useReducer函数
语法:
const [state, dispatch] = useReducer(reducer, initialArg);
state
:useReudcer创建的内部数据
dispatch
:相当于redux中的dispatch
,用于派发action通知reducer更新数据
在根目录下的index.js中注释掉
StirctMode
,否则在开发阶段会有bug,导致reducer计算两次。
root.render(
// <React.StrictMode>
<App />
// </React.StrictMode>
);
案例:
import React, { useReducer } from 'react'
const initState = {
count: 0
};
function reducer(state, action) {
switch(action.type) {
case 'add':
state.count += action.payload;
return {
...state
};
case 'reduce':
state.count -= action.payload;
return {
...state
}
default: return state;
}
}
export default function UseReducer() {
let [state, dispatch] = useReducer(reducer, initState);
console.log(state);
function addCount() {
dispatch({
type: 'add',
payload: 10
});
}
function reduceCount() {
dispatch({
type: 'reduce',
payload: 20
});
}
return (
<div>
count: {state.count}
<button onClick={addCount}>增加</button>
<button onClick={reduceCount}>减少</button>
</div>
)
}
五、useEffect函数
useEffect
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect
完成副作用操作。赋值给 useEffect
的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。
useEffect可以模拟组件的生命周期,比如componentDidMount
、componentWillUnmount
、componentDidUpdate
语法:
useEffect(didUpdate[, arr]);
didUpdate
:是一个effect函数,在初始化时会执行,后续在满足条件是还会运行。
arr
: 是一个数组,可以监听值,当值改变后会执行effect函数。
useEffect可以看做是componentDidMount
、componentWillUnmount
、componentDidUpdate
等生命周期的结合体。
模拟componentDidMount
生命周期
在这里可以做componentDidMount
生命周期的事情,比如:调用接口获取数据、改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作
useEffect(() => {
console.log('模拟componentDidMount')
}, []);
注意:第二个参数是一个空数组,在组件初始化时执行一次,后续不再直接。
模拟componentDidUpdate
useEffect(() => {
console.log('模拟componentDidUpdate');
});
注意:第二个参数没有传递,意味着只要state或props数据改变,那么它都会执行
监听属性模拟componentDidUpdate
useEffect(() => {
console.log('监听属性模拟componentDidUpdate');
}, [title]);
第二个参数数组监听了某些值,那么意味着当这个值改变的时候会执行。
模拟卸载componentWillUnmount
在effect函数中返回一个函数,这个函数会在组件被卸载时执行。与第二个参数数组没有任何关系。
在这个生命周期中可以进行资源的清理工作。
useEffect(() => {
return () => {
console.log('模拟卸载componentWillUnmount');
}
}, []);
资源清理:
在模拟componentDidMount
生命周期中设置定时器
useEffect(() => {
console.log('模拟componentDidMount')
timerRef.current = setInterval(() => {
console.log(1);
}, 1000);
}, []);
模拟componentWillUnmount
进行资源清理
useEffect(() => {
return () => {
console.log('模拟卸载componentWillUnmount');
clearInterval(timerRef.current);
}
}, []);
六、useMemo函数
语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
简单理解:相当于vue中的computed
计算属性。不要在这里面去调用接口等副作用操作。
import React, { useMemo, useState } from 'react'
export default function UseMemo() {
let [firstname, setFirstname] = useState('汪');
let [lastname, setLastname] = useState('宇');
let fullname = useMemo(() => {
return firstname + lastname
}, [firstname, lastname]);
return (
<div>
姓名:{fullname}
<button onClick={() => setFirstname('宋')}>修改姓</button>
<button onClick={() => setLastname('丹丹')}>修改名</button>
</div>
)
}
useMemo函数会返回一个值,这个值被缓存过,当监听的值发生改变后会重新计算得到新的结果。
七、useRef函数
useRef
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数
语法:
在函数顶层使用:
let inputRef = useRef(初始值);
得到一个ref对象,可以用于绑定ref。
绑定ref
import React, { useState, useRef } from 'react'
export default function App() {
let inputRef = useRef();
function login() {
console.log(inputRef.current.value);
}
return (
<div>
<input ref={inputRef} placeholder='请输入' />
<button onClick={login}>登录</button>
</div>
)
}
访问DOM元素:
inputRef.current.value
类似于React.createRef()
去访问属性。
在组件中存储数据
1)在组件外部存储数据
当这个组件被多次使用,外部组件外部存贮的数据会被共享。
import React from 'react'
// 这里存储的数据会被组件多次使用时共享。
let count = 0;
export default function Ref() {
function login() {
console.log('打印', count);
}
function addCount() {
console.log('增加一');
count += 1;
}
return (
<div>
<button onClick={addCount}>增加</button>
<button onClick={login}>登录按钮</button>
</div>
)
}
2)在函数组件内部定义变量
在函数组件内部直接定义变量,会导致问题:
当组件内部state数据或外部props数据修改后会导致函数组件从上到下执行代码,所以这里定义的变量被重置了。
import React, { useState } from 'react'
export default function Ref() {
let [title, setTitle] = useState('蜗牛');
let count = 0;
console.log('重置了');
function login() {
console.log('打印', count);
}
function addCount() {
console.log('增加一');
count += 1;
}
function changeTitle() {
setTitle('前端');
}
return (
<div>
<button onClick={addCount}>增加</button>
<button onClick={login}>登录按钮</button>
<p>标题:{title}</p>
<button onClick={changeTitle}>修改title</button>
</div>
)
}
3)useRef定义变量(推荐)
- 修改数据后不会引起组件的更新渲染
- 当修改state数据或props数据后,它的数据任然保持不变
- 渲染多次组件,数据不会被共享,相当于类的实例的属性不会被共享。
import React, { useRef, useState } from 'react'
// let count = 0;
export default function Ref() {
let [title, setTitle] = useState('蜗牛');
let count = useRef(0);
console.log('重置了');
function login() {
console.log('打印', count);
}
function addCount() {
console.log('增加一');
count.current += 1;
}
function changeTitle() {
setTitle('前端');
}
return (
<div>
<button onClick={addCount}>增加</button>
<button onClick={login}>登录按钮</button>
<p>标题:{title}</p>
<button onClick={changeTitle}>修改title</button>
</div>
)
}
八、hook函数组件获取redux数据
在类组件中,我们如何获取redux数据的?
class App extends Component {
render() {
this.props // 就拥有了仓库数据
}
}
function mapStateToProps(state) {
return {
...state
}
}
export default connect(mapStateToProps)(App);
在函数组件中如何获取redux数据?
在函数组件中获取redux数据:
1)通过connect
高阶组件注入数据
import React from 'react'
import { connect } from 'react-redux'
function App() {
return (
<div>
</div>
)
}
function mapStateToProps(state) {
console.log(state);
return {
}
}
export default connect(mapStateToProps)(App);
2)使用useSelector
hook函数获取仓库数据
-
引入
import { useSelector } from 'react-redux
-
使用
let userState = useSelector(state => state.userRd); let userList = useSelector(state => state.userRd.userList);
-
全代码
import React from 'react' import { connect, useSelector } from 'react-redux' export default function App() { let userState = useSelector(state => state.userRd); let userList = useSelector(state => state.userRd.userList); return ( <div> </div> ) }
useDispatch派发action对象
-
引入
import { useDispatch } from 'react-redux'
-
使用
let dispatch = useDispatch();
-
修改数据
function addUser() { let action = initUserList([{id: 1, name: '王宇'}]); dispatch(action); }
-
全代码
import React from 'react' import { connect, useSelector, useDispatch } from 'react-redux' import { initUserList } from './store/modules/user' export default function App(props) { let dispatch = useDispatch(); let userState = useSelector(state => state.userRd); let userList = useSelector(state => state.userRd.userList); function addUser() { let action = initUserList([{id: 1, name: '王宇'}]); dispatch(action); } return ( <div> <ul> { userList.map(item => <li key={item.id}>{item.name}</li>) } <button onClick={addUser}>添加用户</button> </ul> </div> ) }
九、自定义hook函数
在项目开发中,官方提供的hook函数api可能满足不了你的需要,你有时候会根据业务需求对代码进行一定的封装,在函数组件中可以使用自定义hook函数去封装代码,达到代码复用性。
完成监听鼠标移动事件案例:
import React, { useEffect, useState } from 'react'
export default function App() {
let [pageX, setPageX] = useState(0);
let [pageY, setPageY] = useState(0);
useEffect(() => {
window.addEventListener('mousemove', function(e) {
console.log(e.pageX, e.pageY);
setPageX(e.pageX);
setPageY(e.pageY)
});
}, []);
return (
<div>
pageX: {pageX}
<br />
pageY: {pageY}
</div>
)
}
功能完成了,但是另一个页面需要实时获取鼠标位置信息进行计算用于其他用途,那么这段代码没有办法复用。
我们可以考虑使用自动以hook对代码进行封装。
自定hook函数封装
规则:
- 以
use
开头定义hook函数名称
特点:
- 在自定义hook函数中可以使用react提供的hook函数。
使用时和平常的react hook函数是一样的。
usePosition.js
import { useEffect, useState } from "react"
// 自定义hook函数中可以使用react提供的hook函数
export default function usePosition() {
let [pageX, setPageX] = useState(0);
let [pageY, setPageY] = useState(0);
useEffect(() => {
window.addEventListener('mousemove', function(e) {
// console.log(e.pageX, e.pageY);
let pageX = e.pageX;
let pageY = e.pageY;
setPageX(pageX);
setPageY(pageY)
})
}, [])
return {
pageX,
pageY
}
}
封装好后,那么在函数组件中可以直接运行使用,达到了代码复用性。
组件使用App.js
import React, { useEffect, useState } from 'react'
import usePosition from './hooks/usePosition'
import Position from './components/Position';
export default function App() {
let {pageX, pageY} = usePosition();
return (
<div>
pageX: {pageX}
<br />
pageY: {pageY}
<Position />
</div>
)
}
路由相关hook函数
回顾类路由组件中使用history
类组件:
-
路由组件
this.props.history.push('/home');
-
非路由组件
import { withRouter } from 'react-router-dom' class App extends Component { goto = () => { this.props.history.push('/home'); } } export default withRouter(App);
函数组件跳转路由
-
路由组件和以前类组件跳转方式是一样的
import React from 'react' export default function Login(props) { function login() { props.history.push('/home'); } return ( <div> <button onClick={login}>登录</button> </div> ) }
-
非路由函数组件
1)通过
withRoter
高阶组件注入路由相关信息import { withRouter } from 'react-router-dom' function Header(props) { function quit() { props.history.push('/login'); } return ( <div> 头部导航 <button onClick={quit}>退出登录</button> </div> ) } export default withRouter(Header);
2)使用
useHistory
hook函数import { useHistory } from 'react-router-dom' export default function Header(props) { const history = useHistory(); function quit() { history.push('/login'); } return ( <div> 头部导航 <button onClick={quit}>退出登录</button> </div> ) }
函数组件获取路由参数
路由参数传递方式:
- params
- query
- state
- search
在函数组件中,路由参数获取和类组件中一样,但是函数组件额外提供其他hook函数来获取路由参数
useParams
用于获取动态路由参数
-
引入
import { useParams } from 'react-router-dom'
-
使用
const params = useParams();
params就是动态路由参数
useLocation
用于获取query
、state
、search
参数
-
引入
import { useLocation } from 'react-router-dom'
-
使用
let loc = useLocation();
loc变量包含了
query
、state
、search
参数。
十、useImperativeHandle函数
在函数组件中,父组件无法通过ref获取函数组件的实例:
父组件App.jsx
import React, { useRef, useState } from 'react'
import MyUpload from './components/MyUpload'
export default function App() {
let uploadRef = useRef();
function getUpload() {
console.log(uploadRef.current);
}
return (
<div>
<MyUpload ref={uploadRef} />
<button onClick={getUpload}>获取upload</button>
</div>
)
}
子组件MyUpload.jsx
export default function MyUpload() {
function getFilenames() {
return '这是图片地址'
}
function isEmpty() {
return '是空的'
}
return (
<div>上传组件</div>
)
}
出事化时,出现如下警告,不能使用ref绑定函数组件:
正确方式:
1)使用forwardRef
高阶函数包裹函数组件,对ref进行转发
-
引入
import { forwardRef } from 'react';
-
包裹函数组件转发ref
export default forwardRef(MyUpload);
转发ref后,函数组件的第二个参数就是ref
2)使用useImperativeHandle
自定义暴露方法或属性给外部使用
-
引入
import { useImperativeHandle } from 'react';
-
自定义暴露方法或属性
// 将函数组件内部的方法或属性暴露给父组件使用。 useImperativeHandle(ref, () => { return { getFilenames, isEmpty } });
暴露出去后,父组件才能通过ref去获取到这些属性或方法取使用。
4)父组件绑定ref去使用
import React, { useRef, useState } from 'react'
import MyUpload from './components/MyUpload'
export default function App() {
let uploadRef = useRef();
function getUpload() {
console.log(uploadRef.current.getFilenames());
}
return (
<div>
<MyUpload ref={uploadRef} />
<button onClick={getUpload}>获取upload</button>
</div>
)
}
十一、useCallback函数
语法:
let fn = useCallback(() => {
return fisrtname + lastname
}, [fisrtname, lastname]);
useCallback
缓存的是一个函数,而useMemo
缓存的是一个值。
使用:
import React, { useCallback, useState } from 'react'
export default function UseCallback() {
let [fisrtname, setFirstname] = useState('汪');
let [lastname, setLastname] = useState('宇');
let fn = useCallback(() => {
return fisrtname + lastname
}, [fisrtname, lastname]);
return (
<div>
{fn()}
<button onClick={(() => setFirstname('宋'))}>修改姓</button>
<button onClick={(() => setLastname('丹丹'))}>修改名</button>
</div>
)
}
性能优化
问题:
父组件App.jsx
import React, { useState } from 'react'
import UseCallback from './components/UseCallback'
import Child from './components/Child'
export default function App() {
let [count, setCount] = useState(0);
return (
<div>
{/* <UseCallback /> */}
count: {count}
<button onClick={() => setCount(++count)}>修改count</button>
<Child />
</div>
)
}
子组件Child.jsx
import React from 'react'
export default function Child() {
console.log('child函数执行了');
return (
<div>Child</div>
)
}
在函数组件中,父组件的state和props更新后引起组件渲染,导致子函数子组件运行。此时父子组件毫不相关,按道理说子组件没有必要执行函数。
如何做优化?
React.memo高阶组件优化
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
React.memo
接收一个函数组件。
如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
React.memo
仅检查 props 变更。如果函数组件被 React.memo
包裹,且其实现中拥有 useState
,useReducer
或 useContext
的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
简单理解:react.memo可以缓存组件,对组件外部props数据做浅层对比(地址对比),发现数据地址没有变化,那么就认为数据没有变化,所以不会再次执行组件渲染。
父组件App.js
import React, { useState } from 'react'
import UseCallback from './components/UseCallback'
import Child from './components/Child'
export default function App() {
let [count, setCount] = useState(0);
return (
<div>
{/* <UseCallback /> */}
count: {count}
<button onClick={() => setCount(++count)}>修改count</button>
<Child />
</div>
)
}
子组件Child.jsx
import React, { memo } from 'react'
function Child() {
console.log('child函数执行了');
return (
<div>Child</div>
)
}
export default memo(Child);
此时子组件通过React.memo
包裹后,更新父组件的state数据不会引起子组件函数运行,达到了性能优化
理由:react.memo可以缓存组件,对组件外部props数据做浅层对比(地址对比),发现数据地址没有变化,那么就认为数据没有变化,所以不会再次执行组件渲染。
父组件传递一个函数给子组件:
import React, { useCallback, useState } from 'react'
import UseCallback from './components/UseCallback'
import Child from './components/Child'
export default function App() {
let [count, setCount] = useState(0);
function fn() {
console.log('fnfnfnfn');
}
return (
<div>
{/* <UseCallback /> */}
count: {count}
<button onClick={() => setCount(++count)}>修改count</button>
<Child fn={fn} />
</div>
)
}
此时,修改state数据导致Child
子组件又执行了函数,在这个案例中也没有必要执行子组件。
执行子组件函数的理由:函数组件修改state或props数据会导致函数从上到下执行,fn函数被重新生成,所以地址发生了改变,而react.memo只做浅层地址对比,所以执行了Child子组件。
还需要继续优化
useCalblack缓存函数
在父组件中使用useCallback
缓存fn函数。
父组件App.jsx
import React, { useCallback, useState } from 'react'
import UseCallback from './components/UseCallback'
import Child from './components/Child'
export default function App() {
let [count, setCount] = useState(0);
let fn = useCallback(() => {
console.log('fnfnfnfn');
}, []);
return (
<div
count: {count}
<button onClick={() => setCount(++count)}>修改count</button>
<Child fn={fn} />
</div>
)
}
子组件Child.jsx
import React, { memo } from 'react'
function Child() {
console.log('child函数执行了');
return (
<div>Child</div>
)
}
export default memo(Child);
缓存fn函数后,修改state数据,Child子组件没有再次执行,达到了性能优化。
理由:useCallback
缓存了函数,当state或props修改后,fn的地址没有发生改变,所以子组件函数没有运行。