likes
comments
collection
share

(react知识记录一)react 18中hook函数超级详解

作者站长头像
站长
· 阅读数 11

前言

在之前学习和写react项目过程中没有完整的记下react中hook函数的使用,今天又特地的去查看官网、翻阅资料,怕以后遗忘特此在这里记录一下。备注:这里使用到的组件都是函数组件,不是类组件。

正文

useState(常用)

使用: let [number,setNumber] = useState(0)

如上所示,useState通过传入一个初始值,返回一个数组,这个数组的第一项是设置的当前组件的值,第二项是修改这个值的方法。如上,如果你直接修改这个值(number=1),就不会触发函数组件重新渲染,但这个状态值的确是改了的。想改变值并且触发更新,那就需要使用setNumber进行修改值,这样修改值会自动让组件重新渲染。

值的注意的是,在使用useState的时候,useState内置了一部分优化,它会比较你传入的值在更新前和更新后是否一样,比如你通过setNumber修改的number两次的值都是1,那么该函数组件并不会重新渲染,这个过程是浅比较的。正因为这样当你的状态值是一个引用数据类型的话,比如一个数组,如果你直接使用push、pop这样改变数组内的元素,那同样不会触发组件的重渲染,因为useState内部通过浅比较判断更新前和更新后的引用地址是同一个。

let [arr,setArr] = useState([0])
//这样不会触发
arr.push(1)
setArr(arr)

//这样会触发更新,因为引用地址变了
setArr([...arr,1])

还值得注意的是,一定要记住,函数组件每次重新渲染都是会重新执行这个函数组件的,也就是每次更新组件都会产生一个全新的“私有上下文”。执行下一段代码你最后得到的number的值始终是1,而不是我们想要的10,因为里面的number的值始终都是我们第一次执行函数组件产生的闭包里的number。

let [number,setNumber] = useState(0)
for(let i = 0;i < 10;i++){
    setNumber(number+1)
}

如果想实现让number变为10,可以这样实现,每次调用setNumber时其实不会立即去重新执行函数组件,我认为它会进入一个类似栈的结构里,当所有的setNumber入栈后统一去执行setNumber,而每一次执行setNumber后会将本次修改的值传给下一个setNumber,所以prev拿到的值就是最新的number值:

setNumber(prev => {
    // prev:存储上一次的状态值
    console.log(prev);
    return prev + 1; //返回的信息是我们要修改的状态值
});

useEffect(常用)

useEffect相当于充当着周期函数的职能,如下所示:

useEffect(()=>{
    console.log("useEffect") //等价于 componentDidUpdate,在组件挂载完成后执行
})

useEffect(()=>{
    console.log("useEffect") //等价于 componentDidMount,在组件每次更新执行
},[])

useEffect(()=>{
    return ()=>{
        console.log("useEffect") //等价于 componentWillUnmount,在组件将要销毁的时候执行
    }
})

但有所不同的是,useEffect的第二项参数可以传入依赖项,当依赖项改变的时候再执行传入的callback,这有点vue中watch的味道了。

useLayoutEffect

useLayoutEffect与useEffect类似,但不同之处在于执行的阶段,在这里我把视图更新步骤分为以下四步。

  • 第一步:基于babel-preset-react-app把JSX编译为createElement格式
  • 第二步:把createElement执行,创建出virtualDOM
  • 第三步:基于root.render方法把virtualDOM变为真实DOM对象「DOM-DIFF」
  • 第四步:浏览器渲染和绘制真实DOM对象

在这里设置这样一个使用场景,我们通过一个点击div来回切换背景色,在运行下面代码后可以发现,每次点击最终都会变成红色,但变成红色过程中会存在闪烁,一瞬间变成绿色再变成红色。而如果将useEffect替换成useLayoutEffect就不会出现闪烁这种情况。

function App() {
  let [flag, setFlag] = useState(false);
  useEffect(() => {
      if (!flag) {
          setFlag(true);
      }
  }, [flag]);
  return <div
      style={{
            backgroundColor: flag  ? 'red' : 'green'
        }}
      onClick={() => {
          setFlag(false);
      }}>
      {+flag}
  </div>;
};

出现这样的原因是因为useLayoutEffect和useEffect执行的时间不同。在函数组件执行的时候,遇到useEffect或者useLayoutEffect并不会第一时间执行,一般情况下会按照代码从上往下的将他们放入一个effect链表当中,等全部放完后再统一依照先后进入链表的顺序执行。useEffect会在上面说的视图更新步骤中的第四步中同时执行的,这里,它相当于异步函数,所有执行的时候不会阻止第四步中浏览器的绘制。而对于useLayoutEffect却是在第三步和第四步之间执行的,并且useLayoutEffect执行中会阻塞第四步操作,因为它相当于同步执行函数,需要等它完全执行完成后,才会进入第四步(如果useLayoutEffect执行过程中产生了修改dom或者改变了视图依赖项的操作会重新从第一步执行)。所以这就是为什么使用useLayoutEffect不会产生闪烁,因为useLayoutEffect会等自身的callback执行完成后再去通知视图更新。

useRef(常用)

在函数组件中,可以基于useRef获取DOM元素

function App() {
  const [num, setNum] = useState(0);
  const btnBox = useRef(null); 

  useEffect(() => {
    console.log(btnBox.current);
  }, [num]);

  return (
    <div>
      <span>{num}</span>
      <button ref={btnBox} onClick={() => setNum(num + 1)}>
        按钮
      </button>
    </div>
  );
}

值得注意的是,createRef也可以在函数组件中创建ref,但是与useRef唯一不同是,createRef会在函数组件每次更新的时候都会创建一个ref,而useRef在第一次创建ref后,后面函数组件更新后获取的ref都指向第一次函数组件产生的ref,并不会每次函数组件更新重新创建ref。所以在函数组件中使用useRef性能方面会好一些。

useImperativeHandle

useImperativeHandle是获取函数子组件内部状态或者方法的hook,使用方法:

const Child = forwardRef(function Child(props, ref) {
  let [text, setText] = useState("child");
  const submit = () => {
    console.log("submit")
  };

  useImperativeHandle(ref, () => {
    return {
      text,
      submit,
    };
  });

  return (
    <div className="child-box">
      <span>Child</span>
    </div>
  );
});

const App = function Demo() {
  let x = useRef(null);
  useEffect(() => {
    console.log(x.current);
  }, []);

  return (
    <div className="demo">
      App
      <Child ref={x} />
    </div>
  );
};

useMemo(常用)

useMemo具备“计算缓存”,类似于vue里面的computed。在其依赖项没有改变的情况下,传给useMemo的callback就不会重新执行,如下所示,当number1或者number2的值改变的时候,sum才会改变,而改变x的值,不会改变sum,也不会重新执行useMemo的callback。

const App = function Demo() {
  let [number1, setNumber1] = useState(10),
      [number2, setNumber2] = useState(5),
      [x, setX] = useState(0);
  
  let sum = useMemo(() => {
      console.log("change sum")
      return number1 + number2
  }, [number1, number2]);

  return <div >
      <div >
          <p>number1:{number1}</p>
          <p>number2:{number2}</p>
          <p>sum:{sum}</p>
          <p>x:{x}</p>
      </div>
      <div className="footer">
          <Button type="primary" onClick={() => setNumber1(number1 + 1)}>number1增加</Button>
          <Button type="primary" danger onClick={() => setNumber2(number2 + 1)}>number2增加</Button>
          <Button onClick={() => setX(x + 1)}>x改变</Button>
      </div>
  </div>;
};

useMemo还有一个妙用就是可以让避免子组件无意义的重渲染。每次更新父组件的时候,子组件依赖的内容是否被修改都会触发子组件的重新渲染,这时可以用useMemo来实现当子组件依赖于父组件的某个依赖改变时再重新渲染,用法如下:

const Child1 = function Child1(props) {
  const { number1 } = props;
  console.log("Child1 change");
  return <div>Child1:{number1}</div>;
};

const Child2 = function Child2(props) {
  const { number2 } = props;
  console.log("Child2 change");

  return <div>Child2:{number2}</div>;
};

const App = function Demo() {
  let [number1, setNumber1] = useState(10),
    [number2, setNumber2] = useState(5),
    [x, setX] = useState(0);

  return (
    <div>
      {useMemo(() => {
        return (
          <>
            <Child1 number1={number1} />
          </>
        );
      }, [number1])}

      <Child2 number2={number2} />
      <div className="footer">
        <Button type="primary" onClick={() => setNumber1(number1 + 1)}>
          number1增加
        </Button>
        <Button type="primary" danger onClick={() => setNumber2(number2 + 1)}>
          number2增加
        </Button>

        <Button onClick={() => setX(x + 1)}>x改变</Button>
      </div>
    </div>
  );
};

当修改number1的时候才会触发Child1的更新,改变number2就不会触发。这里也给出一个React提供的高阶组件memo的实现,可以达到上面同样的效果:

const Child1 = React.memo(
  function Child1(props) {
    const { user } = props;
    console.log("Child1 change");
    return <div>Child1:{user.name}</div>;
  }
)

值得注意的是,React.memo和useMemo中的依赖项,都是将更新前的依赖和更新后的依赖进行浅比较的,这意味着如果传入的依赖项是引用类型且没改变其引用地址,只是改变里面属性的话,是不会触发useMemo或者memo的。

useCallback(常用)

useCallback可以缓存一个函数,在其依赖项不变的情况下,useCallback返回的函数的引用地址不会产生变化

const handle = useCallback(() => {
        //一部分函数逻辑
        console.log("handle")
 }, []);

useCallback相当于useMemo的语法糖,也就是说useCallback能实现的,useMemo也能实现,useMemo实现上述代码如下:

const handle = useMemo(()=> {
    return ()=>{
        //一部分函数逻辑
        console.log("handle")
    }
},[])
  1. 子组件没有从父组件传入的props或者传入的props仅仅为简单数值类型使用memo即可。

⚠️注意:这个高阶组件在function component以及class component都可以使用哦

  1. 子组件有从父组件传来的方法时,在使用memo的同时,使用useCallback包裹该方法,传入方法需要更新的依赖值。
  2. 子组件有从父组件传来的对象和数组等值时,在使用memo的同时,使用useMemo以方法形式返回该对象,传入需要更新的依赖值。

useContext

因为在React中,数据的传递是单向的,父组件通过属性将数据传递给子组件,子组件从props获取被传递的数据,但是,当组件嵌套的多了后,这样一层层传递数据就显得很繁琐了,所以React提供了一个Context Api,使用Context可以避免的组件的层层props嵌套的问题,使用如下:

const ThemeContext = React.createContext();

const App = function App() {
    let [number,setNumber] = useState(0)

    const changeNumber = (n)=>{
      setNumber(n)
    }
    return <ThemeContext.Provider
        value={{
            number,
            changeNumber
        }}>
        <div >
           
            <Child />
        </div>
    </ThemeContext.Provider>;
};

const Child = function Child() {
    let { number, changeNumber } = useContext(ThemeContext);
    return <div className="main">
        <p>child:{number}</p>
        <Button onClick={()=>{changeNumber(++number)}} >点击</Button>
    </div>;
};

useReducer

useReducer相当于是对useState的升级处理,useReducer的思想采用了redux中对状态的管理,每次改变一个状态的时候通过派发一个action去通知reducer去更新状态,但一般使用useReducer的场景是一个组件中含有大量的状态、大量的修改状态的逻辑,才会选择采用useReducer进行管理,使用如下:

const initialState = {
    num: 0
};
const reducer = function reducer(state, action) {
    state = { ...state };
    switch (action.type) {
        case 'plus':
            state.num++;
            break;
        case 'minus':
            state.num--;
            break;
        default:
    }
    return state;
};

const A1 = function A1() {
    //state:获取的状态 dispatch:负责派发action通知reducer更新数据
    //reducer:状态管理者,根据传入的action来进行不同的数据操作 initialState:初始化状态值
    let [state, dispatch] = useReducer(reducer, initialState);

    return <div className="box">
        <span>{state.num}</span>
        <br />
        <button onClick={() => {
            dispatch({ type: 'plus' });
        }}>增加</button>
        <button onClick={() => {
            dispatch({ type: 'minus' });
        }}>减少</button>
    </div>;
};

自定义Hook

使用自定义hook可以将某些组件逻辑提取到可重用的函数中,或者单独抽取某个功能到另一个文件,显得主文件不那么臃肿,易读。如下实现一个简单的自定义hook,在useState中,如果创建了一个对象类型的状态,如果只是修改对象中某个属性,如果这样写会导致name属性丢失掉,因为setObj会将传入的对象进行全部替换更新,而不是部分替换更新:

这里实现一个usePartState来可以让对象状态进行部分更新:

const usePartState = function usePartState(initial) {
    let [state, setState] = useState(initial);
    const setPartState = (partState) => {
        setState({
            ...state,
            ...partState
        });
    };
    return [state, setPartState];
};
function App() {
    let [state, setState] = usePartState({
        x: 10,
        y: 20
    });
    return <div>
        <span>{state.x}</span>
          
        <span>{state.y}</span>
        <button onClick={() => {
            setState({
                x: state.x + 1
            });
        }}>处理x</button>
    </div>;
};

注意的是,如果是自定义hook,最好以use进行开头,因为采用use开头,react可以识别到useXXX是一个hook函数,会帮忙检查一些使用上的错误。

转载自:https://juejin.cn/post/7221047453963812920
评论
请登录