乐趣区

关于前端:浅析React中的state

引言

咱们在一些面试题中常常会遇到一个问题,那就是 state 的扭转是同步还是异步的。 当初本人只是一味的记住事件触发导致 state 的扭转是异步的,其它状况下都是同步的,并不了解其中的原因。接下来就为大家梳理下其中的原因,知其然也要知其所以然。
这篇文章我会别离会对类组件和函数组件中的 state 的应用和更新机制进行简略的介绍。

类组件中的 state

根本用法

在书写类组件时,通过在类中申明 state 变量来保护状态,通过 setState 办法来更新存储的状态信息

this.setState(updater[, callback])

obj这个参数能够传递两个类型:

  • 对象类型:当传入的参数是对象类型时,那么会将这个对象合并到 state 外面去,更新 state 的状态。
  • 函数类型:会将以后的 state 和 props 当做参数,返回值用于与原来的 state 进行合并,更新 state 的状态。

callback回调函数中能够获取到最新的 state 状态信息,能够作为依赖 state 变动执行一些副作用操作。

state 更新机制

class Counter extends React.Component {constructor(props) {super(props);
    this.state = {
      count: 0,
      hello: "hello world",
    };
  }

  handleClick = () => {this.setState({ count: this.state.count + 1}, () => {console.log("callback1", this.state.count);
    });
    console.log("log1", this.state.count);

    this.setState({count: this.state.count + 1}, () => {console.log("callback2", this.state.count);
    });
    console.log("log2", this.state.count);

    this.setState({count: this.state.count + 1}, () => {console.log("callback3", this.state.count);
    });
    console.log("log3", this.state.count);
  };

  render() {console.log("render this", this);
    return (<button onClick={this.handleClick}>
        {this.state.hello}: {this.state.count}
      </button>
    );
  }
}

打印后果:log1 0,log2 0,log3 0;cb1 1,cb2 1,cb3 1
依照原先的想法执行一次点击操作会执行 3 次加 1 的操作,然而从打印后果上来看丝毫没有失效,一起来看下执行流程。

其实每次在执行 setState 的时候,state 的状态信息还没有被更新过,每次都是应用的上次的 state 状态,所以 3 次打印的后果都是 0。这种景象背地的起因是 React 会期待所有事件处理函数都解决函数调用的 setState 调用实现之后,再进行对立更新状态,防止反复渲染的问题。

那么能不能更新 state 而不进行从新渲染呢?答案是不能,起因有以下两点,这个在 React 官网中有提到。

  • 这样会毁坏掉 props 和 state 之间的一致性,造成一些难以 debug 的问题。
  • 一些新性能变得无奈实现。

在 React 的 issue 外面有具体的解释:https://github.com/facebook/react/issues/11527

那么在事件处理函数中为何应用 setTimeoutPromise会突破这种批量更新导致的异步操作?

handleClick = () => {setTimeout(() => {this.setState({ count: this.state.count + 1}, () => {console.log("callback1", this.state.count);
    });
    console.log("log1", this.state.count);

    this.setState({count: this.state.count + 1}, () => {console.log("callback2", this.state.count);
    });
    console.log("log2", this.state.count);

    this.setState({count: this.state.count + 1}, () => {console.log("callback3", this.state.count);
    });
    console.log("log3", this.state.count);
  });
};

在下面的代码出对 handleClick 内的代码应用 setTimeout 来进行包裹,当初来看看打印后果

打印后果:callbakc1 1,log1 1,callbakc2 2,log2 2,callback 3,log3 3。那么当初程序执行的流程图就变为上面的这种模式了。

每次调用 setState 函数后会更新 state,而后进行从新进行渲染,随后调用 setState 提供的回调函数,回调函数执行实现后,代码会继续执行前面的代码。

留神:如果 React 版本是 v18 的话,setTimeout 和 Promise 也是应用批量更新的形式如果须要及时更新,有上面两个方法

  1. React 做降级解决
  2. 应用ReactDOM.render(<MyApp />, document.getElementById("root"));,应用这种形式相当于降级解决。

那如何在上述的代码中开启批量更新呢?

应用 ReactDom 提供的 unstable_batchedUpdates 开启批量更新。不要被前缀 unstable 所影响,现在这个 Api 曾经内置 React v18 中去了,无需放心稳定性问题。
在 Redux 官网文档中,提供了 batch 函数中,其实就是 unstable_batchedUpdates 的别名。上面提供 redux 文档的局部内容:

React’s unstable_batchedUpdates() API allows any React updates in an event loop tick to be batched together into a single render pass. React already uses this internally for its own event handler callbacks. This API is actually part of the renderer packages like ReactDOM and React Native, not the React core itself.
Since React-Redux needs to work in both ReactDOM and React Native environments, we’ve taken care of importing this API from the correct renderer at build time for our own use. We also now re-export this function publicly ourselves, renamed to batch(). You can use it to ensure that multiple actions dispatched outside of React only result in a single render update, like this:

handleClick = () => {setTimeout(() => {ReactDom.unstable_batchedUpdates(() => {this.setState({ count: this.state.count + 1}, () => {console.log("callback1", this.state.count);
      });
      console.log("log1", this.state.count);
  
      this.setState({count: this.state.count + 1}, () => {console.log("callback2", this.state.count);
      });
      console.log("log2", this.state.count);
  
      this.setState({count: this.state.count + 1}, () => {console.log("callback3", this.state.count);
      });
      console.log("log3", this.state.count);
    }
  });
};

函数组件中的 state

React v16.8 中,Hooks的呈现让函 数组件焕发新生,让过后的 React 开发者感到大为惊叹,从此函数组件成为编写 React 组件的首选形式。

根本用法

const [state, dipatchState] = useState(initialData)

state:保护状态信息的变量
dipatchState:用于扭转state 的函数,间接对 state 进行批改是不会被容许的,接管的参数类型如下:

  • 非函数:参数会间接替换原有的state,而不是进行合并。
  • 函数:会接管 stateprops两个参数,这两个参数都是以后最新的状态,返回的后果会更新到 state 外面。

函数组件中 state 更新机制

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

  console.log("outerCount", count);

  const handleClick = () => {setCount(count + 1);
    console.log("count1:", count);
    setCount((count) => {console.log('funcCount', count);
      return count + 1;
    });
    console.log("count2:", count)
  };

  return <button onClick={handleClick}>count:{count}</button>;
}

打印后果:count1 0,count2 0,funcCount 1,outerCount 1。

前三次打印后果都是 0,背地的起因很简略,所有扭转的 state 只有在下一次函数组件被执行的时候才会更新,在以后函数的执行上下文里的 count 都是本次函数组件渲染时的初始值。

留神:通过给 dispathState 传递的函数参数可能拿到最新的 state,但不会影响以后函数执行上下文中 state 的值

在类组件中,能够通过给 setState 传递回调函数来监测 state 的变动,那么在函数组件中如何来检测 state 的变动呢?

答案是能够通过向 useEffect 的依赖中退出 state,当 state 变动后,useEffect 外面的函数会被从新执行。

把下面的代码稍稍精简一下

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

  const handleClick = () => {setCount(count + 1);
  };

  return <button onClick={handleClick}>count:{count}</button>;
}

假如点击两次按钮,那么流程就是

每次执行函数组件都会造成一次快照,上图每个块状图就是一次快照,每次快照都有本人的propsstate、事件处理函数等其它在函数组件外面申明的变量和函数。

// 第一次渲染
function Counter(props) {
  count = 0

  const handleClick = () => {setCount(count + 1);
  };

  <button onClick={handleClick}>count:{count}</button>;
}

// 第二次渲染
function Counter(props) {
  count = 1

  const handleClick = () => {setCount(count + 1);
  };

  <button onClick={handleClick}>count:{count}</button>;
}

// 第三次渲染
function Counter(props) {
  count = 2

  const handleClick = () => {setCount(count + 1);
  };

  <button onClick={handleClick}>count:{count}</button>;
}

如何了解每次渲染都有本人的事件处理函数?借助 Dan 提供的例子来阐明问题。

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

  function handleAlertClick() {setTimeout(() => {alert('You clicked on:' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

在 3s 的工夫内,间断点击 2 次以上,发现在 3s 过后,仍然可能正确的展现数字。每次渲染都产生了新的 handleAlertClick 事件处理函数,这个函数捕捉了当次渲染的count。这里其实借助了闭包的个性,顺利捕捉了正确的 count。

留神:应用 useState 提供的 dipatch 函数更新 state,dispatch 函数会浅比拟前后两次 state,如果比拟相等,那么会造成视图不更新。

function App(){const [ state, dispatchState] = useState({name:'ztq'})
    
    const handleClick = ()=>{ // 点击按钮,视图没有更新。state.name = 'raysuner'
        dispatchState(state) // 间接扭转 `state`,在内存中指向的地址雷同。}
    
    return (
      <div>
         <span> {state.name}</span>
        <button onClick={handleClick}>changeName</button>
      </div>
    )
}
退出移动版