hooks

8
open
sqshada
sqshada
Posted 4 months ago

hooks #24

hooks

在函数组件中使用 state 或者其他一些功能的 API

记忆函数利用闭包,在确保返回结果一定正确的情况下,减少了重复冗余的计算过程。hooks 提供的 api 大多都有记忆功能。例如:useState、useEffect、useLayoutEffect、useReducer、useRef、useMemo、useCallback

解决的问题

类组件的不足

  • 状态逻辑难复用,一半都用 render props 或者 HOC,导致层级冗余
  • 趋向复杂难以维护
  • this 指向问题(容易造成子组件不必要的刷新)

hooks 优势

  • 解决类组件的问题
  • 无需修改组件结构的情况下复用状态逻辑(自定义 hooks)
  • 能将组件中相互关联的部分拆分成更小的函数
  • 副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而 useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。
sqshada
sqshada
Created 4 months ago

useState

react 是怎么保证多个 useState 的相互独立的?

按照顺序执行(不能写在 ifelse 等条件语句当中,来确保hooks的执行顺序一致。)

// state.js
let state = null;

export const useState = (value: number) => {
  // 第一次调用时没有初始值,因此使用传入的初始值赋值
  state = state || value;

  function dispatch(newValue) {
    state = newValue;
    // 假设此方法能触发页面渲染
    render();
  }

  return [state, dispatch];
}
sqshada
sqshada
Created 4 months ago

useReducer

const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter(){
    const [state, dispatch] = useReducer(reducer, initialState,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    )
}
sqshada
sqshada
Created 4 months ago

useLayoutEffect

useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行(类似于 componentDidUpdate)

该方法是同步方法,在浏览器 paint 之前执行,会阻碍浏览器 paint,只有当我们需要进行DOM的操作时才使用该函数(比如设定 DOM 布局尺寸,这样可以防抖动)。

sqshada
sqshada
Created 4 months ago

useRef

useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的 ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref)

sqshada
sqshada
Created 4 months ago

性能优化

memo

memo,效果同 PureComponent,都会判断父组件传入的 props 是否发生改变来重新渲染当前组件。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
  • 第一个参数,工厂函数,返回一个缓存的值,仅当重新渲染时数组中的值发生改变,回调函数才会重新计算缓存数据。
  • 第二个参数,依赖数组,只有依赖项中的数据发生改变才重新计算值。

第一个参数为计算过程(回调函数,必须返回一个结果),第二个参数是依赖项(数组)

useCallback

useCallback一般用于在 React 中给事件绑定函数并需要传入参数的时候

useCallback 的使用几乎与 useMemo 一样,不过 useCallback 缓存的是一个函数体,当依赖项中的一项发现变化,函数体会重新创建。

他们一定能达到优化的目的吗?

记忆函数并非完全没有代价,我们需要创建闭包,占用更多的内存,用以解决计算上的冗余。

什么时候使用 useMemo/useCallback 比较合适?

通常情况下,当函数体或者结果的计算过程非常复杂时,我们才会考虑优先使用 useCallback/useMemo。

例如,在日历组件中,需要根据今天的日期,计算出当月的所有天数以及相关的信息。

sqshada
sqshada
Created 4 months ago

自定义 hooks

// myhooks.js
// 下面自定义了一个获取窗口长宽值的hooks
import React, { useState, useEffect, useCallback } from 'react'

function useWinSize() {
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  })
  const onResize = useCallback(() => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    })
  }, [])

  useEffect(() => {
    window.addEventListener('resize', onResize)
    return () => {
      window.removeEventListener('reisze', onResize)
    }
  }, [onResize])
  return size
}

export const useWinSize
sqshada
sqshada
Created 4 months ago

useEffect

  • 每次副作用执行,都会返回一个新的clear函数
  • clear函数会在下一次副作用逻辑之前执行(DOM渲染完成之后)
  • 组件销毁也会执行一次

相比于直接裸写在函数组件顶层,useEffect 能根据需要,避免多余的 render

假设传入 props 参数 id,改变两次,第一次传入 id: 1,第二次传入id: 2

  • 传入 props.id = 1
  • 组件渲染
  • DOM 渲染完成,副作用逻辑执行,返回清除副作用函数 clear1
  • 传入 props.id = 2
  • 组件渲染
  • 渲染完成,clear1 执行
  • 副作用执行,返回另一个清除函数 clear2
  • 组件销毁,clear2 执行
// 还是利用 Array + Cursor的思路
const allDeps: any[][] = [];
let effectCursor: number = 0;

function useEffect(callback: () => void, deps: any[]) {
  if (!allDeps[effectCursor]) {
    // 初次渲染:赋值 + 调用回调函数
    allDeps[effectCursor] = deps;
    ++effectCursor;
    callback();
    return;
  }

  const currenEffectCursor = effectCursor;
  const rawDeps = allDeps[currenEffectCursor];
  // 检测依赖项是否发生变化,发生变化需要重新render
  const isChanged = rawDeps.some(
    (dep: any, index: number) => dep !== deps[index]
  );
  if (isChanged) {
    callback();
    allDeps[effectCursor] = deps; // 感谢 [email protected] 的指正
  }
  ++effectCursor;
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
  effectCursor = 0; // 注意将 effectCursor 重置为0
}
sqshada
sqshada
Created 4 months ago

在无状态组件每一次函数上下文执行的时候,react用什么方式记录了hooks的状态?

通过hooks链表顺序

先每次执行一个hooks函数,都产生一个hook对象,里面保存了当前hook信息,然后将每个hooks以链表形式串联起来,并赋值给workInProgress的memoizedState updateQueue 放置useEffect和useLayoutEffect副作用组成的链表

多个react-hooks用什么来记录每一个hooks的顺序的 ? 换个问法!为什么不能条件语句中,声明hooks? hooks声明为什么在组件的最顶部?

通过hooks链表顺序进行记录,如果在条件语句中声明将会破坏 hooks 链表结构。

function函数组件中的useState,和 class类组件 setState有什么区别?

react 是怎么捕获到hooks的执行上下文,是在函数组件内部的?

useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值?

useRef 做的事情很简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState 内存中都指向了一个对象

useMemo是怎么对值做缓存的?如何应用它优化性能?

将 deps 依赖 存入 memoizedState,如果 deps 没有改变,就不需要更新

为什么两次传入useState的值相同,函数组件不更新?

如果当前 fiber 没有处于更新阶段。那么通过调用 lastRenderedReducer 获取最新的 state,和上一次的 currentState,进行浅比较,如果相等,那么就退出,这就证实了为什么 useState,两次值相等的时候,组件不渲染的原因了