使用Hooks容易犯错的点

昨天一个小伙伴发了一个Demo给我,让我解释下原因。
我一看,好家伙,小小一个Demo,知识点囊括了:

  • Hooks的闭包问题
  • state是如何组装的
相信看懂这个Demo,对函数组件会有更深的认识。
使用Hooks容易犯错的点
文章图片

让人懵逼的Demo Demo包含一个按钮、一个列表。
{list.map(val => val)}

点击按钮,调用add方法,向列表中插入一项:
let i = 0; export default function App() { const [list, setList] = useState([]); const add = () => { // ... }; return ( {list.map(val => val)}); }

显示效果:
使用Hooks容易犯错的点
文章图片

烧脑的地方在于,调用add方法插入的是一个点击后会调用 add 方法的按钮:
const add = () => { setList( list.concat( ) ); };

点击Add按钮7下后的显示效果:
使用Hooks容易犯错的点
文章图片

那么问题来了,点击带数字按钮(会调用和点击Add按钮一样的add方法)后会有什么效果呢?
使用Hooks容易犯错的点
文章图片

state的组装和闭包问题 【使用Hooks容易犯错的点】如果你认为会插入一个新按钮:
使用Hooks容易犯错的点
文章图片

那就错了。
使用Hooks容易犯错的点
文章图片

正确答案是:点击对应按钮后list长度变为按钮对应数字 + 1,且最后一项的数字为点击前最大数字 + 1。
比如,点击前最大数字为6
使用Hooks容易犯错的点
文章图片

如果点击 0,list长度变为0 + 1 = 1,且最后一项为6 + 1 = 7
使用Hooks容易犯错的点
文章图片

如果点击 2,list长度变为2 + 1 = 3,且最后一项为6 + 1 = 7
使用Hooks容易犯错的点
文章图片

这是两个因素共同作用的结果:
  • Hoo`ks`的闭包问题
  • state是如何组装的
原因分析 再来看看add方法:
const add = () => { setList( list.concat( ) ); };

button点击后调用add,所以会基于add所属上下文(App函数)形成闭包,闭包中包括:
  • add
  • list
  • setList
使用Hooks容易犯错的点
文章图片

i属于module级作用域,不在该闭包内
其中listsetList来自于useState调用后的返回值:
const [list, setList] = useState([]);

一种常见的认知误区是:多次调用useState返回的list是同一个引用。
事实上,每次调用useState返回的list都是基于如下公式计算得出的:
基准state + update1 + update2 + ... = 当前state
所以是一个全新的对象。
首屏渲染时:
  1. App组件首次render
  2. 创建list = []
  3. 依赖add,形成闭包,闭包中的list = []
接下来,点击Add按钮
  1. 调用add方法,该方法来自于首屏渲染创建的闭包
  2. add方法中依赖的list来自于同一个闭包,所以list = []
  3. 依赖add,形成闭包,闭包中的list = []
所以,对于按钮0
使用Hooks容易犯错的点
文章图片

任何时候点击他实际上执行的都是:
setList( [].concat( ) );

那么如何修复这个问题呢,也很简单,将setList的参数改为函数形式:
// 之前 setList(list.concat()); // 之后 setList(list => list.concat());

函数参数中的list来自于Hooks中保存的list,而不是闭包中的list
总结 由于Hooks总是在组件render时才会计算新状态,这为Hooks带来比较重的心智负担。
相比而言,采用细粒度更新实现的Hooks(比如VUEComposition API)可以实时更新状态,操作起来更符合直觉。

    推荐阅读