React Hooks详解

137 阅读5分钟

React Hooks:

状态 useState
Redux useReducer
副作用 useEffect -- useLayoutEffect    
上下文 useContext
记忆 useMemo -- 回调useCallback
引用 useRef -- uselmperativeHandle
自定义 Hook -- useDebugValue

useState

状态

使用步骤

const [n,setN] = React.useState(0)
const [user,setUser] = React.useState({name:'F'})
  • 注意事项1:不可局部更新 如果state是一个对象,则不能局部setState,因为useState不会帮我们合并属性【useReducer也不会合并属性】,需要用...复制属性

uTools_1688520695977.png

  • 注意事项2:地址要变 setState(obj)如果obj地址不变,那么React就认为数据没有变化

uTools_1688521027352.png

想要user地址发生变化,只需要新建setUser对象 uTools_1688520938906.png

  • 特殊使用:函数 useState接受函数,该函数返回初始state且只执行一次【写成对象则js引擎每次都会解析一遍】。该方法使用较少
const [state, setState] = useState(()=>{
    return initialState
})

uTools_1688521699920.png

uTools_1688521739438.png

setState接收函数(示例)

setN(i => i + 1)
setN(i => i + 1)

uTools_1688521886128.png

useReducer

  • useReducer是useState的复杂版,用来践行Flux/Redux思想

使用步骤:

  1. 创建初始值initialState
  2. 创建所有操作reducer(state, action)
  3. 传给useReducer,得到读写API
  4. 调用写({type:'操作类型'})
import React, { useReducer } from "react";
import ReactDOM from "react-dom";

const initial = {
  n: 0
};

const reducer = (state, action) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multi") {
    return { n: state.n * 2 };
  } else {
    throw new Error("unknown type");
  }
};

function App() {
  const [state, dispatch] = useReducer(reducer, initial);
  const { n } = state;
  const onClick = () => {
    dispatch({ type: "add", number: 1 });
  };
  const onClick2 = () => {
    dispatch({ type: "multi", number: 2 });
  };
  return (
    <div className="App">
      <h1>n: {n}</h1>

      <button onClick={onClick}>+1</button>
      <button onClick={onClick2}>*2</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

image.png

代替Redux

useContext

全局变量是全局的上下文;上下文是局部的全局变量。useContext不是响应式,是通过逐级传递的参数

使用方法:

  1. 使用C = createContext(initial)创建上下文
  2. 使用<C.provider>圈定作用域
  3. 在作用域内使用useCotext(C)来使用上下文
import React, { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const C = createContext(null);

function App() {
  console.log("App 执行了");
  const [n, setN] = useState(0);
  return (
    <C.Provider value={{ n, setN }}>
      <div className="App">
        <Baba />
      </div>
    </C.Provider>
  );
}

function Baba() {
  const { n, setN } = useContext(C);
  return (
    <div>
      我是爸爸 n: {n} <Child />
    </div>
  );
}

function Child() {
  const { n, setN } = useContext(C);
  const onClick = () => {
    setN(i => i + 1);
  };
  return (
    <div>
      我是儿子 我得到的 n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useEffect

副作用,即对环境的修改,如document.title。实际叫做afterRender更好,每次render之后运行

使用方法(这三种可同时存在):

  • 作为componentDidMount使用,[]作为第二个参数
  • 作为componentDidupdate使用,可指定依赖
  • 作为componentWillUnmount使用,通过render垃圾回收

特点:如果出现多个useEffect,会按照出现的次序执行

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0);
  const onClick = () => {
    setN((n) => n + 1);
  };
  
  const afterRender = useEffect;
  afterRender(() => {
    console.log("第一次渲染后执行这句话");
  }, []);    
  // []里的变量变化时再次执行,即[]为空不再执行
  
  afterRender(() => {
    console.log("第1,2,3。。。次渲染后执行");
  });
  // 每次都会执行
  
  afterRender(() => {
    if (n !== 0) {
      console.log("n变化后渲染后执行");
    }
  }, [n]);
  // []里的变量变化时再次执行
  
  return (
    <div>
      n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

image.png

useLayoutEffect(优先使用useEffect,因为会影响渲染的效率)

特点:

  • useLayoutEffect总是比useEffect优先执行
  • useLayoutEffect里的任务最好影响了Layout

布局副作用与副作用的区别:useEffect会在浏览器第一次render之后执行,useLayoutEffect会在VNDOM生成DOM之后浏览器render之前执行

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const BlinkyRender = () => {
  const [value, setValue] = useState(0);

  useEffect(() => {
    document.querySelector('#x').innerText = `value: 1000`
  }, [value]);

  return (
      <div id="x" onClick={() => setValue(0)}>value: {value}</div>
  );
};

ReactDOM.render(
    <BlinkyRender />,
    document.querySelector("#root")
);

image.png

以上代码,useEffectuseLayoutEffect看到的不同效果:前者会出现value由0变1000的闪烁画面,后者直接渲染出1000 image.png

useMemo

特点:

  • 一般和React.memo()一起使用,用于在props变化时执行的操作
  • 第一个参数是 () => value()是固定写法
  • 第二个参数是[m,n]
  • 只有当依赖变化时,才会计算新的value
  • 依赖不变,就重用之前的value

注意:

  • 如果你的value是一个函数,就要写成返回函数的函数useMemo(() => (x) => console.log(x))
  • 以上写法如此丑陋,这样就引出了useCallback
import React, { useMemo } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClick2 = () => {
    setM(m + 1);
  };
  const onClickChild = useMemo(() => {
    const fn = div => {
      console.log("on click child, m: " + m);
      console.log(div);
    };
    return fn;
  }, [m]); // 这里呃 [m] 改成 [n] 就会打印出旧的 m
  return (
      <div className="App">
        <div>
          <button onClick={onClick}>update n {n}</button>
          <button onClick={onClick2}>update m {m}</button>
        </div>
        <Child2 data={m} onClick={onClickChild} />
      </div>
  );
}

/*************************************/
function Child(props) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div onClick={e => props.onClick(e.target)}>child: {props.data}</div>;
}

const Child2 = React.memo(Child);
/*************************************/

/* 以上写法可简化如下
const Child2 = React.memo((props) => {
    console.log("child 执行了");
    console.log("假设这里有大量代码");
    return <div onClick={e => props.onClick(e.target)}>child: {props.data}</div>;
});
*/
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

实现效果:只有初次渲染和改变依赖的props(m),才会执行函数

image.png

useCallback(useMemo的语法糖)

  • 用法:useMemo(() => x => log(x), [m]) 等价于 useCallback(x => log(x), [m]

useRef

不同于state,useRef每次都是同一个值,在组件不断render时保持不变

用法:

  • 初始化:const count = useRef(0)
  • 读取:count.current,其中current保证了两次ref为同一个值
import React, { useRef, useEffect, useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [n,setN] = useState(0);
  const count = useRef(0);
  const onClick = () => {
    setN(n + 10);
  }
  useEffect(() => {
    count.current += 1;
    console.log(count.current)
  })
  return (
      <div className="App">
        <div>
          <button onClick={onClick}>update: {n}</button>
        </div>
      </div>
  )
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

实现效果:点击打印count的值

image.png

实现原理:每次render后count是不同的,但指向同一个内存对象current,保证了ref为同一值

image.png

*更新UI

方法一:useEffect更新UI【见上面的案例】

方法二:使用useState

当不使用setN这个方法来改变数据,而直接进行数据变化操作时,重新渲染UI可以用setN手动更新

import React, { useRef, useState } from "react";
import ReactDOM from "react-dom";

function App() {
    const [n, _setN] = useState(0);
    const count = useRef(0);

    const onClick = () => {
        count.current += 1;
        _setN(Math.random())
        console.log(count.current)
    }

    return (
        <div className="App">
            <div>
                <button onClick={onClick}>update: {count.current}</button>
            </div>
        </div>
    )
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

实现效果:点击更新UI

image.png

小结

useStateuseReducer: n每次都变

useMemouseCallback: [m]变化时,fn变

useEffect:第二个参数为空每次都变,不为空参数变化时fn变

useRef永远不变

forwardRef

ref不能直接当作porps传给函数组件,只能传给类组件

因此需要 forwardRefprops 可以传 ref

import React, { useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

function App() {
    const buttonRef = useRef(null);
    return (
        <div className="App">
            <Button3 ref={buttonRef}>按钮</Button3>
        </div>
    );
}

const Button3 = React.forwardRef((props, ref) => {
    return <button className="red" ref={ref} {...props} />;
});

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

uselmperativeHandle

uselmperativeHandle 实际叫做 setRef 更好,用于自定义 ref 的属性

自定义hook

可以用于封装数据,将原生hook封装到自定义hook里

Stale Closure

过时闭包