07-04-函数组件及Hooks

课程目标

  • 掌握函数组件的创建与使用;
  • 掌握 React 常见 Hooks 使用;
  • 掌握如何自定义 Hook。

课程内容

函数式组件

  • 函数式组件,本质就是一个常规函数,接收一个参数 props 并返回一个 reactNodes,即 return 该组件需要输出的视图;
  • 函数式组件中没有 this 和生命周期函数;
  • 使用函数式组件时,应该尽量减少在函数中声明子函数,否则组件每次更新时都会重新创建这个函数。

React Hooks(钩子)

-- React Hooks 是 React 16.8 中的新增功能,它们使您无需编写类即可使用状态和其它 React 功能。

常用 Hook

useState
  • const [state, setState] = useState(initialState);
  • const [状态,修改该状态的方法] = useState(初始值);
    • 在同一组件中可以使用 useState 定义多个状态;
    • 注意 useState 返回的 setState 方法不会进行对象的浅合并,即 setState 方法只接收一个参数,该参数代表的就是更新完之后的新状态;
    • 会受到批处理影响,多次调用 setState 会被合并只执行一次;
    • 注意 useState 返回的 setState 方法同样是异步方法。
    import { useState } from "react";
    
    function Child(props) {
      const [count, setCount] = useState(5);
      const [num, setNum] = useState(0)
      console.log('render');
      return (  
        <>
          <h3>Child:{props.info}</h3>
          <h3>Count: {count}</h3>
          <h3>Num: {num}</h3>
          <button 
            onClick={ () => {
              setCount(count+1);
              setCount(count+1);
              setCount(count+1);
              setCount(count+1);
              setCount(count+1);
              console.log('setCount');  // 先打印 setCount,然后打印 render,count只加1
            }}
          >Count递增</button>
          <button 
            onClick={ () => {
              setTimeout(() => {
                setNum(num+1)
                setNum(num+1)
                setNum(num+1)
                setNum(num+1)
                setNum(num+1)
                console.log('setNum');  // 先打印两次 render,再打印 setNum,num只加1
              }, 0);
            }}
          >Num递增</button>
        </>
      );
    }
    
    export default Child;
    
  • 只在 React 函数中调用 Hook:
    • React 函数组件中;
    • React 自定义 Hook 中;
  • 只在最顶层使用 Hook:在 React 中要保证 hook 的执行顺序在组件更新前后一致,否则会报错 React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render,原因且看下面 hook 实现原理:
    // useState 实现原理初版
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
    <div id="root"></div>
    <script>
    let state=null;    
    function useState(init){
        let nowState = state === null?init:state; 
        return [nowState,(newState)=>{
            state=newState;
            render();
        }];
    }
    function render(){
        let root = document.querySelector("#root");
        let p = document.createElement("p");
        let btn = document.createElement("button");
        let [count,setCount] = useState(10);
        p.innerHTML = "count:" + count;
        btn.innerHTML = "递增";
        root.innerHTML = "";
        btn.onclick=()=>{
            setCount(count+1);
        }
        root.appendChild(p);
        root.appendChild(btn)
    }    
    render();
    </script>    
    </body>
    </html>
    
    // hook 为什么要保持调用顺序
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="root"></div>
    </body>
    <script>
      let state = [];
      let index = 0;
      function useState(init) {
        let nowIndex = index++;
        
        if(state[nowIndex] === undefined) {
          state[nowIndex] = init;
        }
        
        return [state[nowIndex], val => {
          state[nowIndex] = val;
          console.log(nowIndex, val)
          render();
        }]
      }
    
      function render() {
        index = 0;
        const div = document.createElement('div');
        const button = document.createElement('button');
        const root = document.querySelector('#root');
        const [count, setCount] = useState(10);
        div.innerHTML = 'Count: ' + count;
        button.innerHTML = '递增';
        button.onclick = function() {
          setCount(count + 1);
        }
        root.innerHTML = '';
        root.appendChild(div);
        root.appendChild(button);
    
        const div2 = document.createElement('div');
        const button2 = document.createElement('button');
        const [num, setNum] = useState(20);
        div2.innerHTML = 'Count2: ' + num;
        button2.innerHTML = '递增2';
        button2.onclick = function() {
          setNum(num + 1);
        }
        root.appendChild(div2);
        root.appendChild(button2);
      }
      render();
    </script>
    </html>
    
useEffect
  • 可以完成的作用等同于类组件中的生命周期函数:componentDidMount、componentDidUpdate 和 componentWillUnmount;
  • 需要清除的副作用。
    // 基本使用
    import { useEffect, useState } from "react";
    
    function Effect() {
      /**
       * useEffect 副作用钩子:用于在 React 函数中来处理副作用【数据请求、DOM 操作】
        useEffect( () => {
          // effect 函数
          return () => {
            // cleanUp 函数, 可选
          }
        }, [依赖数据])  // 依赖函数也是可选的
    
        挂载阶段:
          从上向下一行行执行代码,如果碰到 useEffect,则将对应的 effect、cleanUp 函数分别存储到一个队列中,当组件挂在完成后,按添加顺序执行 effect 函数;cleanUp 函数不执行;
    
        更新阶段:
          从上向下一行行执行代码,如果碰到 useEffect,则将对应的 effect 函数存储到一个队列中;当组件更新完成之后,找到之前存储的 cleanUp 队列,依次执行;然后再按照添加顺序依次执行 effect 队列、获取 cleanUp 函数并存储到一个队列中;如果有依赖参数,则对应的依赖参数发生变化才会执行更新阶段的代码;如果没有依赖参数,则只要组件有更新就会执行更新阶段代码;
    
        卸载阶段:
          找到对应的 cleanUp 队列,依次执行。
    
        依赖参数:(只对更新阶段有影响)
          1、不写依赖参数 useEffect(()=>{}),组件有更新,就会执行 cleanUp 函数 和 effect 函数;
          2、有具体依赖参数(1-多个)useEffect(()=>{},[a[,b,...]),则组件更新时,其依赖参数有变化,会执行其 cleanUp 函数和 effect 函数;
          3、有具体依赖参数(0个)useEffect(()=>{},[]),则组建更新时,不会执行其 cleanUp 和 effect 函数,只有加载完成和卸载阶段会执行这里的代码。
    
      */
    
      const [count, setCount] = useState(5);
        useEffect( () => {
          console.log('effect-1');
      
          return () => {
            console.log('cleanUp-1');
          }
        });
    
        useEffect( () => {
          console.log('effect-2');
      
          return () => {
            console.log('cleanUp-2');
          }
        });
    
      console.log('render');
    
      return (  
        <>
          <h3>Count: {count}</h3>
          <button onClick={()=>setCount(count+1)}>递增</button>
        </>
      );
    }
    
    export default Effect;
    
  • 和类组件的生命周期进行对应:能对应到的生命周期如下
    • 挂在完成之后,做某事;
    • 更新完成之后,做某事;
    • 挂载完成和更新完成之后,做某事;
    • 即将卸载时,做某事。
    import { useEffect, useRef } from "react";
    
    function EffectLife() {
      useEffect( () => {
        console.log('2--挂载完成之后');
    
        return () => {
          console.log('即将卸载时做某事');
        }
      }, []);
      useEffect( () => {
        console.log('3--更新、挂载完成之后都会做某事');
      });
    
      // 只在更新阶段做某事【需要使用到 useRef】
      const isMount = useRef(false); 
      useEffect( () => {
        if(isMount.current) {
          console.log('更新阶段做某事');
        } else {
          isMount.current = true;
        }
      })
    
      console.log('1--render');
      return ( 
        <>
          <h3>Effect Life</h3>
        </>
      );
    }
    
    export default EffectLife;
    
useRef
  • 用户关联原生 DOM 节点;
  • 或者用来记录组件更新前的一些数据。
    import { useEffect, useRef, useState } from "react";
    
    function RefTest() {
      // const ref = useRef(init);
      // 1.关联节点实例
      // 2.传递组件更新前的一些数据,当 useRef 中存储的是某项数据时,该数据并不会随着组件的更新而自动更新
      const [count, setCount] = useState(5888);
      const ref = useRef(count);
      const countRef = useRef();
      useEffect( () => {
        ref.current = count;
        console.log(ref, countRef.current.innerHTML);
      })
    
      
      return (  
        <>
          <div ref={countRef}>Count: {count}</div>
          <button 
            onClick={()=> {
              setCount(count+1);
            }}
          >递增</button>
        </>
      );
    }
    
    export default RefTest;
    
useMemo
  • const data = useMemo(cb, []); 类似于 useState,有一个依赖参数。
    // 发现做任何操作,都会执行 maxValue 函数
    import { useState } from "react";
    
    function MemoTest() {
      const [count, setCount] = useState(1);
      const [num, setNum] = useState(5);
      const [val, setVal] = useState('');
      const plusCount = () => {
        console.log('plusCount');
        setCount(count+1);
      };
      const minusCount = () => {
        console.log('minusCount');
        setCount(count-1);
      };
      const plusNum = () => {
        console.log('plusNum');
        setNum(num+1);
      };
      const minusNum = () => {
        console.log('minusNum');
        setNum(num-1)
      };
      const valueChange = ({target}) => {
        console.log('valueChange');
        setVal(target.value)
      };
      const maxValue = () => {
        console.log('maxValue');
        return count>num?"count":"num";
      };
      return (  
        <>
          <h3>MemoTest</h3>
          <div>Count: {count}; <button onClick={plusCount}>+</button>, <button onClick={minusCount}>-</button></div>
          <div>Num: {num}; <button onClick={plusNum}>+</button>, <button onClick={minusNum}>-</button></div>
          <p>当前比较大的值为:{maxValue()}</p>
          <div><input value={val} onChange={valueChange} /></div>
          <p>{val}</p>
        </>
      );
    }
    
    export default MemoTest;
    
    import { useMemo, useState } from "react";
    /**
     * useMemo(cb, [])
     * 不是函数,直接等于 cb 的返回值。
     * 发现只有修改 count、num 的值才会打印 maxValue,在输入框中输入内容时不再打印
     */
    
    function MemoTest() {
      const [count, setCount] = useState(1);
      const [num, setNum] = useState(5);
      const [val, setVal] = useState('');
      const plusCount = () => {
        console.log('plusCount');
        setCount(count+1);
      };
      const minusCount = () => {
        console.log('minusCount');
        setCount(count-1);
      };
      const plusNum = () => {
        console.log('plusNum');
        setNum(num+1);
      };
      const minusNum = () => {
        console.log('minusNum');
        setNum(num-1)
      };
      const valueChange = ({target}) => {
        console.log('valueChange');
        setVal(target.value)
      };
      const maxValue = useMemo(() => {
        console.log('maxValue');
        return count>num?"count":"num";
      }, [num, count]);
      return (  
        <>
          <h3>MemoTest</h3>
          <div>Count: {count}; <button onClick={plusCount}>+</button>, <button onClick={minusCount}>-</button></div>
          <div>Num: {num}; <button onClick={plusNum}>+</button>, <button onClick={minusNum}>-</button></div>
          <p>当前比较大的值为:{maxValue}</p>
          <div><input value={val} onChange={valueChange} /></div>
          <p>{val}</p>
        </>
      );
    }
    
    export default MemoTest;
    
useCallback
  • 是 useMemo 的一个进化体;
  • 当 useMemo 返回一个函数时,可以使用 useCallback 来改进;
  • 将上面的代码进行改进,修改 plusCount,只有在 count 进行更新时才会进行声明子函数:
    import { useCallback, useMemo, useState } from "react";
    /**
     * useMemo(cb, [])
     * 当 useMemo 返回值是一个函数时,可以使用 useCallback 来优化
     */
    
    function CallbackTest() {
      const [count, setCount] = useState(1);
      const [num, setNum] = useState(5);
      const [val, setVal] = useState('');
      const plusCount = useCallback( () => {
        console.log('plusCount');
        setCount(count+1);
      }, [count]);
      const minusCount = useCallback(() => {
        console.log('minusCount');
        setCount(count-1);
      }, [count]);
      const plusNum = useCallback(() => {
        console.log('plusNum');
        setNum(num+1);
      }, [num]);
      const minusNum = useCallback(() => {
        console.log('minusNum');
        setNum(num-1)
      }, [num]);
      const valueChange = useCallback(({target}) => {
        console.log('valueChange');
        setVal(target.value)
      }, [val]);
      const maxValue = useMemo(() => {
        console.log('maxValue');
        return count>num?"count":"num";
      }, [num, count]);
      return (  
        <>
          <h3>MemoTest</h3>
          <div>Count: {count}; <button onClick={plusCount}>+</button>, <button onClick={minusCount}>-</button></div>
          <div>Num: {num}; <button onClick={plusNum}>+</button>, <button onClick={minusNum}>-</button></div>
          <p>当前比较大的值为:{maxValue}</p>
          <div><input value={val} onChange={valueChange} /></div>
          <p>{val}</p>
        </>
      );
    }
    
    export default CallbackTest;
    
  • 使用 useCallback、useMemo,会更麻烦一点,但是性能会有很大提升。

Memo

  • React.memo(Component, [areEqual(prevProps, nextProps]);

Hook 使用规则

  • 只在 React 函数中调用 Hook;
    • React 函数组件中;
    • React 自定义 Hook 中;
  • 只在最顶层使用 Hook。

自定义 Hook

  • 常用来解决(与状态相关的)逻辑复用问题;
  • 自定义 hook 就是一个普通函数,但是该函数必须以 use 开始命名;
    import { useCallback, useMemo, useState } from "react";
    
    function useCount(init) {
      const [count, setCount] = useState(init);
      const plusCount = useCallback(() => {
        setCount(count + 1);
      }, [count]);
      const minusCount = useCallback(() => {
        setCount(count - 1);
      }, [count]);
      return [count, plusCount, minusCount];
    }
    function HookTest() {
      const [count, plusCount, minusCount] = useCount(1);
      const [num, plusNum, minusNum] = useCount(5);
      const [val, setVal] = useState('');
      const valueChange = useCallback(({target}) => {
        console.log('valueChange');
        setVal(target.value)
      }, [val]);
      const maxValue = useMemo(() => {
        console.log('maxValue');
        return count>num?"count":"num";
      }, [num, count]);
      return (  
        <>
          <h3>MemoTest</h3>
          <div>Count: {count}; <button onClick={plusCount}>+</button>, <button onClick={minusCount}>-</button></div>
          <div>Num: {num}; <button onClick={plusNum}>+</button>, <button onClick={minusNum}>-</button></div>
          <p>当前比较大的值为:{maxValue}</p>
          <div><input value={val} onChange={valueChange} /></div>
          <p>{val}</p>
        </>
      );
    }
    
    export default HookTest;
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容