1.hooks组件本质

React - 12 Hooks组件之useState_react

2.useState

①基础用法

import React, { useState } from "react";
import { Button } from 'antd';
/*
 useState:React Hook函数之一,目的是在函数组件中使用状态,并且后期基于状态的修改,可以让组件更新
   let [num,setNum] = useState(initialValue);
     + 执行useState,传递的initialValue是初始的状态值
     + 执行这个方法,返回结果是一个数组:[状态值,修改状态的方法]
       + num变量存储的是:获取的状态值
       + setNum变量存储的是:修改状态的方法
     + 执行 setNum(value) 
       + 修改状态值为value
       + 通知视图更新

  函数组件「或者Hooks组件」不是类组件,所以没有实例的概念「调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文而已」,所以在函数组件中不涉及this的处理!!
 */
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底层机制

React - 12 Hooks组件之useState_react_02

函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的“私有上下文”!
   + 内部的代码也需要重新执行
   + 涉及的函数需要重新的构建{这些函数的作用域(函数执行的上级上下文),是每一次执行DEMO产生的闭包}
   + 每一次执行DEMO函数,也会把useState重新执行,但是:
     + 执行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];
}
let [num1, setNum] = useState(0); //num1=0  setNum=setState 0x001
setNum(100); //=>_state=100 通知视图更新
// ---
let [num2, setNum] = useState(0); //num2=100  setNum=setState 0x002

一个有关底层机制的例子

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;

原理: 定时器中输出为0,因为函数组件的每一次渲染,都是把函数(重新)执行,产生一个全新的“私有上下文”! 涉及的函数需要重新的构建{这些函数的作用域(函数执行的上级上下文),是每一次执行DEMO产生的闭包}

执行setNum(100)使得num变为100,Demo会直接重新执行形成下图中的Demo2,不会等待定时器。2秒之后打印num,会去第一次Demo渲染handle执行形成的上下文中寻找num(下图中的Demo1),此时num是0。

React - 12 Hooks组件之useState_react_03

useState细节知识和同步异步

③ 更新多个状态

/* 
执行一次useState:把需要的状态信息都放在对象中统一管理
    + 执行setState方法的时候,传递的是啥值,就把状态“整体”改为啥值 
      setState({
        supNum: state.supNum + 1
      }) 
      => 把状态值修改为 {supNum:11} ,oppNum成员就丢失了
      => 并不会像类组件中的this.setState一样,不支持部分状态的更新
    + 应该改为以下的处理方案(官方不推荐)
      setState({
        ...state, //在修改值之前,先把原有的所有状态,都展开赋值给新对象,再去修改要改动的那一项值即可
        supNum: state.supNum + 1
      });
*/
const Vote = function Vote(props) {
    let [state, setState] = useState({
        supNum: 10,
        oppNum: 5
    });

    const handle = (type) => {
        if (type === 'sup') {
            setState({
                ...state,
                supNum: state.supNum + 1
            });
            return;
        }
        setState({
            ...state,
            oppNum: state.oppNum + 1
        });
    };

    return <div className="vote-box">
        <div className="header">
            <h2 className="title">{props.title}</h2>
            <span className="num">{state.supNum + state.oppNum}</span>
        </div>
        <div className="main">
            <p>支持人数:{state.supNum}人</p>
            <p>反对人数:{state.oppNum}人</p>
        </div>
        <div className="footer">
            <Button type="primary" onClick={handle.bind(null, 'sup')}>支持</Button>
            <Button type="primary" danger onClick={handle.bind(null, 'opp')}>反对</Button>
        </div>
    </div>;
};
/* 官方建议是:需要多个状态,就把useState执行多次即可 */
const Vote = function Vote(props) {
    let [supNum, setSupNum] = useState(10),
        [oppNum, setOppNum] = useState(5);

    const handle = (type) => {
        if (type === 'sup') {
            setSupNum(supNum + 1);
            return;
        }
        setOppNum(oppNum + 1);
    };

    return <div className="vote-box">
        <div className="header">
            <h2 className="title">{props.title}</h2>
            <span className="num">{supNum + oppNum}</span>
        </div>
        <div className="main">
            <p>支持人数:{supNum}人</p>
            <p>反对人数:{oppNum}人</p>
        </div>
        <div className="footer">
            <Button type="primary" onClick={handle.bind(null, 'sup')}>支持</Button>
            <Button type="primary" danger onClick={handle.bind(null, 'opp')}>反对</Button>
        </div>
    </div>;
};

④更新队列机制

handle执行,修改三个状态x/y/z,但是RENDER渲染只执行了一次(第一次执行Demo会输出一次除外),说明setX/setY/setZ是异步的

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);
    // flushSync可以将异步立马执行,变同步
    // const handle = () => {
    //     flushSync(() => {
    //         setX(x + 1);
    //         setY(y + 1);
    //     });
    //     setZ(z + 1);
    // };
    const handle = () => {
        setX(x + 1);
        console.log(x) // 10 不是 11,与setX是同步异步没关系,变量作用域链往上找
        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;

setState是异步的

React - 12 Hooks组件之useState_react_04

放在哪里执行没关系,都是异步的,即便放在定时器中也是

React - 12 Hooks组件之useState_react_05

想同步更新怎么办,比如让setX和setY同步执行,用flushSync把setX和setY包起来

React - 12 Hooks组件之useState_react_06

React - 12 Hooks组件之useState_react_07

例子:渲染几次

渲染1次,x: 11

React - 12 Hooks组件之useState_react_08

修改 -->

只渲染一次,点击新增后不会更新视图(不会打印渲染),因为useState做了优化机制,前后修改的值一样,不会触发更新。

【usestate自带了性能优化的机制: 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较「基于bject.is作比较」如果发现两次的值是一样的,则不会修改状态,也不会让视图更新可以理解为: 类似于Pureomponent,在shouldComponentupdate中做了浅比较和优化】

React - 12 Hooks组件之useState_react_09

修改 --> 不是更新10次,实际更新2次(点击按钮后)【理论上点击按钮后应该只执行一次,因为x=11,值相同不会再执行了】

React - 12 Hooks组件之useState_react_10

修改 -->

import React, { useState } from "react";
import { Button } from 'antd';
import './Demo.less';
import { flushSync } from 'react-dom';

/*
  useState自带了性能优化的机制:
    + 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较「基于Object.is作比较」
    + 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新「可以理解为:类似于PureComponent,在shouldComponentUpdate中做了浅比较和优化」
 */

// 需求:让函数只更新一次,但是最后的结果是20
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>;
};

export default Demo;

执行原理

React - 12 Hooks组件之useState_react_11

⑤设置初始值时使用惰性

useState中传入的函数只在第一次渲染组件时触发,这是用来计算初始值的

import React, { useState } from "react";
import { Button } from 'antd';
import './Demo.less';

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

export default Demo;