文章目录
useState
作用:在函数组件中使用状态,修改状态值可让函数组件更新,类似于类组件中的setState
语法:
const [state, setState] = useState(initialState);
返回一个 state,以及更新 state 的函数
seXXX(value)修改状态值为value,并通知视图更新。注意,不同于类组件setState的部分更新语法,而是直接修改成value
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会被重新执行,产生一个全新的私有上下文,内部的代码也重新执行
// 函数组件每一次渲染/更新,都具备独立的闭包
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>;
};
实现原理
-
执行handle方法时,由于所用到的setNum num都不是当前作用域的私有变量,所以里面会沿着作用域链找到上级上下文里面的num和setNum(闭包)
-
每次更新都重新执行一次内部的代码、都创建一个新的私有上下文如EC(DEMO2),涉及的函数需要进行重新构建。这些函数的作用域会沿着函数的作用域链向上查找,找到每一次执行DEMO产生的新的闭包
-
每一次执行DEMO函数,也会把useState重新执行。但是:
-
返回的状态:只有第一次设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态,而不是初始值。
-
返回的修改状态的方法:每一次都是新的方法函数
-
每次运行useState返回的结果都用新的num和setNum变量保存,因此状态和修改状态的方法的地址和之前的都不同,是全新的
-
那么它是如何确保每一次获取的是最新状态值,而不是传递的初始值呢?
// 核心原理:闭包
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
更新多状态
方案一:类似于类组件中一样,让状态值是一个对象(包含需要的全部状态),每一次只修改其中的一个状态值——setXXX不支持类组件setState的partial state change
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,把不同状态分开进行管理「推荐方案」——解耦
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刷新渲染队列
检验方式一:在handler里面修改state之后直接log ❌
不能在handler里面修改state之后直接log,因为这时log的变量仍然是上一次闭包中的,无论同步还是异步更新,都只能是上一个闭包中的值
因此,每次log的结果都是上一次的state
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”三次
若异步更新,则只在最后批处理更新一次,所以只输出一次
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设置同步操作
异步操作与闭包的深入理解
异步:handle里面的10次setX都会放在更新队列里面,然后在其他事情都做完之后,批处理一次更新完毕所有队列中的数据和视图,因此只’RENDER渲染’一次
闭包:x最后的状态值是11,因为handle里面的所有x都是在上一级闭包中拿到的,都是10,因此批处理中10个setX都是将x更新为11
setXXX的两种传参方式
1.直接传入新对象,不支持this.setState的部分更新
2.函数式更新——配合for循环、updater机制可以实现结果累计、只更新状态和视图一次 setXXX(prev => { return curV; })
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState;该函数将接收先前的 state,并返回一个更新后的值!
惰性初始state——复杂的初始化逻辑只执行一次
如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用,之后更新视图以后,状态值不再是undefined,所以不会再执行初始的惰性回调!
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>;
};
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>;
};
// 核心原理:闭包
var _state; // 创建全局state。
function useState(initialState) {
_state = _state | initialState;
if(typeof _state === 'undefined'){
if(typeof initialState === 'function'){
_state = initialState(); // 惰性初始化
}else{
_state = initialState;
}
}
function setState(value) {
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
优点:如果将回调里的逻辑写到外面,则一旦视图更新,不管是第一次还是后续更新的时候,这段逻辑都会执行。即使在更新阶段,num不再是undefined,初始值不再生效,这段逻辑依然会执行,浪费资源效率低下
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;)!
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>;
};
// 核心原理:闭包
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,不更新状态和视图
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
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多次(理论上是一次,这里是因为这些操作作用的都是一个闭包中的同一个状态值x,在第一次改状态还没改成功之前,其他次操作访问的状态值仍旧是还没改的10,直到第一次改状态成功,剩余次操作不会再通过Object.is的测试。这里render次数和浏览器的效率有关,可能是3次也可能是5次,不过绝对不是10次 ),最后状态值是11
在第一次渲染时创造出来顶级的函数作用域,_state私有属性就是在这个顶级作用域里面的
点击handle之后会执行10次同步清空更新队列的操作
在第一次flushSync,更新队列里只有一个setX,立即同步执行,使用的是第一次Demo创建出来的EC的闭包中的state,也就是10。进入setX,通过了Object.is的比较,更新状态和视图,此时最外部的_state也被更新为11
第二次flushSync,更新队列里只有一个setX,立即同步执行,使用的也是第一次Demo创建出来的EC的闭包中的state(因为这些flushSync方法都存在于第一个上下文中),也就是10。进入setX,第一次flushSync还没来得及彻底修改完成,所以也通过了Object.is的比较,更新状态和视图
直到某一次flushSync的时候,闭包中的X被彻底修改完了,此时不通过Object.is的比较,不再更新状态和视图,之后的若干轮轮同样不更新