brickspert/blog

React Hooks 原理

brickspert opened this issue · 68 comments

前言

目前,Hooks 应该是 React 中最火的概念了,在阅读这篇文章之前,希望你已经了解了基本的 Hooks 用法。

在使用 Hooks 的时候,我们可能会有很多疑惑

  1. 为什么只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用?
  2. 为什么 useEffect 第二个参数是空数组,就相当于 ComponentDidMount ,只会执行一次?
  3. 自定义的 Hook 是如何影响使用它的函数组件的?
  4. Capture Value 特性是如何产生的?
  5. ......

这篇文章我们不会讲解 Hooks 的概念和用法,而是会带你从零实现一个 tiny hooks,知其然知其所以然。

useState

  1. 最简单的 useState 用法是这样的:

    demo1: https://codesandbox.io/s/v0nqm309q3

    function Counter() {
      var [count, setCount] = useState(0);
    
      return (
        <div>
          <div>{count}</div>
          <Button onClick={() => { setCount(count + 1); }}>
            点击
          </Button>
        </div>
      );
    }
  2. 基于 useState 的用法,我们尝试着自己实现一个 useState:

    demo2:https://codesandbox.io/s/myy5qvoxpp

    function useState(initialValue) {
      var state = initialValue;
      function setState(newState) {
        state = newState;
        render();
      }
      return [state, setState];
    }
  3. 这时我们发现,点击 Button 的时候,count 并不会变化,为什么呢?我们没有存储 state,每次渲染 Counter 组件的时候,state 都是新重置的。

    自然我们就能想到,把 state 提取出来,存在 useState 外面。

    demo3:https://codesandbox.io/s/q9wq6w5k3w

    var _state; // 把 state 存储在外面
    
    function useState(initialValue) {
      _state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
      function setState(newState) {
        _state = newState;
        render();
      }
      return [_state, setState];
    }

到目前为止,我们实现了一个可以工作的 useState,至少现在来看没啥问题。

接下来,让我们看看 useEffect 是怎么实现的。

useEffect

useEffect 是另外一个基础的 Hook,用来处理副作用,最简单的用法是这样的:

demo4:https://codesandbox.io/s/93jp55qyp4

 useEffect(() => {
    console.log(count);
 }, [count]);

我们知道 useEffect 有几个特点:

  1. 有两个参数 callback 和 dependencies 数组
  2. 如果 dependencies 不存在,那么 callback 每次 render 都会执行
  3. 如果 dependencies 存在,只有当它发生了变化, callback 才会执行

我们来实现一个 useEffect

demo5:https://codesandbox.io/s/3kv3zlvzl1

let _deps; // _deps 记录 useEffect 上一次的 依赖

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray; // 如果 dependencies 不存在
  const hasChangedDeps = _deps
    ? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
    : true;
  /* 如果 dependencies 不存在,或者 dependencies 有变化*/
  if (hasNoDeps || hasChangedDeps) {
    callback();
    _deps = depArray;
  }
}

到这里,我们又实现了一个可以工作的 useEffect,似乎没有那么难。

此时我们应该可以解答一个问题:

Q:为什么第二个参数是空数组,相当于 componentDidMount

A:因为依赖一直不变化,callback 不会二次执行。

Not Magic, just Arrays

到现在为止,我们已经实现了可以工作的 useState 和 useEffect。但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。比如

const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');

count 和 username 永远是相等的,因为他们共用了一个 _state,并没有地方能分别存储两个值。我们需要可以存储多个 _state 和 _deps。

如 《React hooks: not magic, just arrays》所写,我们可以使用数组,来解决 Hooks 的复用问题。

demo6:https://codesandbox.io/s/50ww35vkzl

代码关键在于:

  1. 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
  2. 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
  3. 如果还是不清楚,可以看下面的图。
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

我们用图来描述 memoizedState 及 cursor 变化的过程。

1. 初始化


1

2. 初次渲染

2

3. 事件触发

3

4. Re Render

4

到这里,我们实现了一个可以任意复用的 useState 和 useEffect。

同时,也可以解答几个问题:

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

A:memoizedState 数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。

Q:自定义的 Hook 是如何影响使用它的函数组件的?

A:共享同一个 memoizedState,共享同一个顺序。

Q:“Capture Value” 特性是如何产生的?

A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

真正的 React 实现

虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,实现方式却有一些差异的。

  • React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。

    type Hooks = {
    	memoizedState: any, // 指向当前渲染节点 Fiber
      baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
      baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
      queue: UpdateQueue<any> | null,// UpdateQueue 通过
      next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
    }
     
    type Effect = {
      tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
      create: () => mixed, // 初始化 callback
      destroy: (() => mixed) | null, // 卸载 callback
      deps: Array<mixed> | null,
      next: Effect, // 同上 
    };
  • memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?

    我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

5

参考文章

  1. Deep dive: How do React hooks really work?
  2. React hooks: not magic, just arrays

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

为什么 state声明在外面,点击的时候,view才变化,没想明白 @brickspert

@liang520 如果放在 useState 里面,是不是重新渲染每次都会重新调用 useState 重置掉?

@brickspert 看明白了

memoizedState => memorizedState

砖家写的文章依旧简单明了~

很清晰明了了

除了用变量存state还有别的吗

当我以为我会用hook的时候 useEffect 内的 setInterval 把我打蒙了,闭包是啥时候出现的都没看懂

一直关注,一直学习,感谢你的陪伴,可以借鉴学习React Hooks 入门教程http://www.ruanyifeng.com/blog/2019/09/react-hooks.html

follow

rtxu commented

赞,会用与会实现,理解的深度就是不一样

为什么 state声明在外面,点击的时候,view才变化,没想明白 @brickspert

不然的话state只是一个useState函数里边的局部变量,每次执行都会重新初始化为0呀

是否需要在setState函数中把cursor重置为0,在rerender的时候才能按照hook在组件函数内的书写顺序从0依次获取memoizedState中的值?

function setState(newState) {
  memoizedState[currentCursor] = newState;
  // reset
  cursor = 0;
  render();
}

是否需要在setState函数中把cursor重置为0,在rerender的时候才能按照hook在组件函数内的书写顺序从0依次获取memoizedState中的值?

function setState(newState) {
  memoizedState[currentCursor] = newState;
  // reset
  cursor = 0;
  render();
}

例子中在 render() 里面把 cursor 置为 0 了。

image
这个事件触发的时候应该下标应该是 1 吧

image
这个事件触发的时候应该下标应该是 1 吧

我也觉得此时的cursor不应该为0,为1的是currentCursor 这个局部变量,用来获取userName这个state在数组中的位置

image
这个事件触发的时候应该下标应该是 1 吧

我也觉得此时的cursor不应该为0,为1的是currentCursor 这个局部变量,用来获取userName这个state在数组中的位置

同意。随处可见的闭包啊~

image
这个事件触发的时候应该下标应该是 1 吧

为什么是 1 呢?每次都是从 0 开始重新执行的。可以在这个例子中试试呢:
https://codesandbox.io/s/50ww35vkzl

请问,这个useState在多个组件中引入,彼此之间会不会有影响(全局只有一个memoizedState )?

请问,这个useState在多个组件中引入,彼此之间会不会有影响(全局只有一个memoizedState )?

这个例子里面会。因为共用了一个 memoizedState。
但是在 React 中,是把数据存在 fiber node 上的。也就是,每个组件都有自己的 memoizedState.

@brickspert 多谢多谢

棒! _state = _state | initialValue 应该是 _state = _state || initialValue

棒! _state = _state | initialValue 应该是 _state = _state || initialValue

感谢~,已修正。

image
这个事件触发的时候应该下标应该是 1 吧

为什么是 1 呢?每次都是从 0 开始重新执行的。可以在这个例子中试试呢:
https://codesandbox.io/s/50ww35vkzl

我看你代码是 render 的时候才修改 cursor 为 0 吧,事件触发的时候,当时的 currentCursor 是 1,这时就会改变 memorizState[1] 的值

memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?

我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

这句话我不能理解,博主能引用源代码来证明么?
就我理解,函数式组件既然已经是stateless,就不会有所谓出生,死亡等生命周期了。我更倾向于react的hook信息,即整个hook的链表是一个全局变量。

memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?

我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

这句话我不能理解,博主能引用源代码来证明么?
就我理解,函数式组件既然已经是stateless,就不会有所谓出生,死亡等生命周期了。我更倾向于react的hook信息,即整个hook的链表是一个全局变量。

收回我的猜想。全局的hook无法实现条件渲染,应该是每个组件对应各自的hook。

还是无法理解: “Capture Value” 特性是如何产生的

wbcs commented

还是无法理解: “Capture Value” 特性是如何产生的

每次rerender都会重新执行一遍函数组件本身, 每次执行都会是全新的context,capture只是闭包保存对应的context,所以后续更新 不会影响之前的context,那之前闭包捕获的context也就不会变了。

理解了, 每次render 就是一个闭包, 会缓存住当前的所有状态, react class 是直接更新this.props的指向, 且没有闭包的概念, 所以全变了

在demo3中,没有调用诸如useState或者this.setState之类的方法,那react是如何知道并且触发更新的呢?

在demo3中,没有调用诸如useState或者this.setState之类的方法,那react是如何知道并且触发更新的呢?

抱歉,忽略了里面的render方法

function setState(newState) {
    memoizedState[currentCursor] = newState;
    cursor = currentCursor; //是不是少了这一句??
    render();
  }
function setState(newState) {
    memoizedState[currentCursor] = newState;
    cursor = currentCursor; //是不是少了这一句??
    render();
  }

不需要吧
下一步render()就會把cursor = 0
並且再次依照順序調用所有函數組件

大神 有一点想请教一下
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
对于memoizedState[cursor] = memoizedState[cursor] || initialValue;这么实现的话 是不是如果setState(null)或者是setState(undefind)是不是 state都会被付成默认值了

大神 有一点想请教一下
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
对于memoizedState[cursor] = memoizedState[cursor] || initialValue;这么实现的话 是不是如果setState(null)或者是setState(undefind)是不是 state都会被付成默认值了

我也是有这么个疑问,如果值本身就是false的话,怎么办?

大神 有一点想请教一下
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
对于memoizedState[cursor] = memoizedState[cursor] || initialValue;这么实现的话 是不是如果setState(null)或者是setState(undefind)是不是 state都会被付成默认值了

确实会有这个问题。
这个文章只是一个示例 demo,不是百分百严谨。

大神 有一点想请教一下
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
对于memoizedState[cursor] = memoizedState[cursor] || initialValue;这么实现的话 是不是如果setState(null)或者是setState(undefind)是不是 state都会被付成默认值了

确实会有这个问题。
这个文章只是一个示例 demo,不是百分百严谨。

明白啦!还是感谢作者带来质量那么高的文章。

image
这个事件触发的时候应该下标应该是 1 吧

为什么是 1 呢?每次都是从 0 开始重新执行的。可以在这个例子中试试呢:
https://codesandbox.io/s/50ww35vkzl

这里从0开始,是指render都会从0开始执行hook。但是对于setUsername('fan hello')这个setState方法来说,cursor应该是1。
这是我个人的理解,这个图上标注似乎会引起一点歧义,如果不对请指正

image
这个事件触发的时候应该下标应该是 1 吧

为什么是 1 呢?每次都是从 0 开始重新执行的。可以在这个例子中试试呢:
https://codesandbox.io/s/50ww35vkzl

这里从0开始,是指render都会从0开始执行hook。但是对于setUsername('fan hello')这个setState方法来说,cursor应该是1。
这是我个人的理解,这个图上标注似乎会引起一点歧义,如果不对请指正

第一句描述的没有异议,第二句中的 "cursor 应该是 1" 这句存在歧义,我们可以观察一下 setState 方法执行过程,发现其中维护memoizedState 数组下标的变量是 useState 方法中定义的 currentCursor 变量而不是 cursor,实际上 currentCursor 的值才是1。变量 cursor 的值在执行setState 中 render 方法之前是 memoizedState 数组的长度(即所存储hooks的数量,因为 useState 和 useEffect 中最后都执行了cursor++ 操作),在执行setState 中 render方法之后才会重置为 0。
图中描述的是变量 cursor 值的变化,所以作者描述的没什么问题,如果加上 currentCursor 值得变化大家可能就更容易理解了。

看了好久那里没想明白cursor怎么重置为0的,看了demo才知道,哭了。

🧱家就是强 通俗易懂 顺便问个问题 React为何要使用链表串联多个hook 相比数组的优势是什么呢?

🧱家就是强 通俗易懂 顺便问个问题 React为何要使用链表串联多个hook 相比数组的优势是什么呢?

感觉链表的灵活性会比数组来的高吧?

🧱家就是强 通俗易懂 顺便问个问题 React为何要使用链表串联多个hook 相比数组的优势是什么呢?

数组存储区间需要连续的内存,链表是则是非连续、非顺序的存储结构

不懂就问,render函数怎么来的,是如何触发函数组件跟新的呢?

acmu commented

不懂就问,render函数怎么来的,是如何触发函数组件跟新的呢?

看 demo 代码,有定义 render 函数

acmu commented

最开始有个疑问:为什么 useState 的 memoizedState 要是数组?如果是对象多好,这样就能一一对应上了,就可以突破 hooks 只能写在函数组件顶层的限制了。

看到最后,发现其实是个链表,并不是简单的数组呀。。。 所以我猜测为什么 memoizedState 不用 key value 这种方式存的原因是出于性能考虑吗?(也就是 key value 形式的数据结构 并没有 链表 形式的快)

acmu commented

还有建议:给 useState 里的 render() 那块加个注释,我刚开始看这块的时候,也比较懵

最开始有个疑问:为什么 useState 的 memoizedState 要是数组?如果是对象多好,这样就能一一对应上了,就可以突破 hooks 只能写在函数组件顶层的限制了。

看到最后,发现其实是个链表,并不是简单的数组呀。。。 所以我猜测为什么 memoizedState 不用 key value 这种方式存的原因是出于性能考虑吗?(也就是 key value 形式的数据结构 并没有 链表 形式的快)

最近面试遇到这个问题,大致了解了一下,链表是在内存里申请了一块连续的空间,而map是随机分散存储的,所以链表的查询会更快一些,一般大佬们的程序都会考虑要结合硬件层面来设计的

受教了

feibi commented

最开始有个疑问:为什么 useState 的 memoizedState 要是数组?如果是对象多好,这样就能一一对应上了,就可以突破 hooks 只能写在函数组件顶层的限制了。
看到最后,发现其实是个链表,并不是简单的数组呀。。。 所以我猜测为什么 memoizedState 不用 key value 这种方式存的原因是出于性能考虑吗?(也就是 key value 形式的数据结构 并没有 链表 形式的快)

最近面试遇到这个问题,大致了解了一下,链表是在内存里申请了一块连续的空间,而map是随机分散存储的,所以链表的查询会更快一些,一般大佬们的程序都会考虑要结合硬件层面来设计的

链表是动态分配内存,是非连续的。数组才是连续的,且是静态分配的

最开始有个疑问:为什么 useState 的 memoizedState 要是数组?如果是对象多好,这样就能一一对应上了,就可以突破 hooks 只能写在函数组件顶层的限制了。
看到最后,发现其实是个链表,并不是简单的数组呀。。。 所以我猜测为什么 memoizedState 不用 key value 这种方式存的原因是出于性能考虑吗?(也就是 key value 形式的数据结构 并没有 链表 形式的快)

最近面试遇到这个问题,大致了解了一下,链表是在内存里申请了一块连续的空间,而map是随机分散存储的,所以链表的查询会更快一些,一般大佬们的程序都会考虑要结合硬件层面来设计的

链表是动态分配内存,是非连续的。数组才是连续的,且是静态分配的

貌似JS底层的数组也有分快数组和慢数组,慢数组用的是hash表来管理数组,也就是说其数组的内存应该不是连续的?

最开始有个疑问:为什么 useState 的 memoizedState 要是数组?如果是对象多好,这样就能一一对应上了,就可以突破 hooks 只能写在函数组件顶层的限制了。
看到最后,发现其实是个链表,并不是简单的数组呀。。。 所以我猜测为什么 memoizedState 不用 key value 这种方式存的原因是出于性能考虑吗?(也就是 key value 形式的数据结构 并没有 链表 形式的快)

最近面试遇到这个问题,大致了解了一下,链表是在内存里申请了一块连续的空间,而map是随机分散存储的,所以链表的查询会更快一些,一般大佬们的程序都会考虑要结合硬件层面来设计的

链表是动态分配内存,是非连续的。数组才是连续的,且是静态分配的

应该是使用key value 会有key冲突的问题需要解决吧

···
let _deps; // _deps 记录 useEffect 上一次的 依赖

function useEffect(callback, depArray) {
const hasNoDeps = !depArray; // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
: true;
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
if (hasNoDeps || hasChangedDeps) {
callback();
_deps = depArray;
}
}
···
depArray为空数组的时候,const hasNoDeps = !depArray;的值不一直是true,那么每次都callback都会执行吗?

···
let _deps; // _deps 记录 useEffect 上一次的 依赖

function useEffect(callback, depArray) {
const hasNoDeps = !depArray; // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
: true;
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
if (hasNoDeps || hasChangedDeps) {
callback();
_deps = depArray;
}
}
···
depArray为空数组的时候,const hasNoDeps = !depArray;的值不一直是true,那么每次都callback都会执行吗?

空数组的话,!depArray 应该是 false 呀。

厉害,学习了

pfan8 commented

佩服!看了很多个讲解 hooks 原理的,讲得这么清晰易懂的就是🧱家这

为什么 state声明在外面,点击的时候,view才变化,没想明白 @brickspert

写在里面函数每次执行都会重置

···
let _deps; // _deps 记录 useEffect 上一次的 依赖
function useEffect(callback, depArray) {
const hasNoDeps = !depArray; // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
: true;
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
if (hasNoDeps || hasChangedDeps) {
callback();
_deps = depArray;
}
}
···
depArray为空数组的时候,const hasNoDeps = !depArray;的值不一直是true,那么每次都callback都会执行吗?

空数组的话,!depArray 应该是 false 呀。

就是要 false 呀

全量 rerender 确实不会影响到 hooks 游标 index;
局部update如何确保index对应关系 (我理解这里只触发了部分hooks的,那游标是不是错乱了)?
还是在全量render的时候就跟vnode绑定,到更新的时候再动态重置游标?

更新(2021-10-15):

可能是实现上每个组件都绑定一个hooks数组,只需要在每个组件运行的时候维护一个独立游标即可,所以不存在全局的hookIndex紊乱问题

更新(2021-12-3):

依照上面的思路实现了一个简单版本的Hooks(可以局部更新)

有毛病,如果我把state变成undefined,null,0, false这些的时候就会重置成initialState

有毛病,如果我把state变成undefined,null,0, false这些的时候就会重置成initialState

确实 其实 useState() 在上下文中只有初始化hooks才会用吧,每一次初始化都是新的对象,state存在与否没有影响。

有毛病,如果我把state变成undefined,null,0, false这些的时候就会重置成initialState

确实 其实 useState() 在上下文中只有初始化hooks才会用吧,每一次初始化都是新的对象,state存在与否没有影响。

没毛病,immutable模式

unbro commented

一个组件内,react 是如何抓区和解析多个 useEffect 的

一个组件内,react 是如何抓区和解析多个 useEffect 的

可以理解成每个组件内都会关联一个hooks队列

unbro commented

一个组件内,react 是如何抓区和解析多个 useEffect 的

可以理解成每个组件内都会关联一个hooks队列

请问能讲下这块的工作原理吗

unbro

楼主讲的算比较清楚了,具体实现可以看源码,啃不动的可以试试看这个简单的实现

对这里不是很理解
Q:“Capture Value” 特性是如何产生的?
A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

困惑点在于:虽然每一次都会进行ReRender,但是如果都会从同一个memoizedState中取值的话,应该是能够获取到最新的值,不会存在“Capture Value” 特性

拜托大佬解惑~

为什么 state声明在外面,点击的时候,view才变化,没想明白 @brickspert

因为每次重新render 的时候会重新执行 useState(0)呀,如果不放在外面的话,返回的state还是0