Bootstrap

react学习:hook

问题导向

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节点

学习更多

react学习地图

;