Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
LightXJ opened this issue Jul 12, 2020 · 0 comments
Open

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

LightXJ opened this issue Jul 12, 2020 · 0 comments

Comments

@LightXJ
Copy link
Owner

LightXJ commented Jul 12, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant