一、React Hooks介绍
1、hook是react 16.8的新增特性,它可以让开发者在不编写class的情况下使用state以及其他的react特性
2、hook的使用规则
①class组件中不能使用hook
constructor(props) {
super(props)
const [count, setCount] = useState(0)
}
package.json中关闭react-hooks/rules-of-hooks提示
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"react-hooks/rules-of-hooks": "off"
}
},
页面中会报语法错误
②只能在函数最外层调用,不能在循环、条件判断、子函数中调用
if (count < 3) {
useEffect(() => {
console.log({ count })
})
}
这样写会破坏hook调用的顺序:当count大于等于3时,此时的useEffect的调用顺序被改变,react会报错。create-react-app创建的项目中,内置了react-hooks/rules-of-hooks进行hook语法校验,默认为开启状态
react怎么知道哪个state对应哪个useState?----react靠的是hook调用的顺序。所以我们在使用hook时,一定要确保hook的调用顺序在每次渲染中都是相同的
③只能在react的函数式组件或者自定义hook函数中调用hook,不能在其他JavaScript函数中调用
函数式组件和普通函数的区别:函数式组件会return一个dom。
1、使用create-react-app创建项目
npx create-react-app react_hooks // node版本14及以上,我的是14.16.0
要注意,最新版的react已经到18.2.0了,需要手动改为17版本
"react": "^17.0.1",
"react-dom": "^17.0.1",
同时入口文件(index.js)中格式相应的调整
import React from 'react';
// import ReactDOM from 'react-dom/client';
import ReactDOM from 'react-dom'
import App from './App';
// import reportWebVitals from './reportWebVitals';
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>
// );
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
// reportWebVitals();
2、类式组件和函数式组件(hooks)的编写形式对比
类式组件【Count】:
import React, { Component } from 'react'
export default class Count extends Component {
// constructor(props) {
// super(props) // 如果写constructor,必须写super;如果希望构造函数中通过this访问props,就将props传入
// this.state = { count: 0 }
// }
state = { count: 0 } // 类式组件中构造函数能不写就不写
render() {
return (
<>
<p>点击了{this.state.count}次</p>
<button onClick={this.add.bind(this)}>点击</button>
</>
)
}
add() {
this.setState({ count: this.state.count + 1 })
}
}
函数式组件【Count1】:
import React, { useState } from 'react'
const Count1 = () => {
const [count, setCount] = useState(0)
const add = () => {
setCount(count + 1)
// setCount((count) => count + 1)
}
return (
<>
<p>点击了{count}次</p>
<button onClick={add.bind(this)}>点击</button>
</>
)
}
export default Count1
二、useState
1、useState的介绍
useState是react自带的一个hook函数,它的作用是用来声明状态变量
(1)声明方式
const [count, setCount] = useState(0)
这句代码使用了es6的数组解构,等价于
const _useState = useState(0)
const count = _useState[0]
const setCount = _useState[1]
- useState函数接收的参数是状态的初始值,它返回一个数组,数组的第0位是当前的状态值,第1位是可以改变状态值的方法函数
- 惰性state:useState中的参数只会在组件初始化时起作用,后续渲染时会被忽略
- 复杂初始state可以通过传入函数,在函数中计算并返回初始值
const [num, setNum] = useState(() => (props.num > 100 ? props.num : 0))
(2)如何读取状态中的值
<p>点击了{count}次</p>
count就是js中的一个变量,想要在jsx中使用,值用 {} 包裹就可以
(3)如何改变state的值
setCount(count + 1)
setCount(() => count + 1)
如果定义的初始数据是引用类型,使用扩展运算符
const [person, setPerson] = useState({name: '小明', age: 18})
setPerson(() => ({ ...person, name: '大明' }))
- 直接调用setCount函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给react,它会重新渲染组件
- react自动帮助我们记忆了组件的上一次状态值,虽然这种记忆也会有麻烦,但是遵循它这种规则,就可以愉快的编码
2、useState()比this.setState()效率高
- 在类式组件中,只要执行了
this.setState(),即使不改变数据,也会触发render,导致效率低。需要通过shouldComponentUpdate钩子或使用PureComponent替代Component进行优化 - 在函数式组件中,如果调用
useState的更新函数时没有改变数据,那么react将跳过子组件的渲染及effect的执行,底层使用Object.is比较算法来比较state
三、useEffect
1、useEffect代替常用生命周期函数componentDidMount和componentDidUpdate
(1)类式组件为计时器添加生命周期函数,componentDidMount和componentDidUpdate分别在初次渲染和计数器发生改变后打印count
import React, { Component } from 'react'
export default class Count extends Component {
state = { count: 0 }
componentDidMount() {
console.log('componentDidMount', this.state.count)
}
componentDidUpdate() {
console.log('componentDidUpdate', this.state.count)
}
render() {
return (
<>
<p>点击了{this.state.count}次</p>
<button onClick={this.add.bind(this)}>点击</button>
</>
)
}
add() {
this.setState({ count: this.state.count + 1 })
}
}
(2)用useEffect代替这两个生命周期函数
import React, { useState, useEffect } from 'react'
const Count1 = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('小明')
useEffect(() => {
console.log('useEffect', count) // 任意一个state发生变化,都会触发这里
})
return (
<>
<p>姓名:{name}</p>
<p>点击了{count}次</p>
<button onClick={() => setCount(count + 1)}>点击</button>
<button onClick={() => setName('xx')}>改变名字</button>
</>
)
}
export default Count1
注意点:
- react初次渲染(DOM更新后)和之后的每次渲染都会调用useEffect函数,之前需要用componentDidMount和componentDidUpdate两个钩子来完成
- useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而componentDidMount和componentDidUpdate中的代码都是同步执行的
2、useEffect代替componentWillUnmount生命周期函数
通常,组件卸载时需要清除effect创建的定时器id或订阅等资源,在类式组件中通过componentWillUnmount钩子(该钩子可以理解成解绑副作用)完成,在函数式组件中通过useEffect函数中返回一个函数,在返回的函数(该函数的作用类似于componentWillUnmount)中进行清除资源
写个路由切换的demo体会下:
(1)安装react-router-dom
npm i react-router-dom@5.2.0
(2)src下创建Home组件
const Home = () => {
return <h1>Home</h1>
}
export default Home
(3)Count1组件改一下
import React, { useState, useEffect } from 'react'
const Count1 = () => {
const [count, setCount] = useState(0)
const [date, setDate] = useState(`今天是:${new Date()}`)
useEffect(() => {
console.log('useEffect', count)
}, [])
useEffect(() => {
const timer = setInterval(() => {
setDate(`今天是:${new Date()}`)
}, 1000)
return () => {
console.log('销毁前', timer)
clearInterval(timer) // 这里相当于componentWillUnmount钩子,可以做一些清除资源的操作,清除定时器和取消订阅
}
}, [])
return (
<>
<p>日期:{date}</p>
<p>点击了{count}次</p>
<button onClick={() => setCount(count + 1)}>点击</button>
</>
)
}
export default Count1
(4)App.js
import { NavLink, Route, Switch, Redirect } from 'react-router-dom'
import Home from './Home'
import Count1 from './Count1'
const App = () => {
return (
<>
<NavLink activeClassName="highlight" to="/home">
Home
</NavLink>
<NavLink activeClassName="highlight" to="/count1">
Count1
</NavLink>
<Switch>
<Route path="/home" component={Home} />
<Route path="/count1" component={Count1} />
<Redirect to="/home" /> {/* Redirect要写在后面 */}
</Switch>
</>
)
}
export default App
3、useEffect的第二个参数
useEffect第二个参数不传,则表示监听所有的state,也就是说name的值改变了也会触发count的打印,此时useEffect相当于componentDidMount和componentDidUpdate两个钩子。这种情况几乎不用- 如果只需要监听count,只需要传入第二个参数
[count],此时useEffect相当于componentDidUpdate - 如果传入
[],此时useEffect相当于componentDidMount。若此时useEffect中有return一个函数,那么这个函数相当于componentWillUnmount
4、useEffect第一个参数前不能加async
这种async/await的方式在开发中可以说很常见了
useEffect(async () => {
const res = await listApi()
}, [])
但useEffect中不允许给第一个参数加async,async会导致函数的返回值是一个promise,这样写会报错
react为什么这么设计呢?
- useEffect的返回值是在组件卸载前调用的(类似componentWillUnmount)
- useEffect可能有个隐藏逻辑:第二次触发useEffect的回调前,前一次触发的行为都已经执行完成,返回的清理函数也执行完成,这样设计逻辑才清楚。如果回调函数是异步的,情况会变得复杂起来,很容易出现bug
推荐的写法:
const getList = async () => {
const res = await listApi()
}
useEffect(() => {
getList()
}, [])
四、useContext
1、context
context的作用是让它所包含的组件树可以共享数据,实现【祖组件】和【后代组件】之间的通信,类似vue中的provide/injectcontext,和react-redux中的Provider组件很像,【祖组件】和【后代组件】之间的关系是【提供】和【消费】的关系
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
)
2、如何使用context
①Themes/context.js
import { createContext } from 'react'
export const themes = {
primary: { color: '#fff', backgroundColor: 'skyblue' },
danger: { color: '#fff', backgroundColor: 'red' }
}
export const themesContext = createContext(themes)
②Themes/index.js
import { useState, useContext } from 'react'
import { themes, themesContext } from './context'
const Parent = () => {
const [type, setType] = useState('primary')
return (
<>
<button onClick={() => setType(type === 'primary' ? 'danger' : 'primary')}>
设置为 {type === 'primary' ? 'danger' : 'primary'} 按钮
</button>
<hr />
{/* 1、被themesContext.Provider包裹的组件,通过value属性传递值,该组件和其后代组件都具有context */}
<themesContext.Provider value={themes[type]}>
<Child1 />
</themesContext.Provider>
</>
)
}
const Child1 = () => {
// 2、通过 useContext hook 可以接收context中的值
const value = useContext(themesContext)
return (
<>
<button style={value}>按钮</button>
<GrandSon />
</>
)
}
const GrandSon = () => {
return (
<>
{/* 3、通过themesContext.Consumer组件也可以接收context中的值 */}
<themesContext.Consumer>
{(value) => <button style={value}>按钮</button>}
</themesContext.Consumer>
</>
)
}
export default Parent
- 定义:被
CountContext.Provider包裹的组件,通过value属性传递值,该组件和其后代组件都具有context - 接收:通过
useContexthook 可以接收context中的值;如果不用hooks通过CountContext.Consumer组件也可以接收context中的值
3、类式组件中如何接收context
class Child2 extends Component {
static contextType = themesContext
render() {
return (
<>
{/* 类式组件第一种方式接收context(需要声明static contextType = themesContext): */}
<button style={this.context}>按钮</button>
{/* 类式组件第二种方式接收context */}
<themesContext.Consumer>
{(value) => <button style={value}>按钮</button>}
</themesContext.Consumer>
</>
)
}
}
五、useReducer
1、reducer是什么
reducer是一个纯函数,该函数接收两个参数,一个是状态,一个是用来控制业务逻辑的判断参数
纯函数的特点:
- 不得改写参数数据
- 不会产生任何副作用,例如:网络请求、输入和输出设备
- 不能调用Date.now()或Math.rendom()等不纯的方法
const initState = { count: 0 }
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return { count: ++state.count }
case 'sub':
return { count: --state.count }
case 'reset':
default:
return initState
}
}
2、useReducer的使用
import { useReducer } from 'react'
const ReducerDemo = () => {
const initState = { count: 0 }
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return { count: ++state.count }
case 'sub':
return { count: --state.count }
case 'reset':
default:
return initState
}
}
const [state, dispatch] = useReducer(reducer, initState)
return (
<>
<h2>当前值:{state.count}</h2>
<button onClick={() => dispatch({ type: 'add' })}>+</button>
<button onClick={() => dispatch({ type: 'sub' })}>+</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</>
)
}
export default ReducerDemo
六、useMemo
useMemo主要用来解决使用react hooks产生的无用渲染的性能问题。函数式组件失去了shouldComponentUpdate钩子,也就是说没有办法在组件更新前通过条件判断决定组件是否更新。而且在函数式组件中,没有mount和update的区别,这意味着函数式组件的每一次调用都会执行内部的所有逻辑,带来了非常大的性能损耗。useMemo和useCallback都是解决上述问题的。
1、回顾类式组件中如何解决render无效调用
import React, { useState, Component } from 'react'
const Test = () => {
const [count, setCount] = useState(0)
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child />
</>
)
}
class Child extends Component {
render() {
console.log('Child render执行')
return <div>Child组件</div>
}
}
export default Test
这段代码会有个问题:类组件继承于Component组件,当父组件(Test)中的state(count)改变时,子组件(Child)会跟着重新render,这种render是无效的
如何优化: (1)第一种方案:使用shouldComponentUpdate钩子
// 重写shouldComponentUpdate钩子,比较新旧state或props,有变化返回true,没有返回false。这样写的好处:this.setState({})将不会触发render
shouldComponentUpdate(nextProps, nextState) {
// 该钩子是在render前调用的,但是可以拿到render后的props和state,所以参数名叫nextProps、nextState
return this.state.count !== nextState.count
}
缺点:当state和props是多个时,需要写的判断太多了
(2)第二种方案:使用PureComponent钩子替代Component(推荐),PureComponent内部重写了shouldComponentUpdate钩子,只有当state或props发生变化时才调用render函数
class Child extends PureComponent {
render() {
console.log('Child render执行')
return <div>Child组件</div>
}
}
优化后效果:
2、函数式组件中如何优化state更新导致的子组件无效渲染
函数式组件中也会有Component的那个问题,父组件中的某个state更新,会导致子组件重新渲染
import React, { useState, memo } from 'react'
const Test = () => {
const [count, setCount] = useState(0)
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child />
</>
)
}
const Child = () => {
console.log('Child render执行')
return <div>Child组件</div>
}
export default Test
可以使用memo函数将子组件包裹起来,memo和PureComponent的作用是一样的
const Child = memo(() => {
console.log('Child render执行')
return <div>Child组件</div>
})
1、性能问题展示demo:点击按钮a改变a的时间戳,点击按钮b改变b的时间戳
index.js
import { useState } from 'react'
import Child from './Child'
const Memo = () => {
const [a, setA] = useState('0')
const [b, setB] = useState('0')
return (
<>
<button onClick={() => setA(Date.now())}>改变a</button>
<button onClick={() => setB(Date.now())}>改变b</button>
<Child a={a} b={b} />
</>
)
}
export default Memo
Child.js
const Child = ({ a, b }) => {
const changeA = (a) => {
console.log('a被改变')
return `a的时间戳:${a}`
}
const changeB = (b) => {
console.log('b被改变')
return `b的时间戳:${b}`
}
const currentA = changeA(a)
const currentB = changeB(b)
return (
<>
<p>{currentA}</p>
<p>{currentB}</p>
</>
)
}
export default Child
当我在改变a的时间戳的时候,请问我改变b了吗?但是这里的changeB就会被调用,这种调用毫无意义
2、使用useMemo优化
Child.js
import { useMemo } from 'react'
const Child = ({ a, b }) => {
const changeA = (a) => {
console.log('a被改变')
return `a的时间戳:${a}`
}
const changeB = (b) => {
console.log('b被改变')
return `b的时间戳:${b}`
}
// const currentA = changeA(a)
// const currentB = changeB(b)
const currentA = useMemo(() => changeA(a), [a])
const currentB = useMemo(() => changeB(b), [b])
return (
<>
<p>{currentA}</p>
<p>{currentB}</p>
</>
)
}
export default Child
此时,点击按钮a只会触发changeA,点击按钮b只会触发changeB
3、useMemo实现computed的效果
import { useState, useMemo } from 'react'
const Memo = () => {
const [a, setA] = useState(0)
const [b, setB] = useState(0)
const [d, setD] = useState(0)
const add = (type) => {
switch (type) {
case 'a':
setA(a + 1)
break
case 'b':
setB(b + 1)
break
case 'd':
setD(d + 1)
break
default:
return false
}
}
const c = useMemo(() => {
console.log('useMemo => a或b发生变化了触发')
return a + b
}, [a, b])
return (
<>
<p>a:{a}</p>
<p>b:{b}</p>
<p>c:{c}</p>
<p>d:{d}</p>
<button onClick={() => add('a')}>+a</button>
<button onClick={() => add('b')}>+b</button>
<button onClick={() => add('d')}>+d</button>
</>
)
}
export default Memo
此时,a和b发生变化会触发useMemo中的函数,c的值为a+b;d的变化不会触发useMemo中的函数
另外,useMemo中的函数可以返回DOM:
const c = useMemo(() => {
console.log('useMemo => a或b发生变化了触发')
return <h1>{a + b}</h1>
}, [a, b])
4、useMemo和useCallback的区别
- useMemo传入的函数需要有返回值,useMemo的返回值是一个具体的值,useCallback的返回值是一个函数
- useMemo只能声明在函数式组件内部,而React.memo是将子组件包裹起来
- 使用useCallback的场景:事件的回调、处理一些逻辑的函数。使用useMemo的场景:根据已有状态计算额外的数据,计算的过程中很消耗性能
- useCallback(fn, deps)相当于useMemo(() => fn, deps)
import React, { useState, memo, useCallback, useMemo } from 'react'
const Test = () => {
const [range, setRange] = useState({ max: 10 })
const [count, setCount] = useState(0)
const render = useMemo(() => {
console.log('调用我了')
const list = []
for (let i = 0; i < range.max; i++) {
list.push(<li key={i}>{i}</li>)
}
return list
}, [range])
// const render = useCallback(() => {
// console.log('调用我了')
// const list = []
// for (let i = 0; i < range.max; i++) {
// list.push(<li key={i}>{i}</li>)
// }
// return list
// }, [range])
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setRange({ max: range.max + 1 })}>
change range
</button>
<List render={render}></List>
</>
)
}
const List = memo((props) => {
console.log('子组件被调用')
return <ul>{props.render}</ul>
return <ul>{props.render()}</ul>
})
export default Test
七、useCallback
1、基本写法
const memoizedCallback = useCallback(() => {
doSomething(a, b)
}, [a, b])
2、为什么要使用useCallback
- 在函数式组件中,定义在组件内的函数会随着状态值的更新而重新渲染,函数 中定义的函数会被频繁定义。
- 父子组件通信时,会在父组件中定义修改状态的方法传入到子组件中,子组件中接收到父组件的函数被频繁更新,会造成父组件状态变更时子组件重新渲染。
- 使用useCallback结合memo可以有效的减少子组件更新频率,提高效率
- 父组件传递到子组件中的函数,在父组件中定义该函数时要通过useCallback缓存起来
未使用useCallback和使用useCallback的区别:
①count值的改变会造成aa函数的重新定义,CallBack中的自组件也会被重新渲染
import React, { useState } from 'react'
const CallBack = () => {
const aa = () => {
console.log('aa')
}
aa()
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<Child />
</>
)
}
const Child = () => {
console.log('子组件渲染')
return <>Child</>
}
export default CallBack
②使用memo将子组件包裹,子组件的props发生变化时才会重新渲染
const Child = React.memo(() => {
console.log('子组件渲染')
return <>Child</>
})
③当Child组件传入props时,由于aa函数被重新定义,props会更新,还是会造成子组件的重新渲染
<Child aa={aa} />
const Child = React.memo((props) => {
console.log('子组件渲染', props)
return <>Child</>
})
④使用useCallback和memo结合
import React, { useState, useCallback } from 'react'
const CallBack = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('小明')
// 当count发生变化时,aa函数才会重新定义;name的改变不会引发aa的重新定义,也就不会造成Child组件的重新渲染
const aa = useCallback(() => {
console.log('aa')
}, [count])
aa()
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<p>{name}</p>
<button onClick={() => setName(name === '小明' ? '大明' : '小明')}>
changeName
</button>
<Child aa={aa} />
</>
)
}
const Child = React.memo((props) => {
console.log('子组件渲染', props)
return <>Child</>
})
export default CallBack
⑤useCallback不依赖任何的state,又有memo的加持,此时count和name的变化都不会造成Child重新渲染
// aa函数只会在CallBack组件初始化时定义,当count和name值发生变化时不会被重新定义
const aa = useCallback(() => {
console.log('aa')
}, []) // 这里没有依赖的值,useCallback返回的是“记忆函数”,也就是上一次定义的函数,那么aa就没有发生变化,那么Child的props就没有发生变化,Child组件被memo缓存,就不会重新渲染
aa()
八、useRef
1、获取DOM,操作DOM
import { useState, useRef } from 'react'
const Ref = () => {
const [value, setValue] = useState('初始值')
const inputRef = useRef(null)
return (
<>
{/* 1、useState和value/change结合进行赋值/取值 */}
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type="text"
/>
<button onClick={() => setValue('给你个大逼斗')}>点击赋值</button>
<button onClick={() => console.log(value)}>点击取值</button>
<hr />
{/* 2、使用useRef进行赋值/取值 */}
<input ref={inputRef} defaultValue="100" type="text" />
<button onClick={() => (inputRef.current.value = 'hello hooks')}>
点击赋值
</button>
<button onClick={() => console.log(inputRef.current.value)}>
点击取值
</button>
</>
)
}
export default Ref
2、forwardRef
用于向组件中传递ref
const FInput = React.forwardRef((props, ref) => {
return <input type="text" {...props} ref={ref} />
})
const Home = () => {
const inputRef = useRef()
useEffect(() => {
inputRef.current.focus()
inputRef.current.value = 'hello'
}, [])
return (
<>
<FInput placeholder="请输入" ref={inputRef} />
</>
)
}
九、自定义hook函数获取窗口大小
import { useState, useEffect, useCallback } from 'react'
// 自定义hook,需要以use开头
const useWinSize = () => {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
const onResize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}, [])
useEffect(() => {
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
}, [])
return size
}
const Size = () => {
const size = useWinSize()
return <>窗口的大小:{`${size.width} * ${size.height}`}</>
}
export default Size