「React Hooks」手写 useState & useEffect

1,888 阅读3分钟

React Hooks 不是魔法,仅仅是数组(链表)。

Preview in codesandbox →

前言

React Hooks 是 2019 年由 Facebook 开发团队在 React v16 版本中推出的新特性,比起类组件,他们认为函数组件更加适合前端 UI 开发,更适合 React 未来的发展方向。那么,如何在函数组件里使用状态副作用,成为了亟待解决的问题,于是, React Hooks 在这种趋势下应运而生。

1. 闭包函数

手写一个叫做 useState 的函数,函数内部会形成一个闭包,外部可以通过 setState 设置闭包内部的局部变量,并通过运行 state() 获取闭包内的最新的状态值 _val

function useState<T>(initVal: T): [() => T, (v: T) => void] {
  let _val = initVal;
  let state = () => _val;
  let setState = (newVal: T) => {
    _val = newVal;
  };
  return [state, setState];
}

const [count, setCount] = useState<number>(1);
console.log(count()); // -> 1
setCount(2);
console.log(count()); // -> 2

运行以上代码,可以看到控制台能先后打印出 12

2. 作为 React 的一个方法

使用立即执行函数生成一个 React 对象,将 function useState() 放入其中并返回,模拟 React 库:

const React = (() => {
  function useState<T>(initVal: T): [() => T, (v: T) => void] {
    let _val = initVal;
    let state = () => _val;
    let setState = (newVal: T) => {
      _val = newVal;
    };
    return [state, setState];
  }
  return {useState}
})();

const [count, setCount] = React.useState<number>(1);
console.log(count()); // -> 1
setCount(2);
console.log(count()); // -> 2

3. 手写 Component 渲染

手动实现一个 React.render,它能够打印出 count,在控制台模拟实现渲染的功能。

const React = (() => {
  let _val: any;
  function useState<T>(initVal: T): [T, (v: T) => void] {
    let state = _val || initVal;
    let setState = (newVal: T) => {
      _val = newVal;
    };
    return [state, setState];
  }
  function render(Com: () => any) {
    let C = Com();
    C.render();
    return C;
  }
  return { useState, render };
})();

function Component() {
  const [count, setCount] = React.useState<number>(1);
  return {
    render: () => {
      console.log(count);
    },
    click: () => {
      setCount(count + 1);
    }
  };
}

var app = React.render(Component); // => 1
app.click();
var app = React.render(Component); // => 2

4. 多个 useState

当我们需要使用多个 useState 的时候,发现会打印混乱,因为多个 useState 共享了同一个变量 let _val;,这显然不是我们想要的。

const React = (() => {
  let _val: any;
  function useState<T>(initVal: T): [T, (v: T) => void] {
    let state = _val || initVal;
    let setState = (newVal: T) => {
      _val = newVal;
    };
    return [state, setState];
  }
  function render(Com: () => any) {
    let C = Com();
    C.render();
    return C;
  }
  return { useState, render };
})();

function Component() {
  const [count, setCount] = React.useState<number>(1);
  const [text, setText] = React.useState<string>("React");
  return {
    render: () => {
      console.log({ count, text });
    },
    click: () => {
      setCount(count + 1);
    },
    type: (word: string) => {
      setText(word);
    }
  };
}

var app = React.render(Component); // => {count: 1, text: "React"}
app.click();
var app = React.render(Component); // => {count: 2, text: 2}
app.type("Vue");
app.type("Vue");
var app = React.render(Component); // => {count: "Vue", text: "Vue"}

5. 使用数组存储 state

使用数组能够稳定的保存多个 useState 的状态,当每次组件 render 时,将数组下标恢复至 0

const React = (() => {
  let hooks: any[] = [];
  let idx = 0;
  function useState<T>(initVal: T): [T, (v: T) => void] {
    let state = hooks[idx] || initVal;
    let _idx = idx;
    let setState = (newVal: T) => {
      hooks[_idx] = newVal;
      console.log(`hooks[${_idx}]`, newVal);
    };
    idx++;
    return [state, setState];
  }
  function render(Com: () => any) {
    idx = 0;
    let C = Com();
    C.render();
    return C;
  }
  return { useState, render };
})();

function Component() {
  const [count, setCount] = React.useState<number>(1);
  const [text, setText] = React.useState<string>("React");
  return {
    render: () => {
      console.log({ count, text });
    },
    click: () => {
      setCount(count + 1);
    },
    type: (word: string) => {
      setText(word);
    }
  };
}

var app = React.render(Component); // => {count: 1, text: "React"}
app.click(); // => hooks[0] 2
var app = React.render(Component); // => {count: 2, text: 2}
app.type("Vue"); // => hooks[1] Vue
app.type("Vue"); // => hooks[1] Vue
app.click(); // => hooks[0] 3
app.type("Angular"); // => hooks[1] Angular
var app = React.render(Component); // => {count: 3, text: "Angular"}

总结

本文章采用关注点分离的方式,目的在于描述 useState 是如何通过闭包数组遍历在组件中实现多个状态共存,并稳定的存取的。演示代码屏蔽了 React Diff 和 React.render 渲染 DOM 的细节,采用 console.log() 一笔带过,实际上 React 的渲染过程没那么简单。

正因为 React Hooks 的实现机制是基于闭包数组遍历,因此使用 React Hooks 需要遵循以下规则

  1. 不要在循环、条件或嵌套函数中调用 Hook;
  2. 仅在 React 的函数组件中调用 Hook;

参考资源

[1] How do React hooks really work Under The Hood ? By Engineers.SG - Youtube