前言
useState是 react hooks组件最常用的一个hook,其实整理useState知识让我体会到为什么说js学的好的话,react会很简单,useState 里面的细节知识还是挺多的。useState 我对他的评价是:简约而并不简单。
前置知识
我又理了理这些基础知识,我发现平时工作的大部分时间都是在写业务,或者与同事沟通业务层面的东西(沟通真的很耗时),对自己技术的提升可谓微乎其微,根本没时间好好的思考。
今天周末来公司加班了 顺便摸鱼写点掘金
1.执行上下文
执行上下文是JS中一个重要的概念,它代表了代码执行时的环境,执行上下文共有三种分别是
- 全局执行上下文(GEC)
-
- 全局上下文是最外层的上下文即宿主环境 可以理解为一个执行容器
函数执行上下文(FGC)
-
- 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,它在函数被调用时创建。函数上下文可以有任意多个
Eval函数执行上下文
-
- 不懂 不瞎说
上下文在其全部代码都执行完毕后才会被销毁
以浏览器中宿主环境为例,执行上下文示意图:
更详细的讲解详见友情link
2.作用域链
作用域:定义的变量在起作用和效果的范围。
作用域链: 当前执行上下文中定义的变量,如果在当前执行上下文中找不到定义,就去他的上级上下文中找,直至查找找到全局上下文,若没找到则报未定义错误,找到了读取其值。这种链式查找的机制称为作用域链。
3.闭包
“浑元形意太极门”掌门人马保国马老师曾是古希腊掌管闭包的神,所以我们顺着马老师的思路,
从「形」「意」展开
形
闭包的形可谓非常简单,闭包指的是那些引用了另一个函数作用域中变量的函数,通常在嵌套函数中实现的。-摘自《红宝书》
总结下来就是 闭包的形要包含以下2点:
1函数嵌套调用
2内部函数引用外部函数中的变量
其实只要是你用react hook 你就每天都在使用闭包,举一个简单的例子:handleClick 函数引用了父级函数中定义的count 变量。
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count+1)
}
return ...;
};
意
了解了形想要领悟其中的意就没那么容易了,自己也算不上真正领会,如下一段代码片段中定义了一个MyClosure函数,该函数定义中使用到了闭包的语法,这里仅把我所理解到的在这里与各位讨论。
function MyClosure(){
let myPrivateVariable = 0;
return {
setPrivateVariable: (value)=>{
myPrivateVariable = value;
},
getPrivateVariable: ()=>{
return myPrivateVariable;
}
}
}
1. 替身文学理解法
因为我之前常用的编程语言是c++,所以我总是尝试用过往的经验去理解现在的知识,因为JS也是由底层语言演变出来的,我想这样去理解也算是合理的,如果用c++中类定义去实现上述定义的功能将会写成:
class MyClosure {
private:
int myPrivateVariable;
public:
// 构造函数
MyClosure() {
myPrivateVariable = 0;
}
// 成员函数
void setPrivateVariable(int value) {
myPrivateVariable = value;
}
int getPrivateVariable() {
return myPrivateVariable;
}
};
MyClosure类中通过定义访问控制为private的属性以及2个成员函数setPrivateVariable&getPrivateVariable,就可以实现与上述代码一样的功能,这更深刻的体会到了闭包就是用来变量私有化的。
类: 仅 MyClosure类中的方法可以访问和修改私有属性myPrivateVariable
闭包: 仅MyClosure函数返回的方法可以访问和修改myPrivateVariable变量
2. 执行上下文理解法
MyClosure函数执行上下文示意图(2-1)
如果用JS执行上下文的概念去理解MyClosure函数,MyClosure函数执行上下文示意图所示,setPrivateVariable getPrivateVariable引用了上级上下文中的局部变量myPrivateVariable
首先 根据作用域链的取值规则定义,set和get方法内部访问使用的myPrivateVariable 变量一直都是父级上下文中定义的。
其次 能访问到myPrivateVariable 变量的仅有 setPrivateVariable getPrivateVariable 方法。
最后 为了这种取值方式一直生效,上下文执行机制是在其全部代码都执行完毕后才会被销毁,由 MyClosure 创建的上下文将一直为 setPrivateVariable 方法 和 getPrivateVariable方法保留,让局部引用关系一直存在,直至 setPrivateVariable = null getPrivateVariable= null set和get方法创建的上下文被销毁后,对MyClosure创建上级上下文的变量引用关系不再存在后 MyClosure创建上级上下文也会随之被销毁,这也是闭包为什么会占用内存的原因。
当使用执行上下文去理解闭包现象时,就会觉得闭包这个名字起的很形象,像是为了打造一个局部的语义环境/句柄设计出的语法规则,引用关系存在,被引用的父级上下文则一直被保留,就像虽然孩子跑远了,但是家永远在,不论什么时候需要家里的东西都可以。
不管是变量私有化还是创造局部语以块,用程序语言表达就是闭包用于数据封装, 防止使用全局变量被意外修改,提高代码清晰度、稳定性等
终于 我们回归正题
useState
基础用法
react中 以Couter计数器为例,useState的基础用法如下
import React, { useState, } from 'react';
import { Button } from 'antd'
import './index.css'
export default function Counter() {
const [count, setCount] = useState(0);
return <div>
<h2>Let's learn this through this example</h2>
<div className='count_style'>
<span>Count:</span>
<h2>{count}</h2>
</div>
<div>
press <Button type="dashed" onClick={() => setCount(count + 1)}>add</Button>
to increase.
</div>
</div>
};
关于我要改名
对于useState hook 解构出来的变量和方法 [something, setSomething] , 这里结构出的方法名是不是一定要定义为setSomething,答案是不一定啦,完全可以自定义名称,这里的写法只是官方建议,是为了增加代码可读性,但是你可以不予采纳。嘻嘻。
如果我们自己实现useState 就能更明白这个问题的本质,简略版实现如下,为说明这个问题仅供参考
var _state;
const usemyState = (initValues) => {
if (typeof _state === "undefined") _state = initValues;
var setState = function setState(value) {
_state = value;
};
return [_state, setState];
};
所以这里定义状态以及更改状态的方法本质是数组解构赋值,我愿意定义成这个样 [something, abcd] = useState() 也不是不可以,只是不建议。
参数传递
const[state,setState] = useState(initialState) 使用useState我们往往会传递一个初始值,作为state的初始值,如果不传的话为undefined,如果传函数的话,函数的返回值作为初始值。初始值仅在第一次渲染时赋值一次。
这里主要提下传函数的场景:
function initCalc() {
console.log('initCalc executed');
{/*do some complicate calculate*/}
return 0;
}
export default function Counter() {
const [count, setCount] = useState(initCalc);
return <div> ...</div>
};
如果业务组件中某个state初始值需要昂贵的计算时,可以传递一个函数作为state的初始值,因为这个方法仅在初次渲染时调用一次,之后不会再调用,提高性能。
多状态维护解决方案
平时我们写业务,每个页面都有很多状态需要维护,官方虽然推荐我们单独定义每个变量,但是状态多起来,会比较繁琐, 针对这个问题,有如下解决方案供参考:
状态对象
将state定义为一个状态,在更新时,需要使用展开运算符把不需要更改的状态都带上,这里我曾有一个小小的疑惑🤔️,展开运算符明明是可迭代数据类型才可以使用,实现了symbol.iterator接口的才能使用但是这里对象类型为啥可以使用,因为js的对象类型是没有实现symbol.iterator接口的 ,查了一些资料发现这个是es9 新增的特性,支持对象使用展开运算符,但是具体实现不是通实现symbol.iterator接口而是其他方法。
ahooks useSetState
使用第三方js库,里面封装useSetState 可以把状态定义为一个对象,而且更新时,只需要针对性的更新你要更新的变量就行了。当然ahooks 里面不仅有useSetState 还有一些别的工具函数,比如针对form表单封装的一些hook方法,可以发掘下,好用。
useState状态更新对应函数组件更新逻辑
在讲解更新逻辑之前先默念一遍这句话:函数组件的本质是函数 函数更新的本质是函数的重新执行
这里还是使用基础用法中提到的Couter计数器组件为例,组件在初次渲染以及用户点击add 按钮后组件更新的详细流程
Couter组件初次渲染
用户点击add按钮后组件更新
小练习
为了更好的理解组件更新流程中执行上下文和闭包产生的影响,通过下面2个小练习更深刻的领会其中的道理, 这里我们只关注打印结果分别是什么?
(1)
//假设 count 初始值为0,用户第一次按下add
const handleClick = () =>{
setCount(count+10)
setTimeOut(()=>{
console.log(count)
},1000)
}
(2)
// count 初始值为0,用户第一次按下add
const handleClick = () =>{
for(let i = 0; i< 10;i++){
setCount(count+10)
}
}
分析问题1
分析问题2
useState状态更新优化
批量更新机制
对于之前提到的这个场景:
// count 初始值为0,用户第一次按下add
const handleClick = () =>{
for(let i = 0; i< 10;i++){
setCount(count+10)
}
}
如果说每次遇到状态更新setCount方法,组件都重新渲染的话,Counter组件会渲染10次,但事实上,除初次渲染外,组件就更新了一次。
至此至少说明了一个问题,就是React中状态的更新时异步的,一定不是遇到改变状态的方法,组件就立即更新的
那具体是怎么做的呢,react会维护一个更新队列,做批量更新,比如一次点击事件,导致了哪些状态发生变化,这些状态会被保存在更新队列中,在统计完一次点击事件所有能影响到的状态变化后,进行批量更新
官网的这段话也是说的非常形象啊
所以很多时候,不是我们刚更新了某个状态,就能取到更新后的状态值,但很多时候我们确实需要在刚更新状态后,就使用更新后的状态值,这时候,我们可以通过给set方法传递一个函数,这个函数的入参能收到上一次更新的状态值。
比如点击事件这样定义,就可以看到count直接更新到了101
const handleClick = () => {
setCount(100);
setCount(prev => prev + 1)
}
再比如之前for循环让count+1的例子,如果这样写就可以实现点击一次让count值加10
const handleClick = () => {
for(let i = 0; i< 10; i++){
setCount(prev => prev + 1)
}
}
关于这里为什么状态更新函数传递函数就可以拿到最新的值,官网给出了很详细的说明👉react.nodejs.cn/learn/queue…
如果需要在更改某个状态后强制刷新组件,可以调用方法flushSync方法,flushSync 允许你强制 React 同步刷新提供的回调中的任何更新。这确保了 DOM 立即更新。flushSync 会严重损害性能,并且可能会意外地强制挂起的 Suspense 边界显示其回退状态。大多数时候,flushSync 是可以避免的,所以不得已才使用 flushSync。
状态相同的时候不更新
react在组件状态更新时还有一个优化的点就是,如果上一次状态和下一次要更新的状态,通过Object.is方法比较后返回真,表示2个状态值相同,此时组件不会再重新渲染。
<div>press <Button type="dashed" onClick={() => setCount(10)}>add</Button>
to increase.
</div>
react16 & 18 状态更新的区别
react18 中任何地方的状态更新都是异步的
react16中settimout里面里面的状态更新是同步的
测试版本 "react": "16.8.0",