React hooks,我来带你研究

1,310 阅读6分钟

简介

React在16.8版本以上可以使用,hooks优点在于能够更好的复用性,也解决无状态组件的生命周期以及状态管理的问题,替代class,可以通过自定义hook的形式将组件分割的更细粒度,方便拓展和维护。

Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题 的解决方案,无需学习复杂的函数式或响应式编程技术。

我们大致看一看react整体渲染流程,方便大家更好的理解react的实现过程,以及接下来的hooks原理讲解

截屏2021-09-13 下午8.51.59.png

react相关hooks api

业内精选-阿里ahooks库

React性能优化的中流砥柱——Immutable数据流

基本使用

useStateuseReducer进行派发更新(使函数组件重新渲染)。

useRef缓存变量且更改变量不进行派发更新ref.current获取缓存的变量

useEffect可以更新副作用,在dom挂载到页面进行渲染之后触发回调函数,比如网络请求,更新时间等。

useLayoutEffect在创建dom之后,挂载dom到页面之前调用,可以进行与渲染无关的操作,比如发布订阅等

useCallback可以缓存函数,当派发更新时,依赖的参数没有变化情况下,不创建新的函数useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useMemo可以缓存状态,做性能优化

useContext可以获取React.createContext创建的上下文对象

const Context = createContext(null);

function Parent () {
  return (<Context.Provider value={{ a: 1 }}>
    <Child />
  </Context.Provider>);
}

function Child () {
  const data = useContext(Context)
  console.log(data);
  return null;
}

export default Parent;

useImperativeHandle配合forwardRef可以将子组件参数透传到父组件,父组件通过ref.current拿到传入的参数

// 使用forwardRef,将接收第二个参数,用于透传ref或结合useImperativeHandle绑定子组件数据
function SetStatePage(props, ref) {
  const [count ,setCount] = useState(0)
  // 第一个参数接收透传ref,第二个参数接收函数,返回要透传的数据
  useImperativeHandle(ref, () => ({
    count,
    setCount
  }))
  return (
      <div>
          <p>You clicked {count} times</p>
      </div>
  )
}

// forwardRef透传ref
const SetStatePageWithRef = forwardRef(SetStatePage);

function Parent () {
  const ref = useRef(null);
  return (<div>
    <SetStatePageWithRef ref={ref} />
    {/* 通过ref.current获取子组件的数据 */}
    <button onClick={() => ref.current.setCount(ref.current.count + 1)}>Click me</button>
  </div>);
}

export default Parent;

useDebugValue可用于在 React 开发者工具中显示自定义 hook 的标签(必须在自定义Hook中使用),就像下面这样,它还可以接收第二个参数,作为延迟加载的回调函数,接收debug值作为实参,返回处理过的debug值进行显示

export default function SetStatePage(props) {
  function useOnline (state) {
    const [isOnline, setIsOnline] = useState(state);
    useDebugValue(isOnline > 5 ? 'Online' : 'Offline', (debug) => {
      if (debug === 'Online') {
        return true;
      } else {
        return false;
      }
    });
  
    return [isOnline, setIsOnline];
  }

  function useFriendStatus(friendID) {
    const [id, setID] = useState(friendID);
    // 在开发者工具中的这个 Hook 旁边显示标签
    // "FriendStatus: Online"
    useDebugValue(id > 5 ? 'Online' : 'Offline');
  
    return [id, setID];
  }
  const [id, setID] = useFriendStatus(3);
  const [isOnline, setIsOnline] = useOnline(3);
  return (
    <div>
        <p>You clicked {isOnline} times</p>
        <button onClick={() => setID(id + 1)}>Click ID</button>
        <button onClick={() => setIsOnline(isOnline + 1)}>Click Online</button>
    </div>
  )
}

截屏2021-09-13 上午11.20.00.png

Hooks实现原理

像上面useDebugValue中我们可以利用谷歌浏览器的React devtools插件可以查看到当前组件的hooks状态,这个状态是怎么拿到的呢?我们可以深究一下它的实现,然后找到答案

在React中,每次执行useState或者useReducer的更新函数都是触发派发更新,重新执行函数组件进行重新渲染,如果不缓存hooks的状态,那么每次获取到的状态那不都是同一个值吗

那么如何缓存hooks的状态,有缓存在哪里呢? 答案就在react16 fiber架构

react在大版本16的时候,将虚拟dom的树形结构转换成链表fiber,通过child、sibling、return指向其它节点,这样做的好处是实现了可以打断设置dom操作优先级的新旧dom对比,diff过程进行碎片化,类似下面的数据结构

{
    tag: 标记节点类型,
    type: dom标签字符串/函数/类,
    elementType: 与type属性基本一致,
    key: 节点当前层级下的唯一值,
    props: 属性,
    stateNode: 原生dom、类实例,
    child: 子节点,
    return: 父节点,
    sibling: 兄弟节点,
    ref: ,
    alternate: 旧节点,
    flags: 操作指令(添加,替换,更新),
    deletions: 要删除子节点 null或者[],
    index: 当前层级下的下标,
    pendingProps: 没有映射到dom时的属性,
    memoizedProps: 映射之后的dom属性,
    updateQueue: 更新函数队列 进行批量更新,
    memorizedState: hook链表的头节点,
    flags: 操作节点,
}

image.png

hooks的状态其实都存到了fiber.memorizedState上面,这样回答了React devtools插件是怎么拿到状态的,为什么memorizedState为什么是链表结构而不是数组呢,笔者认为是需要非连续的存储空间

为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用

要回答上面的问题,要了解react hooks的实现原理,我们在对Fiber链表进行dfs时,会判断type属性,如果为函数,就会通过type(props)执行函数,获取新的Fiber,reconcileChildren进行新旧节点的diff,以下是对源码实现的简化,便于理解:

// 函数组件
export function updateFunctionComponent(wip) {
  renderWithHooks(wip);

  const {type, props} = wip;
  const newFiber = type(props);

  // 协调子节点
  reconcileChildren(wip, newFiber);
}

renderWithHooks(重置当前执行Fiber的hook状态)

在执行函数组件之前,会先调用renderWithHooks函数传入当前正在工作的Fiber,将全局变量currentlyRenderingFiber进行赋值,以便后续hooks API的调用

function renderWithHooks(fiber) {
  currentlyRenderingFiber = fiber;
  currentlyRenderingFiber.memoizedState = null;
  // 源码中是一个updateQueue数组进行存储,这里为了简便,分开写区分useEffect和useLayoutEffect
  currentlyRenderingFiber.updateQueueOfEffect = [];
  currentlyRenderingFiber.updateQueueOfLayout = [];

  workInProgressHook = null;
}

updateWorkInProgressHook(获取当前执行的hook)

通过Fiber.alternate(代表旧Fiber)来判断是否为初次渲染

通过全局变量currentlyRenderingFiber当前正在工作的Fiber,workInProgressHook当前正在执行的Hook,currentHook当前正在工作的hook对应的老hook,用于获取当前执行的hook以及提取hook依赖项进行浅比较

function updateWorkInProgressHook() {
  let hook = null;

  // todo get hook
  // 老节点
  let current = currentlyRenderingFiber.alternate;
  if (current) {
    // 更新阶段 新的hook在老的hook基础上更新
    currentlyRenderingFiber.memoizedState = current.memoizedState;
    if (workInProgressHook) {
      // 不是第0个hook
      hook = workInProgressHook = workInProgressHook.next;
      currentHook = currentHook.next;
    } else {
      // 是第0个hook
      hook = workInProgressHook = current.memoizedState;
      currentHook = current.memoizedState;
    }
  } else {
    // 初次渲染阶段
    currentHook = null;
    hook = {
      memoizedState: null, // 状态值
      next: null, // 下一个hook
    };
    if (workInProgressHook) {
      // 不是第0个hook
      workInProgressHook = workInProgressHook.next = hook;
    } else {
      // 是第0个hook
      workInProgressHook = currentlyRenderingFiber.memoizedState = hook;
    }
  }

  return hook;
}

useState

  • 通过updateWorkInProgressHook获取当前执行的hook
  • 通过currentlyRenderingFiber判断是否非首次渲染
  • 通过setter函数传入新的状态newState来更新hook状态以及派发更新
function useState (state) {
  const hook = updateWorkInProgressHook();

  if (!currentlyRenderingFiber.alternate) {
    // 初次渲染
    hook.memoizedState = state;
  }
  const dispatch = (newState) => {
    hook.memoizedState = newState;
    scheduleUpdateOnFiber(currentlyRenderingFiber);
  };
  
  return [hook.memoizedState, dispatch];
}

use(Layout)Effect

  • 通过currentHook保存的之前的依赖和现有依赖进行浅比较
  • 通过hookFlag判断异步还是同步执行回调
  • Fiber全部diff完成后,对updateQueue中的回调函数进行同部或异步执行
export function useEffect(create, deps) {
  return updateEffectIml(HookPassive, create, deps);
}

export function useLayoutEffect(create, deps) {
  return updateEffectIml(HookLayout, create, deps);
}

export function updateEffectIml(hookFlag, create, deps) {
  const hook = updateWorkInProgressHook();

  const effect = {hookFlag, create, deps};

  // 组件更新的时候,且依赖项没有发生变化
  if (currentHook) {
    const prevEffect = currentHook.memoizedState;
    if (deps) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(deps, prevDeps)) {
        return;
      }
    }
  }

  hook.memoizedState = effect;
  if (hookFlag & HookPassive) {
    currentlyRenderingFiber.updateQueueOfEffect.push(effect);
  } else if (hookFlag & HookLayout) {
    currentlyRenderingFiber.updateQueueOfLayout.push(effect);
  }
}

当Fiber节点diff完成后,commit进行提交dom挂载时,useLayoutEffect回调立即执行,useEffect函数通过scheduleCallback进行异步调度,会在dom渲染之后执行

参考文章

React Hooks 原理