React Hooks学习-2

443 阅读6分钟

一、useReducer

1.基本用法

  • 引入useReducer以及创建初始值

  • 创建所有操作reducer(state,action)

  • 传给useReducer,得到读取数据和修改数据的API

  • 调用写({type:'操作类型'})

它是useState的替代,如果useState接受的参数复杂,推荐useReducer

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

const initFormData = {
  name: "",
  age: 18,
  nationality: "汉族"
};

function reducer(state, action) {
  switch (action.type) {
    case "patch":
      return { ...state, ...action.formData };
    case "reset":
      return initFormData;
    default:
      throw new Error();
  }
}

function App() {
  //传给useReducer,得到读写数据的API
  const [formData, dispatch] = useReducer(reducer, initFormData);

  const onSubmit = () => {};
  const onReset = () => {
    dispatch({ type: "reset" });
  };
    
  return (
    <form onSubmit={onSubmit} onReset={onReset}>
      <div>
        <label>
          姓名
          <input
            value={formData.name}
            onChange={e =>
              dispatch({ type: "patch", formData: { name: e.target.value } })
            }
          />
        </label>
      </div>
      <div>
        <label>
          年龄
          <input
            value={formData.age}
            onChange={e =>
              dispatch({ type: "patch", formData: { age: e.target.value } })
            }
          />
        </label>
      </div>
      <div>
        <label>
          民族
          <input
            value={formData.nationality}
            onChange={e =>
              dispatch({
                type: "patch",
                formData: { nationality: e.target.value }
              })
            }
          />
        </label>
      </div>
      <div>
        <button type="submit">提交</button>
        <button type="reset">重置</button>
      </div>
      <hr />
      {JSON.stringify(formData)}
    </form>
  );
}

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

效果:

image-20211211192209699.png

2.模拟redux的功能

基本使用:

  • 将数据集中到store对象
  • 所有操作放到reducer
  • 创建一个Context(读写接口可以传给子组件)
  • 创建对数据的读写API
  • 读写API放到Context
  • Context.Provider将Context提供给所有组件
  • 各个组件用useContext获取读写API

可以参考示例代码:codesandbox.io/s/priceless…

二、useEffect

作用:用于模拟生命周期DidMount和DidUpdate,如果同时存在多个useEffect,会按照出现次数执行

import React, { useState, useEffect } from 'react'

function LifeCycles() {
    const [count, setCount] = useState(0)

    // // 模拟 class 组件的 DidMount 和 DidUpdate
    // useEffect(() => {
    //     console.log('在此发送一个 ajax 请求')
    // })

    // // 模拟 class 组件的 DidMount
    // useEffect(() => {
    //     console.log('加载完了')
    // }, []) // 第二个参数是 [] (不依赖于任何 state)

    // // 模拟 class 组件的 DidUpdate
    // useEffect(() => {
    //     console.log('更新了')
    // }, [count, name]) // 第二个参数就是依赖的 state

    // 模拟 class 组件的 DidMount
    useEffect(() => {
        let timerId = window.setInterval(() => {
            console.log(Date.now())
        }, 1000)

        // 返回一个函数
        // 模拟 WillUnMount
        return () => {
            window.clearInterval(timerId)
        }
    }, [])

    function clickHandler() {
        setCount(count + 1)
    }

    return <div>
        <p>你点击了 {count} 次</p>
        <button onClick={clickHandler}>点击</button>
    </div>
}

export default LifeCycles

总结:

  • 模拟componentDidMount - useEffect依赖[]
  • 模拟componentDidUpdate - useEffect依赖[a,b]
  • 模拟componentDidMount和componentDidUpdate - useEffect无依赖
  • 模拟componentWillUnMount - useEffect中返回一个函数

useEffect让纯函数有了副作用

  • 默认情况下,执行纯函数,输入参数,返回结果,无副作用
  • 所谓副作用就是对函数之外造成影响,如设置全局定时任务
  • 而组件需要副作用,所以需要useEffect

【注意】模拟WillUnMount,但不是说完全相等

import React, { useState, useEffect } from 'react'

function FriendStatus({ friendId }) {
    const [status, setStatus] = useState(false)

    // DidMount 和 DidUpdate
    useEffect(() => {
        console.log(`开始监听 ${friendId} 在线状态`)

        // 【特别注意】
        // 此处并不完全等同于 WillUnMount
        // props 发生变化,即更新,也会执行结束监听
        // 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
        return () => {
            console.log(`结束监听 ${friendId} 在线状态`)
        }
    })

    return <div>
        好友 {friendId} 在线状态:{status.toString()}
    </div>
}

export default FriendStatus

useEffect中返回函数fn:

  • useEffect依赖为[],组件销毁执行fn,等于WillUnMount
  • useEffect无依赖或者依赖[a,b],组件更新时执行fn

三、useLayoutEffect

  • useEffect在浏览器渲染完成之后执行
  • useLayoutEffect在浏览器渲染之前执行,如果执行的代码过多,会延长用户看到画面的时间
  • 当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用useLayoutEffect

四、useMemo

1.React.memo

React会有多余的render,父组件的数据改变,虽然传给子组件的数据(Props)没变,但会再次渲染子组件

import React 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);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m}/>
    </div>
  );
}

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

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

动画.gif

可以看到父组件的n改变了,虽然传递给子组件的m没有变化,但仍然触发子组件的重新渲染

解决方式:使用React.memo包装子组件

...

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child2 data={m}/>
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log('假设这里有大量代码')
  return <div>child: {props.data}</div>;
}
const Child2 = React.memo(Child);

...

但是这种方法仍然存在问题:如果添加监听函数,APP组件运行再次执行,生成新的函数,新旧函数虽然功能相同,但地址不一样,触发重新渲染,此时子组件也会再次渲染

import React 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);
  };
  
  //添加函数,每次点击打印m的值
  const onClickChild = () => {
    console.log(m);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
    </div>
  );
}

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

const Child2 = React.memo(Child);

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

效果:

1.gif

此时可以考虑搭配useMemo配合使用

2.useMemo和useCallback

  • 第一个参数是()=>value
  • 第二个参数是依赖[m,n]
  • 只有当依赖变化时候,才会计算出新的value,如果依赖不变,那么就重用之前的value
  • 如果你的value是一个函数,那么就要写成一个返回函数的函数
useMemo(()=>(x)=>console.log(x),[m])

这样就可以使用useCallback

usecallBack(x=>console.log(x),[m]) 

对上面的案例使用useMemo进行优化

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]); 
  
  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 rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

效果:

3.gif

总结:

  • React默认更新所有子组件
  • class组件使用SCU和PureComponent做优化
  • Hooks中使用useMemo,优化的原理相同

五、forwardRef

props是不支持传递ref的

如果一个函数组件需要接受传递过来的ref,那么需要把自己用forwarRef包装起来

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

function App() {
  const inputRef = useRef(null);
  const onClick = () => {
    inputRef.current.focus();
  };
  return (
    <div className="App">
      <!--父组件向子组件传递Ref-->
      <Button3 ref={inputRef}></Button3>
      <button onClick={onClick}>父组件点击聚焦</button>
    </div>
  );
}

//子组件用forwardRef包裹起来,然后通过参数获取父组件传递过来的inputRef
const Button3 = React.forwardRef((props, inputRef) => {
  const onClick = () => {
    inputRef.current.focus();
  };
  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={onClick}>子组件点击聚焦</button>
    </div>
  );
});

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

效果:

3-16392349941031.gif

六、useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])
  • ref:定义 current 对象的 ref
  • createHandle:一个函数,返回值是一个对象,即这个 ref 的 current
  • 对象 [deps]:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的实例属性输出到父组件

可以理解为setRef,对传递过来的ref进行设置

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

function App() {
  const buttonRef = useRef(null);
  useEffect(() => {
    console.log(buttonRef.current);
  });
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按钮</Button2>
      <button
        className="close"
        onClick={() => {
          console.log(buttonRef);
          buttonRef.current.remove();
        }}
      >
        remove
      </button>
    </div>
  );
}

const Button2 = React.forwardRef((props, ref) => {
  const realButton = createRef(null);
  const setRef = useImperativeHandle;
  setRef(ref, () => {
    return {
      remove: () => {
        realButton.current.remove();
      },
      realButton: realButton
    };
  });
  return <button ref={realButton} {...props} />;
});

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

可以理解为子组件通过useImperativeHandle内部设置真正的ref,然后通过useImperativeHandle设置假的ref给父组件引用

七、Hooks的踩坑

1.useState的初始化值,只有第一次有效

函数组件第一次执行的时候,会初始化state的值,如果组件再次执行(比如说数据变化),只恢复初始化的state的值(即不能通过初始化的方式来修改state的值),要修改state的值只能通过setState来修改state的值

2.依赖为[]的话,useEffect内部不能修改state的值

依赖为[],useEffect模拟的是DidMount,数据变化的时候不会重新执行effect函数

可以设置useRef来解决

import React, { useState, useRef, useEffect } from 'react'
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
    const [count, setCount] = useState(0)

    // 模拟 DidMount
    const countRef = useRef(0)
    useEffect(() => {
        console.log('useEffect...', count)

        // 定时任务
        const timer = setInterval(() => {
            console.log('setInterval...', countRef.current)
            // setCount(count + 1)
            setCount(++countRef.current)
        }, 1000)

        // 清除定时任务
        return () => clearTimeout(timer)
    }, []) 

    // 依赖为 [] 时: re-render 不会重新执行 effect 函数
    // 没有依赖:re-render 会重新执行 effect 函数

    return <div>count: {count}</div>
}

ReactDOM.render(<App />, rootElement);

3.useEffect可能出现死循环

useEffect的依赖如果是数组或者对象,可能会造成死循环(引用类型,例如两个空对象或空数组之间是不相等的,所以依赖会一直在变化),所以要把数据或者对象中的属性拆出来放到依赖中