目录

手写简易React Hooks

前言

React 16.8 版本出现了React Hooks, 目标是让函数组件能彻底取代Class组件, ⼲掉state、⽣命周期这些概念。不管是Class组件还是函数组件,其底层思路都是类似的:

  1. 组件里维护数据状态,和提供修改这些状态的方法;
  2. 视图里使用到这些数据状态;
  3. 当用户触发修改状态的方法时数据状态被修改并且导致组件再次被渲染。

下面是函数组件以及使用React Hooks的一个范例。 codesandbox查看效果

import React, { useState } from "react";
import ReactDOM from "react-dom";
 
function Counter() {
  let [count, setCount] = useState(0);
  return (
    <>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click</button>
    </>
  );
}
 
ReactDOM.render(<Counter />, document.querySelector("#root"));

思路

如何造出上面代码中的 useState 呢?

从上面的使用方式我们能得到这些信息:

  1. useState是一个函数
  2. useState执行返回一个数组,数组第一项是内部维护的数据(通过函数第一次调用的参数传入,可被修改),数组第二项是一个能修改内部数据的函数
  3. 当触发修改数据修改的方法时,会修改数据,并且会再次渲染组件
  4. 再次渲染组件时,会再次执行useState,获取修改后新值而不是初始值
import React from "react";
import ReactDOM from "react-dom";
 
let value;
function useState(initValue) {
  value = value === undefined ? initValue : value;
  function dispatch(newValue) {
    value = newValue;
    scheduleWork();
  }
  return [value, dispatch];
}
 
function Counter() {
  let [count, setCount] = useState(0);
  return (
    <>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}> Add count</button>
    </>
  );
}
 
function scheduleWork() {
  ReactDOM.render(<Counter />, document.querySelector("#root"));
}
 
scheduleWork(); 

于是有了上述代码。 codesandbox查看效果

问题

当在组件里连续使用多个 useState 时,上述代码就没法正常工作了。因为只有一个全局的value ,无法同时代表多个数据。

可以沿着当前思路继续往下走。

把每个数据都放到一个对象节点里,这些节点构成一个单向链表,这样我们就能存储多个数据。

hook = {
  state: null,    //数据
  dispatch: null, //修改数据的方法
  next: null      //指向下一个节点
}

把执行过程分为 mount 和 update 两个阶段,两个阶段做的事情不一样。

在 mount 阶段依次执行 useState 时,会使用初始化的数据依次创建多个hook节点,构造链表。

在 update 阶段依次执行 useState 时,会从链表开头依次遍历 hook 节点,返回节点信息(如[age, setAge]) 。

执行修改数据的方法时会修改当前hook节点的数据,定位到链表开头, 修改mount阶段到update阶段。

代码如下:

import React from "react";
import ReactDOM from "react-dom";
 
const Dispatcher = (() => {
  let isMount = true;
  let firstWorkInProgressHook = null;
  let workInProgressHook = null;
 
  function mountWorkInProgressHook() {
    const hook = {
      state: null,
      dispatch: null,
      next: null
    };
    if (workInProgressHook === null) {
      firstWorkInProgressHook = workInProgressHook = hook;
    } else {
      workInProgressHook = workInProgressHook.next = hook;
    }
    return workInProgressHook;
  }
 
  function updateWorkInProgressHook() {
    let curHook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
    return curHook;
  }
 
  function useState(initialState) {
    let hook;
    if (isMount) {
      hook = mountWorkInProgressHook();
      hook.state = initialState;
    } else {
      hook = updateWorkInProgressHook();
    }
 
    hook.dispatch = function (newState) {
      this.state = newState;
      workInProgressHook = firstWorkInProgressHook;
      isMount = false;
 
      scheduleWork();
    }.bind(hook);
 
    return [hook.state, hook.dispatch];
  }
 
  return {
    useState
  };
})();
 
function Counter() {
  let [count, setCount] = Dispatcher.useState(1);
  let [age, setAge] = Dispatcher.useState(10);
  return (
    <>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}> Add count</button>
      <p>Age is {age}</p>
      <button onClick={() => setAge(age + 1)}> Add age</button>
    </>
  );
}
 
function scheduleWork() {
  ReactDOM.render(<Counter />, document.querySelector("#root"));
}
 
scheduleWork();

codesandbox 查看效果

最后

以上仅仅简单实现了React Hooks的 useState,里面参考了小部分React源码的实现和思路,欢迎评论区多多交流。

饥人谷一直致力于培养有灵魂的编程者,打造专业有爱的国内前端技术圈子。如造梦师一般帮助近千名不甘寂寞的追梦人把编程梦变为现实,他们以饥人谷为起点,足迹遍布包括facebook、阿里巴巴、百度、网易、京东、今日头条、大众美团、饿了么、ofo在内的国内外大小企业。 了解培训课程:加微信 xiedaimala03,官网:https://jirengu.com