实现useState钩子函数(小白级教程,简单易懂)

294 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

核心

  1. 贯穿hooks的核心就是index数组
  2. 一定要理解惰性初始化

前言

使用了react技术栈的同学,想必对hooks都比较熟悉,他的优点也是很多的,为了让我们能够更加顺手的使用hooks,我们来探索一下他的原理,并且实现一下他

希望大家在学完这几篇文章以后,能懂的一下几个问题

  1. 为什么只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用?

Q:useState是用来干什么的?
A:简单来说就是用来保存状态的和更改状态的

保存状态大家很熟悉,第一反应就是闭包,所以useState是如何用闭包来保存状态和更改状态的,这就是我们今天实现的核心内容

准备工作

先使用create-react-app创建一个项目

create-react-app hooks

进入他的App.js文件

我们把没用的东西都删除掉,制造一个干净的环境

function App() {

  return (
    <div>
     
    </div>
  );
}

export default App;

然后把App.js换成App.tsx,这一步非必要,主要是我ts太菜了,所以能用ts的地方,我绝对不用js

我们要实现的功能

import { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      {count}
      <button onClick={()=>setCount(count+1)}>点击</button>
    </div>
  );
}

export default App;

这段功能很简单

image.png

动手实现

// 首先我们建立一个target,也就是我们要改变的state值
let target: any;

// 改变state的值的方法
function setState(state:any) {
  // update阶段
  target = state;
}

// 我们要实现的useState
function useState(initialState:any) {
  // mount阶段
  target = initialState;
  return [target, setState]
}

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      {count}
      <button onClick={()=>setCount(count+1)}>点击</button>
    </div>
  );
}

export default App;

此时我们点击按钮还是不会改变的,因为我们还没有更新组件

现在我们引入ReactDOM去更新组件

import ReactDOM from 'react-dom';

let target: any;

function setState(state) {
  // update阶段
  target = state;
  // 更新组件
  ReactDOM.render(<App />, document.getElementById('root'));
}

此时我们点击button发现count还是没有变,这是为啥呢?

因为每次在执行render的时候,又会走useState,此时一走,就会又赋值初始值,所以我们对useState方法在更改一下,如果有值那么取state,没有值的话,在取初始值

所以这也就是**惰性初始化(useState的初始值只会执行一次)**的原因

function useState(initialState) {
  target = target ? target : initialState;
  return [target, setState]
}

此时我们在点击按钮,终于可以动了

image.png

问题又来了,我再添加一个state,发现错误出现了

function App() {
 
  const [count, setCount] = useState(0);
  const [name, setName] = useState('张三');
  return (
    <div>
      <div>
        count:{count}
        <button onClick={() => setCount(count + 1)}>点击</button>
      </div>
      <div>
        name:{name}
        <button onClick={() => setName('李四')}>点击</button>
      </div>
    </div>
  );
}

image.png

因为他们使用的是同一个变量target,那么你第二次初始化的时候,他自然把target值给改了,所以在渲染的时候,两个target值是相同的

所以接下来我们要做的就是解耦,解耦就要改变target的数据结构,因为基础类型,只能保存一个值,复杂类型就不一样了,能够保存的值就多了,复杂类型保存数据主要有两种,一个数组,一个对象,因为对象需要定义不同的key值,所以我们用简单一些的数组

let target: any[]=[];
let index = 0;

有数组了,那么肯定要有索引的,所以上面我们又定义了一个index

此时我们的useState中就要变成往数组中存储数据了

function useState(initialState) {
  let value = target[index] ? target[index] : initialState;
  target[index] = value;
  // 这一块大家可能会有疑问,稍后我们会讲
  let set = setState(index);
  // 每次usetState以后,index需要++
  index++;
  return [value, set]
}

上面这段代码,其他地方都比较好理解,主要是 setState(index)可能会有疑问,为啥要这么写呢,主要是为了存储index,否则每次setState更新的值都不是我们想要更新的值,这一块就是useState的核心,这一块大家一定要弄明白

然后我们在改写一下我们的setState

function setState(currentIndex:number) {
  return function (state: any) {
     // update阶段
    target[currentIndex] = state;
    // 每次渲染都要把index设置为0,这样才能取到正确的值
    index = 0;
    // 更新组件
    ReactDOM.render(<App />, document.getElementById('root'));
  }
}

存储index,一般要存储一个值要用什么方法呢?对闭包了解的同学,想必都会清楚,用闭包呀

完整内容

import ReactDOM from 'react-dom';

let target: any[]=[];
let index = 0;

function setState(currentIndex:number) {
  return function (state: any) {
     // update阶段
    target[currentIndex] = state;
    render();
  }
}

function useState(initialState) {
  let value = target[index] ? target[index] : initialState;
  target[index] = value;
  let set = setState(index);
  index++;
  return [value, set]
}

function render() {
  // 更新组件
  index = 0;
  ReactDOM.render(<App />, document.getElementById('root'));
}

function App() {
 
  const [count, setCount] = useState(0);
  const [name, setName] = useState('张三');

  return (
    <div>
      <div>
        count:{count}
        <button onClick={() => setCount(count + 1)}>点击</button>
      </div>
      <div>
        name:{name}
        <button onClick={() => setName('李四')}>点击</button>
      </div>
    </div>
  );
}

export default App;

总结

上面的这个内容并非真实的useState源码,但是原理还是差不多的,我们理解一个内容,主要是理解他的原理,而不是非要一模一样。

如果感觉上面的内容对你有帮助,还请点个赞哦👍🏻

参考

React Hook: 手动实现useState - 掘金 (juejin.cn)