70行代码带你手写一个useState及完成对hook原理的初探

首先原理和源码这个玩意他需要你对这个东西使用的比较多且比较熟, 你才能知道我在说什么, 然后就是你必须具备一定的基础知识储备, 希望这篇博客可以帮助到你去理解react hook的内部运作机制, 我也希望我能够以尽可能平滑的姿态去描述这个东西

本身因为hooks在源码中是在renderWithHooks调用的, 但是在调用前他会去获得一些其他的环境数据(这就会涉及到一些fiber的操作, 再扯到fiber的双缓存模型, 扯到fiber的commit phase和render phase那就扯远了), 这里如果我铺开来说的一个是知识点比较多, 可能每个几万字说不出来, 其次是代码量较大, 对你理解hook是有影响的, 那我这里就假设这些环境数据我已经取得了, 而且我的fiber节点已经构造好了, 我们只用专心去实现useState这个hook

在写代码之前我希望通过一张图, 让你稍微能够理解一下useState乃至其他hook的工作流程

手写useState.png
上面的图还是比较草率的, 但是你至少可以知道hook的一个大致流程

然后还有一个需要注意的点就是你需要了解循环链表的概念, 因为在状态的更新中, react是通过使用循环链表来保证每次都稳稳的找到正确的状态索引从而进行更新(这也是为什么hooks不能写在if else中和必须出现在顶部的根本原因)

话不多说, 上才艺:

我们新建一个useState.js文件, 然后一步一步跟着我的思路来完成这个文件

其实我们大概能根据现象去写一些东西(ps: 这也是手写工具的一个技巧, 我们从现象切入去写他的代码)

1. useState调用过后他会返回给你一个状态和一个update状态的值,这一点和useReducer很像, 所以我们导出一个函数, 把对应的基本现象都写上

// useState.js
export default function(initailState) { // initailState: 就是初始值
  let baseState = initailState;  // 这行代码我相信大家都能够懂吧, 当然后续我们会改造他
  return [baseState, ?]; // 这个修改的函数我们没有, 不急, 后面补上
}
复制代码

其实经过上面的一部分, 我们先来看一个基础问题:

useState写在函数组件中, 我们知道函数组件状态和数据的变化都会导致整个函数重新执行, 那么意味着useState会被重新调用, 那么我们应该怎么去缓存useState的值? 以确保函数组件被重新调用的时候我们的状态还稳稳的保存下来?

Ok, 我们就来切入这一个问题来解决:

// useState.js
let isMount = false; // 我引入一个是否是首次挂载的flag值来协助我们进行是否是首次加载的判定
// fiber是React中的一个比较重要的概念, fiber是一种数据结构, 一个fiber对应一个虚拟dom
// 这里我们不对fiber进行展开描述, 因为他是在render phase之前就会去做的一个事情
// 当fiber树构建完毕以后, 其实每个React Node就对应了一个fiberNode 
let workInProgressHook = null; // 这个就是帮助我们缓存state结果的
const fiberNode = { 
  staticNode: App, // 其实这里staticNode是在React进行fiber树构建的时候注入进来的属性, 因为我们没有这一步, 所以就直接把App挂载进来了, staticNode就代表的是你当前fiberNode所指向的React Node
  // 这个memorizedState是帮我们记录一下state hook之间的链表关系, 与下面的hookEnv是一致的
  // 这是一个全局的状态控制器
  mermorizedState: null,
}
export default function(initailState) {
  let hookEnv = { // 我们构造一个hookEnv对象出来, 方便我们将一些有用的数据集合进这个hook中, 如果你想的话你也可以分散成不同的变量
    mermorizedState: initailState, 
    // next是干嘛的, 就是我希望做一件事, 就是我这个hookEnv是第几个进来的我不知道, 但是我希望
    // 用这个next来形成一个链表关系: 比如: firstState.next = secState, 这样我们就可以通过这个
    // 链表来记录整个组件中useState的调用顺序和关系
    next: null, 
  }
  if (!isMount) {
    // 代表是首次渲染, 首次渲染我们要做的事情就是将这个hook保存到一个地方去
    if (fiberNode.mermorizedState === null) {
      // 代表当前的fiber节点中还没有一个状态的记录, 那也意味着我们本次的useState调用是第一次
      fiberNode.mermorizedState = hookEnv;

    } else {
      // 代表这已经不是第一次调用useState了, 所以我们直接将这一次的hookEnv给我推入链表中
      fiberNode.mermorizedState.next = hookEnv
    }

    // 经过上面的操作以后, 本次statehook的调用在挂载阶段的初始步骤已经走完了, 为了方便后续的
    // 更新阶段使用, 我们要讲本次的hookEnv直接假设为我们最后一次的hook调用, 所以缓存数据是必然的
    workInProgressHook = hookEnv;
    isMount = true;
  } else {
    // 如果是更新阶段, 我们完全不需要走上述流程, 我们只需要将我们的hookEnv设置为我们之前缓存的结果
    hookEnv = workInProgressHook; 
    // 这样还不够, 因为当每一次函数组件的调用, 所有的useState都会被重新按照顺序调用
    // 如果你每次更新都让他始终是一个workInProgressHook, 这显然是不行的
    // 别忘了: 我们上面可是通过next保存了引用的
    workInProgressHook = workInProgressHook.next; 
  }


  // 注意就是: 下方的hookEnv在mount阶段和update阶段取值不同哦
  let baseState = hookEnv.mermorizedState; // 修改一下baseState的取值

  return [baseState, ?];
复制代码

Tips: 你可以把代码复制到你的编辑器中, 然后把注释删掉看, 可能会直观些

走到上面这一步, 我相信通过代码和注释, 你已经基本搞明白了react怎么去区分mount阶段和update阶段关于状态值的缓存处理, 第一个问题迎刃而解, 我们来第二个问题:

  • 我们返回出去的第二个更新状态的函数哪里来?

答案是我们自己构造给用户, 跑不掉的哈哈

// react管这个函数叫做disptachAction

// 我们来看个例子
// const [num, setNumber] = useState(0);
// 上面的setNumber其实就是这个dispatchAction, 当我们调用dispatchAction的时候
// 其实我们是可以传递一个函数或者一个状态的, 我们来写写
function dispatchAction(updateValue) { 
  
}

export default function(initailState) {
  // ... 略过: 就是上面的代码

  return [baseState, dispatchAction]; // 就是这样返回的, 但是还不够我们后续还需要修改
}
复制代码

我上面为啥停了呢, 因为我写不下去了, 我们看看这个问题:

  • 这个dispatchAction函数接受一个新状态, 那我应该去改谁的状态啊, 我拿不到之前的那个状态啊, 比如说:

    function App() {
      const [num, setNumber] = useState(0);
      const [str, setString] = useState("");
    
      setNumber(1);
      setString("hello"); 
    }
    复制代码

    就上面这个例子, 我触发setString的时候我怎么知道我应该改哪个state啊?

ok, 我们继续来解决这个问题

// useState.js
// 要处理这个问题: 我们首先要想想, 我们有没有缓存过之前的状态, 答案是有的: 可不就是当前的hookEnv
// 那我们需要将hookEnv传递给dispatchAction让他去直接去修改里面的mermorizedState吗? 
// 答案是可以的, 但是react是通过传递给dispatchAction一个环境数据, 让他去进行
// 单纯的action赋值, 我们来看看他是怎么做的
import App from "./App"
let isMount = false;
let workInProgressHook = null;
const fiberNode = { 
  staticNode: App,
  mermorizedState: null, 
}

// 我们在dispatchAction中新增一个queue变量, 这个变量用来存放环境数据
function dispatchAction(queue, updateValue) {
  // 当每一次dispatch被调用的时候, 我们创建一个update对象
  const updateTarget = {
    updateValue,
    next: null, // 看到next你一定又知道这肯定要做链表操作了
  }

  if (queue.pedding === null) {
    // 代表这是第一次触发dispatchAction
    // updateTarget.next = updateTarget; // 这个updateTarget的尾巴指向updateTarget, 是不是就形成了一个循环链表

    // 强调一下我们为什么要环形链表哈

  } else {
    // updateTarget.next = queue.pedding.next
    queue.pedding.next = updateTarget
  }

  queue.pedding = updateTarget; // 赋值这个不用多说了吧
  // 然后到这里我们触发一次App的重新渲染
  // 当然还需要传递props和一些其他的东西的哈, 只是我们这里作为只聚焦useState很多特殊的就不搞了
  // 真实情况下组件的重新渲染是在一个函数环境中被执行的, 那个函数环境中还包含了他的上下文和props
  // 会重新渲染他
  fiberNode.staticNode();
}



export default function(initailState) {
  let hookEnv = { 
    mermorizedState: initailState, 
    next: null, 
    queue: {
      pedding: null, // 所以我们在这里就要更新一个queue的环境变量出来了
    }
  }
  if (!isMount) {
    if (fiberNode.mermorizedState === null) {
      fiberNode.mermorizedState = hookEnv;
      hookEnv.next = fiberNode.mermorizedState;

    } else {
      fiberNode.mermorizedState.next = hookEnv
      hookEnv.next = fiberNode.mermorizedState
    }
    workInProgressHook = hookEnv;
    workInProgressHook = workInProgressHook.next; 

  } else {

    hookEnv = workInProgressHook; 
    workInProgressHook = workInProgressHook.next; 
  }


  let baseState = hookEnv.mermorizedState;

  // 因为如果是mount阶段的话queue.pedding一定是null
  // 更新阶段就是用户一定调用了我们传递出去的dispatchAction, 一旦调用了dispatchAtion
  // 那么queue.pedding就会被赋值
  if (hookEnv.queue.pedding !== null) {
    let updateInstance = hookEnv.queue.pedding;
    do {
      baseState = typeof updateInstance === "function" ? updateInstance(baseState) : updateInstance.updateValue;
      updateInstance = updateInstance.next;

    } while(updateInstance !== null)
  }
  hookEnv.mermorizedState = baseState; // 当每次结束以后其实我们都需要去更新我们的整个缓存的数据, 这里有个闭包

  hookEnv.queue.pedding = null
  return [baseState, dispatchAction.bind(null, hookEnv.queue)]; // 改成bind默认绑定第一个参数没毛病把
}
复制代码

到这一步我们的整个useState就完毕了, 你可以直接去项目中引用我们自己写的useState了 因我们的写法是每次dispatch的调用都只是返回一个action操作对象, 那么本质上我们的多次dispatch调用只会造成一次组件状态的变更

这篇博客不长, 但是如题, 只是带你初探一下React Hooks背后强大的逻辑思维, 希望可以帮助到你更好的理解及运用useState

另外, 如果小伙伴对hook有更好奇的地方, 可以去阅读一下下面这篇文章:

juejin.cn/post/694486…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享