问题导向
react - hook的使用?以及应用场景?
如果你都有了答案,可以忽略本文章,或去react学习地图寻找更多答案
Hook
Hook是16.8一个新增项,本质是高阶函数,只在函数组件中使用
作用:在不编写class组件的情况下使用state以及生命周期等特性。
类组件的不足:类组件的复用机制:高阶组件和渲染属性render props
1.状态逻辑复用难
缺少复用机制
渲染属性和高阶组件导致层级冗余
2.趋向复杂,难以维护
生命周期函数混杂不相干逻辑
相关逻辑分散在不同生命周期
3.this指向困扰
内联函数过渡创建新句柄
类成员函数不能保证this
Hook的特点:
使你在无需修改组件结构的情况下复用状态逻辑
可将组件中相互关联的部分拆分成更小的函数,复杂组件将变得更容易理解
更简洁,更易理解的代码
Hook优势:
1.函数组件无this问题
2.自定义hook方便复用状态逻辑
3.副作用的关注点分离
Hook钩子
useState状态钩子,替代class的state
useEffect副作用钩子,替代class的componentDidMount,componentDidUpdate,componentWillUnmount
useReducer数据钩子,
useContext上下文钩子,
useState状态钩子
替换state
useState()
参数:
参数一:值:初始化参数,number,string,object,boolean
参数二:函数:延迟初始化,只会运行一次
默认值的用法:只在第一次渲染时生效,再次渲染不会改变内部的值
1.直接赋值:支持传入数值,字符串,数组,对象
setCount(1)
2.传入回调函数,c是最新的count
setCount((c) => c + 1)
值类型
const [count, setCount] = useState(0)
setCount(count + 1)
setCount((count) => count + 1)
使用函数:延迟初始化 || 默认值
const [ count, setCount ] = useState(() => {
return props.count || 0
})
function App(props){
//初始值依赖于组件传来的值
//如果使用这种写法,App每次渲染时都会执行该语句,会浪费资源
const defaultCount = props.defaultCount || 0
const [count,setCount] = useState(defaultCount)
//优化写法:传入函数,只执行一次
const [count,setCount] = useState(() => props.defaultCount || 0)
}
例子:useState实现动画
const Candidate = memo((props) => {
const { tickets } = props
动画效果依赖,使用-1是用来关闭抽屉,默认是全部关闭
const [expandedIndex, setExpandedIndex] = useState(-1)
//再次点击时,传入的index和expandedIndex是相同的,就设置为 -1,关闭抽屉
const onToggle = useCallback(
(index) => {
setExpandedIndex(index === expandedIndex ? -1 : index)
},
[expandedIndex]
)
return (
<div className="candidate">
<ul>
{tickets.map((ticket, index) => {
return (
<Seat
//精妙,下级组件没有index,自己传过去
index={index}
onToggle={onToggle}
//序号相等才是true,才做展开
expanded={expandedIndex === index}
{...ticket}
key={ticket.type}
/>
)
})}
</ul>
</div>
)
})
//点击时,设置高度,使用expanded控制浮层是否打开
channels.length * 55 是动态设置高度,有多个子组件都适配
style={{ height: expanded ? channels.length * 55 + 'px' : 0 }}
useEffect副作用钩子
副作用:渲染视图之外的东西,如:绑定事件,网络请求,访问DOM
//副作用时机:
Mount之后
Update之后
Unmount之前
//替代生命周期函数:生命周期函数和useEffect都是处理副作用的(渲染和重渲染)
与componentDidMount,componentDidUpdate,componentWillUnmount有相同之处,
只不过被合并成了API,可存在多个useEffect钩子
//渲染机制:
1.没有第二个参数(数组):组件初始化和render(重渲染)后都会执行1次, = render
2.第二个参数是空数组:组件初始化执行1次,只执行1次, = componentDidMount
3.第二个参数是空数组中有值,值变化就会执行(依赖渲染) = componentDidUpdate
4. return的函数,组件卸载时执行 =componentWillUnmount
useEffect(() => {
console.log('空数组只渲染一次')
return () => {
console.log('回调函数用于解绑事件')
}
},[count])
useEffect(() => {
const interval = serInertval(() => {
setCount(c => c + 1)
},1000)
return () => clearInterval(interval)
},[])
//页面重渲染就执行
useEffect(() => {
document.title = `您点击了${count}次`
})
//count改变就执行
useEffect(() => {
document.title = `您点击了${count}次`
}, [count])
//优化关注点分离:count和resize分开写
useEffect(() => {
绑定事件
window.addEventListener('resize', onResize, false)
资源回收,使用回调,解绑事件,在视图销毁之前触发(重渲染和卸载)
return () => {
window.removeEventListener('resize', onResize, false)
}
}, []) 只要数组里的值不变,就不会执行,节省渲染性能,第一次会执行
//操作DOM,如果需要频繁操作,就不需要加[],始终追踪DOM元素的变化,切换DOM也能获取最新的数据
useEffect(() => {
document.querySelector('#size').addEventListener('click', onClick, false)
return () => {
document
.querySelector('#size')
.removeEventListener('click', onClick, false)
}
})
useLayoutEffect
useLayoutEffect会比useEffect更快执行
特点:useLayoutEffect会在渲染页面之前执行,完成才渲染页面,如果执行程序过大就会影响体验
import React,{useState,useEffect,useLayoutEffect,useReducer} from 'react'
function MyCountFunc() {
const [count,dispatchCount] = useReducer(countReducer,0)
const [name,setName] = useState('litao')
useEffect(() => {
console.log('组件更新之后,执行2')
return console.log('组件更新之前,执行1')
},[name])
useLayoutEffect(() => {
console.log('组件更新之后,执行2')
return console.log('组件更新之前,执行1')
},[name])
return (
<div>
<input value={name} onChange={e => setName(e.target.value)}>
<button onClick={() => dispatchCount({ tyep : 'add'})}>{count}</button>
</div>
)
}
export default MyCountFunc
useRef
useRef可以接受一个默认值,并返回一个含有current属性的可变对象;
使用场景:
1、获取子组件的实例(子组件需为react类继承组件);
2、获取组件中某个DOM元素;
3、用做组件的全局变量,useRef返回对象中含有一个current属性,
该属性可以在整个组件色生命周期内不变,不会因为重复render而重复申明,类似于react类继承组件的类属性成员一样
用法一:获取DOM节点
通过给节点的ref属性赋值,建立连接,获取节点
import React,{useRef,useState,useEffect} from 'react'
function CountFn(){
const inputRef = useRef()
const [value,setValue] = useState('hello world')
useEffect(() => {
console.log(inputRef) // input节点
console.log(inputRef.current.value) //'hello world'
},[])
return (
获取dom元素
<input value={value} ref={inputRef} onChange={e => setValue(e.target.value)} />
)
}
用法二:获取类组件实例
import React,{useState,useEffect,useRef} from 'React'
class Counter extends PureComponet{
speak(){
console.log('speak')
}
const {onClick, count} = this.props
render(){
return (
<h1 onClick={onClick}>{count}</h1>
)
}
}
function App(props){
const counterRef = useRef()
const onClick = useCallback(() => {
setCount((count) => count + 1)
//current就是counter组件,调用函数成员
counterRef.current.speak()
},[count])
return (
<div>
<button>
click({count})
</button>
<Counter ref={counterRef} count={count} onClick={onClick}
</div>
)
}
用法三:同步不同渲染周期之间需要共享的数据(跨越渲染周期保存数据)
import React,{useState,useEffect,useRef} from 'React'
class Counter extends PureComponet{
speak(){
console.log('speak')
}
const {onClick, count} = this.props
render(){
return (
<h1 onClick={onClick}>{count}</h1>
)
}
}
function App(props){
const [count, setCount] = useState(0)
const counterRef = useRef()
const timer = useRef()
const onClick = useCallback(() => {
setCount((count) => count + 1)
counterRef.current.speak()
},[count])
//场景:自动+1,等于10的时候,就不再加了
useEffect(() => {
timer.current = setInterval(() => {
setCount((count) => count + 1)
})
},[])
useEffect(() => {
if(count >= 10) clearInterval(timer.current)
},[count])
return (
<div>
<button>
click({count})
</button>
<Counter ref={counterRef} count={count} onClick={onClick}
</div>
)
}
useReducer数据钩子
参数一:reducer
参数二:初始化状态
import React,{useState,useEffect,useReducer} from 'react'
function countReducer(state, action){
switch(action.type){
case: 'add':
return state + 1
case: 'minus':
return state - 1
default:
return state
}
}
function countComponet(props){
const [count,diapatchCount] = useReducer(countReducer, 0)
useEffect(() => {
const timer = setInterval(() => {
diapatchCount({type:'add'})
},1000)
return () => clearInterval(timer)
})
return <span>{count}</span>
}
useContext上下文钩子
context拥有两个成员,一个是provider提供者,一个是Consumer消费者
跨层级通信时使用,类组件任需要使用Consumer和contextType
import React,{ useContext } from 'react'
第一步:React.createContext()创建上下文,使用定义的变量与useContext接收的变量对应
const context = React.createContext()
第二步:使用provider提供数据给子组件
const { provider } = context
function Father(){
return (
<provider value = {{name:'dashen'}}>
<Child />
</provider >
)
}
第三步:使用useContext接收定义好的context,就可以获取到使用provider传递的值
function Child(props){
const ctx = useContext(context)
return <div>Child:{ctx.foo}</div>
}
memo性能优化
作用:对内部对象进行浅比较,判断是否重新渲染组件
memo = PuerComponent = shouldComponentUpdate
const App = memo(() => {
return xxx
})
useMemo和useCallback性能优化
作用:监听 [] 中的值,判断一个函数是否重复执行
语法:useMemo(() => {}, [n])
参数:
第一个参数:回调函数
第二个参数:要监听的值 或 [],[]是只运行一次,不填[]是每次都运行。
import React,{useState,useMemo} from 'react'
function Fot(props){
return <div>{props.count}</div>
}
function App(props) {
const [count, setCount] = useState(0)
//[]的参数用法1:普通值
count变动,就使用回调函数,参与渲染
const double = useMemo(() => { count * 2 }, [count])
//[]的参数用法2:布尔值
count === 3的时候为真,才执行,第三次等于6,再点击一次是第4次,结果是8,判断条件变为false,继续点击也不再触发
const double = useMemo(() => { count * 2 }, [count === 3])
//[]的参数用法3:依赖另一个useMemo
const half = useMemo(() => { double / 4 }, [double])
return (
<div>
<button onClick={() => {setCount(count + 1)}}>
click:({count}),double:({double}), half: ({half})
</button>
<Fot count={count}/>
</div>
)
}
问题:给子组件传递函数,在父组件更新时,子组件不应该渲染,但是渲染了
解决:使用参数一的回调函数,或者useCallback
import React, { useState, memo, useMemo, useCallback } from 'react'
import Test from './component/hook/Test'
function App() {
const [count, setCount] = useState(0)
const [num, setNum] = useState(0)
const click = useMemo(() => {
使用该回调解决传递函数也触发子组件渲染问题
return () => {
console.log('777')
}
}, [])
useCallback替代useMemo,只需要写一个函数
const click = useCallback(() => {
console.log('777')
}, [])
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>点击{count}</button>
<button onClick={() => setNum(num + 1)}>刷新{num}</button>
这里传递了一个函数click
<Test count={count} click={click} />
</div>
)
}
export default App
useMemo(() => fn, []) === useCallback(fn, []) 语法糖
const click = useMemo(() => {
return () => {
console.log('click')
}
},[])
const click = useCallback(() => {
console.log('click')
}, [])
hook常见问题
生命周期函数如何映射(对应)到Hooks?
state -> useState
shouldComponentUpdate -> Memo
componentDidMount -> useEffect(有空数组时)
componentDidUpdate -> useEffect(没有空数组,每次都调用)
componentWillUnmount -> useEffect(有空数组,和return时)
问:类实例成员变量如何映射到Hooks?
答:使用useRef,可以传入初始值参数,不能传入函数
类实例成员可以在不同的渲染周期记录状态,还不会触发渲染
class App {
id = 0 //类组件实例成员
}
function App(){
const it = useRef(0)
}
问:Hooks中如何获取历史props和state?
答:使用useRef跨域组件渲染,可以保留值
function Counter(){
const [count,setCount] = useState(0)
const prevCountRef = useRef()
useEffect(() => {
prevCountRef.current = count
})
const prevCount = prevCountRef.current
return <p>now:{count}, before: {prevCount}</P>
}
问:如何强制更新一个Hooks组件?(时机与方法)
答:创建一个不参与页面渲染的state,更新它,页面就会更新,间接强制更新
function Counter() {
const [count, setCount] = useState(0)
const [update, setUpdate] = useState(0) //强制更新
const prevCountRef = useRef()
useEffect(() => {
prevCountRef.current = count
})
const prevCount = prevCountRef.current
console.log('render')
return (
<div>
<button onClick={() => {setCount(count + 1)}}>更新</button>
<button onClick={() => {setUpdate(update + 1)}}>强制更新</button>
<p>
now:{count}, before: {prevCount}
</p>
</div>
)
}
Hooks闭包陷阱
在函数组件中,每一次的状态都存在于不同的闭包中,所以每一次的调用都是不一样的
使用useRef解决问题,让每次的对象都是一样的
function countReducer(state,action){
switch (action.type){
case 'add':
return state + 1
case 'munus':
return state - 1
default:
return state
}
}
function MyCountFunc() {
const [count,dispatchCount] = useReducer(countReducer,0)
//解决问题
const countRef = useRef() //{current:''},每次返回的都是同一个对象
countRef.current = count
const handleButtonClick = function(){
setTimeout(() => {
//alert(count)
alert(countRef.current)
},2000)
}
const handleButtonClick = useCallbcak(() => dispatchCount({ type:'add'}),[])
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<Child config={config} onButtonClick={handleButtonClick} />
<button onClick={handleButtonClick}>点击添加</button>
</div>
)
}
使用规则:使用位置
1.仅仅在顶层调用Hook,不能在循环语句,条件语句,嵌套函数中调用,原因是怕影响顺序,变成死循环
2.仅仅在函数组件和自定义Hook中调用Hook
不要在if里写hook,但可以在hook里写if
import React, {useState, useEffect} from 'react'
const Greetings = () => {
return <p>hello hooks</p>
}
export default () => {
const [count, setCount] = useState(0)
const [display, setDisplay] = useState(false)
错误答案:死循环操作,每次更新状态就会重新渲染,setDisplay改变display为true,触发重渲染,程序再次运行,又满足条件,再次循环...
if(count > 1){
setDisplay(x => true)
}
错误答案:顺序错误,react无法识别,不要在if里写hook
if(count > 1){
useEffect(() => {
setDisplay(x => true)
}, [])
}else {
useEffect(() => {
setDisplay((x) => false)
}, [])
}
正确答案:useEffect第一次会渲染,所以要写在条件语句里,只要条件满足count > 1,再次渲染,进入条件执行结果,一共渲染了两次
useEffect(() => {
if (count > 1) {
setDisplay((x) => true)
}
}, [count > 1])
return (
<div>
<p>{count}</p>
<button onClick=(() => setCount(x => x + 1))></button>
{display && <Greetings />}
</div>
)
}
简单总结
useState 方便,替换state
useEffect 功能,替换3个生命周期
useMemo 性能优化
useCallback 性能优化
PureComponent:在类组件中,解决子组件是否重复渲染
memo:在函数组件中,解决子组件是否重复渲染,替代PureComponent
useMemo:解决一段函数逻辑是否重复执行,监听某个值是否发生改变或满足条件,
如改变,则执行回调的代码,在渲染之前完成,需要返回值,参与渲染
useMemo 和 useEffect的区别:
useEffect执行的是副作用,在渲染之后运行
useMemo有返回值,返回值参与渲染,在渲染之前执行
useMemo 和 useCallback的区别:
useCallback是useMemo的变种
如果useMemo参数一的函数中返回的是一个函数,则可以用useCallback第一个参数:函数即可
useState 和 useRef的区别:
两者都可以跨域组件生命周期渲染
useState的赋值会触发重渲染,useRef不会
useRef还可以获取dom节点
学习更多