精华内容
下载资源
问答
  • 在使用React过程中,中可以使用this.state来访问需要的某些状态,但是需要更新或者修改state时,一般而言,我们都会使用setState()函数,从而达到更新state的目的,setState()函数执行会触发页面重新渲染UI。...
  • React:setState详解

    2018-01-02 18:14:53
    react之setState遇到的坑、解释setState为什么是异步的、怎样变成同步setState。。。。。。
  • 这个特性则要归功于setState()方法。React中利用队列机制来管理state,避免了很多重复的View刷新。下面我们来从源码角度探寻下setState机制。 1 还是先声明一个组件,从最开始一步步来寻源; class App extends ...
  • 本篇文章主要介绍了深入掌握 react的 setState的工作机制,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 在react中通过 state 以及 setState() 来控制组件的状态。 state state 是 react 中用来存储组件数据状态的,可以类比成 vue 中的 data。 1.state的作用 state是React中组件的一个对象.React把用户界面当做是状态机,...
  • setState

    2017-01-18 00:34:45
    Using State Correctly ...There are three things you should know about setState(). Do Not Modify State Directly For example, this will not re-render a component: // Wrong this.state.comment =

    Using State Correctly

    There are three things you should know about setState().

    Do Not Modify State Directly

    For example, this will not re-render a component:

    // Wrong
    this.state.comment = 'Hello';
    

    Instead, use setState():

    // Correct
    this.setState({comment: 'Hello'});
    

    The only place where you can assign this.state is the constructor.

    State Updates May Be Asynchronous

    React may batch multiple setState() calls into a single update for performance.

    Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

    For example, this code may fail to update the counter:

    // Wrong
    this.setState({
      counter: this.state.counter + this.props.increment,
    });
    

    To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument:

    // Correct
    this.setState((prevState, props) => ({
      counter: prevState.counter + props.increment
    }));
    

    We used an arrow function above, but it also works with regular functions:

    // Correct
    this.setState(function(prevState, props) {
      return {
        counter: prevState.counter + props.increment
      };
    });
    

    State Updates are Merged

    When you call setState(), React merges the object you provide into the current state.

    For example, your state may contain several independent variables:

      constructor(props) {
        super(props);
        this.state = {
          posts: [],
          comments: []
        };
      }
    

    Then you can update them independently with separate setState() calls:

      componentDidMount() {
        fetchPosts().then(response => {
          this.setState({
            posts: response.posts
          });
        });
    
        fetchComments().then(response => {
          this.setState({
            comments: response.comments
          });
        });
      }
    

    The merging is shallow, so this.setState({comments}) leaves this.state.posts intact, but completely replaces this.state.comments.

    展开全文
  • 深入理解setState

    2021-02-16 09:20:04
    作者:虹晨,来源:https://juejin.im/post/5b45c57c51882519790c7441面试官:“react中setState是同步的还是异步?”我:“异步的,se...

    作者:虹晨,来源:https://juejin.im/post/5b45c57c51882519790c7441

    面试官:“react中setState是同步的还是异步?”
    我:“异步的,setState不能立马拿到结果。”

    面试官:“那什么场景下是异步的,可不可能是同步,什么场景下又是同步的?”
    我:“......”

    setState真的是异步的吗?

    这两天自己简单的看了下 setState 的部分实现代码,在这边给到大家一个自己个人的见解,可能文字或图片较多,没耐心的同学可以直接跳过看总结(源码版本是16.4.1)。

    看之前,为了方便理解和简化流程,我们默认react内部代码执行到performWork 、performWorkOnRoot、performSyncWork、performAsyncWork这四个方法的时候,就是react去update更新并且作用到UI上。

    一、合成事件中的setState

    首先得了解一下什么是合成事件,react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。

    class App extends Component {
      state = { val: 0 }
      increment = () => {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val) // 输出的是更新前的val --> 0
      }
      render() {
        return (
          <div onClick={this.increment}>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }
    

    合成事件中的setState写法比较常见,点击事件里去改变 this.state.val 的状态值,在 increment 事件中打个断点可以看到调用栈,这里我贴一张自己画的流程图:

    从 dispatchInteractiveEvent 到 callCallBack 为止,都是对合成事件的处理和执行,从 setState 到 requestWork 是调用 this.setState 的逻辑,这边主要看下 requestWork 这个函数(从 dispatchEvent 到 requestWork 的调用栈是属于 interactiveUpdates$1 的 try 代码块,下文会提到)。

    function requestWork(root, expirationTime) {
      addRootToSchedule(root, expirationTime);
      if (isRendering) {
        // Prevent reentrancy. Remaining work will be scheduled at the end of
        // the currently rendering batch.
        return;
      }
      if (isBatchingUpdates) {
        // Flush work at the end of the batch.
        if (isUnbatchingUpdates) {
          // ...unless we're inside unbatchedUpdates, in which case we should
          // flush it now.
          nextFlushedRoot = root;
          nextFlushedExpirationTime = Sync;
          performWorkOnRoot(root, Sync, false);
        }
        return;
      }
      // TODO: Get rid of Sync and use current time?
      if (expirationTime === Sync) {
        performSyncWork();
      } else {
        scheduleCallbackWithExpiration(expirationTime);
      }
    }
    

    在 requestWork 中有三个if分支,三个分支中有两个方法 performWorkOnRoot 和 performSyncWork ,就是我们默认的update函数,但是在合成事件中,走的是第二个if分支,第二个分支中有两个标识 isBatchingUpdates 和 isUnbatchingUpdates 两个初始值都为 false ,但是在 interactiveUpdates$1 中会把 isBatchingUpdates 设为 true ,下面就是 interactiveUpdates$1 的代码:

    function interactiveUpdates$1(fn, a, b) {
      if (isBatchingInteractiveUpdates) {
        return fn(a, b);
      }
      // If there are any pending interactive updates, synchronously flush them.
      // This needs to happen before we read any handlers, because the effect of
      // the previous event may influence which handlers are called during
      // this event.
      if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
        // Synchronously flush pending interactive updates.
        performWork(lowestPendingInteractiveExpirationTime, false, null);
        lowestPendingInteractiveExpirationTime = NoWork;
      }
      var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
      var previousIsBatchingUpdates = isBatchingUpdates;
      isBatchingInteractiveUpdates = true;
      isBatchingUpdates = true; // 把requestWork中的isBatchingUpdates标识改为true
      try {
        return fn(a, b);
      } finally {
        isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
        isBatchingUpdates = previousIsBatchingUpdates;
        if (!isBatchingUpdates && !isRendering) {
          performSyncWork();
        }
      }
    }
    

    在这个方法中把 isBatchingUpdates 设为了 true ,导致在 requestWork 方法中, isBatchingUpdates 为 true ,但是 isUnbatchingUpdates 是 false ,而被直接return了。
    那return完的逻辑回到哪里呢,最终正是回到了 interactiveUpdates 这个方法,仔细看一眼,这个方法里面有个try finally语法,前端同学这个其实是用的比较少的,简单的说就是会先执行 try 代码块中的语句,然后再执行 finally 中的代码,而 fn(a, b) 是在try代码块中,刚才说到在 requestWork 中被return掉的也就是这个fn(上文提到的 从dispatchEvent 到 requestWork 的一整个调用栈)。
    所以当你在 increment 中调用 setState 之后去console.log的时候,是属于 try 代码块中的执行,但是由于是合成事件,try代码块执行完state并没有更新,所以你输入的结果是更新前的 state 值,这就导致了所谓的"异步",但是当你的try代码块执行完的时候(也就是你的increment合成事件),这个时候会去执行 finally 里的代码,在 finally 中执行了 performSyncWork 方法,这个时候才会去更新你的 state 并且渲染到UI上。

    二、生命周期函数中的setState

    class App extends Component {
      state = { val: 0 }
     componentDidMount() {
        this.setState({ val: this.state.val + 1 })
       console.log(this.state.val) // 输出的还是更新前的值 --> 0
     }
      render() {
        return (
          <div>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }
    

    钩子函数中setState的调用栈:

    其实还是和合成事件一样,当 componentDidmount 执行的时候,react内部并没有更新,执行完componentDidmount 后才去 commitUpdateQueue 更新。这就导致你在 componentDidmount 中 setState 完去console.log拿的结果还是更新前的值。

    三、原生事件中的setState

    class App extends Component {
      state = { val: 0 }
      changeValue = () => {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val) // 输出的是更新后的值 --> 1
      }
     componentDidMount() {
        document.body.addEventListener('click', this.changeValue, false)
     }
      render() {
        return (
          <div>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }
    

    原生事件是指非react合成事件,原生自带的事件监听 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 这种绑定事件的形式都属于原生事件。

    原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发click事件,到 requestWork ,在requestWork里由于 expirationTime === Sync 的原因,直接走了 performSyncWork 去更新,并不像合成事件或钩子函数中被return,所以当你在原生事件中setState后,能同步拿到更新后的state值。

    四、setTimeout中的setState

    class App extends Component {
      state = { val: 0 }
     componentDidMount() {
        setTimeout(_ => {
          this.setState({ val: this.state.val + 1 })
          console.log(this.state.val) // 输出更新后的值 --> 1
        }, 0)
     }
      render() {
        return (
          <div>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }
    

    在 setTimeout 中去 setState 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 setTimeout ,可以在钩子函数中 setTimeout ,也可以在原生事件setTimeout,但是不管是哪个场景下,基于event loop的模型下, setTimeout 中里去 setState 总能拿到最新的state值。
    举个栗子,比如之前的合成事件,由于你是 setTimeout(_ => { this.setState()}, 0) 是在 try 代码块中,当你 try 代码块执行到 setTimeout 的时候,把它丢到列队里,并没有去执行,而是先执行的 finally 代码块,等 finally 执行完了, isBatchingUpdates 又变为了 false ,导致最后去执行队列里的 setState 时候, requestWork 走的是和原生事件一样的 expirationTime === Sync if分支,所以表现就会和原生事件一样,可以同步拿到最新的state值。

    五、setState中的批量更新

    class App extends Component {
      state = { val: 0 }
      batchUpdates = () => {
        this.setState({ val: this.state.val + 1 })
        this.setState({ val: this.state.val + 1 })
        this.setState({ val: this.state.val + 1 })
     }
      render() {
        return (
          <div onClick={this.batchUpdates}>
            {`Counter is ${this.state.val}`} // 1
          </div>
        )
      }
    }
    

    上面的结果最终是1,在 setState 的时候react内部会创建一个 updateQueue ,通过 firstUpdate 、 lastUpdate 、 lastUpdate.next 去维护一个更新的队列,在最终的 performWork 中,相同的key会被覆盖,只会对最后一次的 setState 进行更新,下面是部分实现代码:

    function createUpdateQueue(baseState) {
      var queue = {
        expirationTime: NoWork,
        baseState: baseState,
        firstUpdate: null,
        lastUpdate: null,
        firstCapturedUpdate: null,
        lastCapturedUpdate: null,
        firstEffect: null,
        lastEffect: null,
        firstCapturedEffect: null,
        lastCapturedEffect: null
      };
      return queue;
    }
    function appendUpdateToQueue(queue, update, expirationTime) {
      // Append the update to the end of the list.
      if (queue.lastUpdate === null) {
        // Queue is empty
        queue.firstUpdate = queue.lastUpdate = update;
      } else {
        queue.lastUpdate.next = update;
        queue.lastUpdate = update;
      }
      if (queue.expirationTime === NoWork || queue.expirationTime > expirationTime) {
        // The incoming update has the earliest expiration of any update in the
        // queue. Update the queue's expiration time.
        queue.expirationTime = expirationTime;
      }
    }
    

    看个????

    class App extends React.Component {
      state = { val: 0 }
      componentDidMount() {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)
        setTimeout(_ => {
          this.setState({ val: this.state.val + 1 })
          console.log(this.state.val);
          this.setState({ val: this.state.val + 1 })
          console.log(this.state.val)
        }, 0)
      }
      render() {
        return <div>{this.state.val}</div>
      }
    }
    

    结合上面分析的,钩子函数中的 setState 无法立马拿到更新后的值,所以前两次都是输出0,当执行到 setTimeout 里的时候,前面两个state的值已经被更新,由于 setState 批量更新的策略, this.state.val 只对最后一次的生效,为1,而在 setTimmout 中 setState 是可以同步拿到更新结果,所以 setTimeout 中的两次输出2,3,最终结果就为 0, 0, 2, 3 。

    总结 :

    1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。

    2. setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。

    3. 如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

    以上就是我看了部分代码后的粗浅理解,对源码细节的那块分析的较少,主要是想让大家理解setState在不同的场景,不同的写法下到底发生了什么样的一个过程和结果,希望对大家有帮助,由于是个人的理解和见解,如果哪里有说的不对的地方,欢迎大家一起指出并讨论。

    “在看”吗?在看就点一下吧

    展开全文
  • 深入理解 React 的 setState前言參考正文setState 到底是同步還是異步的?setState 原理結語 前言 這篇來探討一個 react 中很基本的東西,setState,探討這個一方面其實也是打好 react 的基礎,另一方面是因為我自己...

    前言

    這篇來探討一個 react 中很基本的東西,setState,探討這個一方面其實也是打好 react 的基礎,另一方面是因為我自己拿來練手的 side-project 確實用到 setState,但是卻遇到了一個 bug,我思索應該就是 setState 的問題,沒有理解透徹。因此趕緊來學習這個東西,好掌握這個知識點。前情提要一下,這篇真的頗長,沒耐心地趕緊換篇看,不過對於認真看完的應該都能多少有一點點幫助~

    參考

    參考鏈接
    React 官方文檔https://reactjs.org/docs/react-component.html#setstate
    StackOverflow Dan Abramov 大神的解答https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973

    正文

    setState 到底是同步還是異步的?

    對於已經熟悉 react 的人來說,應該已經都知道了,來看看一段原文,官方文檔對 setState 的說明如下:

    setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.

    Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.

    setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

    如果英文不好的話也沒關係,上面這麼一大段話就是在說,setState 會依序更新 state,然後告訴 react 要重新渲染這個組件以及他的子組件,這個函數可以運用在事件觸發或是或是對服務端請求所帶來的數據變化。

    不過重點其實在第二段話,、不過重點其實在第二段話,第二段話告訴我們要把 setState 想成一個『非同步』的操作,也就是說,執行了 setState 其實不代表 component 的 state 一定被立即更新了,這其實是 react 為了提高性能的一個設計,後面也會再提到。

    也因此,所以如果在 setState 之後馬上去讀取 this.state,可能會發現 state 沒有更新。這時候該怎麼辦呢?其實上面的原文也有提到,我們可以用 componentDidUpdate() 鉤子,如果不清楚歡迎參考 關於 React 組件生命週期,或是如果想要精準的基於先前的 state 進行後續操作的話,推薦使用 updater 函數。

    updater 函數長這樣:

    this.setState((state, props) => {
      return { ...一個新的對象 };
    });
    

    state is a reference to the component state at the time the change is being applied. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from state and props.

    這麼一直看理論啥的其實很抽象,馬上帶入一個例子,假設我們今天有一個組件,如下:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    
        <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
      </head>
      <body>
        <div id="app"></div>
        <script type="text/babel">
          class Count extends React.Component {
            constructor(props) {
              super(props);
              this.state = {
                count: 0,
              };
              this.handleAddCount = this.handleAddCount.bind(this);
            }
    
            handleAddCount = () => {
              this.setState((state) => ({
                count: state.count + 1,
              }));
            // 寫成下面這樣當然也可以
            //   this.setState((state) => {
            //     return {
            //       count: state.count + 1,
            //     };
            //   });
            };
    
            render() {
              return (
                <div>
                  <h2>{this.state.count}</h2>
                  <button onClick={this.handleAddCount}>Add</button>
                </div>
              );
            }
          }
          ReactDOM.render(<Count />, document.getElementById("app"));
        </script>
      </body>
    </html>
    

    上面的 handleAddCount 使用了 updater,所以一切更新看起來都挺正常的。那如果我就執意不用 updater 呢?

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

    結果其實會發現,還是可以正常 add 啊?!那好像用不用 updater 沒差?上面說到的 setState 的同步非同步又是怎樣?別急,有耐心點繼續看下去。

    如果我今天是一次想要加三上去,也我就執意不用 updater,來看看會發生什麼:

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

    好的問題出現了!我們明明就三次執行了 setState,但卻不是一次加 3 上去,仍然還是每次只加 1。這就是前面提到的,每次 setState 不一定會馬上更新,因此執行完 setState 後馬上讀取 this.state 不見得會如我們預期。

    所以我們就改成用 updater,如下:

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


    發現確實如我們所預期一次加 3 上去~當時學到這邊得出一個結論,反正以後基於先前的 state 操作,用 updater 就對了,而且大部分情況我們都是希望 state 要按我們的 setState 操作更新,所以應該都用 updater 就對了。

    如果你也有一樣的想法,只能說,很遺憾你跟我一樣菜,沒關係我們繼續學。其實不是這樣那麼簡單的,其實並不是都會像上面一樣,多次執行 setState 後『一定不會』馬上更新。(有點拗口 hhh)

    來看看下面這個例子:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    
        <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
      </head>
      <body>
        <div id="app"></div>
        <script type="text/babel">
          class Clock extends React.Component {
            constructor(props) {
              super(props);
              this.state = {
                date: new Date(),
                count: 0,
              };
              this.tick = this.tick.bind(this);
            }
    
            componentDidMount() {
              setInterval(this.tick, 1000);
            }
    
            tick = () => {
              this.setState({
                date: new Date(),
              });
              for (let i = 0; i < 3; i++) {
                this.setState((state) => {
                  return {
                    count: state.count + 1,
                  };
                });
              }
            };
    
            render() {
              return (
                <div>
                  <h1>Learn React</h1>
                  <h2>{this.state.date.toLocaleDateString()}</h2>
                  <p>{this.state.count}</p>
                </div>
              );
            }
          }
          ReactDOM.render(<Clock />, document.getElementById("app"));
        </script>
      </body>
    </html>
    

    在這個例子中,一個簡單時間顯示的 clock 組件,同時 state 中還有一個 count,希望每秒的 count 都基於前面的 count 加上去。

    tick 函數中,我三次調用了 setState,這邊我使用了 updater,但其實你自己改成不用 updater 試試看就會發現,也是一樣的效果~

    這個例子告訴我們,很明顯先前的想法反正都用 updater 不一定正確,因為這邊不管用不用都一樣。所以這邊進入說明:

    什麼時候 setState 才會變成非同步的?

    其實是這樣的,在 react 中,調用 setState 確實會立刻更新 state 並重新 render,但是如果處理更新的 handler 綁定在『事件處理』上,像是 onClick、onChange、onKeyDown 等等,那這些 setState 的執行只會觸發最後一次 render,而不是每次 setState 後都會 re-render。這就是前面提到的 react 為了性能上的設計,設想如果每次事件發生,比如按鈕被點擊(onClick),觸發 eventHandler,組件以及他的子組件都執行 setState,每次都進行重繪,顯然當項目越來越大、組件越來越多等等,就會嚴重消耗系統性能。

    我們把上面的代碼重構一下,分成父組件以及子組件的結構,來看看下面代碼:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    
        <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
      </head>
      <body>
        <div id="app"></div>
        <script type="text/babel">
          class Clock extends React.Component {
            constructor(props) {
              super(props);
              this.state = {
                date: new Date(),
              };
            }
    
            componentDidMount() {
              setInterval(() => this.tick(), 1000);
            }
    
            tick() {
              this.setState({
                date: new Date(),
              });
            }
    
            render() {
              console.log("Parent render");
              return (
                <div>
                  <ShowClock date={this.state.date} />
                </div>
              );
            }
          }
    
          class ShowClock extends React.Component {
            constructor(props) {
              super(props);
              this.state = {
                count: 0,
              };
            }
    
            componentDidMount() {
              setInterval(() => this.addCountPerSec(), 1000);
            }
    
            addCountPerSec() {
              this.setState({
                count: this.state.count + 1,
              });
            }
    
            render() {
              console.log("Child render");
              console.log("-------------");
              return (
                <div>
                  <h1>Learn React</h1>
                  <h2>{this.props.date.toLocaleTimeString()}</h2>
                  <p>count : {this.state.count}</p>
                </div>
              );
            }
          }
          ReactDOM.render(<Clock />, document.getElementById("app"));
        </script>
      </body>
    </html>
    

    可以看到控制台輸出,我們在 Clock component 以及 ShowClock component 的 render 中都方一個 console.log(),而我們發現,每過一秒,ShowClock 都 re-render 了兩次,因為第一次是 Clock 通過了 setState 更改了傳給 ShowClock 的 props,第二次則是 ShowClock 自己通過了 setState 更改了自己的 state。

    這就是為什麼 react 要幫我們把觸發的 eventHandler 的 setState 包裝成非同步的,因為就是要避免這種大量重繪的問題。

    來看看這個例子:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    
        <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
      </head>
      <body>
        <div id="app"></div>
        <script type="text/babel">
          class Father extends React.Component {
            constructor(props) {
              super(props);
              this.state = {
                a: false,
              };
            }
    
            render() {
              console.log("Father render");
              return (
                <Son
                  parentState={this.state.a}
                  setParentState={this.setState.bind(this)}
                />
              );
            }
          }
    
          class Son extends React.Component {
            constructor(props) {
              super(props);
              this.state = {
                b: false,
              };
            }
    
            handleClick = () => {
              const parentState = this.props.parentState;
              this.props.setParentState({
                a: parentState === true ? false : true,
              });
              this.setState({
                b: this.state.b === true ? false : true,
              });
            };
    
            render() {
              console.log("Son render");
              return (
                <div>
                  <p>FatherState : {`${this.props.parentState}`}</p>
                  <p>SonState : {`${this.state.b}`}</p>
                  <button onClick={this.handleClick}>click</button>
                </div>
              );
            }
          }
          ReactDOM.render(<Father />, document.getElementById("app"));
        </script>
      </body>
    </html>
    

    控制台輸出如下:

    邏輯也很單純,就是一個父組件、一個子組件,同樣也是由父組件通過 props 將參數(自身狀態)傳給子組件,不同的是,這邊我將兩個 setState 操作綁定在 onClick 事件上,所以可以看到,在 Son component 中,雖然也是做了兩次 setState,但是實際上觸發了一次 render(只打印出一次 Son render),這就是 react 幫我們做的異步化,讓整個事件中只觸發一次 render。

    關於這個概念,我們來看看大神 Dan Abramov 的總結:

    The key to understanding this is that no matter how many setState() calls in how many components you do inside a React event handler, they will produce only a single re-render at the end of the event. This is crucial for good performance in large applications because if Child and Parent each call setState() when handling a click event, you don’t want to re-render the Child twice.

    setState() calls happen inside a React event handler. Therefore they are always flushed together at the end of the event (and you don’t see the intermediate state).

    不愧是大牛,說得真好~ ok 現在我們知道在事件處理的函數中,setState 的執行是異步的,那我們回到最一開始的例子,為什麼明明三次執行了 setState,但還是以 1 為單位加上去?

    // 沒有使用 updater
    
    handleAddCount = () => {
      this.setState({
        count: this.state.count + 1,
      });
      this.setState({
        count: this.state.count + 1,
      });
      this.setState({
        count: this.state.count + 1,
      });
    };
    

    原因就是因為 react 其實把他們處理成這樣:

    Object.assign(
      previousState,
      { count: this.state.count + 1 },
      { count: this.state.count + 1 },
      { count: this.state.count + 1 }
    );
    

    我們都知道,setState 的參數必須是一個對象,扣回到剛剛講的,react 對事件處理函數中的 setState 所引發的 render 是異步的,也就是說 react 會把三次 setState 中的對象合併在一起後,才會實際執行更新 state, re-render 的動作。

    我們都知道,如果我們這樣寫:

    const person = {
      name: "cyzsb",
      name: "csdnb",
      name: "cclin",
    };
    

    那其實 person 對象如下:

    {
      name: "cclin";
    }
    

    因為同一個對象不能有重名的屬性,若有,會以後面的覆蓋前面的(根本就在講廢話 hhh)。

    看到這裡,其實應該對 setState 更加明白了,就是因為這個原因,所以其實你以為自己三次執行了 setState,但經過 react 背後的處理,實際上最後只剩下一次的效果。

    這才是 updater 的用處。所以其實 updater 是用於如果在一個事件觸發的處理函數中要多次更新 state,且希望每次 setState 都基於先前的 state 操作,才要用 updater,如果 setState 不是通過事件綁定觸發,其實用不用 updater 就根本無所謂。

    setState 原理

    看到這邊應該都懂了,應該也沒多少人看到這邊,畢竟這篇確實有點長。看到這邊的話,其實可以停了也知道怎麼使用 setState 了,不過還是建議再多堅持一下下,看看具體 react 中的 setState 的原理。

    其實一句話簡單就可以概括 setState 的原理:

    在 React 生命周期内,也可以理解主线程中 setState 就是异步的;子线程或者说异步任务中,例如 setInterval,setTimeout 里,setState 才是同步更新的。

    看一個例子,非常清楚:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    
        <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
      </head>
      <body>
        <div id="app"></div>
        <script type="text/babel">
          class App extends React.Component {
            constructor(props) {
              super(props);
              this.state = {
                num: 0,
              };
            }
    
            componentDidMount() {
              this.setState((state) => ({
                num: state.num + 1,
              }));
              console.log("第一次:", this.state.num);
              this.setState((state) => ({
                num: state.num + 1,
              }));
              console.log("第二次:", this.state.num);
              setTimeout(() => {
                this.setState((state) => ({
                  num: state.num + 1,
                }));
                console.log("第三次:", this.state.num);
                this.setState((state) => ({
                  num: state.num + 1,
                }));
                console.log("第四次:", this.state.num);
              }, 0);
            }
    
            componentDidUpdate() {
              console.log("componentDidUpdate");
            }
    
            render() {
              return <div>app</div>;
            }
          }
          ReactDOM.render(<App />, document.getElementById("app"));
        </script>
      </body>
    </html>
    

    当第一次调用 setState,newState 被放入 pending 队列,此时 batch update(源码中 isBatchingUpdates) 是 true ,所以 newState 会被保存在 dirtyComponents 数组中,并不会调用 updateComponent,所以 state 并没有更新,但是 state 的值已经被合并了,第二次调用 setState 的情况也是一样的。

    第三次调用 setState 时,newState 被放入 pending 队列,此时 batch update (源码中 isBatchingUpdates) 是 false,此时会马上遍历 dirtycomponents 数组,调用 updateComponent 方法,然后更新 state,第四次同第三次。

    注意到這邊我們用了 updater,既然上面講到了那麼多,不仿再來看看如果不用 updater 會發生什麼?

    componentDidMount() {
        //   this.setState((state) => ({
        //     num: state.num + 1,
        //   }));
        //   console.log("第一次:", this.state.num);
        //   this.setState((state) => ({
        //     num: state.num + 1,
        //   }));
        //   console.log("第二次:", this.state.num);
        //   setTimeout(() => {
        //     this.setState((state) => ({
        //       num: state.num + 1,
        //     }));
        //     console.log("第三次:", this.state.num);
        //     this.setState((state) => ({
        //       num: state.num + 1,
        //     }));
        //     console.log("第四次:", this.state.num);
        //   }, 0);
        this.setState({
            num: this.state.num + 1,
        });
        console.log("第一次:", this.state.num);
        this.setState({
            num: this.state.num + 1,
        });
        console.log("第二次:", this.state.num);
        setTimeout(() => {
        this.setState({
            num: this.state.num + 1,
        });
        console.log("第三次:", this.state.num);
        this.setState({
            num: this.state.num + 1,
        });
        console.log("第四次:", this.state.num);
        }, 0);
    }
    

    可以看到跟使用了 updater 有一點點的不一樣。所以其實就可以知道,react 在背後幫我們做的 setState 的異步,其實就是合併的動作,而 updater 做到的就是精準的合併每一次 setState 後的操作,而不用 updater 的話,則會被 react 處理成上面 Object.assign() 的樣子。

    也正是因為這樣,所以在這個例子中,如果不使用 updater,會發現第三次打印出來的 num 是 2,而使用 updater 打印出來的會是 3。

    結語

    這篇詳細了講解了關於 React 的 setState 方法,如果能看完我只能說,你真有耐心。整篇下來應該算是非常詳細的了,其實也只是本菜雞學習 react 的剛開始而已,希望以後也還能繼續堅持下去。對於本文如果有不完整或是錯誤,歡迎大老指出,也感謝那些認真讀完的人,應該會有所收穫的!

    展开全文
  • 详解setState 方法

    千次阅读 2020-08-02 21:47:31
    文章目录为什么要是用 setStatesetState 异步更新为什么要异步更新如何获取更新后的值同步 or 异步setState的合并数据的合并多个setState合并setState性能优化react 更新机制diff算法的作用传统 diff算法react diff...

    为什么要是用 setState

    当我们希望通过点击一个按钮从而改变一个文本时,就需要使用到 setState方法,比如:

    import React, { Component } from 'react'
    
    export default class App extends Component {
      constructor(props) {
        super(props);
    
        this.state = {
          message: "Hello World"
        }
      }
    
      render() {
        return (
          <div>
            <h2>{this.state.message}</h2>
            <button onClick={e => this.changeText()}>改变文本</button>
          </div>
        )
      }
    
     changeText() {
        this.setState({
          message: "文本已经被改变"
        })
      }
    }
    

    当我们调用setState时,会重新执行render函数,根据最新的State来创建ReactElement对象;然后再根据最新的ReactElement对象,对DOM进行修改

    另外,setState方法是从Component中继承过来的

    setState 异步更新

    changeText() {
      this.setState({
        message: "文本已经被改变"
      })
      console.log(this.state.message); // Hello World
    }
    

    上面代码中再使用 setState 方法后,再打印 message,发现 message 没有被改变,由此可以看到 setState 是异步操作,在执行完setState之后不能够立刻拿到最新的state的结果

    为什么要异步更新

    传送门

    • 设计成异步,可以显著提升性能,如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的,所以最好的办法是获取到多个更新,然后进行批量的更新
    • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步,这样做会引起很多问题

    如何获取更新后的值

    1、setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行

    changeText() {
      this.setState({
        message: "文本已经被改变"
      }, () => {
        console.log(this.state.message); // 文本已经被改变
      });
    }
    

    2、通过生命周期函数 componentDidUpdate

    componentDidUpdate(prevProps, provState, snapshot) {
      console.log(this.state.message);
    }
    

    同步 or 异步

    setState 方法到底是同步还是异步分两种情况

    • 在组件生命周期或React合成事件中,setState是异步

    • 在setTimeout或者原生dom事件中,setState是同步

    // setTimeout
    changeText() {
      setTimeout(() => {
        this.setState({
          message: "文本已经被改变"
        });
        console.log(this.state.message); // 文本已经被改变
      }, 0);
    }
    
    // 原生DOM事件
    componentDidMount() {
      const btnEl = document.getElementById("btn");
      btnEl.addEventListener('click', () => {
        this.setState({
          message: "文本已经被改变"
        });
        console.log(this.state.message); // 文本已经被改变
      })
    }
    

    setState的合并

    数据的合并

    this.state = {
      name: '小冯',
      age: 19
    }
    
    // 通过setState去修改age,是不会对name产生影响的
    this.setState({
      age: 18
    })
    

    多个setState合并

    this.state = {
      counter: 0
    }
    
    
    increment() {
        this.setState({
          counter: this.state.counter + 1
        });
    
        this.setState({
          counter: this.state.counter + 1
        });
    
        this.setState({
          counter: this.state.counter + 1
        });
      }
    

    setState 方法执行了3次,但是 counter 的值还是为1,这就是多个state进行合并

    如果想要变为3,如何去做:传入一个函数

    increment() {
      this.setState((state, props) => {
        return {
          counter: state.counter + 1
        }
      })
    
      this.setState((state, props) => {
        return {
          counter: state.counter + 1
        }
      })
    
      this.setState((state, props) => {
        return {
          counter: state.counter + 1
        }
      })
    }
    

    setState性能优化

    react 更新机制

    react 渲染流程

    在这里插入图片描述

    react 更新流程

    在这里插入图片描述

    React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。

    React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:

    • 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量;
    • https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;
    • 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
    • 这个开销太过昂贵了,React的更新性能会变得非常低效;

    于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?

    • 同层节点之间相互比较,不会跨层级进行节点的比较。
    • 不同类型的节点,产生不同的树结构;
    • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;

    diff算法的作用

    计算出虚拟 dom中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面

    传统 diff算法

    通过循环递归节点进行依次比较,复杂度为O(n^3) ,n为是树中节点(元素)的数量

    如果在 React 中使用了该算法,那么展示 1000 个元素就需要进行上亿次比较,太浪费性能

    传送门

    react diff算法

    • 同层节点之间相互比较,不会跨节点比较;
    • 不同类型的节点,产生不同的树结构;
    • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
    对比不同类型的元素

    当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:

    • 当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成<div> 都会触发一个完整的重建流程;
    • 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行componentWillUnmount() 方法;
    • 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法,紧接着 componentDidMount() 方法;

    比如下面的代码更改:

    在这里插入图片描述

    • React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用;
    <div>
      <Counter />
    </div>
    
    <span>
      <Counter />
    </span>
    
    对比同一类型的元素

    当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。

    比如下面的代码更改:

    • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性;
    <div className="before" title="stuff" />
    
    <div className="after" title="stuff" />
    

    比如下面的代码更改:

    • 当更新 style 属性时,React 仅更新有所更变的属性。
    • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight
    <div style={{color: 'red', fontWeight: 'bold'}} />
    
    <div style={{color: 'green', fontWeight: 'bold'}} />
    

    如果是同类型的组件元素:

    • 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps()componentWillUpdate() 方法;
    • 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归;
    对子节点进行递归

    在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。

    我们来看一下在最后插入一条数据的情况:

    • 前面两个比较是完全相同的,所以不会产生mutation;
    • 最后一个比较,产生一个mutation,将其插入到新的DOM树中即可;
    <ul>
      <li>first</li>
      <li>second</li>
    </ul>
    
    <ul>
      <li>first</li>
      <li>second</li>
      <li>third</li>
    </ul>
    

    但是如果我们是在中间插入一条数据:

    • React会对每一个子元素产生一个mutation,而不是保持 <li>星际穿越</li><li>盗梦空间</li>的不变;
    • 这种低效的比较方式会带来一定的性能问题;
    <ul>
      <li>星际穿越</li>
      <li>盗梦空间</li>
    </ul>
    
    <ul>
      <li>大话西游</li>
      <li>星际穿越</li>
      <li>盗梦空间</li>
    </ul>
    

    推荐阅读1

    推荐阅读2

    keys的优化

    在遍历数据进行渲染时,需要加 key 值,没有 key 值得话会出现下面的错误

    在这里插入图片描述

    分析一下下面这个例子

    import React, { Component } from 'react'
    
    export default class App extends Component {
      constructor(props) {
        super(props);
    
        this.state = {
          movies: ["星际穿越", "盗梦空间"]
        }
      }
    
      render() {
        return (
          <div>
            <h2>电影列表</h2>
            <ul>
              {
                this.state.movies.map((item, index) => {
                  return <li>{item}</li>
                })
              }
            </ul>
            <button onClick={e => this.insertMovie()}>插入数据</button>
          </div>
        )
      }
    
      insertMovie() {
      }
    }
    
    在最后位置插入数据

    这种情况,有无key意义并不大

    insertMovie() {
      const newMovies = [...this.state.movies, "大话西游"];
      this.setState({
        movies: newMovies
      })
    }
    
    在前面插入数据
    • 这种做法,在没有key的情况下,所有的li都需要进行修改;
    insertMovie() {
      const newMovies = ["大话西游", ...this.state.movies];
      this.setState({
        movies: newMovies
      })
    }
    

    当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:

    • 在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改;
    • 将key为333的元素插入到最前面的位置即可;
    <ul>
      <li key="111">星际穿越</li>
      <li key="222">盗梦空间</li>
    </ul>
    
    <ul>
      <li key="333">Connecticut</li>
      <li key="111">星际穿越</li>
      <li key="222">盗梦空间</li>
    </ul>
    

    key的注意事项:

    • key应该是唯一的;
    • key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
    • 使用index作为key,对性能是没有优化的;
    展开全文
  • 深入 setState 机制

    千次阅读 2019-02-11 14:23:06
    React 是通过管理状态来实现对组件的管理,即使用 this.state 获取 state,通过 this.setState() 来更新 state,当使用 this.setState() 时,React 会调用 render 方法来重新渲染 UI。 首先看一个例子: class ...
  • 关于setState的那点事

    2021-04-23 13:35:23
    文章目录 一、setState执行初探 事件里的setState 定时器里的setState 二、setState的使用 setState的第二个参数(callback) 关于多个setState的执行 setState第一个参数(函数写法) 三、浅谈setState源码 总结 一...
  • React中的setState的同步异步与合并

    千次阅读 2021-02-20 23:16:45
    这篇文章主要是因为自己在学习React中setState的时候,产生了一些疑惑,所以进行了一定量的收集资料和学习,并在此记录下来 引入 使用过React的应该都知道,在React中,一个组件中要读取当前状态需要访问this.state...
  • 浅谈 setState 更新机制

    2020-05-05 11:26:37
    了解 React 同学想必对setState函数是再熟悉不过了,setState也会经常作为面试题,考察前端求职者对 React 的熟悉程度。 在此我也抛一个问题,阅读文章前读者可以先想一下这个问题的答案。 给 React 组件的状态每次...
  • setState 到底是同步的,还是异步的?   setState 对于许多的 React 开发者来说,像是一个“最熟悉的陌生人”: 当你入门 React 的时候,接触的第一波 API 里一定有 setState——数据驱动视图,没它就没法创造...
  • 今天同事在开发过程中遇到了个问题,在使用AntD的Form组件时,内置的onFinish方法里面调用了2次setState方法,发现return函数渲染了2次,不过我记得多次调用setState时,会批量合并,所以就产生了一些疑惑,就上网查...
  • Flutter踩坑记录之setState

    千次阅读 2019-11-06 22:09:47
    How to refresh the content of a Dialog via setState? 在上面链接中的这篇文章中,主要介绍了在Flutter中使用setState刷新Dialog的问题,并提供了一种解决方案。这篇文章大部分内容翻译自这个链接,另外除了坐着...
  • 实际上在之前React v15相关原理的文章中就setState的几个特点有了较为清晰的梳理,虽然在React v16中引入了Fiber架构,但是setState整体的处理逻辑以及使用并没有什么区别,只是底层处理不同而已,为什么还有梳理v16...
  • 所以都给合并了,render 一起执行了 // setState 不是立刻更新的,在调用setState 如果依赖state里面的数据会有隐患 // 不能依赖上以一个状态 来 计算下一个状态 this.setState({a:this.state.a + 1}) console.log...
  • setState总结

    2020-11-14 14:10:20
    State React 组件中的数据可以来源于使用者,也可以组件自身维护。使用者传递的数据就是组件的属性 (props),而组件自身维护的数据就是组件的状态(state)。...setState 由于 React 无法监测到组件状态的变化,这也
  • 首先我们要明确一点:setState并不是一个异步方法,很像异步的原因:hook()与合成事件的调用顺序与setState的执行顺序存在偏差,最终产生异步的展示形式,从而造成异步的假象。记录setState必然要在各生命周期中...
  • React setState() 的原理解析

    千次阅读 2020-01-19 15:10:50
    setState 的原理机制解析 我们本章节主要是要来分析一下React中常见的setState方法,熟悉React的小伙伴应该都知道,该方法通常用于改变组件状态并用新的state去更新组件。但是,这个方法在很多地方的表现总是与我们...
  • setState的同步和异步 1.为什么使用setState 开发中我们并不能直接通过修改 state 的值来让界面发生更新: 因为我们修改了 state 之后, 希望 React 根据最新的 Stete 来重新渲染界面, 但是这种方式的修改 React 并...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 109,138
精华内容 43,655
关键字:

setstate