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 ——函数式更新
