【React】useState及底层处理机制
目录
- useState
- useState底层处理机制
- useState的简写源码
- useState打印值的问题
- 批量处理set修改状态值的方法
- useState函数更新和优化机制
- 1、循环修改状态值的方法
- 2、使用flushSync
- 3、设置修改状态的方法为函数形式(setX(prev=> prev+1))
- 4、组件更新的惰性处理
- 案例1
- 案例2
- 惰性处理的相关`useState`源码
- 和React16的区别
- 好书推荐
useState
useState:React Hook函数之一,目的是在函数组件中使用状态,并且后期基于状态的修改,可以让组件更新。
let [num,setNum] = useState(initialValue);
执行useState
,传递的initialValue
是初始的状态值。执行这个方法,返回结果是一个数组:[状态值,修改状态的方法]
- num变量存储的是:获取的状态值
- setNum变量存储的是:修改状态的方法
- 执行 setNum(value)
- 修改状态值为value
- 通知视图更新
const Demo = function Demo() {
let [num, setNum] = useState(0);
const handle = () => {
setNum(num + 10);
};
return <div className="demo">
<span className="num">{num}</span>
<Button type="primary" size="small" onClick={handle}>新增</Button>
</div>;
};
useState底层处理机制
函数组件「或者Hooks组件」不是类组件,所以没有实例的概念「调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文而已」
,所以,在函数组件中不涉及this
的处理!
函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的私有上下文
。
- 其内部的代码也需要重新执行。
- 涉及的函数需要重新的构建(这些函数的作用域(函数执行的上级上下文),是一块执行DEMO产生的闭包)
- 执行useState,只有第一次,设置的初始值会生效,
以后执行获取的状态都是最新的状态值「不是初始值」
返回的修改状态的方法,每一次都是返回一个新的
useState的简写源码
var _state;
function useState(initialValue) {
if (typeof _state === "undefined") {
if(typeof initialValue==="function"){
_state = initialValue();
}else{
_state = initialValue
}
};
return [_state, setState];
}
useState打印值的问题
为什么在2s后打印出num
是0?
num
不是定时器私有的变量,所以在获取变量值时就往上找,接着找到handle
函数生成私有上下文,发现么有num
,就继续往上找,最终发现num
为0。
要确定找的作用域,找哪个闭包
const Demo = function Demo() {
let [num, setNum] = useState(0);
const handle = () => {
setNum(100);
setTimeout(() => {
console.log(num); // 0
}, 2000);
};
return <div className="demo">
<span className="num">{num}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
批量处理set修改状态值的方法
点击新增按钮之后,页面只会渲染一次
const Demo = function Demo() {
console.log('RENDER渲染');
let [x, setX] = useState(10),
[y, setY] = useState(20),
[z, setZ] = useState(30);
const handle = () => {
setX(x + 1);
setY(y + 1);
setZ(z + 1);
};
return <div className="demo">
<span className="num">x:{x}</span>
<span className="num">y:{y}</span>
<span className="num">z:{z}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
把多个修改状态值的方法放在定时器中,点击按钮也是只渲染一次
const handle = () => {
setTimeout(() => {
setX(x + 1);
setY(y + 1);
setZ(z + 1);
}, 1000)
};
使用flushSync,此时点击按钮,页面会渲染两次
const handle = () => {
setX(x + 1);
setY(y + 1);
flushSync();
setZ(z + 1);
};
useState函数更新和优化机制
1、循环修改状态值的方法
在事件中for循环
执行 setX(x+1)
,页面最终显示11
。
每一次执行handle
都会创建新的闭包,而setX(x+1)
中的x
的值 向上级上下文找,上级上下文中x
的值为10
,最终结果为1
const Demo = function Demo() {
console.log('RENDER渲染');
let [x, setX] = useState(10);
const handle = () => {
for (let i = 0; i < 10; i++) {
setX(x+1);
}
};
return <div className="demo">
<span className="num">x:{x}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
2、使用flushSync
点击新增按钮时,会渲染两次,正常情况应该是只有一次渲染,说明在执行handle时,可能没有识别出x的值已经变成11。
const Demo = function Demo() {
console.log('RENDER渲染');
let [x, setX] = useState(10);
const handle = () => {
for (let i = 0; i < 10; i++) {
flushSync(()=>setX(x+1))
}
};
return <div className="demo">
<span className="num">x:{x}</span>
<span className="num">y:{y}</span>
<span className="num">z:{z}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
点击按钮之后,第一次循环setX(11)
执行,视图渲染;第二次循环还是setX(11)
,由于跟初始值一样(第一次循环之后,初始值就变成了11),所以在此执行setX(11)
不回更新视图。这是因为useState
自带了性能优化的机制:
- 每一次修改状态值的时候,会拿最新的修改的值和之前的状态的值做比较「类似
Object.is
做比较」 - 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新「可以理解为:类型PureComponent,在shouldComponentUpdate」中做了浅比较和优化」
3、设置修改状态的方法为函数形式(setX(prev=> prev+1))
这里我们要求handle函数执行一次,结果为20,如何处理。可以将setX
写成函数形式。
setX(prev=> {
// prev:存储上一次的状态值
console.log(prev)
return prev+1
})
const Demo = function Demo() {
console.log('RENDER渲染');
let [x, setX] = useState(10);
const handle = () => {
for (let i = 0; i < 10; i++) {
setX(prev => {
// prev:存储上一次的状态值
console.log(prev);
return prev + 1; //返回的信息是我们要修改的状态值
});
}
};
return <div className="demo">
<span className="num">x:{x}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
点击新增按钮之后,打印prev
如下:
4、组件更新的惰性处理
每一次函数组件更新时,初始化的一些逻辑都会再次执行,但是其实并没有什么用处。
useState的惰性初始化
是指在组件的初次渲染时,状态的初始值不一定是直接提供给useState
的常量或变量,而是可以通过一个函数来惰性计算。这种机制可以有效优化性能,特别是当初始状态计算较为复杂时。
适用场景:
惰性初始化最有用的场景是计算初始状态值较为昂贵时。例如,假设你需要从本地存储或计算一个复杂的值作为初始状态,而这个过程并不需要每次渲染时都执行,那么使用惰性初始化可以提高性能,因为它只会在组件的第一次渲染时执行
。
- 第一次渲染时:React会调用传给
useState
的函数(如果传了函数),并将该函数的返回值作为初始状态值。
如果没有传递函数,React直接使用传入的常量值作为初始状态。 - 后续渲染时:在后续渲染中,React不会再次调用这个函数,而是直接使用
第一次渲染时
计算出来的状态值。
只有当状态发生变化时,setState
才会触发组件的重新渲染。
案例1
父组件:
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ConfigProvider locale={zhCN}>
<Demo x={10} y={20}/>
</ConfigProvider>
);
子组件:
const Demo = function Demo(props) {
let { x, y } = props,
total = 0;
for (let i = x; i <= y; i++) {
total += +String(Math.random()).substring(2);
}
let [num, setNum] = useState( total);
const handle = () => {
setNum(1000);
};
return <div className="demo">
<span className="num">{num}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
比如上面的写法,每次函数组件更新的时候,都会执行一些无用代码。
let { x, y } = props,
total = 0;
for (let i = x; i <= y; i++) {
total += +String(Math.random()).substring(2);
}
当组件更新时,复杂的逻辑不再重新执行,设置初始值的时候,可以使用惰性写法,只有第一次渲染组件处理这些逻辑,以后组件更新这样的逻辑就不会再运行了。
const Demo = function Demo(props) {
// 我们需要把基于属性传递进来的x/y,经过其他处理的结果作为初始值
// 此时我们需要对初始值的操作,进行惰性化处理:只有第一次渲染组件处理这些逻辑,以后组件更新,这样的逻辑就不要再运行了!!
let [num, setNum] = useState(() => {
let { x, y } = props,
total = 0;
for (let i = x; i <= y; i++) {
total += +String(Math.random()).substring(2);
}
return total;
});
const handle = () => {
setNum(1000);
};
return <div className="demo">
<span className="num">{num}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
案例2
import React, { useState } from 'react';
const Demo = () => {
const [count, setCount] = useState(() => {
// 假设从本地存储获取初始值
const savedValue = localStorage.getItem('count');
return savedValue ? parseInt(savedValue) : 0;
});
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
在这个例子中,useState
的初始值是通过一个函数来计算的,该函数在第一次渲染时读取localStorage中的值。如果存在这个值,就使用它作为初始值,否则使用0。这个计算只会在第一次渲染时执行,而不会在后续渲染时重复执行
惰性处理的相关useState
源码
var _state;
function useState(initialValue) {
if (typeof _state === "undefined") {
if(typeof initialValue==="function"){
_state = initialValue();
}else{
_state = initialValue
}
};
var setState = function setState(value) {
if(Object.is(_state,value)) return;
if(typeof value==="function"){
_state = value(_state);
}else{
_state = value;
}
// 通知视图更新
};
return [_state, setState];
}
和React16的区别
注意:在React 16中,也和this.setState一样,放在合成事件中,是异步的;放在其他的一步操作中(比如:定时器,手动的事件绑定)它是同步的。
下面是异步的操作,跟React 18一样,点击按钮更新一次
此时是同步的,点击按钮会渲染三次
此时更新两次,和React18一样
简单总结一下:
好书推荐
鸿蒙HarmonyOS NEXT开发之路 卷1:ArkTS语言篇
《鸿蒙HarmonyOS NEXT开发之路 卷1:ArkTS语言篇》为有志于掌握HarmonyOS NEXT应用开发的读者提供系统性学习资源,从语法讲解到性能优化全面覆盖,可以作为读者学习ArkTS语言和开发HarmonyOS应用的参考教材。
《鸿蒙HarmonyOS NEXT开发之路 卷1:ArkTS语言篇》全面、深入地介绍华为HarmonyOS NEXT操作系统中的ArkTS语言。《鸿蒙HarmonyOS NEXT开发之路 卷1:ArkTS语言篇》分为基础知识、ArkTS进阶和高级特性三部分,引领读者逐步掌握从ArkTS基础到高级特性的开发能力。基础知识部分涵盖ArkTS的核心语法,包括声明式UI、函数、类、接口、泛型类型、空安全和模块化开发,为读者打下坚实的开发基础。ArkTS进阶部分深入探讨ArkTS语言的高级特性和最佳实践,例如高性能编程、声明式UI描述、自定义组件和装饰器,全面提升读者在HarmonyOS NEXT平台上的开发能力。高级特性部分则聚焦于状态管理机制,详细讲解状态变量的声明和管理,以及它们在UI渲染中的实际应用,帮助读者优化应用性能,实现从TypeScript到ArkTS的平滑过渡。