依据React Hooks的原理,写一个简易的 useState

参考文章:React Hooks 原理
先回顾一下 useState 的用法

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

总结:调用 useState 得到一个状态和一个修改该状态的函数
依据这个特点,来实现一个简易的 useState 吧

预设一个执行环境

先预设一个执行环境,在这里我们用 console.log 去模拟视图的渲染

let onClick;

function render() {
    const [count, setCount] = useState(0);
    console.log("使用 clg 模拟视图渲染", count);
    // 使用 onClick 模拟更新操作
    onClick = () => {setCount(count + 1)};
}

render();
onClick();
onClick();

使用上面这段代码,来模拟 React 界面的渲染和点击两次按钮

最初版本的,满足一个状态和修改状态函数的返回

function useState(defaultState) {
    let _state = defaultState;
    function setState(newState) {
        _state = newState;
        render();
    }

    return [_state, setState];
}

使用结果如下:

能够很明显的看到,其实两次点击都是没有效果的

这是因为,每次 setState 时,它state改变然后需要重新 render,在重新 render 时,执行 useState 又了赋初始值,这样就导致每次的 state 都被初始值覆盖了。

改进版本,修复了 setState 无效的 bug

针对上面的问题,我们可以使用闭包的性质,把 state 提取出来,让它成为一个自由变量,然后每次 调用 useState 时都判断一下,当前state有没有值,有的话就不要让初始值对它进行覆盖了。

// 提取成为全局的自由变量
let _state;

function useState(defaultState) {
  // 赋初始值前,先进行判断当前state是不是没用过
    _state = _state !== undefined ? _state : defaultState;
    function setState(newState) {
        _state = newState;
        render();
    }

    return [_state, setState];
}

使用结果如下:

但是,如果我们又调用一个 useState 去开辟一个名为 name 的 state,然后通过一个 onChange 方法去使用会怎么样呢?

改造一下最初模拟的运行环境,让它变成这样:

let onClick;
let onChange;

function render() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState("77")
    console.log("使用 clg 模拟视图渲染 --- count", count);
    console.log("使用 clg 模拟视图渲染 --- name", name);
    // 使用 onClick 模拟更新操作
    onClick = () => { setCount(count + 1) };
    onChange = (name) => { setName(name) };
}

render();
onClick();
onClick();

onChange("kiana")
onChange("kiana_k423")

运行结果如下:

可以看到完全错乱了

原因也很显而易见,就是因为,多个 state 共用了一个全局的 _state 自由变量

最后改进的版本,修复多次调用 useState,各个 state 状态错乱的 bug

针对这个问题,react 是如何解决的呢?
react 选择的是链表结构,每个 hook 除了自身的state,另外还有一个 next 属性,用于指定下一个 hook
这里,我们选择数组简单模拟一下:

let _memoizedState = []; // 多个 hook 存放在这个数组
let _idx = 0; // 当前 memoizedState 下标

/**
 * 模拟实现 useState
 * @param {any} defaultState 默认值
 * @returns state 和 setState 方法
 */
function useState(defaultState) {
    // 查看当前位置有没有值
    _memoizedState[_idx] = _memoizedState[_idx] || defaultState;
    // 再一次利用闭包,让 setState 更新的都是对应位置的 state
    const curIdx = _idx;
    function setState(newState) {
        // 更新对应位置的 state
        _memoizedState[curIdx] = newState;
        // 更新完之后触发渲染函数
        render();
    }

    // 返回当前 state 在 _memoizedState 的位置
    return [_memoizedState[_idx++], setState];
}

最后根据上面那个模拟的执行环境再来使用一下:

// 模拟的 react render
let onClick;
let onChange;

function render() {
    // _idx 重新置为 0, 也是契合react每次更新时都从 hooks 头节点开始更新每一个 hook
    // 重置的操作也可以写在 useState 的 render 之前,都是一样的思路
    _idx = 0;
    const [count, setCount] = useState(0);
    const [name, setName] = useState("77")
    console.log("使用 clg 模拟视图渲染 --- count", count);
    console.log("使用 clg 模拟视图渲染 --- name", name);
    // 使用 onClick, onChange 简单模拟一下更新操作
    onClick = () => { setCount(count + 1) };
    onChange = (name) => { setName(name) };
}

render();
console.log("-------------");
onClick();
onClick();
console.log("-------------");
onChange("kiana")
onChange("kiana_k423")

效果如下:

能看见更新都是按部就班地更新了自己对应位置的state

总结

这个简易实现和 react hooks 源码还是有很大的出入的,首先 react hooks 源码中采用的是链表结构,然后链表中单个节点的数据结构定义如下:

// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
  memoizedState: any, // 最新的状态值
  baseState: any, // 初始状态值,如`useState(0)`,则初始值为0
  baseUpdate: Update<any, any> | null, // 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
  queue: UpdateQueue<any, any> | null, // 临时保存对状态值的操作,更准确来说是一个链表数据结构中的一个指针
  next: Hook | null,  // 指向下一个链表节点
};

能看到还是比较复杂的,这是因为 react hooks 它除了上述的核心功能之外,还需要考虑很多边界情况,异步更新,优先级调度以及封装自定义hook的情况。

文章采用数组结构,是忽略了很多 react 异步渲染和优先级调度的一些场景的。但是也足够契合 react hook的核心思路,也更方便去理解和实现。(其实就是执行 setState后,函数式组件重新render,同时也会重新去从头到下去执行 hooks)
就比如:const [count, setCount] = useState(0); 很明显,这个 count 是const 声明是不可变的,但是执行 setCount 之后视图上的 count 就更新了,这就是因为,当执行 setCount 时,该函数式组件就重新 render 了,重新 render 的过程中 count 又被重新地 const 声明了。

而文章的简易实现,只是为了更好地理解在使用 react hooks 时为什么要写在函数式组件顶端且一定要保证顺序调用。这就是是因为初始化阶段和更新阶段 hooks 都是按照同一个顺序去执行的,倘若更新阶段执行的 hooks 比初始化阶段的 hooks 要少或者要多都是会报错的。 另外有关 useEffect 的简易实现也可以继续看一下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335