react hook 学习

309 阅读1分钟

Hook基础学习

1. useState()

const [state, setState] = useState(initalState) // 解构赋值

2. useEffect()

useEffect( () => {}, ?[dependencies]); // 第二项非必填

function Demo3() {
    const [data, setData] = useState(0);
    useEffect(() => {
        console.log("useEffect--[]");
        setData(20000);
        setTimeout(() => setData(10000), 10000);
        console.log(data);
    }, []);
    useEffect(() => {
        console.log("useEffect ---> 无依赖");
    });
    useEffect(() => {
        console.log("useEffect 依赖data: data发生了变化");
    }, [data]);
    return (
        <div>
            {(() => {
                console.log("render");
                return null;
            })()}
            <p>data: {JSON.stringify(data)}</p>
        </div>  
    );   
}

输出结果为 image.png

  • 可见setState是异步执行的
  • useEffect 是在render之后按先后顺序生效执行的
  • useEffect 在没有任何依赖的情况下(useEffect( () => {})),render后每次都执行
  • 依赖[]可以实现类似componentDidMount的作用,render后只执行一次

3. useContext

跨组件数据共享

const value = useContext(MyContext);
// MyContext 为 context 对象(React.createContext 的返回值) 
// useContext 返回MyContext的返回值。
// 当前的 context 值由上层组件中距离当前组件最近的<MyContext.Provider> 的 value prop 决定。

eg.

import React, { useContext, useState } from “react”;
const MyContext = React.createContext();
function Demo5() {
  const [value, setValue] = useState("init");
  console.log("Demo5");
  return (
    <div>
      {(() => {
        console.log("render");
        return null;
      })()}
      <button onClick={() => {
        console.log('click:更新value')
        setValue(`${Date.now()}_newValue`)
      }}>
        改变value
      </button>
      <MyContext.Provider value={value}>
        <Parent1 />
        <Parent2 />
      </MyContext.Provider>
    </div>
  );
}
function Parent1() {
    console.log("Parent1");
    return <Child1 />;
}

function Parent2() {
    console.log("Parent2");
    return <Child2 />;
}

function Child1() {
  const value = useContext(MyContext);
  console.log("Child1-value", value);
  return <div>Child1-value: {value}</div>;
}

function Child2(props) {
  console.log('Child2')
  return <div>Child2</div>;
}

输出结果:

image.png

  • <MyContext.Provider> 的 value 发生变化时候, 包裹的组件无论是否订阅content value,所有组件都会从新渲染,所以<MyContext.Provider>包裹的越多,层级越深,性能会造成影响。
  • parent2 没有订阅value不应该rerender, 如何避免不必要的render?可以使用React.memo优化。
const Parent2 = React.memo(() => {
    console.log("Parent2");
    return <Child2 />;
})

输出结果:

image.png 注意: 通过memo第二个参数可以控制对比过程。(具体看下memo的使用,待补充。。)

4. useRefuseImperativeHandle

useRef使用场景

- 用于获取并存放组件的 dom 节点, 以便直接对 dom 节点进行原生的事件操作
  • 操作原生子组件
    • 一共分两个步骤:
      1. useRef创建一个ref对象
      2. ref={xx}挂到react元素上
function CustomTextInput(props) {
    // 这里必须声明 textInput,这样 ref 才可以引用它
    const textInput = useRef(null);//创建一个包含current属性的对象
    
    console.log(textInput);
    function handleClick() {
      textInput.current.focus();
    }
  
    return (
      <div>
        <input type="text" ref={textInput} />//挂到内部dom上
        <input type="button" value="Focus the text input" onClick={handleClick} />
      </div>
    ); 
}
  • 操作自定义自组件(需要forwardRef配合使用)

    • forwardRef是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
    • 第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。 上面的例子可以写成:
    function CustomTextInput(props) {
      // 这里必须声明 textInput,这样 ref 才可以引用它
      const textInput = useRef(null);
    
      console.log(textInput);
      function handleClick() {
        textInput.current.focus();
      }
      return (
        <div>
          <Child ref={textInput} />  //**依然使用ref传递**
          <input type="button" value="Focus the text input" onClick={handleClick} />
        </div>
      );
    }
    const Child = forwardRef((props, ref) => {  //** 看我 **
      return <input type="text" ref={ref} />;//** 看我挂到对应的dom上 **
    });
    
    

    forwardRef的做法本身没有什么问题, 但是我们是将子组件的DOM直接暴露给了父组件:

    • 直接暴露给父组件带来的问题是某些情况的不可控
    • 父组件可以拿到DOM后进行任意的操作
    • 我们只是希望父组件可以操作的focus,其他并不希望它随意操作其他方法
    • useImperativeHandle介绍 useImperativeHandle(ref, createHandle, [deps])
    通过useImperativeHandle可以只暴露特定的操作
    1.通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
    2.所以在父组件中, 调用inputRef.current时, 实际上是调用返回的对象
    

    上面的例子可以写成:

    const Child = forwardRef((props, ref) => {
        const inputRef = useRef(null);
        useImperativeHandle(ref, () => ({
            focus: () => {
                inputRef.current.focus();
            }
        }));
        return <input type="text" ref={inputRef}/>;
    });
    
- 利用 useRef 解决由于 hooks 函数式组件产生闭包时无法获取最新 state 的问题。

看下面例子,点击add 在state 为6时,点击输出,继续点击add ,输出时 state时多少呢?是界面上 tate 的实时状态 ? 还是在点击 button 时tate 的快照 ?

function Demo() {
    const [stateNumber, setStateNumber] = useState(0);
    function alert() {
        setTimeout(() => console.log(stateNumber), 2000);
    }
    function add() {
        setStateNumber(stateNumber + 1);
    }
    return (
        <div>
            <button onClick={alert}>输出</button>
            <h4>state: {stateNumber}</h4>
            <button onClick={add}>add</button>
        </div>
    );
}

屏幕录制2021-10-13 下午5.gif 最终结果是,当点击add增加state到6时,再继续增加state,点击输出,输出的值为6而不是12. 原因是,当点击add 更新state状态时,React 会重新渲染组件,会重新执行一遍add函数,里面的局部变量包括state会再一次创建并且被赋值,当state = 6时,点击输出,此时,执行alert函数,state 为6,再次点击点击add 更新state状态时,局部变量包括state会再一次创建并且被赋值,而alert函数中state值依旧指向上一次的值,因此输出还是6。

function Demo() {
    const [stateNumber, setStateNumber] = useState(0);
    const res = useRef(0);
    useEffect(() => {
        res.current = stateNumber;
    });
    function alert() {
        setTimeout(() => console.log(res.current), 2000);
    }
    function add() {
        setStateNumber(stateNumber + 1);
    }
    return (
        <div>
            <button onClick={alert}>输出</button>
            <h4>state: {stateNumber}</h4>
            <button onClick={add}>add</button>
        </div>
    );
}

屏幕录制2021-10-13 下午6.gif 此时输出的结果为最新值,因为使用useRef,返回的始终是一个相同的引用,也就是说赋值的时候始终是对res的current属性进行赋值,因此add函数重新执行的时候,ref返回的值始终是对current的引用,没有变,因此可以返回最新值。

- 获取上一个值

利用useEffect每次render后才执行和useRef引用在整个生命周期中保持不变的特性,封装成自定义 获取上一个值的hook。

const usePrevProps = (value) => {
    const preRef = useRef(0);
    
    useEffect(() => {
        preRef.current = value;
    });
    
    return preRef.current;
};

tips

useRef相当于this,只是一个引用,在整个生命周期内保持不变,因此,当useRef的current属性改变时,并不会造成re-render,因此ref.current 不可以作为其他hooks(useMemouseCallbackuseEffect)的依赖项,也不会造成页面的主动渲染

function Demo() {
  const [minus, setMinus] = useState(0);
  const ref = useRef(null);
  const r = useRef(0);
  
  const handleClick = () => {
    setMinus(minus + 1);
  };

  console.log(`ref.current=${ref.current && ref.current.innerText}`)

  useEffect(() => {
    r.current += 1;
    if (r.current > 1) {
      console.log("r.current:" + r.current);
    }
  });
   
  // #1 uesEffect
  useEffect(() => {
    console.log(`denp[ref.current] >`, ref.current && ref.current.innerText);
  }, [ref.current]);

  // #2 uesEffect
  useEffect(() => {
    console.log(`denp[minus]>`, ref.current && ref.current.innerText);
  }, [minus]);

  return (
    <div className="App">
      {console.log("ref的current:", ref.current)}
      <h1 ref={ref}>Num: {minus}</h1>
      <h1>r的current:{r.current}</h1>
      <button onClick={handleClick}>Add</button>
    </div>
  );
}

输出分析:

  • 第一次输出: image.png 分析: 依赖项解析是在render阶段发生的,发生在ref.current更新之前,而useEffect是在render之后执行。此时r.current值为1,不符合输出条件。
  • 点击add之后,输出:

image.png

分析:console输出时,<h1>还没加载,此时ref.current值为<h1>Num: 0<h1>,所以 console.log输出ref.current=Num: 0,render时,effetc未运行,r.current值为1,因此屏幕显示r的current:1,render之后,effetc依次输出, #1 uesEffect的依赖项发生变化,输出当前dom,r.current+1,此时值为2。

  • 再次点击add后,输出: image.png

分析: 此时ref.current值为<h1>Num: 1<h1>,所以 #1 uesEffect的依赖项没有发生变化,故 #1 uesEffect的effect函数不会被执行。

5.