React 内置常用 Hooks

170 阅读8分钟

State Hook

State Hook:是一个在函数组件中使用的函数(useState),用于在函数组件中的使用状态

useState

  • 函数有一个参数,这个参数的值表示状态的默认值

  • 函数的返回值是一个数组,该数组一定包含两项

    第一项:当前状态的值

    第二项:改变当前状态的函数

一个函数组件中可以有多个状态,这种做法非常有利于横向切分关注点

注意细节

  1. useState最好写到函数的起始位置,便于阅读

  2. useState严禁出现在代码块(判断、循环)中

  3. useState返回的函数(数组的第二项),引用不变(节约内存空间)

  4. 使用函数改变数据,若数据和之前的数据完全相等(使用Object.is比较),不会导致重新渲染,以达到优化效率的目的

  5. 使用函数组件改变数据,传入的值不会和原来的数据进行合并,而是直接替换

  6. 如果要实现强制刷新

     1. 类组件:使用forceUpdate函数
     2. 函数组件:使用一个空对象的useState
    
  7. 如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象

  8. 和类组件的状态一样,函数组件中要改变状态可能是异步的(在DOM事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。(如这一点不能理解,请理解下面的例子

export default function App() {
    console.log("App render")
    const [n, setN] = useState(0); //使用一个状态,该状态的默认值是0
    return <div>
        <button onClick={() => {
            // setN(n - 1);
            // setN(n - 1);
            setN(prevN => prevN - 1); //传入的函数,在事件完成之后统一运行
            setN(prevN => prevN - 1);
        }}>-</button>
        <span>{n}</span>
        <button onClick={() => {
            // setN(n + 1) //不会立即改变,事件运行完成之后一起改变
            // setN(n + 1) //此时,n的值仍然是0

            setN(prevN => prevN + 1); //传入的函数,在事件完成之后统一运行
            setN(prevN => prevN + 1);
        }}>+</button>
    </div>
}

Effect Hook

Effect Hook:用于在函数组件中处理副作用

副作用:

  1. ajax请求
  2. 计数器
  3. 其他异步操作
  4. 更改真实的DOM对象
  5. 本地存储
  6. 其他会对外部产生影响的操作

useEffect:该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数

注意细节

  1. 副作用函数的运行时间点,是在页面完成真实的UI渲染之后。因此它的执行是异步的,并且不会阻塞浏览器

     1. 与类组件中componentDidMount和componentDidUpdate的区别
     2. componentDidMount和componentDidUpdate,更改了真实DOM,但是用户还没有看到UI更新,同步的
     3. useEffect中的副作用函数,更改了真实DOM,并且用户已经看到了UI更新,异步的
    
  2. 每个函数组件中,可以多次使用useEffect,但不要放在判断或者循环等代码块中

  3. useEffect中的副作用函数,可以有返回值,返回值必须是一个函数,该函数通常被叫做清理函数

     1. 该函数运行时间点,在每次运行副作用函数之前(意思就是说,不管什么时候,只要返回了清理函数,都会在副作用函数运行之前,先运行清理函数)
     2. **注意** 清理函数在组件首次渲染的时候不会运行
     3. 组件被销毁时,一定会运行
    
  4. useEffect函数,可以传递第二个参数

     1. 第二个参数是一个数组
     2. 数组中记录了该副作用函数的依赖数据
     3. 当组件重新渲染后,只有依赖数据与上次不一样时,才会执行副作用
     4. 当传递了依赖数据之后,如果数据没有发生变化
         1. 副作用函数仅在第一次渲染后运行
         2. 清理函数仅在卸载组件后运行
    
  5. 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。因此尽量避免在副作用函数中使用外部变量

  6. 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数的稳定性,,否则控制起来会比较复杂

Reducer Hook

Flux:Facebook出品的一个数据流框架

  1. 规定了数据是单向流动的

  2. 数据存储在数据仓库中(可以认为state就是一个存储数据的仓库)

  3. action是改变数据的唯一原因(本质上就是一个对象,action有两个属性)

    1. type:字符串,动作的类型
    2. payload:任意类型,动作发生后的附加信息
  4. 具体改变数据的是一个函数,该函数叫做reducer

    1. 该函数接收两个参数
      1. state:表示当前数据仓库中的数据
      2. action:描述了如何去改变数据,以及改变数据的一些附加信息
    2. 该函数必须有一个返回结果,用于表示数据仓库变化之后的数据
      1. Flux要求,对象是不可变的,如果返回对象,必须创建新的对象
    3. reducer必须是纯函数,不能有任何副作用
  5. 如果要触发reducer,不可以直接调用,而是应该调用一个辅助函数dispatch

    1. 该函数仅接收一个参数:action
    2. 该函数会间接去调用reducer,以达到改变数据的目的
/**
 * 该函数,根据当前的数据,已经存在的action,生成一个新的数据
 * @param {*} state 
 * @param {*} action 
 */
function reducer(state, action) {
    switch (action.type) {
        case "increase":
            return state + 1;
        case "decrease":
            if (state === 0) {
                return 0;
            }
            return state - 1;
        default:
            return state;
    }
}

export default function App() {
    const [n, dispatch] = useReducer(reducer, 10, (args) => {
        console.log(args)
        return 100
    });
    return (
        <div>
            <button onClick={() => {
                dispatch({ type: "decrease" })
            }}>-</button>
            <span>{n}</span>
            <button onClick={() => {
                dispatch({ type: "increase" })
            }}>+</button>
        </div>
    )
}

Context Hook

作用:用获取由React.createContext()函数创建的上下文数据

const ctx = React.createContext();

function Test() {
    const value = useContext(ctx);
    return <h1>Test,上下文的值:{value}</h1>
}

export default function App() {
    return (
        <div>
            <ctx.Provider value="abc">
                <Test />
            </ctx.Provider>
        </div>
    )
}

Callback Hook

useCallback:用于得到一个固定引用值的函数,通常用它进行性能优化

该函数有两个参数:

  1. 函数,useCallback会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
  2. 数组,记录依赖项

该函数返回:引用相对固定的函数地址

上面的解释可能对一些人理解起来不太容易,请理解下面的代码示例

// React.PureComponent与React.component的最大的区别在于有没有实现shouldComponentUpdate,该函数会进行组件结构的浅比较,在某些情况下可以提高组件渲染性能
class Test extends React.PureComponent {

    render() {
        console.log("Test Render")
        return <div>
            <h1>{this.props.text}</h1>
            <button onClick={this.props.onClick}>改变文本</button>
        </div>
    }
}

function Parent() {
    console.log("Parent Render")
    const [txt, setTxt] = useState(123)
    const [n, setN] = useState(0)
    return (
        <div>
            {/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
            <Test text={txt} onClick={() => {
                setTxt(Math.random());
            }} />
            <input type="number"
                value={n}
                onChange={e => {
                    setN(parseInt(e.target.value))
                }}
            />
        </div>
    )
}

export default function App() {

    return (
        <div>
            <Parent />
        </div>
    )
}

使用useCallback返回的函数在当前作用于的引用不会改变(就可以避免上面的这种情况)

function Parent() {
    console.log("Parent Render")
    const [txt, setTxt] = useState(1)
    const [n, setN] = useState(0)
    const handleClick = useCallback(() => {
        setTxt(txt + 1)
    }, [txt])

    return (
        <div>
            <Test text={txt} onClick={handleClick} />
            <input type="number"
                value={n}
                onChange={e => {
                    setN(parseInt(e.target.value))
                }}
            />
        </div>
    )
}

Memo Hook

用于保持一些比较稳定的数据,通常用于性能优化

如果React元素本身的引用没有发生变化,一定不会重新渲染

在useMemo函数内通过复杂计算获取当前值得时候,不需要再父组件每次更新的时候重新计算,只要在依赖项发生变化的时候计算即可

Ref Hook

useRef:1. 一个参数(为默认值) 2. 返回一个固定的对象,{current: 值}

通常用于获取React元素的对象,用法类似于vue中ref获取DOM结构的方式

ImperativeHandle Hook

useInperativeHandle:可以让你在使用ref时自定义暴露给父组件的实例值。useImperativeHandle应当与forwardRef一起使用

  1. 父组件使用useRef(或creatRef)创建一个ref对象,将这个ref对象赋给子组件的ref属性
  2. 子组件使用forwardRef包装自己,允许作为函数组件的自己使用ref。然后使用useImpreativeHandle钩子函数,在该钩子函数的第二个参数中返回一些状态或方法,这个被返回的状态或方法就可以被父组件访问到
  3. 父组件使用创建的ref对象的current属性获取子组件暴露出的状态或方法
const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} type="text" />
});

const App = props => {
  const fancyInputRef = useRef();
  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button
        onClick={() => fancyInputRef.current.focus()}      // 调用子组件的方法
      >父组件调用子组件的 focus</button>
    </div>
  )
}

LayoutEffect Hook

useEffect:浏览器渲染完成后,用户看到新的渲染结果之后 useLayoutEffectHook:完成了DOM改动,但还没有呈现给用户

应该尽量使用useEffect,因为它不会导致渲染阻塞,如果出现了问题,再考虑使用useLayoutEffectHook

并非所有 effect 都可以被延迟执行。例如,一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。