useState深层剖析

useState

作用:在函数组件中使用状态,修改状态值可让函数组件更新,类似于类组件中的 setState

语法:

const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数

seXXX(value)修改状态值为 value,并通知视图更新。注意,不同于类组件 setState 的部分更新语法,而是直接修改成 value

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { useState } from "react";
export default function Demo(props) {
let [num, setNum] = useState(10);
const handler = () => {
setNum(num + 1);
};
return (
<div>
<span>{num}</span>
<button onClick={handler}>新增</button>
</div>
);
}

函数组件【Hooks 组件】不是类组件,所以没有实例的概念,调用组件不再是创建类的实例,而是执行函数并产生一个私有上下文而已,所以在函数组件中不涉及 this 的处理

设计原理

类组件只在初次渲染时创建一个实例,之后的更新都是按照生命流程走,并不是重新创造实例。

函数组件的每一次渲染或更新是让函数重新执行,也就是 useState 会被重新执行,产生一个全新的私有上下文,内部的代码也重新执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 函数组件每一次渲染/更新,都具备独立的闭包
import React, { useState } from "react";
export default function Demo(props) {
let [num, setNum] = useState(10);
const handler = () => {
setNum(100);
setTimeout(() => {
console.log(num); //10
}, 1000);
};
return (
<div>
<span>{num}</span>
<button onClick={handler}>新增</button>
</div>
);
}

实现原理

  1. 执行 handle 方法时,由于所用到的 setNum num 都不是当前作用域的私有变量,所以里面会沿着作用域链找到上级上下文里面的 num 和 setNum(闭包)

  2. 每次更新都重新执行一次内部的代码、都创建一个新的私有上下文如 EC(DEMO2),涉及的函数需要进行重新构建。这些函数的作用域会沿着函数的作用域链向上查找,找到每一次执行 DEMO 产生的新的闭包

  3. 每一次执行 DEMO 函数,也会把 useState 重新执行。但是:

    • 返回的状态:只有第一次设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态,而不是初始值。
    • 返回的修改状态的方法:每一次都是新的方法函数
    • 每次运行 useState 返回的结果都用新的 num 和 setNum 变量保存,因此状态和修改状态的方法的地址和之前的都不同,是全新的

那么它是如何确保每一次获取的是最新状态值,而不是传递的初始值呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 核心原理:闭包
var _state; // 创建全局state。
function useState(initialState) {
_state = _state | initialState;
function setState(state) {
if(Object.is(_state, value)) return;
if(typeof value === 'function'){
_state = value(_state) // 相当于传入prevalue后,return经过处理得到的新value
}else{
_state = value
}
// 通知视图更新
//...重新渲染组件
}
return [_state, setState]; // 数组是新的变量,里面的每项自然也是新的,栈地址也不相同
}

let [num1, setNum] = useState(0); //初始时num1=0 setNum=setState 0x001
setNum(100); //=>_state=100 通知视图更新
// ---
再次执行整个函数组件,在执行到useState的时候:
let [num2, setNum] = useState(0); //由于初次渲染时,全局state被赋值了,不再为undefined,所以不再赋值为initialState
在内部又产生了一个新的setState,地址和之前不同,使用这次新的闭包作为父级上下文
最后返回新的state和新的setState并被声明为新的变量
num2=100 setNum=setState 0x002

setXXX 沿着作用域查找闭包的理解——与同步异步无关

第一个 setTimeout 沿着作用域链找到的闭包里的 num 是初始渲染的 num,和 setNum 后产生的新的闭包(作用域)无关,因此输出 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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>
);
};

export default Demo;

更新多状态

方案一:类似于类组件中一样,让状态值是一个对象(包含需要的全部状态),每一次只修改其中的一个状态值——setXXX 不支持类组件 setState 的 partial state change

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState } from "react";
export default function Demo(props) {
let [state, setState] = useState({
x: 10,
y: 20,
});
const handler = () => {
// setState({ x: 100 }); //state={x:100}
setState({
...state,
x: 100,
});
};
return (
<div>
<span>{state.x}</span>
<span>{state.y}</span>
<button onClick={handler}>处理</button>
</div>
);
}

问题:不能像类组件的 setState 函数一样,支持部分状态更新!

方案二:执行多次 useState,把不同状态分开进行管理「推荐方案」——解耦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState } from "react";
export default function Demo(props) {
let [x, setX] = useState(10),
[y, setY] = useState(20);
const handler = () => {
setX(100);
};
return (
<div>
<span>{x}</span>
<span>{y}</span>
<button onClick={handler}>处理</button>
</div>
);
}

更新队列机制【updater,异步批处理】——异步和闭包是两码事

和类组件中的 setState 一样,每次更新状态值,也不是立即更新,而是利用了更新队列 updater 机制来处理

① 遇到 setState 会立即将其放入到更新队列中,此时状态和视图还都未更新

当所有的代码操作结束,会刷新队列,也就是通知更新队列中的所有任务执行:把所有放入的 setState 合并在一起执行,只触发一次状态更新和视图更新

  • React 18 全部采用批更新
  • React 16 中也和 this.setState 一样,只在合成事件/生命周期函数中异步,在定时器、手动 DOM 事件绑定等操作中同步
  • 可以基于 flushSync 刷新渲染队列

img

检验方式一:在 handler 里面修改 state 之后直接 log ❌

不能在 handler 里面修改 state 之后直接 log,因为这时 log 的变量仍然是上一次闭包中的,无论同步还是异步更新,都只能是上一个闭包中的值

因此,每次 log 的结果都是上一次的 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { useState } from "react";
import { Button } from "antd";
import "./Demo.less";
import { flushSync } from "react-dom";

const Demo = function Demo() {
let [x, setX] = useState(10),
[y, setY] = useState(20),
[z, setZ] = useState(30);

const handle = () => {
setX(x + 1);
console.log(x);
// 1.异步批处理:所有的setXXX操作放到更新队列里面,执行完所有操作之后才会一次清空更新队列,因此console.log先执行
// 2.闭包:由于handle始终拿到的是父级作用域的闭包,也就是更新前的闭包
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>
);
};

export default Demo;

检验方式二:比较输出”验证值的次数“

若同步更新,那么会顺序输出“render”三次

若异步更新,则只在最后批处理更新一次,所以只输出一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { useState } from "react";
import { Button } from "antd";
import "./Demo.less";
import { flushSync } from "react-dom";

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>
);
};

export default Demo;

只更新了一次,说明是异步执行的,与位置无关

更新队列 flushSync 设置同步操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { useState } from "react";
import { flushSync } from "react-dom";

export default function Demo(props) {
console.log("OK");
let [x, setX] = useState(10);
let [y, setY] = useState(20);

const handler = () => {
/* setX(100);
setY(200); */

/* setTimeout(() => {
setX(100);
setY(200);
}, 1000); */

flushSync(() => {
setX(100);
});
setY(200);
};

return (
<div>
<span>{x}</span>
<span>{y}</span>
<button onClick={handler}>处理</button>
</div>
);
}

通过 setXXX 传入(prev) =>跳出闭包

异步操作与闭包函数作用域例题

异步:handle 里面的 10 次 setX 都会放在更新队列里面,然后在其他事情都做完之后,批处理一次更新完毕所有队列中的数据和视图,因此只’RENDER 渲染’一次

闭包:x 最后的状态值是 11,因为 handle 里面的所有 x 都是在上一级闭包中拿到的,都是 10,因此批处理中 10 个 setX 都是将 x 更新为 11

setXXX 的两种传参方式

1.直接传入新对象,不支持 this.setState 的部分更新

2.函数式更新——配合 for 循环、updater 机制可以实现结果累计、只更新状态和视图一次 setXXX(prev =>),可以有效解决 updater 的闭包问题

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState;该函数将接收先前的 state,并返回一个更新后的值!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useState } from "react";
export default function Demo() {
let [num, setNum] = useState(10);
const handler = () => {
for (let i = 0; i < 10; i++) {
// 函数式更新
setNum((num) => {
return num + 1;
});
}
};
return (
<div>
<span>{num}</span>
<button onClick={handler}>处理</button>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 核心原理:闭包
var _state; // 创建全局state。
function useState(initialState) {
_state = _state | initialState;
if(typeof _state === 'undefined'){
if(typeof initialState === 'function'){
_state = initialState();
}else{
_state = initialState;
}
}
function setState(state) {
if(Object.is(_state, value)) return;
if(typeof value === 'function'){
_state = value(_state) // 相当于传入prevalue后,return经过处理得到的新value
}else{
_state = value
}
// 通知视图更新
//...重新渲染组件
}
return [_state, setState]; // 数组是新的变量,里面的每项自然也是新的,栈地址也不相同
}

let [num1, setNum] = useState(0); //初始时num1=0 setNum=setState 0x001
setNum(100); //=>_state=100 通知视图更新
// ---
再次执行整个函数组件,在执行到useState的时候:
let [num2, setNum] = useState(0); //由于初次渲染时,全局state被赋值了,不再为undefined,所以不再赋值为initialState
在内部又产生了一个新的setState,地址和之前不同,使用这次新的闭包作为父级上下文
最后返回新的state和新的setState并被声明为新的变量
num2=100 setNum=setState 0x002

惰性初始 state——复杂的初始化逻辑只执行一次

如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用,之后更新视图以后,状态值不再是 undefined,所以不会再执行初始的惰性回调

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useState } from "react";
export default function Demo(props) {
let [num, setNum] = useState(() => {
let { x, y } = props;
return x + y;
});
return (
<div>
<span>{num}</span>
</div>
);
}

优点:如果将回调里的逻辑写到外面,则一旦视图更新,不管是第一次还是后续更新的时候,这段逻辑都会执行。即使在更新阶段,num 不再是 undefined,初始值不再生效,这段逻辑依然会执行,浪费资源效率低下

image-20230616150504612

useState 性能优化机制——Object.is 类似 PureComponent 的浅比较

useState 自带了性能优化的机制:

  • 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较「基于 Object.is 作比较,而不是更严格的===。如果前后状态都是 NaN,Object.is 返回 true 不更新状态和视图,===返回 false 更新状态和视图」
  • 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新「可以理解为:类似于 PureComponent,在 shouldComponentUpdate 中做了浅比较和优化,注意函数组件中不可能有 PureComponent」

调用 State Hook 的更新函数,并传入当前的 state 时,React 将跳过组件的渲染(原因:React 使用 Object.is 比较算法,来比较新老 state;注意不是因为 DOM-DIFF;)!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState } from "react";
export default function Demo() {
console.log("render");
let [num, setNum] = useState(10);
return (
<div>
<span>{num}</span>
<button
onClick={() => {
setNum(num);
}}
>
处理
</button>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 核心原理:闭包
var _state; // 创建全局state。
function useState(initialState) {
_state = _state | initialState;
function setState(state) {
if(Object.is(_state, value)) return;
if(typeof value === 'function'){
_state = value(_state) // 相当于传入prevalue后,return经过处理得到的新value
}else{
_state = value
}
// 通知视图更新
//...重新渲染组件
}
return [_state, setState]; // 数组是新的变量,里面的每项自然也是新的,栈地址也不相同
}
let [num1, setNum] = useState(0); //初始时num1=0 setNum=setState 0x001
setNum(100); //=>_state=100 通知视图更新
// ---
再次执行整个函数组件,在执行到useState的时候:
let [num2, setNum] = useState(0); //由于初次渲染时,全局state被赋值了,不再为undefined,所以不再赋值为initialState
在内部又产生了一个新的setState,地址和之前不同,使用这次新的闭包作为父级上下文
最后返回新的state和新的setState并被声明为新的变量
num2=100 setNum=setState 0x002

例 1 前后 state 浅比较 true,不更新状态和视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState } from "react";
import { Button } from "antd";
const UseStateDemo = function UseStateDemo() {
console.log("RENDER");
let [x, setX] = useState(10);
const handle = () => {
setX(10);
};
return (
<div className="UseStateDemo">
<span className="num">x:{x}</span>
<Button type="primary" size="small" onClick={handle}>
新增
</Button>
</div>
);
};

export default UseStateDemo;

不更新视图和状态

例 2 更新多次,最终值 11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useState } from "react";
import { Button } from "antd";
import { flushSync } from "react-dom";

const UseStateDemo = function UseStateDemo() {
console.log("RENDER");
let [x, setX] = useState(10);
const handle = () => {
for (let i = 0; i < 10; i++) {
flushSync(() => {
setX(x + 1);
});
}
};
return (
<div className="UseStateDemo">
<span className="num">x:{x}</span>
<Button type="primary" size="small" onClick={handle}>
新增
</Button>
</div>
);
};

export default UseStateDemo;

render 两次(理论上是一次,这里是因为这些操作作用的都是一个闭包中的同一个状态值,在第一次改状态还没改成功之前,其他次操作访问的状态值仍旧是还没改的 10,直到第一次改状态成功,剩余次操作不会再通过 Object.is 的测试。这里 render 次数和浏览器的效率有关,不过绝对不是 10 次 ),最后状态值是 11

在第一次渲染时创造出来顶级的函数作用域,_state 私有属性就是在这个顶级作用域里面的

点击 handle 之后会执行 10 次同步清空更新队列的操作

在第一次 flushSync,更新队列里只有一个 setX,立即同步执行,使用的是第一次 Demo 创建出来的 EC 的闭包中的 state,也就是 10。进入 setX,通过了 Object.is 的比较,更新状态和视图,此时最外部的_state 也被更新为 11

第二次 flushSync,更新队列里只有一个 setX,立即同步执行,使用的也是第一次 Demo 创建出来的 EC 的闭包中的 state(因为这些 flushSync 方法都存在于第一个上下文中),也就是 10。进入 setX,未通过了 Object.is 的比较,因此不更新状态和视图

第三。。。十轮同样不更新

例 3 更新 1 次,最终值 20 ——函数式更新


useState深层剖析
https://hugtyftg.github.io/2023/06/14/useState/
作者
mmy@hugtyftg
发布于
2023年6月14日
许可协议