React笔记(六) React Hooks

React笔记(六)

1.React Hooks
  • 在之前的React中,函数式组件相对于类式组件,缺少了很多部分,比如函数式组件内没有状态,没有生命周期,没有this等。这些导致了React函数式组件只能进行简单的展示,而无法实现通过状态切换或管理页面。而编写类式组件,又使得程序过重。因此React在16.8之后提供了一个可以在不编写类式组件的情况下,使用state或其他的React特性的方法:Hooks。
  • 在使用时,需要哪些功能,就可以将对应的Hooks引入到函数式组件内,甚至在有特殊需要时,书写自己的Hooks。
2.基础Hooks
  • useState用来控制函数组件中的状态。在之前的函数组件中,是不存在state的。但当使用这个Hooks后,在函数式组件内部也会存在有可以读写的state状态。具体使用方法为

    const [xxx,setXxx]  = React.useState(initValue);`
    

    其中xxx代表函数组件内部当前的状态值,setXxx代表更新此状态值的函数,initValue代表第一次初始化时的状态值。在函数式组件中,如果需要多个状态值,可以使用多次useState定义多个状态值与用于更改状态值的函数。并且区别于类式组件内的state,在修改状态时传给setState的是一个state对象;函数式组件在使用setXxx的时候传入为此方法对应的状态值。下面为一个示例。

    class Demo extends Component {
      state ={
        count:0
      };
      render() {
        const {count} = this.state;
        return <div>
          <p>{this.state.count}</p>
          <button onClick={()=>{this.setState({count:count+1})}}>点我加一</button>
        </div>
      }
    }
    
    function DemoF(props) {
      const [ count, setCount ] = React.useState(0);
      return (<div>
        <p>{count}</p>
        <button onClick={() => { setCount(count + 1) }}>点我加一</button>
      </div>)
    }
    

    两种组件都实现了点击按钮将数字加1的功能,相比较,函数式组件的代码量更少,结构更简单。在使用State Hook时也需要注意一点:setXxx与setState相似,都是异步操作,那么也就是说,如果在单一操作内对相同的状态值重复更改,其会进行覆盖。

    export default function DemoF(props) {
      const [ count, setCount ] = React.useState(0);
    
    //使用直接赋状态值的方法,后项覆盖前项,结果+1
      function addCount(){
        setCount(count + 1);
        setCount(count + 1);
      }
        
      //根据旧状态值更新状态值,结果+2
      function addCountF(){
        setCount(pre=>pre + 1);
        setCount(pre=>pre + 1);
      }
    
      //在定时器中操作状态,状态更新是同步的,结果+2
      function addCountAs(){
        setTimeout(()=>{
          setCount(pre=>pre + 1);
          setCount(pre=>pre + 1);
        },1000);
      }
    
      //在定时器中操作状态,状态更新是同步的,结果+2
      function addCountAsF(){
        setTimeout(() => {
          setCount(pre=>pre + 1);
          setCount(pre=>pre + 1);
        }, 1000);
      }
    
      return (<div>
        <p>{count}</p>
        <button onClick={addCount}>点我加二(直接赋状态值)</button>
        <button onClick={addCountF}>点我加二(跟据旧状态值更新状态值)</button>
        <button onClick={addCountAs}>点我加二(直接赋状态值,异步)</button>
        <button onClick={addCountAsF}>点我加二(跟据旧状态值更新状态值,异步)</button>
      </div>)
    }
    
  • useEffect可以在函数组件中执行副作用操作(例如发送ajax请求,设置订阅/启动定时器,手动更改真实DOM等)。这个Hook相当于类组件的生命周期函数ComponentDidMountComponentDidUpdateComponentWillUnmount。并且也可以在函数式组件中使用多次。

    export default function Root(props) {
      const [show, setShow] = React.useState(true);
      return <div>
        {show && <DemoF />}
        <button onClick={() => { setShow(false) }}>点我卸载组件</button>
      </div>
    }
    
    function DemoF(props) {
      const [count, setCount] = React.useState(0);
      const [time, setTime] = React.useState(new Date());
    
      function addCount() {
        setCount(count + 1);
      }
    
      //监控着count状态值的变化,一但发生变化,就进行真实DOM的更新
      //充当着ComponentDidUpdate的作用,但更为精确,只会监控配置的状态值的变化
      React.useEffect(() => {
        console.log('ComponentDidUpdate');
        document.title = `你已经点了${count}次了`;
      }, [count]);
    
      //不监控任何状态值的变化,也就是说在整个周期中,只运行一次
      //充当着ComponentDidMount的作用
      //返回值的函数代表着组件卸载前进行的,充当着ComponentWillUnmount的作用
      React.useEffect(() => {
        console.log('ComponentDidMount');
        var timer = setInterval(() => {
          setTime(new Date());
        }, 1000);
        return function () {
          console.log('ComponentWillUnmount');
          clearInterval(timer);
        }
      }, []);
    
      return (<div>
        <p>{count}</p>
        <p>{time.toString()}</p>
        <button onClick={addCount}>点我加一,同时刷新title</button>
      </div>)
    }
    

    同样对于useEffect也存在着一部分补充。例如,useEffect所监控内容的变化时,其对比前后内容是否发生变化所采取的是浅比较,也就是说,如果是引用类型,其比较的是地址是否发生了变化,这与setState触发render相同。此外监控的内容并不局限于使用state Hook创建的状态值,也可以状态值内部的属性,或是一些变量。

    export default function DemoF(props) {
      const [count, setCount] = React.useState({value:0});
    
      //由于count的地址未发生改变,因此即使count内部的value发生了改变,也不会触发useEffect
      //并且当将useEffect中监控的内容改成count.value时,其也不会触发
      React.useEffect(() => {
        console.log('ComponentDidUpdate');
        document.title = `你已经点了${count.value}次了`;
      }, [count]);
    
      
      //当仅使用useState时,也需要注意此项内容,因为其也不会重新渲染
      function addCount(){
        console.log('点击',count.value);
        count.value++;
        setCount(count);
      }
    
      return (<div>
        <p>{count.value}</p>
        <button onClick={addCount}>点我加一,同时刷新title</button>
      </div>)
    }
    

    另外,对于useEffect返回值,其不仅仅只应用于组件卸载前,甚至可以用于消除状态带来的副作用。当状态值由当前向另一个新的变化时,首先触发的是页面的重新渲染,在之后会清除掉原状态的副作用,最后为函数添加一个新的副作用。也就是:render=>clear prev effect=>add next effect。

    export default function DemoF(props) {
      const [count, setCount] = React.useState(0);
    
      React.useEffect(() => {
        //最后触发
        console.log('ComponentDidUpdate');
        document.title = count;
        return function(){
          //其次触发
          console.log('clear inner',count);
          document.title = '';
        }
      }, [count]);
    
      function addCount(){
        setCount(count+1);
      }
        
      //首先触发
      console.log('render');
      return (<div>
        <button onClick={addCount}>点我title+1</button>
      </div>)
    }
    
  • useContext其用来实现祖组件与后代组件的通信,在类式组件中使用this.context来获取传入的context,但在函数组件中由于不存在this,所以无法使用此方法,但通过使用Context Hook可以实现对此内容的应用

    const ValueVontext = React.createContext();
    
    export default function Grand(props) {
      const value = '内容';
      return (<div>
        我是祖组件直接获取:{value}
        <ValueVontext.Provider value={value}>
          <Parent value={value}/>
        </ValueVontext.Provider> 
      </div>)
    }
    
    function Parent(props){
      return <div>
        我是父组件从props获取:{props.value}
        <Child/>
      </div>
    }
    
    function Child(props){
      //接收内容:传入Context
      const value = React.useContext(ValueVontext);
      return <div>
        我是子组件从context获取:{value}
      </div>
    }
    

    其整体与类式组件中的context用法一致,只有接收时不一样。并且需要注意的是,对于调用此Hook的组件,在context变化时,其一定会进行重新渲染。

3.额外Hooks
  • useReducer,其实现了Redux的状态管理的功能,具体的使用方法为

    //state:管理的状态
    //dispatch:为此状态绑定action的方法
    //reducer:形为(state, action) => newState的方法,接收action与旧state并返回新state
    //initialArg:默认初始值
    //init:处理默认初始值,并返回一个新的初始值
    const [state, dispatch] = useReducer(reducer, initialArg, init);
    

    需要注意的是,由于React的浅比较,因此当调用了reducer后的state与原state相同,此组件依旧不会进行再次渲染。

    function reducer(state,action){
      const {count} = state;
      switch (action.type) {
        case 'add':
          return {count:count+1};
        case 'sub':
          return {count:count-1}; 
        case 'reset':
          return {count:action.count};
          // 惰性初始化
          // return initState(action.count);
        default:
          throw new Error();
      }
    }
    
    function initState(count){
      return {count:count};
    }
    
    export default function DemoF(props) {
      const [state,dispatch] = React.useReducer(reducer,{count:0});
      // 惰性初始化
      // const [state,dispatch] = React.useReducer(reducer,0,initState);
      return <div>
        <p>{state.count}</p>
        <button onClick={()=>{dispatch({type:'add'})}}>加一</button>
        <button onClick={()=>{dispatch({type:'sub'})}}>减一</button>
        <button onClick={()=>{dispatch({type:'reset',count:0})}}>重置</button>
      </div>
    }
    
    
  • useMemo,其常常应用于性能优化,其把创建函数与函数的依赖项数组作为参数传入,在使用时与useEffect类似。但useEffect执行的是画面的副作用,而useMemo是返回一个记忆值(当后项相同时,前项返回值不变);类似于一个纯函数的功能。

    //在父组件中点击按钮修改的是count,不会修改子组件中使用的name
    //但子组件依旧重新渲染了,这种就是不必要的渲染,因此需要使用memo。
    export default function Demo(props){
      const [name,setName] = React.useState('xxx');
      const [count,setCount] = React.useState(0);
      return <div>
        <p>{count}</p>
        <button onClick={()=>{setCount(count+1)}}>点我加一</button>
        <Child name={name}/>
      </div>
    }
    
    function Child(props){
      console.log('childRender了');
      return <p>{props.name}</p>
    }
    

    对上面这段代码进行简单的修改后。

    export default function Demo(props){
      const [name,setName] = React.useState('xxx');
      const [count,setCount] = React.useState(0);
     
      //使用useMemo,只有后项name改变了,前项才会改变
      //返回的JSX元素节点也随之改变,在name不变的情况下减少了页面渲染
      const ChildEle = React.useMemo(()=>{
        return <Child name={name}/>;
      },[name])
      return <div>
        <p>{count}</p>
        <button onClick={()=>{setCount(count+1)}}>点我加一</button>
        {ChildEle}
      </div>
    }
    
    function Child(props){
      console.log('childRender了');
      return <p>{props.name}</p>
    }
    

    虽然上面这段代码也可以通过useEffect实现同样的效果,但useEffect面向的更多的是副作用,相比较而言,memo更合适。并且在下面这个例子中也可以看出memo执行于子页面刷新之前,其更像类式组件生命周期中的shouldComponentUpdate

    export default function Demo(props){
      const [name,setName] = React.useState('xxx');
      const [count,setCount] = React.useState(0);
        
      const ChildEleM = React.useMemo(()=>{
        console.log('memo 替换')
        return <Child name={name}/>;
      },[name]);
    
      var ChildEleE = <Child name={name}/>;
    
      React.useEffect(()=>{
        console.log('Effect 替换')
        ChildEleE = <Child name={name}/>;
      },[name]);
        
      return <div>
        <p>{count}</p>
        <button onClick={()=>{setCount(count+1)}}>点我加一</button>
        <button onClick={()=>{setName([...name,'x'])}}>点我改名</button>
        {ChildEleM}
      </div>
    }
    
    function Child(props){
      console.log('childRender了');
      return <p>{props.name}</p>
    }
    
  • useRef与类式中的createRef相同,创建一个Ref,当挂载上之后,会将这个Ref中的current指向被挂载的元素。同样也不可以对函数式组件使用ref属性。

    export default function Demo(props){
      const ChildRef = React.useRef();
      return <div>
      <input ref={ChildRef}/> 
      <button onClick={()=>{ChildRef.current.focus()}}>聚焦输入框</button>
      </div>
    }
    
  • 但凡事无绝对,在使用forwardRef创建的函数式组件就可以使用ref属性。forwardRef接收一个渲染函数作为参数,这个渲染函数在函数式组件的基础上,增加了一个ref参数用于转发,将ref参数传递给内部的元素。

    export default function Demo(props){
      const ChildRef = React.useRef();
      return <div>
        <Child ref={ChildRef}/>
        <button onClick={()=>{ChildRef.current.focus()}}>聚焦输入框</button>
        <button onClick={()=>{console.log(ChildRef);ChildRef.current.value='123'}}>填写内容</button>
      </div>
    }
    
    function Child(props,ref){
      return <input ref = {ref}/> 
    }
    
    Child = React.forwardRef(Child);
    

    但此时,挂载在父组件的ref上的元素过于庞大,这时可以采用useImperativeHandle来控制暴露给父组件的实例值。用法为

    //ref=>ref对象
    //createHandle=>创建自定义的实例对象
    //deps=>依赖项:可选
    React.useImperativeHandle(ref, createHandle, [deps])
    

    现在上方的示例中,父组件可以聚焦子组件的输入框,或为其填写内容。但如果在子组件中存在着两个输入框,父组件可以分别为两个子组件填写内容,这时可以使用这个Hook。

    export default function Demo(props){
      const ChildRef = React.useRef();
      return <div>
        <Child ref={ChildRef}/>
        <button onClick={()=>{ChildRef.current.focus()}}>聚焦输入框</button>
        {/* 点击后,查看控制台,也会发现ref对象中的current比较干净,只存在自定义的一些属性 */}
        <button onClick={()=>{console.log(ChildRef);ChildRef.current.setValueA('123')}}>填写A内容</button>
        <button onClick={()=>{ChildRef.current.setValueB('456')}}>填写B内容</button>
      </div>
    }
    
    function Child(props,ref){
      const inputRefA = React.useRef();
      const inputRefB = React.useRef();
      React.useImperativeHandle(ref,()=>({
        focus:()=>inputRefA.current.focus(),
        setValueA:value=>{inputRefA.current.value=value},
        setValueB:value=>{inputRefB.current.value=value}
      }));
      return <div>
      A:<input ref = {inputRefA}/> 
      B:<input ref = {inputRefB}/> 
      </div>
    }
    
    Child = React.forwardRef(Child);
    
  • useLayoutEffect用法与useEffect相同,但其只会在所有的DOM变更后才会同步调用内部effect,可以使用其阻塞浏览器渲染,减少页面的错误渲染。在下面的示例中。由于存在一个耗时的操作,因此数字会先变,但数字的奇偶性没有改变,过了三秒后才正常显示。但当将useEffect替换成useLayoutEffect时,则不会出现此问题。

    export default function DemoF(props) {
      const [count, setCount] = React.useState(0);
      const [name, setName] = React.useState('偶数');
    
      React.useEffect(() => {
        //存在一个耗时的操作
        const pre = Date.now();
        while(Date.now() - pre < 3000) {}
        
        if(count%2==0){
          setName('偶数');
        }
        else{
          setName('奇数');
        }
      }, [count]);
    
      function addCount(){
        setCount(count+1);
      }  
      return (<div>
        <p>{name}</p>
        <p>{count}</p>
        <button onClick={addCount}>点我+1</button>
      </div>)
    }
    
  • useDebugValue为自定义Hook服务,其可以在React开发工具中显示hook的标签,一般情况下不推荐使用,只有在自定义Hook被复用时,才最有价值。

    function useCustom(ok){
      //一般用法
      React.useDebugValue(ok?'OK':'不OK');
      //延迟格式化用法
      React.useDebugValue(ok,(ok)=(ok?'OK':'不OK'));
      return ok?'OK':'不OK';
    }
    
4.Hooks规则
  • 由于Hooks本质上是一个函数,因此不要在循环,条件或嵌套语句中使用,这样会导致Hook被重复调用或不调用。同样,也需要在return前使用Hook,防止不被调用。
  • 另外Hook只能在React的函数式组件或自定义Hook中被调用
5.自定义Hook
  • 通过自定义Hook,可以将一部分组件中的逻辑提取出来,提取到可重用的函数中。在下面的示例中就将判断奇偶数的这部分逻辑提取出来,形成了一个自定义Hook。在定义时,建议使用use开头,使得可以一眼看出是一个自定义Hook。

    export default function DemoF(props) {
      const [count,setCount] = React.useState(0);
      const a = useCustom(count);
      return <div>
        <p>{count}</p>
        <p>{a}</p>
        <button onClick={()=>{setCount(count+1)}}>点我加一</button>
      </div>
    }
    
    function useCustom(num){
      const [name,setName] = React.useState(null);
      React.useEffect(()=>{
        setName(num%2==0?'偶数':'奇数');
      });
      return name;
    }
    

参考文章

Hook API 索引 – React (reactjs.org)

useState用法指南_前端精髓的博客-CSDN博客_usestate

useEffect使用指南 - 简书 (jianshu.com)

React Hooks 详解 - 简书 (jianshu.com)

何时使用useLayoutEffect? - SegmentFault 思否

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值