react-hooks详解

1 关于hook

1.1 为什么使用hook

在react类组件(class)写法中,有setState和生命周期对状态进行管理,但是在函数组件中不存在这些,故引入hooks(版本:>=16.8),使开发者在非class的情况下使用更多react特性。

以下是实现一个输入框,类组件和函数组件两种写法的对比:

/**
 * @name 类组件
 */
import React from 'react';
export default class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'world'
    };
  }
  componentDidMount() {
    console.log('组件挂载后要做的操作')
  }
  componentWillUnmount() {
    console.log('组件卸载要做的操作')
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevState.name !== this.state.name) {
      console.log('组件更新后的操作')
    }
  }
  render() {
    return (
      <div>
        <p>hello {this.state.name}</p>
        <input type="text" placeholder="input new name"
          onChange={(e) => this.setState({ name: e.target.value })}>
        </input>
      </div>
    );
  }
}


/**
 * @name 函数组件
 */
import React, { useState, useEffect } from 'react';

export default function Home() {
  const [name, setName] = useState('world');
  return (
    <div>
      <p>hello {name}</p>
      <DemoState />
    </div>
  )
}

function DemoState() {
  const [n1, setN1] = useState(1)
  const [n2, setN2] = useState(2)
  const [n3, setN3] = useState(3)

  useEffect(() => {
    setN1(10)
    setN1(100)
  }, [])
  const handleClick = () => {
    setN2(20)
    setN3(30)
  }
  console.log('demo-state', n1, n2, n3)
  return <button onClick={handleClick}>click</button>
}

上述例子中,useState相当于constructor,完成数据的初始化;

useEffect相当于componentDidMount和componentDidUpdate两个生命周期,通过return () => {}的方式解绑生命周期,相当于componentWillUnmount周期,以监听页面滚动为例,通过effect实现监听与解绑如下:

useEffect(() = >{
window.addEventListener(‘scroll’, throttleFunc)
return () = >{
window.removeEventListener(‘scroll’, throttleFunc)
}
}, [])

在同一个effect钩子中实现绑定与解绑,使状态的管理更加方便、代码更简洁。

此外还有发生在页面渲染前的useMemo相当于shouldComponentUpdate周期等,具体关系如下表:

class组件 hooks
shouldComponentUpdate useMemo
render 函数本身
getDerivedStateFromProps useState 中的 update
getDerivedStateFromError
constructor useState
componentWillUnmount useEffect中的return函数
componentDidUpdate useEffect
componentDidMount useEffect
componentDidCatch

结论:使用hooks的函数组件,简化了很多代码,不用维护复杂的生命周期,也不用担心this的指向问题。

1.2 什么是hook

hooks挂载在Fiber结点上的memoizedState,filber结构如下:

FiberNode { // fiber结构
  memoziedState, // 组件更新的依据
  type, // 原生或react   
  key,
  tag,
  ...
}

memoziedState这个字段很重要,是组件更新的唯一依据。在class组件里,它就是this.state的结构,调用this.setState的时候,其实就是修改了它的数据,数据改变了组件就会重新执行。

也就是说,即使是class组件,也不会主动调用任何生命周期函数,而是在memoziedState改变后,组件重新执行,在执行的过程中才会经过这些周期。

所以,这就解释了函数式组件为什么可以通过hooks改变状态,实际上就是修改了对应fiber节点的memoziedState。

hooks主要有以下特点:

1、无需修改组件结构的情况下复用状态逻辑;

2、可将组件中相互关联的部分拆分成更小的函数,复杂组件将变得更容易理解;

3、每一个组件内的函数(包括事件处理函数,effects,定时器或者api调用等等)会捕获某次渲染中定义的props和state;

4、memo缓存组件 ,useMemo缓存值, useCallback缓存函数

5、每次render都有自己的props、state和effects。(每一个组件内的函数,包括事件处理函数,effects,定时器或者api调用等等,会捕获某次渲染中定义的props和state);

6、更新状态的时候(如setCount(count + 1)),React会重新渲染组件,每一次渲染都能拿到独立的count状态,这个状态值是函数中的一个常量;

7、没有了显性的生命周期,所有渲染后的执行方法都在useEffect里面统一管理

8、函数式编程,不需要定义constructor、render、class;

9、某一个组件,方法需不需要渲染、重新执行完全取决于开发者,方便管理。

1.3 常见hook

useState、useEffect、useMemo、useCallback、useRef、useContext、useReducer…。

所有的钩子都是为函数引入外部功能,react约定,钩子一律使用use前缀命名。

2 常用hook

2.1 useState

示例:

const [stateA, setStateA] = useState(0)

参数是初始state(定义初始state最好给出初始值,方便后期维护, 0/false/’’/[]/{})。

返回值:一个是当前state,一个是更新state的函数。

useState的实现很简单,只有两行

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}


重点都在dispatcher上,dispatcher通过resolveDispatcher()来获取,这个函数只是将ReactCurrentDispatcher.current的值赋给了dispatcher

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}

useState挂在dispatcher上,resolveDispatcher() 返回的是 ReactCurrentDispatcher.current,所以useState(xxx)等价于ReactCurrentDispatcher.current.useState(xxx)。

useState(hooks)的具体执行过程如下:

1.png
  • updateContainer → … → beginWork
  • beginWork中会根据当前要执行更新的fiber的tag来判断执行什么,在函数式组件,执行了updateFunctionComponent(判断执行函数式/组件式更新)
    首次渲染时,React Fiber 会从 packages/react-reconciler/src/ReactFiberBeginWork.js 中的 beginWork() 开始执行。在beginWork函数中,可以根据workInProgress(是一个Fiber节点)上的tag值来走不通的方法加载或更新组件,如下:
function beginWork(current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ) : Fiber | null {
    /****/
    // 根据不同的组件类型走不同的方法
    switch (workInProgress.tag) {
        // 不确定组件    
    case IndeterminateComponent:
        {
            const elementType = workInProgress.elementType;
            // 加载初始组件       
            return mountIndeterminateComponent(current, workInProgress, elementType, renderExpirationTime, );
        }
        // 函数组件     
    case FunctionComponent:
        {
            const Component = workInProgress.type;
            const unresolvedProps = workInProgress.pendingProps;
            const resolvedProps = workInProgress.elementType === Component ? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);
            // 更新函数组件       
            return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderExpirationTime, );
        }
        // 类组件     
    case ClassComponent:
        {
            /****/
        }
    }
}

  • 在updateFunctionComponent中,对hooks的处理如下
nextChildren = renderWithHooks(
  current,
  workInProgress,
  Component,
  nextProps,
  context,
  renderExpirationTime,
);

所以,React Hooks 的渲染核心是renderWithHooks,在renderWithHooks函数中,初始化了Dispatcher。

export
function renderWithHooks < Props, SecondArg > (current: Fiber | null, workInProgress: Fiber, 
             Component: (p: Props, arg: SecondArg) = >any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ) : any {

    // 若Fiber为空,则认为是首次加载
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

    // 挂载时的Dispatcher
    const HooksDispatcherOnMount: Dispatcher = {
        readContext,
        // ...
        useCallback: mountCallback,
        useContext: readContext,
        useEffect: mountEffect,
        useMemo: mountMemo,

        useState: mountState,
        // ...
    };

    // 更新时的Dispatcher
    const HooksDispatcherOnUpdate: Dispatcher = {
        readContext,
        // ...
        useCallback: updateCallback,
        useContext: readContext,
        useEffect: updateEffect,
        useMemo: updateMemo,
        useRef: updateRef,
        useState: updateState,
        // ....
    };
}

  • 在renderWithHooks中,会先根据fiber的memoizedState是否为null,来判断是否已经初始化。因为memoizedState在函数式组件中是存放hooks的。是则mount,否则update(判断是否执行过,没有则挂载,有则更新)
  • 在mount(挂载)时,函数式组件执行,ReactCurrentDispatcher.current为HooksDispatcherOnMount,被调用,会初始化hooks链表、initialState、dispatch函数,并返回。这里就完成了useState的初始化,后续函数式组件继续执行,完成渲染返回。(首次渲染过程)
  • 在update(更新)时,函数式组件执行,ReactCurrentDispatcher.current为HooksDispatcherOnUpdate,被调用,updateWorkInProgressHook用于获取当前work的Hook。然后根据numberOfReRenders 是否大于0来判断是否处理re-render状态:是的话,执行renderPhaseUpdates,获取第一个update,然后循环执行,获取新的state,直到下一个update为null;否的话,获取update链表的第一个update,进行循环,判断update的优先级是否需要更新,对于优先级高的进行更新。(更新过程)
  • 结果返回当前状态和修改状态的方法
    以挂载为例,生成一个hook对象(mountState),并对hook对象进行初始化(mountWorkInProgressHook),具体如下:
function mountState < S > (initialState: (() = >S) | S, ) : [S, Dispatch < BasicStateAction < S >> ] {
    // 创建一个新的hook对象,并返回当前workInProgressHook
    const hook = mountWorkInProgressHook();
    if (typeof initialState === 'function') {
        initialState = initialState();
    }
    hook.memoizedState = hook.baseState = initialState; // 第二步:获取初始值并初始化hook对象
    const queue = hook.queue = { // 新建一个队列  
        // 保存 update 对象
        pending: null,
        // 保存dispatchAction.bind()的值
        dispatch: null,
        // 一次新的dispatch触发前最新的reducer
        // useState 保存固定函数: 可以理解为一个react 内置的reducer
        // (state, action) => { return typeof action === 'function' ? action(state) : action }
        lastRenderedReducer: reducer
        // 一次新的dispatch触发前最新的state
        lastRenderedState: (initialState: any),
    }
    // 绑定当前 fiber 和 queue.
    const dispatch: Dispatch < BasicStateAction < S > ,
    >=(queue.dispatch = (dispatchAction.bind(null, currentlyRenderingFiber, queue, ) : any));
    // 返回当前状态和修改状态的方法
    return [hook.memoizedState, dispatch];
}

function mountWorkInProgressHook() {

    // 初始化的hook对象
    var hook = {
        memoizedState: null,
        // 存储更新后的state值
        baseState: null,
        // 存储更新前的state
        baseQueue, // 更新函数
        queue: null,
        // 存储多次的更新行为
        next: null // 指向下一次useState的hook对象
    };

    // workInProgressHook是一个全局变量,表示当前正在处理的hook
    // 如果workInProgressHook链表为null就将新建的hook对象赋值给它,如果不为null,那么就加在链表尾部。
    if (workInProgressHook === null) {
        currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
    } else {
        workInProgressHook = workInProgressHook.next = hook;
    }

    return workInProgressHook;
}

初始化完成后,setFn又是怎么对stateA值进行更新的呢?实际上就是通过dispatchAction方法进行更新的,如下:

// currentlyRenderingFiber$1是一个全局变量,表示当前正在渲染的FiberNode
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);

此外还有对hook对象的更新(dispatchAction),如下:

function dispatchAction(fiber, queue, action) {
    // ...
    // 1. 创建update对象
    // 该对象保存的是调度优先级/state/reducer以及用户调用dispatch/setState 时传入的action
    const update: Update < S,
    A > ={
        lane,
        action,
        eagerReducer: null,
        eagerState: null,
        next: (null: any),
    };
    // 2. 将update更新到queue.pending中,最后的update.next 指向第一个update对象,形成一个闭环。
    const pending = queue.pending;
    if (pending === null) {
        // This is the first update. Create a circular list.
        update.next = update;
    } else {
        update.next = pending.next;
        pending.next = update;
    }
    queue.pending = update;
}

简单理解:

  • 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中,共享同一个 memoizedState,共享同一个顺序。
  • 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

  • 具体 看下图
    1、初始化时:数组为空,下标置0
1.png

2、首次渲染:将遇到的hook的依赖项加入数组,与下标一一对应

2.png

3、事件触发:触发hook的内容被修改,修改后的数据替换掉数组中原先的数据

3.png

4、重渲染:ReRender 的时候,重新去执行函数组件,但是对之前已经执行过的函数组件并不会做任何操作

4.png

下面看一个实例:

const DemoState = memo(() => {
  const [n1, setN1] = useState(1)
  const [n2, setN2] = useState(2)
  const [n3, setN3] = useState(3)

  useEffect(() => {
    setN1(10)
    setN1(100)
  }, [])
  const handleClick = () => {
    setN2(20)
    setN3(30)
  }
  console.log('demo-state', n1, n2, n3)
  return <div>
    <div className={`${classPrefix}-title`}>---useState---</div>
    <button onClick={handleClick}>改变n2、n3</button>
  </div>
})
// demo-state 1 2 3 => demo-state 100 2 3 => demo-state 100 20 30

渲染时,effect对n1进行了两次赋值,实际上仅刷新一次;点击事件分别对n1和n2进行赋值,实际上也仅刷新一次。

结论:setState返回的函数执行会导致re-render;
框架内部会对多次函数操作进行合并,保证useState拿到最新的状态,避免重复渲染。

如果初始state需要通过复杂计算获得,可以传入一个函数,在函数中计算并返回初始state,此函数只在初始渲染时被调用,具体如下:

const [count, setCount] = useState(() => {
  const initialCount = someExpensiveComputation(props)
  return initialState
})

那么,state在class和function中有什么区别吗?下面两段代码中,1s内点击事件各触发5次会有什么表现吗?

class HooksDemoRule extends React.PureComponent {
  state = {
    count: 0
  }
  increment = () => {
    console.log('---setState---')
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1
      })
    }, 3000)
  }
  render() {
    return <div>
      <div className={`${classPrefix}-title`}>---useState && setState---</div>
      <div className={`${classPrefix}-text`}>setState 当前值:{this.state.count}</div>
      <button onClick={this.increment}>+1</button>
      <IKWebviewRouterLink
        ikTo='home'
        className={`${classPrefix}-to-home`}
      ><p>返回</p></IKWebviewRouterLink>
    </div>
  }
}
const SecondDemo = memo(() => {
  const [count, setCount] = useState(0)
  const increment = () => {
    console.log('---useState---')
    setTimeout(() => {
      setCount(count + 1)
    }, 3000)
  }
  return <div>
    <div className={`${classPrefix}-title`}>---useState && setState---</div>
    <div className={`${classPrefix}-text`}>useState 当前值:{count}</div>
    <button onClick={increment}>+1</button>
    <IKWebviewRouterLink
      ikTo='rule'
      className={`${classPrefix}-to-home`}
    ><p>返回</p></IKWebviewRouterLink>
  </div>
})

最终结果:在类组件中,页面上的数字依次从0增加到5;在函数组件中,页面上的数字只会从0增加到1。

原因在于,在类组件中,通过this.state引用count,每一次setTimeout的时候都能通过引用拿到上一次的最新count,所以最后加到5。但是在函数组件中,每一次更新都是重新执行当前函数,1s内setTimeout里读取的count实际上都是初始值0,所以最后只加到1。如果想让函数组件也加到5要怎么实现呢,下文useRef会讲到。

简单来说,类组件的state依赖上一次state,函数组件的state是重新执行当前函数。

2.2 useEffect

useEffect的实现很简单,也是只有两行:

export function useEffect (create: () = >(() = >void) | void, deps: Array < mixed > |void | null, ) : void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

useEffect产生的hook会放在fiber.memoizedState上,调用后生成一个effect对象,存储到对应hook的memoizedState中,与其他effect连接成环形链表。
单个的effect对象包含以下几个属性:
create:传入useEffect函数的第一个参数,即回调函数;
destory:回调函数中的return函数,在改effect销毁的时候执行,默认发生在第一次渲染后,也可以让它在依赖项数组中的值改变时执行,通过return清除副作用函数(如监听、订阅、计时器等);
deps:依赖项,传入的第二个参数,用来控制该Effect包裹的函数执不执行。如果依赖项为空数组[],则该Effect在每次组件挂载时执行,且仅执行一次,相当于class组件中的componentDidMount和componentDidupdate生命周期的融合;如果没有第二个参数,则effect会不停地调用。
next:指向下一个effect;
tag:effect的类型,区分useEffect和useLayoputEffect。
hook会挂载到fiber.memoizedState上。hook按出现顺序进行存储,memoizedState存储了useEffect的effect对象(effect1),next指向useLayoutEffect的effect对象(effect2),effect2的next又会指向effect1,最终形成闭环。结构如下:

1.png
const DemoEffect = memo(() => {
  useEffect(() => {
    console.log('useEffect1');
    const timeId = setTimeout(() => {
      console.log('useEffect1-setTimeout-2000');
    }, 2000);
    return () => {
      clearTimeout(timeId);
    };
  }, []);
  useEffect(() => {
    console.log('useEffect2');
    const timeId = setInterval(() => {
      console.log('useEffect2-setInterval-1000');
    }, 1000);
    return () => {
      clearInterval(timeId);
    };
  }, []);
  return (
    <div>
      <div className={`${classPrefix}-title`}>---useEffect---</div>
      {(() => {
        console.log('render');
        return null;
      })()}
    </div>
  );
})
// render => useEffect1 => useEffect2 => useEffect2-setInterval-1000 => useEffect1-setTimeout-2000 => index.js:67 useEffect2-setInterval-1000 * n

结论:effect在页面完成渲染后按照先后顺序执行,并且内部执行时异步的

useEffect和useLayoutEffect:
useLayoutEffect也是一个hook方法,跟useEffect类似,区别在于渲染时机不同,useEffect发生在浏览器渲染结束后执行,useLayoutEffect则是发生在dom更新完成后。

PS:useEffect和useLayoutEffect都是effect钩子
下面是一个方块移动的例子,在effect中添加右移的方法,理解两者的区别:两者都发生在render之后,且useLayoutEffect发生在useEffect之前

const moveTo = (dom, delay, options) => {
  dom.style.transform = `translate(${options.x}px)`
  dom.style.transition = `left ${delay}ms`
}
const Animate = memo(() => {
  const squRef = useRef()
  const squRef1 = useRef()
  const squRef2 = useRef()

  useLayoutEffect(() => { // 方块直接出现在右侧,不会闪一下
    console.log('useLayoutEffect-1')
    moveTo(squRef1.current, 500, { x: 600 })
  }, [])
  useEffect(() => { // 会有方块移动的过程,闪一下
    console.log('useEffect')
    moveTo(squRef.current, 500, { x: 600 })
  }, [])
  useLayoutEffect(() => {
    console.log('useLayoutEffect-2')
    moveTo(squRef2.current, 500, { x: 600 })
  }, [])
  console.log('render')
  return (
    <>
      <div className={`${classPrefix}-title`}>---useEffect && useLayoutEffect---</div>
      <div className={`${classPrefix}-square`} ref={squRef}></div>
      <div className={`${classPrefix}-square1`} ref={squRef1}></div>
      <div className={`${classPrefix}-square1`} ref={squRef2}></div>
    </>
  )
})
// render -> useLayoutEffect-1 -> useLayoutEffect-2 -> useEffect

useLayoutEffect和useEffect很像,唯一的不同点就是useEffect是异步执行,而useLayoutEffect是同步执行的。

当函数组件刷新(渲染)时,

包含useEffect的组件整个运行过程如下:
1、触发组件重新渲染(通过改变组件state或者组件的父组件重新渲染,导致子节点渲染)
2、组件函数执行
3、组件渲染后呈现到屏幕上
4、useEffect hook执行

包含useLayoutEffect的组件整个运行过程如下:
1、触发组件重新渲染(通过改变组件state或者组件的父组件重新渲染,导致子组件渲染)
2、组件函数执行
3、useLayoutEffect hook执行, React等待useLayoutEffect的函数执行完毕
4、组件渲染后呈现到屏幕上

useEffect异步执行的优点是,react渲染组件不必等待useEffect函数执行完毕,造成阻塞。

百分之99的情况,使用useEffect就可以了,唯一需要用到useLayoutEffect的情况就是,在使用useEffect的情况下,我们的屏幕会出现闪烁的情况(组件在很短的时间内渲染了两次)。

2.3 useReducer

参数:第一个是reducer纯函数,第二个是初始state,第三个是修改初始state,用于重置
返回值是一个数组,数组第一个元素是state的当前值,第二个元素是发送action的dispatch函数

管理包含多个子值的state对象时,应该怎么处理呢?以获取某一接口为例,具体操作如下:

const fetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT': // 接口初始化
      return {
        ...state,
        status: '初始化',
        loading: true,
        error: false
      };
    case 'FETCH_SUCCESS': // 请求成功
      return {
        ...state,
        status: '成功',
        loading: false,
        error: false,
        data: action.data
      };
    case 'FETCH_FAIL': // 请求失败
      return {
        ...state,
        status: '失败',
        loading: false,
        error: true
      };
    default:
      return null
  }
};

const DemoReducer = memo(() => {
  const [state, dispatch] = useReducer(fetchReducer, {
    loading: false,
    error: false,
    status: '',
    data: {}
  });

  const getData = async () => {
    const { data } = await Apis.GET_USER_INFO().catch(() => {
      dispatch({ type: 'FETCH_FAIL', data: null })
      return false
    })
    if (!data) return
    dispatch({ type: 'FETCH_SUCCESS', data })
  }

  useEffect(() => {
    dispatch({ type: 'FETCH_INIT' })
    getData()
  }, [])
  console.log('state---', state)

  return <div>
    <div className={`${classPrefix}-title`}>---useReducer---</div>
    <div className={`${classPrefix}-text`}>请求状态: {state.status}</div>
  </div>
})

结论:useReducer可以处理多个用useState实现的逻辑(加载状态、错误信息、请求数据)

思考题:useState的出现,让我们可以使用多个state变量来保存state,如

const [width, setWidth] = useState(100)
const [height, setHeight] = useState(100)
const [left, setPageX] = useState(0)
const [top, setPageY] = useState(0)

也可以像class组件的this.state一样,将所有state放在一个obj中,如

const [state, setState] = useState({ width: 100, height: 100, left: 0, top: 0 });

也可以使用useReducer处理,如

const stateReducer = (state, action) => {

  switch  (action.type) {
    case 'WIDTH':
      return {
        ...state,

        width: action.width
      };
   case 'HEIGHT':
    return {
      ...state,
      height: action.height
    };

   ......

}

const [state, dispatch] = useReducer(stateReducer, { width: 100, height: 100, left: 0, top: 0 })

这三种方法哪种好呢?

2.4 useMemo

参数是创建函数和依赖项数组。

返回值是一个带有memoized的值,发生在render之前, 并且这个值仅在依赖项改变时才重新计算。

const DemoMemo = memo(() => {
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(2)
  const expensive = useMemo(() => {
    console.log('运算')
    let sum = 0
    for (let i = 0; i < num1 * 100; i++) {
      sum += i
    }
    return sum
  }, [num1])
  const handleClick1 = () => {
    console.log('num1++')
    setNum1(num1 + 1)
  }
  const handleClick2 = () => {
    console.log('num2++')
    setNum2(num2 + 1)
  }
  return <div>
    <div className={`${classPrefix}-title`}>---useMemo---</div>
    <div className={`${classPrefix}-text`}>当前num1:{num1}</div>
    <div className={`${classPrefix}-text`}>当前num2:{num2}</div>
    <div className={`${classPrefix}-text`}>当前expensive(仅依赖num1):{expensive}</div>
    <div>
      {(() => {
        console.log('render');
        return null;
      })()}
      <button onClick={handleClick1}>num1++</button>
      <button onClick={handleClick2}>num2++</button>
    </div>
  </div>
})
// 运算 => render
// 点击num1++:  num1++ => 运算 => render
// 点击num2++:  num1++ => render

结论:useMemo发生在render前,返回一个缓存的数据,且仅在依赖项改变后变化。

使用useMemo可以避免多余的计算开销。

2.5 useCallback

参数是内联回调函数和依赖项数组,

返回值是回调函数的memoized版,该回调函数仅在某个依赖项改变时才会更新。

const set = new Set();
const DemoCallback = memo(() => {
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(2)

  const callback = useCallback(() => {
    // 这里做复杂运算
    return num1;
  }, [num1])
  set.add(callback)
  const handleClick1 = () => {
    setNum1(num1 + 1)
  }
  const handleClick2 = () => {
    setNum2(num2 + 1)
  }
  console.log('demo-callback', set.size)
  return <div>
    <div className={`${classPrefix}-title`}>---useCallback---</div>
    <div className={`${classPrefix}-text`}>当前num1:{num1}</div>
    <Child callback={callback}/>
    <div>
      <button onClick={handleClick1}>num1++</button>
      <button onClick={handleClick2}>num2++</button>
    </div>
  </div>
})

const Child = memo(({ callback }) => {
  console.log('---child render')
  return <div>
    <div className={`${classPrefix}-text`}>child刷新(仅依赖num1):{set.size}</div>
  </div>
})
// demo-callback 1 => ---child render
// 点击num1++:  demo-callback 2 => ---child render
// 点击num2++:  demo-callback 2

结论:返回一个缓存的函数,添加依赖项数组可以避免函数的无意义计算,降低了子组件的渲染开销。

2.6 useRef

返回值是一个可变的ref对象,并且这个对象的值发生改变时不会引起页面的渲染。

const DemoRef = memo(() => {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus()
    inputEl.current.value = '自定义'
  };
  return (
    <>
      <div className={`${classPrefix}-title`}>---useRef---</div>
      {(() => {
        console.log('render');
        return null;
      })()}
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>click</button>
    </>
  );
})
// render
// 点击click:  

结论:useRef可以存储不需要引起页面渲染的数据;修改useRef值的唯一方法是修改.current,且修改后不会引起重渲染。

2.1中的问题,可以通过以下方法解决

const SecondDemoNew = memo(() => {
  const [ count, setCount ] = useState(0)
  const ref = useRef(0)
  const increment = () => {
    console.log('---useState---')
    setTimeout(() => {
      setCount((ref.current += 1))
      // setCount(count => count + 1)
    }, 3000)
  }
  return <div>
    <div className={`${classPrefix}-title`}>---useState && setState---</div>
    <div className={`${classPrefix}-text`}>当前值:{count}</div>
    <button onClick={increment}>+1</button>
  </div>
})

另外, useRef也可以用来实现锚点跳转,具体如下

const scrollRef = useRef(null)

const scrollToRef = (ref) = >{ // 跳转
  if (!ref) return

  ref.current.scrollIntoView({
    behavior: 'smooth'
  })
}

......

< div onClick = { () = >scrollToRef(scrollRef) } > 航行进度 < /div>

<span ref={scrollRef}></span >

2.7 useContext

跨组件共享数据的钩子函数,接收一个context对象,并返回该对象的当前值。

当前的context值由上层组件中距离当前组件最近的<MyContext.Provider>的value决定,并且父组件的context发生改变是,子组件都会重新渲染。

const MyContext = React.createContext() // 创建context,用于支持调用
const DemoContext = memo(() => {
  const [value, setValue] = useState('initValue')
  return (
    <div>
      <div className={`${classPrefix}-title`}>---useContext---</div>
      {(() => {
        console.log('render');
        return null;
      })()}
      <button onClick={() => {
        setValue('newValue')
      }}>
        改变value
      </button>
      <MyContext.Provider value={value}>
        <Child1 />
        <Child2 />
      </MyContext.Provider>
    </div>
  );
})

const Child1 = memo(() => {
  const value = useContext(MyContext)
  console.log('Child1-value', value)
  return <div className={`${classPrefix}-text`}>Child1-value: {value}</div>
})

const Child2 = () => {
  console.log('Child2');
  return <div className={`${classPrefix}-text`}>Child2</div>;
}
// render => Child1-value initValue => Child2
// 点击btn后:  render => Child1-value newValue => Child2

结论:useContext会在context值变化时重新渲染,<MyContext.Provider>的value发生变化时,包裹的组件无论是否订阅value值,都会重新渲染,可以使用memo对未使用value的子组件进行优化。

2.8 自定义hook

有时候我们需要重复使用一些状态逻辑,怎么处理可以在不增加组件的前提下实现复用,自定义hook可以达到这一目的。

通过自定义hook,抽取多个组件重复使用的逻辑,将这些重复的逻辑添加到一个叫做useSomething的自定义hook中,调用这一hook达到逻辑复用的目的,在不增加组件的情况下实现了逻辑共享。

自定义的hook是一个函数,名称以“use”开头,函数内部可用调用其他hook。以处理请求过程为例,自定义hook如下:

3 优化

hook主要从以下三个方面对函数式组件进行优化:

useCallback用于缓存函数
useMemo用于缓存计算结果,简单理解useCallback(fn, deps) === useMemo(() => fn, deps)
useReducer用于处理多状态的state

4 小结

hooks执行流程:

在react中,组件返回的JSX元素被转换为虚拟DOM,就是下方的vnode,每个vnode上挂载了一个_component属性,这个属性指向组件实例。在组件实例上又挂载了一个_hooks属性,这个_hooks属性里保存了执行一个组件时,里面所有Hook方法的相关信息。

1.png

首先,有一个全局的currentIndex变量,当组件第一次渲染或更新时,它会在每次进入一个函数组件的时候都重置为0,每次遇到一个hook方法就会加1,同时将这个hook方法加到_list(缓存)中。当下次进来或进入下一组件时,currentIndex又被重置为0;在组件更新时,则会从_list中根据currentIndex取出对应项。具体如下添加链接描述:

组件渲染 => currentIndex 重置 0 => 遇到 Hooks 方法,放进 _list数组 => 索引currentIndex++ => 渲染结束
组件更新 => currentIndex 重置 0 => 遇到 Hooks 方法,获取 _list[currentIndex]=> currentIndex++ => 重复上面步骤 => 更新结束

hooks使用规则:(eslint-plugin-react-hooks控制以下两条规则)

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用(hook2拿到的state其实是上一次hook1执行后的state, 而不是上一次hook2执行后的state。如果把hook1放在一个if语句中,当这个hook没有执行时,这样显然会发生错误)。
  • 只能在 React 的函数组件中调用 Hook(因为只有函数组件的更新才会触发renderWithHooks函数,处理hooks的相关逻辑)。
    hooks优化策略:
    优化本身也会带来大量的计算,无意义的优化反而会增加额外的开销。所以针对3中优化需谨慎。

https://blog.csdn.net/qq_30997503/article/details/117228632?spm=1001.2014.3001.5502
https://github.com/brickspert/blog/issues/26

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Effect Hook可以使得你在函数组件中执行一些带有副作用的方法。 上面这段代码是基于上个state hook...
    xiaohesong阅读 4,328评论 0 0
  • React Hooks 在了解React Hooks之前, 我们先看一下 Class 和函数式 的一般写法 Cla...
    YM雨蒙阅读 2,864评论 0 1
  • useState 1.基本使用 等价于 2. 复杂的state 3.使用状态 4. 注意事项 1). 如果stat...
    sweetBoy_9126阅读 3,006评论 0 6
  • 什么是hooks? hooks 是 react 在16.8版本开始引入的一个新功能,它扩展了函数组件的功能,使得函...
    JoeRay61阅读 553评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,520评论 28 53