手写简易React Hooks

293 阅读2分钟

React 16.8 新特性 - Hook

在 React 16.8 版本出现了React Hooks, 目标是让函数组件能彻底取代Class组件, ⼲掉state、⽣命周期这些概念。它可以让你在不编写 Class 的情况下使用 state 以及其他的 React 特性。

下面我们通过React Hooks来编写一个简单的计数器,当你点击按钮,计数器的值就会增加:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
ReactDOM.render(<Counter />, document.querySelector("#root"));

useState 唯一的参数就是初始 state。在上面的例子中,我们的计数器是从零开始的,所以初始 state 就是 0。

那么我们可以实现这个 useState 吗?

从上述实例中我们可以看出来:

  • 这个 useState 是一个返回值为数组的函数
  • 返回的数组第一项是一个可修改的数据,第二项是一个可修改第一项的函数。
  • 当通过函数修改数据时,整个组件会重新渲染
  • 重新渲染组件时,获得的count是修改后的新值,而不是初始值

代码如下:

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(); 

但是当你想要使用多个state时,上述代码便不能使用了,因为只有一个全局的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();

以上代码仅仅简单实现了React Hooks的 useState。

参考自React