Skip to content

React Hooks系列(二)—— useState介绍和源码实现 #65

@LightXJ

Description

@LightXJ

state Hook——useState

作用

在函数式组件中定义state

import React, { useState } from 'React';
const [count, setCount] = useState(0);

useState做了啥

我们可以看到,在此函数中,我们通过useState定义了一个'state变量',它与 class 里面的 this.state 提供的功能完全相同.相当于以下代码。

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

useState参数

在代码中,我们传入了0作为useState的参数,这个参数的数值会被当成count初始值。当然此参数不限于传递数字以及字符串,可以传入一个对象当成初始的state。如果state需要储存多个变量的值,那么调用多次useState即可

useState返回值

返回值为:当前 state 以及更新 state 的函数,这与 class 里面 this.state.count 和 this.setState 类似,唯一区别就是你需要成对的获取它们。看到[count, setCount]很容易就能明白这是ES6的解构数组的写法。相当于以下代码

let _useState = useState(0);// 返回一个有两个元素的数组
let count = _useState[0];// 数组里的第一个值
let setCount = _useState[1];// 数组里的第二个值

读取状态值

只需要使用变量即可
以前写法

<p>You clicked {this.state.count} times</p>

现在的写法

<p>You clicked {count} times</p>

更新状态

通过setCount函数更新
以前写法

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
    Click me
 </button>

现在写法

<button onClick={() => setCount(count + 1)}>
  Click me
</button>

这里setCount接收的参数是修改过的新状态值

声明多个state变量

我们可以在一个组件中多次使用state Hook来声明多个state变量

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的
为什么React要规定每次渲染它们时的调用顺序不变呢,这个是一个理解Hook至关重要的问题
注意:useState的初始值,只在第一次有效

举例:
当我点击按钮修改name的值的时候,我发现在Child组件,是收到了,但是并没有通过useState赋值给name!

const Child = memo(({data}) =>{
    console.log('child render...', data)
    const [name, setName] = useState(data)
    return (
        <div>
            <div>child</div>
            <div>{name} --- {data}</div>
        </div>
    );
})

const Hook =()=>{
    console.log('Hook render...')
    const [count, setCount] = useState(0)
    const [name, setName] = useState('rose')


    return(
        <div>
            <div>
                {count}
            </div>
            <button onClick={()=>setCount(count+1)}>update count </button>
            <button onClick={()=>setName('jack')}>update name </button>
            <Child data={name}/>
        </div>
    )
}

运行结果:
children里的name始终展示的是rose,点击按钮设置为jack后,也还是显示rose

快照(闭包)vs 最新值

在开始前,先抛出这么一个问题。在 1s 内频繁点击10次按钮,下面代码的执行表现是什么

class App extends React.Component {
  state = {
    count: 0
  }

  increment = () => {
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1
      });
    }, 1000);
  }

  render() {
    return <h1 onClick={ this.increment }>{this.state.count}</h1>;
  }
}

如果是这段代码呢?它又会是什么表现?

function App() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000);
  };

  return <h1 onClick={increment}>{count}</h1>;
}

第一个例子,1s内连续点10次,从最开始点击1s后页面上的数字会从0增长到10。而第二个例子中,连续点击10次,页面上的数字只会从0增长到1。
这是为什么呢?其实这主要是引用和闭包的区别
class 组件里面可以通过 this.state 引用到 count,所以每次 setTimeout 的时候都能通过引用拿到上一次的最新 count,所以点击多少次最后就加了多少。
在 function component 里面每次更新都是重新执行当前函数,也就是说 setTimeout 里面读取到的 count 是通过闭包获取的,而这个 count 实际上只是初始值,并不是上次执行完成后的最新值,所以最后只加了1次。

快照和引用的转换

如果我想让函数组件也是从0加到10,那么该怎么来解决呢?聪明的你一定会想到,如果模仿类组件里面的 this.state,我们用一个引用来保存 count 不就好了吗?没错,这样是可以解决,只是这个引用该怎么写呢?我在 state 里面设置一个对象好不好?就像下面这样:

const [state, setState] = useState({ count: 0 })

答案是不行,因为即使 state 是个对象,但每次更新的时候,要传一个新的引用进去,这样的引用依然是没有意义。

setState({
    count: state.count + 1
})

useRef

想要解决这个问题,那就涉及到另一个新的Hook方法——useRef。useRef是一个对象,它拥有一个current属性,并且不管函数组件执行多少次,而useRef返回的对象永远都是原来那一个。

function App() {
  const [count, setCount] = useState(0);
  const ref = useRef(0);

  const increment = () => {
    setTimeout(() => {
      setCount((ref.current += 1));
    }, 2000);
  };

  return <h1 onClick={increment}>{count}</h1>;
}

useRef有下面几个特点:

  • 1、useRef 是一个只能用于函数组件的方法。
  • 2、useRef 是除字符串 ref、函数 ref、createRef 之外的第四种获取 ref 的方法。
  • 3、useRef 在渲染周期内永远不会变,因此可以用来引用某些数据。
  • 4、修改 ref.current 不会引发组件重新渲染。

useRef vs createRef
1、两者都是获取 ref 的方式,都有一个 current 属性。
2、useRef 只能用于函数组件,createRef 可以用在类组件中。
3、useRef 在每次重新渲染后都保持不变,而 createRef 每次都会发生变化。
关于 React.useRef() 返回的 ref 对象在组件的整个生命周期内保持不变,我们来和 React.createRef() 来做一个对比,代码如下:

function MyInput() {
  const [count, setCount] = useState(0);

  const myRef = createRef(null);
  const inputRef = useRef(null);
  console.log('执行');

  // 仅执行一次,因为第二个参数是空数组
  useEffect(() => {
    console.log('sss');
    inputRef.current.focus();
    myRef.current.focus();
    window.myRef = myRef;
    window.inputRef = inputRef;
  }, []);

  // 每次componentDidUpdate都执行
  useEffect(() => {
    console.log('ddd');
    console.log(myRef);
    console.log(inputRef);
    // 除了第一次为true, 其它每次都是 false 【createRef】
    console.log('myRef === window.myRef', myRef === window.myRef);
    // 始终为true 【useRef】
    console.log('inputRef === window.inputRef', inputRef === window.inputRef);
  });
  return (
    <>
      <input type="text" ref={myRef} value="myRef" />
      <input type="text" ref={inputRef} value="inputRef" />
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </>
  );
}

useState实现原理

import React from 'react';
import ReactDOM from 'react-dom';

let index = 0;
const lastStates = [];
function useState(initialState) {
  lastStates[index] = lastStates[index] || initialState;
  const current = index;
  function setState(state) {
    lastStates[current] = state;
    render();
  }
  index += 1;
  return [lastStates[current], setState];
}

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Mary');
  console.log(count);

  return (
    <div>
      <p>
        click
        {' '}
        {count}
        {' '}
        times
      </p>
      <p>
        name is
        {' '}
        {name}
      </p>

      <button type="button" onClick={() => { setCount(count + 1); }}>+</button>
      <button type="button" onClick={() => { setName(`${Date.now()}`); }}>change Name</button>
    </div>
  );
}

function render() {
  index = 0;
  ReactDOM.render(
    <App />,
    document.getElementById('app')
  );
}

render();

参考:
1、React Hooks原理与最佳实践:https://mp.weixin.qq.com/s/DS2OjlwWjClboDIgLFuG6A

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions