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); }, 1000); }; return ( <div> <span>{num}</span> <button onClick={handler}>新增</button> </div> ); }
|
实现原理
执行 handle 方法时,由于所用到的 setNum num 都不是当前作用域的私有变量,所以里面会沿着作用域链找到上级上下文里面的 num 和 setNum(闭包)
每次更新都重新执行一次内部的代码、都创建一个新的私有上下文如 EC(DEMO2),涉及的函数需要进行重新构建。这些函数的作用域会沿着函数的作用域链向上查找,找到每一次执行 DEMO 产生的新的闭包
每一次执行 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; function useState(initialState) { _state = _state | initialState; function setState(state) { if(Object.is(_state, value)) return; if(typeof value === 'function'){ _state = value(_state) }else{ _state = value } } return [_state, setState]; }
let [num1, setNum] = useState(0); setNum(100);
再次执行整个函数组件,在执行到useState的时候: let [num2, setNum] = useState(0); 在内部又产生了一个新的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); }, 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({ ...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 刷新渲染队列
检验方式一:在 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); 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 = () => {
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; 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) }else{ _state = value } } return [_state, setState]; }
let [num1, setNum] = useState(0); setNum(100);
再次执行整个函数组件,在执行到useState的时候: let [num2, setNum] = useState(0); 在内部又产生了一个新的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,初始值不再生效,这段逻辑依然会执行,浪费资源效率低下
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; function useState(initialState) { _state = _state | initialState; function setState(state) { if(Object.is(_state, value)) return; if(typeof value === 'function'){ _state = value(_state) }else{ _state = value } } return [_state, setState]; } let [num1, setNum] = useState(0); setNum(100);
再次执行整个函数组件,在执行到useState的时候: let [num2, setNum] = useState(0); 在内部又产生了一个新的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 ——函数式更新