React Hooks 原理
brickspert opened this issue · 69 comments
前言
目前,Hooks 应该是 React 中最火的概念了,在阅读这篇文章之前,希望你已经了解了基本的 Hooks 用法。
在使用 Hooks 的时候,我们可能会有很多疑惑
- 为什么只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用?
- 为什么 useEffect 第二个参数是空数组,就相当于 ComponentDidMount ,只会执行一次?
- 自定义的 Hook 是如何影响使用它的函数组件的?
- Capture Value 特性是如何产生的?
- ......
这篇文章我们不会讲解 Hooks 的概念和用法,而是会带你从零实现一个 tiny hooks,知其然知其所以然。
useState
-
最简单的 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> ); }
-
基于 useState 的用法,我们尝试着自己实现一个 useState:
demo2:https://codesandbox.io/s/myy5qvoxpp
function useState(initialValue) { var state = initialValue; function setState(newState) { state = newState; render(); } return [state, setState]; }
-
这时我们发现,点击 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 有几个特点:
- 有两个参数 callback 和 dependencies 数组
- 如果 dependencies 不存在,那么 callback 每次 render 都会执行
- 如果 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
代码关键在于:
- 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
- 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
- 如果还是不清楚,可以看下面的图。
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. 初始化
2. 初次渲染
3. 事件触发
4. Re Render
到这里,我们实现了一个可以任意复用的 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 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。
参考文章
❤️感谢大家
关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。
为什么 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
赞,会用与会实现,理解的深度就是不一样
为什么 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 了。
为什么是 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
感谢~,已修正。
为什么是 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” 特性是如何产生的
还是无法理解: “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,不是百分百严谨。
明白啦!还是感谢作者带来质量那么高的文章。
为什么是 1 呢?每次都是从 0 开始重新执行的。可以在这个例子中试试呢:
https://codesandbox.io/s/50ww35vkzl
这里从0开始,是指render都会从0开始执行hook。但是对于setUsername('fan hello')这个setState方法来说,cursor应该是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函数怎么来的,是如何触发函数组件跟新的呢?
不懂就问,render函数怎么来的,是如何触发函数组件跟新的呢?
看 demo 代码,有定义 render 函数
最开始有个疑问:为什么 useState 的 memoizedState 要是数组?如果是对象多好,这样就能一一对应上了,就可以突破 hooks 只能写在函数组件顶层的限制了。
看到最后,发现其实是个链表,并不是简单的数组呀。。。 所以我猜测为什么 memoizedState 不用 key value 这种方式存的原因是出于性能考虑吗?(也就是 key value 形式的数据结构 并没有 链表 形式的快)
还有建议:给 useState 里的 render() 那块加个注释,我刚开始看这块的时候,也比较懵
最开始有个疑问:为什么 useState 的 memoizedState 要是数组?如果是对象多好,这样就能一一对应上了,就可以突破 hooks 只能写在函数组件顶层的限制了。
看到最后,发现其实是个链表,并不是简单的数组呀。。。 所以我猜测为什么 memoizedState 不用 key value 这种方式存的原因是出于性能考虑吗?(也就是 key value 形式的数据结构 并没有 链表 形式的快)
最近面试遇到这个问题,大致了解了一下,链表是在内存里申请了一块连续的空间,而map是随机分散存储的,所以链表的查询会更快一些,一般大佬们的程序都会考虑要结合硬件层面来设计的
受教了
最开始有个疑问:为什么 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 呀。
厉害,学习了
佩服!看了很多个讲解 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模式
一个组件内,react 是如何抓区和解析多个 useEffect 的
一个组件内,react 是如何抓区和解析多个 useEffect 的
可以理解成每个组件内都会关联一个hooks队列
一个组件内,react 是如何抓区和解析多个 useEffect 的
可以理解成每个组件内都会关联一个hooks队列
请问能讲下这块的工作原理吗
unbro
楼主讲的算比较清楚了,具体实现可以看源码,啃不动的可以试试看这个简单的实现
对这里不是很理解
Q:“Capture Value” 特性是如何产生的?
A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。
困惑点在于:虽然每一次都会进行ReRender,但是如果都会从同一个memoizedState中取值的话,应该是能够获取到最新的值,不会存在“Capture Value” 特性
拜托大佬解惑~
为什么 state声明在外面,点击的时候,view才变化,没想明白 @brickspert
因为每次重新render 的时候会重新执行 useState(0)呀,如果不放在外面的话,返回的state还是0