30行代码从零实现useState和useEffect

2024/07/28

本文完整示例代码在:codesandbox.io

实现一个最基础的useState

让我们从最简单的useState实现开始:

function useState(initVal) {
let _val = initVal
let state = () => _val;
const setState = (data) => {
_val = data;
};
return [state, setState];
}
const [a, setA] = useState(0);
console.log(a()); // 0
setA(1);
console.log(a()); // 1

上线代码虽然可以实现state的初始化和更新,但是不足是:我们使用的是一个函数来获取state的,而React里面使用的是变量获取的。

我们来一步步优化代码。

重构:模拟 React 的工作方式

useState使用React模块导出的,我们也来实现一下:

const React = (function () {
let _val;
function useState(initVal) {
// 注意:state的值优先从_val读取
let state = _val || initVal;
const setState = (data) => {
_val = data;
};
return [state, setState];
}
return {
useState,
};
})();

使用useState

React是在组件内使用useState,组件如下:

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

Component的render方法就是打印当前a变量,而click则会重新设置a

添加render方法

我们需要在React模块内增加一个render方法来渲染上面的Component:

const React = (function () {
// ...
function render() {
let c = Component();
c.render();
return c;
}
return {
useState,
render,
};
})();
function Component() {
// ...
}

接下来执行上面的代码,可以看到每一次点击,我们都会重新渲染组件,得到正确的state值

// 渲染组件
var app = React.render(Component); // 0
app.updateCount();
var app = React.render(Component); // 1
app.updateCount();
var app = React.render(Component); // 2

Untitled

不过现在上面这个实现不支持多个state,我们继续完善

支持多个state

为了支持多个 state,我们需要对实现进行进一步改进。

完整实现如下:

const React = (function () {
let hooks = [];
let index = 0;
function useState(initVal) {
let state = hooks[index] || initVal;
// 使用_index保存index, 否则setState调用的时候里面的index有bug
let _index = index;
const setState = (data) => {
hooks[_index] = data;
};
index++;
return [state, setState];
}
function render() {
// 每次重新渲染必须要hooks index
index = 0;
let c = Component();
c.render();
return c;
}
return {
useState,
render,
};
})();
function Component() {
const [count, setCount] = React.useState(0);
const [fruit, setFruit] = React.useState("apple");
return {
render: () => console.log(count, fruit),
updateCount: () => setCount(count + 1),
updateFruit: (fruit) => setFruit(fruit),
};
}
var app = React.render(Component);
app.updateCount();
var app = React.render(Component);
app.updateFruit("banana");
var app = React.render(Component);

Untitled

这里有一些关键点需要注意:

function useState(initVal) {
let state = hooks[index] || initVal;
const setState = (data) => {
hooks[index] = data;
};
index++;
return [state, setState];
}
function render() {
// 每次重新渲染必须要hooks index
index = 0;
...
}

hooks可以用在条件语句吗?

面试的时候我们经常会遇到这个问题,结合我们上面的实现可以看出,Hooks 不能在条件语句中使用。这是因为:

  1. Hooks 的工作原理依赖于它们被调用的顺序。
  2. 在我们的实现中,我们使用了一个全局的 index 来跟踪当前正在处理的 Hook。每次调用 Hook 时,index 都会递增。
  3. 如果我们在条件语句中使用 Hook,那么在某些渲染中,这个 Hook 可能不会被调用,这会导致 index 的不一致。
  4. 不一致的 index 会导致 Hooks 的状态混乱,可能会把一个 Hook 的状态赋给另一个 Hook。

举个例子:

function Component() {
const [count, setCount] = React.useState(0);
if (count > 5) {
const [name, setName] = React.useState("John");
}
}

在这个例子中,当 count <= 5 时,name 这个 state 不会被创建。但是当 count > 5 时,突然多了一个 state。这会导致后面的 Hooks 的 index 发生错位,从而引起错误。

实现useEffect

让我们来实现 useEffect Hook。useEffect 用于处理副作用,它接受两个参数:一个回调函数和一个依赖数组。

useEffect实现的核心思路是:我们需要吧effect的依赖存在hooks数组,里面然后去对比新旧依赖

  1. useEffect 函数首先检查是否存在旧的依赖数组。
  2. 如果存在旧的依赖数组,它会比较新旧依赖是否有变化。我们使用数组 some 方法进行比较
  3. 如果依赖发生了变化(或者是第一次运行),就执行回调函数。
  4. 最后,保存新的依赖数组,并增加 index。

代码如下:

const React = (function () {
let hooks = [];
let index = 0;
function useState(initVal) {
// ... 之前的 useState 实现 ...
}
function render() {
// ... 之前的 render 实现 ...
}
function useEffect(cb, depArray) {
const oldDeps = hooks[index];
let hasChanged = true;
if (oldDeps) {
if (!depArray.some((dep, i) => dep !== oldDeps[i])) {
hasChanged = false;
}
}
if (hasChanged) {
cb();
}
hooks[index] = depArray;
index++;
}
return {
useState,
render,
useEffect,
};
})();
function Component() {
const [a, setA] = React.useState(0);
const [fruit, setFruit] = React.useState("apple");
React.useEffect(() => {
console.log("hello");
}, [a]);
return {
render: () => console.log(a, fruit),
updateCount: () => setA(a + 1),
updateFruit: (fruit) => setFruit(fruit),
};
}
var app = React.render(Component);
app.updateCount();
var app = React.render(Component);
app.updateFruit("banana");
var app = React.render(Component);

控制台的输出顺序如下:第一行先执行了副作用也就是console,当我们updateCount之后,又会执行一次副作用的console

Untitled

在effect的依赖树组里面再加一个fruit变量,也可以正常运行

Untitled

完。