React Hooks---函数式编程

2,467 阅读11分钟

前言

在react 16.8之前,我们使用基于react的框架开发时,声明一个组件有两种方式:

  1. class声明组件
  2. 函数组件

在class 组件中,我们可以使用state、react的钩子函数等react特性。但是函数组件是一个无状态组件,而且对于操作DOM、发起请求等副作用并不能很好的处理。

什么是React Hooks

React Hooks 是react 16.8 的新特新,它可以让你在函数组件中使用state以及其他的react特性。hooks翻译为钩子,其含义就是向组件中钩入state,effect等。

react Hooks使用规则

在学习之前先跟大家说下react hooks使用的规则:

  1. 只能在react函数组件中使用Hooks。
  2. 只能在组件的最顶层使用Hooks

react Hooks具体使用

一、 useState

作用:在函数组件中调用它,可以添加一些内部的state。

示例:

        import React,{useState} from 'react';

        function Counter(){
            const [count,setCount] = useState(0);
            console.log(count)
            return(
            <div>
                <button onClick={()=>{setCount(count+1)}}>+</button>
                {count}
                <button onClick={()=>{setCount(count-1)}}>-</button>
            </div>
            )
            }
        export default Counter;

使用useState:

const [state变量,修改该变量的set方法] = useState(初始值)

一般来说函数在执行结束后变量就会消失,但是react保留了state变量,当state发生改变后,函数组件都会重新渲染,useState都会将当前的state值传递给state变量。

setCount是一个useState返回的用于更新state变量的方法,接收state的新值作为参数。

下面是使用useState需要注意的地方:

  1. 每次setCount 都会重新渲染函数组件。

  2. 你可以设置多个useState,使用多个变量,但是useState也可以很好的接收 数组、对象作为初始值。

  3. 使用count声明state变量的原因是:使其只能被set函数进行修改,达到setState的效果。

  4. setState()接收新的state或者一个返回state的函数(setCount(prevCount => prevCount - 1)})

  5. 在class组件中使用state,当setState时,是将setState的参数与原来的state进行对象的合并。但是useState则是使用新值替换原来的值。可能这么说不太明白,请看下面的示例:

         import React,{useState} from 'react';
         
         function Counter(){
             const [people,setPeople] = useState({age:16,name:'buzhanhua'});
             console.log(people.name)
             return(
                 <div>
                     <button onClick={()=>{setPeople({age:people.age+1})}}>+</button>
                     {people.age}
                     {people.name}
                     <button onClick={()=>{setPeople({age:people+1})}}>-</button>
                 </div>
             )
         }
         export default Counter;
         
    

当你点击加好以后,你会发现people的name属性变为了undefined,证明该机制是 替换。

二、useEffect

作用:给函数组件增加操作副作用的能力。和class中的componentDidMount、componentDidUpdate、componentWillUnMount的用途一致。

那么是什么是副作用呢?副作用具体指的是什么呢?

那我们就应该提到两个概念了:

纯函数:指一个函数接受相同的参数,返回结果始终相同。也就是说函数不受外部变量的影响,同时也不影响外部变量,这样的函数我们称之为纯函数。

副作用:只要是跟函数外部环境发生交互,都称为副作用,指函数调用时,除了运行函数业务主逻辑,返回返回值之外,还对函数外部环境产生了影响,比如,数据,变量,屏幕输出等等,具体如操作DOM、http请求发起、设置订阅等。大致分为:不需要清除的副作用和需要清除的副作用。不需要清除就像操作DOM,如在组件渲染完成更改页面的title,改了就改了,并不需要清除什么。但是像设置的定时器、延时器等需要在组件卸载的时候进行清除,这种就是需要清除的副作用了。

示例 :

        function UseEffect(){
            const [age,setAge] = useState(0);
            useEffect(()=>{
                let time = setInterval(()=>{
                    setAge(age+1);
                    console.log(age)
                },1000)
                return ()=>{
                    clearInterval(time)
                }
            },[age])
            return(
                <div>
                    {age}
                </div>
            )
        }

语法:

useEffect(function,arr);

注释:

  1. useEffect接收两个参数,第一个为一个函数(必选),第二个是一个数组(可选);

  2. 参数一是一个函数,用于执行你想在在class组件不同生命周期钩子函数执行的一些代码。(下文细说)。

  3. 参数二是一个数组,由于state变量变化时,函数组件都会重新渲染,那么useEffect也会在每次渲染的时候都会执行,显然在某些条件下,你并不想让全部的useEffect都重新执行,这个参数就是加以限制,只有当列表的变量变化时,useEffect才会重新执行。

        useEffect(()=>{
             // 相当于class组件的componentDidMount和componentDidUpdate
             return ()=>{
                 //可选,该函数将在组件将要卸载时执行,相当于componentWillUnMount
             }
         },[age])
    

在class组件中,我们通常会在组件加载完成后,发起请求、操作DOM什么的,这种副作用是不需要清除的。useEffect的第一个参数函数,可以选择是否return一个函数,该函数将在组件将要卸载时执行。但是不同于class组件,在示例中,每次setAge都会重新渲染组件,也就会重复执行useEffect,它的执行是每次在重新渲染前将之前的定时器清除,重新渲染在开启新的定时器,这么做的目的是可以减少bug的出现。

useEffect Hook的第二个参数,是用来控制它什么时候执行的,有些情况我们只希望,监听挂载和卸载在组件的生命周期中只执行一次,比如: window上监听的事件,document.title 的改变等等。这些有可能不依赖任何的state , 那我们如何做到类似, componentDidMount 和 componentWillUnmount 的效果呢 ? 很简单, 如下

     useEffect(()=>{
         window.addEventListener('resize',onResize);
         return ()=>{
             window.removeEventListener('resize',onResize)
         }
     },[])
     
     // 参数列表设置为空数组,这样只会在组件加载完成后绑定事件, 组件销毁前解绑事件
     // 只会执行一次, 但是这种方式要谨慎使用, 亲测 : 这种方式 setInterval 会有问题

4. useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数时异步执行的,而componentDidMonut和componentDidUpdate中的代码都是同步执行的。

三、useContext

在使用useContext之前,先说一下为什么要使用context。

我们在做react开发的过程中,组件之间共享值是肯定避免不了的一个问题,通常我们的做法是,通过props一级级传递,但是,组件共享的话如果不是父子级关系呢?你该如何传值呢?当然我们可以做到,但是会相当麻烦。

context,你可以理解为一个全局的state,里面可以存变量,方法,可以在provider之下的所有位置进行使用,不用关心层级关系。并可以通过里面的方法对值进行统一处理。

  1. 在react 16.8 之前我们使用context

         import React ,{createContext,useState} from 'react';
         
         const UserContext = new createContext();
         // 创建一个context
         
         // 创建provider
         const UserProvider = props => {
             const [username,setUsername] = useState('');
             return (
                 <UserContext.Provider value={{username,setUsername}}>
                     {props.children}
                 </UserContext.Provider>
             )
         }
         
         // 创建Consumer
         const UserConsumer = UserContext.Consumer;
         
         // 使用Consumer进行包裹
         
         const Pannel = ()=>(
             <UserConsumer>
                 {
                     ({username,setUsername})=>(
                         <div>
                             <div>user:{username}</div>
                             <input onChange={e => setUsername(e.target.value)}/>
                         </div>
                     )
                 }
             </UserConsumer>
         )
         
         const UseContext = ()=>(
             <UserProvider>
                 <Pannel/>
             </UserProvider>
         )
         
         export default UseContext;
    

我们可以看到,这种使用方式,在使用provider提供的变量和方法时,需要使用consumer进行包裹,并且需要使用一个函数接受、解构后才可以使用。当然在16.8之前不使用consumer包裹的方式,还可以使用contextType的方式进行使用,但是 16.8 以后这就简单了哦~

顺便说下contextType的使用方法吧:

        import React,{createContext, Component,useState} from 'react';
        const Context = new createContext();
        const ContextProvider = (props)=>{
            const [age,setAge] = useState(16)
            console.log(age)
            return(
                <Context.Provider value={{age,setAge}}>
                    {props.children}
                </Context.Provider>
            )
        }
        
        
        class ContextType extends Component{
            static contextType = Context;
            render(){
                return(
                    <div>
                        {this.context.age}
                        <button onClick={()=>{this.context.setAge(this.context.age+1)}}>长大吧宝贝</button>
                    </div>
                )
            }
            componentDidMount(){
                console.log(this.context) // {age: 16, setAge: ƒ}
            }
        }
        const WarpContextType = ()=>(
            <ContextProvider>
                <ContextType/>
            </ContextProvider>
        )
        export default WarpContextType;

contextType是一个静态属性,该属性会被赋予一个React.createContext的实例,那么你将可以在this.context中获取到距离当前组件最近层级的Context.Provider 中的值,你可以在任意的生命周期中使用它,包括render。很明显,它有一定缺陷,这种方式只能挂载一个Context。

  1. 16.8的函数组件的context

         import React ,{createContext,useState,useContext} from 'react';
         const UserContext = new createContext();
         
         // 创造一个context
         
         // 创建provider
         const UserProvider = props => {
             const [username,setUsername] = useState('');
             return(
                 <UserContext.Provider value={{username,setUsername}}>
                     {props.children}
                 </UserContext.Provider>
             )
         }
         
         const Pannel = () => {
             const {username,setUsername} = useContext(UserContext);// 使用context
             return (
                 <div>
                     <div>user:{username}</div>
                     <input onChange={e => setUsername(e.target.value)}/>
                 </div>
             )
         }
         
         const UseContext = ()=>(
             <UserProvider>
                 <Pannel/>
             </UserProvider>
         )
         
         export default UseContext;
    

在使用useContext后,就不需要再用consumer包裹取值了

四、useReducer

作用:其实就是useState的替代方案,现在useState将之前我们熟悉的state拆分开了,而这种方式则是将state组合起来,统一管理。

示例:

        import React ,{useReducer} from 'react';
        
        function reducer(state,action){
            switch(action.type){
                case 'add':
                    return{
                        ...state,
                        count:state.count+1
                    }
                case 'reduce':
                    return{
                        ...state,
                        count:state.count-1
                    }    
                default:
                    return state    
            }
        }
        const initialState = {
            count : 0,
            name : 'buzhanhua'
        }
        function Count(){
            const [state,dispatch] = useReducer(reducer,initialState);
            return(
                <div>
                    <button onClick={()=>{dispatch({type:'add'})}}>+</button>
                    {state.count}
                    <button onClick={()=>{dispatch({type:'reduce'})}}>-</button>
                    {state.name}
                </div>
            )
        }
        
        export default Count;

使用useReducer

const [state,dispatch] = useReducer(reducer,initialState);

有两种初始化state的方式,demo中是其中的一种,另一种是惰性初始化,但是不推荐使用,如果想了解详情,可参考官方文档。

五、useMemo和useCallback

这两个Hook的作用类似,主要作为性能优化手段进行使用,试想一下这个场景,当你使用set函数改变state时,函数组件将会重新进行渲染,但是你发现,并不是所有的子组件都有必要重新渲染,那么这两个Hook就是解决这种情况的,用于控制当依赖项数组中的state改变时才进行重新渲染。

示例:

        import React,{useState,useMemo,useCallback} from 'react';
        
        function Time(){
            return(
                <div>{Date.now()}</div>
            )
        }
        
        function Count(){
            const [count,setCount] = useState(0);
            const [name,setName] = useState('buzhanhua');
            //const memoizedComponent = useMemo(()=><Time/>,[count]);
            const memoizedComponent = useCallback(<Time/>,[count]);
            return(
                <div>
                    <button onClick={()=>{setCount(count+1)}}>改变数字</button>
                    
                    <button onClick={()=>{setName(name+'ha')}}>改变名字</button>
                    {memoizedComponent}
                    <p>数字:{count}</p>
                    <p>名字:{name}</p>
                </div>
            )
        }
        
        export default Count;

useCallback(fn,依赖项数组)等同于useMemo(()=>fn,依赖项数组)

下面对demo进行一些解释,在的demo中有两个button按钮,在使用useCallback或者useMemo后,你会发现只有当处于依赖项数组中的count改变时,Time组件才会重新渲染。

六、useRef

语法: const myRef = useRef(initialValue);

useRef返回一个可变的ref对象,其 .current属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。其实就是提供给我们一个保存可变变量的空间,大家想一下这个需求:怎么能够记录组件的渲染次数?其他的Hook都是一个常量,只有使用set函数的时候才可以改变,我们如果想实现该需求的话,需要找到一个可更改但又在改变时不会使组件从新渲染的东西。

下面我们先看一个我们熟悉的,获取DOM节点的demo:

        import React,{useRef,useState} from 'react';
        
        function UseRef(){
            const myRef = useRef(null)
            const callback = () =>{
                myRef.current.focus()
            }
            return(
                <div>
                    <input type="type" ref={myRef}/>
                    <button onClick={callback}>聚焦</button>
                </div>
            )
        }
        

保存前一个状态

    import React ,{useState,useEffect,useRef}from 'react';
    
    function Rank(props){
        const [count, setCount] = useState(0);
    
        const prevCountRef = useRef();
        useEffect(() => {
          prevCountRef.current = count;
        });
        const prevCount = prevCountRef.current;
        return(
            <div>RankNow: {count}, before: {prevCount}
            <button onClick={()=>{setCount(count + 1)}}>点击</button>
            </div>
        )
    }

export default React.memo(Rank);

需求实现demo:

        import React,{useRef,useState} from 'react';
        
        function Time(props){
            console.log(props.current)
            return(
                <div>{Date.now()}</div>
            )
        }
        function UseRef(){
            const myRef = useRef(0);
            const [count,setCount] = useState(0);
            myRef.current++
            return(
                <div>
                    <Time current={myRef.current}/>
                    <button onClick={()=>{setCount(count+1)}}>+</button>
                </div>
            )
        }
        
        export default UseRef

注释:

  1. 使用useRef方法返回的{current:...} 和自己创建一个{current:...} 对象的唯一区别就是,每次组件渲染返回的ref对象都是同一个对象,所以它可以实现上面的需求。
  2. useRef比ref属性更有用,它可以方便的保存任何可变值。

useImperativeHandle

自定义在使用ref时,公开给父组件的实例值, 必须和forwardRef一起使用

    import React,{useImperativeHandle,forwardRef,useRef} from 'react';
    
    function Input(props,ref){
        const inputRef = useRef();
        useImperativeHandle(ref,() => ({
            focus: () => {
                inputRef.current.focus()
            }
        }))
        return(
            <input type="text" ref={inputRef}/>
        )
    }
    
    Input = forwardRef(Input);
    
    function Rank(props){
        const rankRef = useRef();
        return(
            <div>
                <Input ref={rankRef}/>
                <button onClick={() => {rankRef.current.focus()}}>点击</button>
            </div>
        )
    }
    
    export default React.memo(Rank);

七、自定义Hook

在react 16.8 之前,我们组件之间复用逻辑的方式,主要是通过props传递和高阶组件完成的,自定义Hook提供给我们另一种组件之间复用逻辑的新方式。

自定义 Hook 是一个函数,其名称以 “use” 开头(规定),函数内部可以调用其他的 Hook。

下面是一个获取浏览器窗口demo的展示,可以监测手动缩放

        import React , {useState,useEffect} from 'react';
        
        function WinSize(){
            const [size,setSize] = useState({
                width:  document.documentElement.clientWidth,
                height:  document.documentElement.clientHeight
            });
        
            const onResize =  ()=>{
                setSize({
                    width: document.documentElement.clientWidth,
                    height: document.documentElement.clientHeight
                })
            }
        
            useEffect(()=>{
                window.addEventListener('resize',onResize);
                return ()=>{
                    window.removeEventListener('resize',onResize);
                }
            },[])
        
            return size;
        }
        
        function Demo6(){
            const size = WinSize();
            return (
                <div>
                    浏览器窗口:宽 {size.width} 高 : {size.height}
                </div>
            )
        }
        
        export default Demo6
        

踩坑

使用useEffect报错缺少依赖项

error : React Hook useLayoutEffect has a missing dependency: 'options'.

原因: 在启用eslint-plugin-react-hooks插件后, 一些配置项会强制提醒我们在使用effect的时候,申明所需要的依赖项。当useEffect里面使用外部的变量的情况,这些变量都属于依赖项,需要在第二个数组参数中放入对应的变量。

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn" // off
  }
}

结束

本文为作者工作学习总结,如有错误或不当,请指出,欢迎共同学习