React Hook 到底是个啥?

本篇文章参考以下博文

前言

  最近在和同事们交流的时候,发现大家对 hook 的理解是,这玩意就是生命周期,原来放在生命周期里的东西,现在都要放到 hook 里。

  如果刚接触 hook 的话,那么这个解释可以帮助你快速明白 hook 的用法,但是往深了说, hook 与生命周期还是有一定区别的。今天咱们就来研究研究,区别在哪?

  阅读本节需要对 react fiber 结构有一定的了解。

举个例子

  我们举一个形象一点的例子,让大家先有一个概念。比如下图:
在这里插入图片描述

   react 的图标大家都很熟悉了哈,这个图标的含义是原子,中间的是原子核,外面的轨迹是飞行的核外电子。那么 hook 的概念,就是像是电子。

自己实现 useState

  接下来我们自己实现一个最常用的 hook useState

  首先过程很简单,大家先别害怕。看下面一个例子

function App() {
    
    
  const [num, updateNum] = useState(0);

  return <p onClick={
    
    () => updateNum(num => num + 1)}>{
    
    num}</p>;
}

  上面是一个简单的函数组件,一个按钮,点击之后会对 num 进行累加。那么这个组件中,涉及 useState 的工作有两部分:

扫描二维码关注公众号,回复: 13049045 查看本文章
  1. 通过一些途径产生更新,更新会造成组件 render

  2. 组件 render useState 返回的 num 为更新后的结果。

  上面第一步的过程,又可以分解为:

  1. 调用 ReactDOM.render 会产生 mount 的更新,更新内容为 useState initialValue (即0)。

  2. 点击 p 标签触发 updateNum 会产生一次 update 的更新,更新内容为 num => num + 1

  接下来我们研究下,这两步是怎么实现的。

  页面产生 更新 后,组件 render ,那么这个 更新 是什么?源码里这个更新是一个链表的节点,

const update = {
    
    
  // 更新执行的函数(num => num + 1)
  action,
  // 与同一个Hook的其他更新形成链表
  next: null
}

  这些更新节点被放在了一个环装单向链表里,类似如下结构:
在这里插入图片描述

  调用 updateNum 的过程,执行 dispatchAction 函数。

function dispatchAction(queue, action) {
    
    
  // 创建update
  const update = {
    
    
    action,
    next: null
  }

  // 环状单向链表操作
  if (queue.pending === null) {
    
     //链表为空时
    update.next = update;
  } else {
    
      //不为空时
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;

  // 模拟React开始调度更新
  schedule();
}

  上面逻辑的效果是 queue.pending 始终指向最后一个插入的 update

  这样做的好处是,当我们要遍历 update 时, queue.pending.next 指向第一个插入的 update

在这里插入图片描述

状态保存

  知道了 update 的逻辑之后,我们来看看状态是怎么保存在 queue 里面的。我们知道 ClassComponent 的状态是保存在属性 state 里面的。而 FunctionComponent 的状态是保存在 fiber 里面的,类似下面:

// App组件对应的fiber对象
const fiber = {
    
    
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: null,
  // 指向App函数
  stateNode: App
};

Hook 数据结构

  接下来我们关注 fiber.memoizedState 中保存的 Hook 的数据结构。

hook = {
    
    
  // 保存update的queue,即上文介绍的queue
  queue: {
    
    
    pending: null
  },
  // 保存hook对应的state
  memoizedState: initialState,
  // 与下一个Hook连接形成单向无环链表
  next: null
}

  可以看到, Hook update 类似,都通过链表连接。不过 Hook 是无环的单向链表。

  注意区分 update hook 的所属关系:

  每个 useState 对应一个 hook 对象。

  调用 const [num, updateNum] = useState(0) ;时 updateNum (即上文介绍的 dispatchAction )产生的 update 保存在 useState 对应的 hook.queue 中。

模拟React调度更新流程

  在上文 dispatchAction 末尾我们通过 schedule 方法模拟 React 调度更新流程。

function dispatchAction(queue, action) {
    
    
  // ...创建update
  
  // ...环状单向链表操作

  // 模拟React开始调度更新
  schedule();
}

  现在我们来实现他。

  我们用 isMount 变量指代是 mount 还是 update 。(源码中对于 mount update 的处理是分开的,这里用一个 isMount 简化了)

// 首次render时是mount
isMount = true;

function schedule() {
    
    
  // 更新前将workInProgressHook重置为fiber保存的第一个Hook
  workInProgressHook = fiber.memoizedState;
  // 触发组件render
  fiber.stateNode();
  // 组件首次render为mount,以后再触发的更新为update
  isMount = false;
}

  通过 workInProgressHook 变量指向当前正在工作的 hook

workInProgressHook = fiber.memoizedState;

  在组件 render 时,每当遇到下一个 useState ,我们移动 workInProgressHook 的指针。

workInProgressHook = workInProgressHook.next;

  这样,只要每次组件 render useState 的调用顺序及数量保持一致,那么始终可以通过 workInProgressHook 找到当前 useState 对应的 hook 对象。

  到这里第一步就完成了,接下来我们进行第二步。即组件 render useState 返回的 num 为更新后的结果。

计算 State

  组件 render 时会调用 useState ,他的大体逻辑如下:

function useState(initialState) {
    
    
  // 当前useState使用的hook会被赋值该该变量
  let hook;

  if (isMount) {
    
    
    // ...mount时需要生成hook对象
  } else {
    
    
    // ...update时从workInProgressHook中取出该useState对应的hook
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    
    
    // ...根据queue.pending中保存的update更新state
  }
  hook.memoizedState = baseState;

  return [baseState, dispatchAction.bind(null, hook.queue)];
}

  首先我们需要先关注以下如何获取 hook 对象:

if (isMount) {
    
    
  // mount时为该useState生成hook
  hook = {
    
    
    queue: {
    
    
      pending: null
    },
    memoizedState: initialState,
    next: null
  }

  // 将hook插入fiber.memoizedState链表末尾
  if (!fiber.memoizedState) {
    
    
    fiber.memoizedState = hook;
  } else {
    
    
    workInProgressHook.next = hook;
  }
  // 移动workInProgressHook指针
  workInProgressHook = hook;
} else {
    
    
  // update时找到对应hook
  hook = workInProgressHook;
  // 移动workInProgressHook指针
  workInProgressHook = workInProgressHook.next;
}

  当找到该 useState 对应的 hook 后,如果该 hook.queue.pending 不为空(即存在 update ),则更新其 state

// update执行前的初始state
let baseState = hook.memoizedState;

if (hook.queue.pending) {
    
    
  // 获取update环状单向链表中第一个update
  let firstUpdate = hook.queue.pending.next;

  do {
    
    
    // 执行update action
    const action = firstUpdate.action;
    baseState = action(baseState);
    firstUpdate = firstUpdate.next;

    // 最后一个update执行完后跳出循环
  } while (firstUpdate !== hook.queue.pending.next)

  // 清空queue.pending
  hook.queue.pending = null;
}

// 将update action执行完后的state作为memoizedState
hook.memoizedState = baseState;

  完整的过程如下:

function useState(initialState) {
    
    
  let hook;

  if (isMount) {
    
    
    hook = {
    
    
      queue: {
    
    
        pending: null
      },
      memoizedState: initialState,
      next: null
    }
    if (!fiber.memoizedState) {
    
    
      fiber.memoizedState = hook;
    } else {
    
    
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    
    
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    
    
    let firstUpdate = hook.queue.pending.next;

    do {
    
    
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending)

    hook.queue.pending = null;
  }
  hook.memoizedState = baseState;

  return [baseState, dispatchAction.bind(null, hook.queue)];
}

  到此为止,我们完整的实现了一个 hook ,它与源码中的 hook 还是有细微差异的,但是原理相同。

  最后点以下题,看到现在大家应该感觉出来 hook 和生命周期还是很不一样的吧,那各位觉得 hook 到底是个啥?

  我个人感觉更像是一个工具,这个工具会帮我们把数据更新到页面上,不同的 hook 更新效果不太相同,有的会同步更新,有的则会延迟更新,我们要做的则是在不同的业务需要下,使用这些工具,至于工具到底是怎么做到的,而且还做的这么好,那就需要我们反复的研究源码,才能熟知。




猜你喜欢

转载自blog.csdn.net/EcbJS/article/details/115168299