簡體   English   中英

React 在重新渲染父組件時如何重用子組件/保留子組件的 state?

[英]How does React re-use child components / keep the state of child components when re-rendering the parent component?

在 React 中,每次渲染/重新渲染組件時,它都會使用createElement重新生成它的所有子節點/組件。 React 如何知道何時在重新渲染之間保留組件 state?

例如,考慮以下代碼:

class Timer extends Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
  }
  tick() {
    this.setState(state => ({ seconds: state.seconds + 1 }));
  }
  componentDidMount() {
    this.interval = setInterval(() => this.tick(), 1000);
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  render() {
    return createElement('div', null,
      'Seconds: ',
      this.state.seconds
    );
  }
}
class Button extends Component {
  constructor(props) {
    super(props);
    this.state = { clicks: 0 };
  }
  click() {
    this.setState(state => ({ clicks: state.clicks + 1 }));
  }
  render() {
    return createElement('button', { onClick: () => this.click() },
      createElement(Timer, null),
      'Clicks: ',
      this.state.clicks
    );
  }
}
render(createElement(Button, null), document.getElementById('root'));

您可以在此處使用 Preact REPL 嘗試此代碼。

請注意,當按下按鈕並更新 clicks 值時, Timer組件的 state 保持不變,不會被替換。 React 如何知道重用組件實例?

雖然一開始這似乎是一個簡單的問題,但當您考慮更改傳遞給子組件的道具或子組件列表等事情時,它會變得更加復雜。 React 如何處理更改子組件的 props? 即使子組件的道具已更改,子組件的 state 是否仍然存在? (在 Vue 中,組件的 state 在其 props 更改時確實存在)列表怎么樣? 當子組件列表中間的條目被刪除時會發生什么? 對這樣的列表進行更改顯然會生成非常不同的 VDOM 節點,但組件的 state 仍然存在。

createElement vs render vs 掛載

當渲染一個 React 組件(例如您的Button )時,會使用createElement創建許多子組件。 createElement(Timer, props, children)不會創建Timer組件的實例,甚至不會渲染它,它只會創建一個“React 元素”,它表示應該渲染組件的事實。

當您的Button被渲染時, react 會將結果與之前的結果進行協調,以決定需要對每個子元素執行什么操作:

  • 如果元素與先前結果中的元素不匹配,則創建一個組件實例,然后安裝然后渲染(遞歸地應用相同的過程)。 請注意,當第一次渲染Button時,所有的孩子都將是新的(因為沒有以前的結果可以匹配)。
  • 如果元素與上一個結果中的一個匹配,則重用組件實例:更新其 props,然后重新渲染組件(再次遞歸地應用相同的過程)。 如果 props 沒有改變,React 甚至可能選擇不重新渲染以提高效率。
  • 先前結果中與新結果中的元素不匹配的任何元素都將被卸載並銷毀。

React 的 diffing 算法

如果 React 比較它們並且它們具有相同的類型,則一個元素“匹配”另一個元素。

React 比較子元素的默認方式是簡單地同時遍歷兩個子元素列表,將第一個元素相互比較,然后再比較第二個,等等。

如果孩子有key s,則將新列表中的每個孩子與舊列表中具有相同密鑰的孩子進行比較。

有關更詳細的說明,請參閱React Reconciliation Docs

例子

你的Button總是只返回一個元素:一個button 因此,當您的Button重新渲染時,該button匹配,並且其 DOM 元素被重新使用,然后比較該button的子項。

第一個孩子總是一個Timer ,所以類型匹配並且組件實例被重用。 Timer props 沒有改變,所以 React 可能會重新渲染它(在具有相同狀態的實例上調用render ),或者它可能不會重新渲染它,從而保持樹的那部分保持不變。 這兩種情況都會在你的情況下產生相同的結果——因為你在render中沒有副作用——並且 React 故意將何時重新渲染的決定作為實現細節。

第二個子元素始終是字符串"Clicks: " ,因此 react 也只保留了該 DOM 元素。

如果this.state.click自上次渲染后發生了變化,那么第三個子節點將是一個不同的字符串,可能從"0"變為"1" ,因此文本節點將在 DOM 中被替換。


如果Buttonrender要返回不同類型的根元素,如下所示:

  render() {
    return createElement(this.state.clicks % 2 ? 'button' : 'a', { onClick: () => this.click() },
      createElement(Timer, null),
      'Clicks: ',
      this.state.clicks
    );
  }

然后在第一步中,將abutton進行比較,因為它們是不同的類型,舊元素及其所有子元素將從 DOM 中刪除、卸載和銷毀。 然后新元素將在沒有先前渲染結果的情況下創建,因此將使用新的 state 創建一個新的Timer實例,並且計時器將返回 0。


Timer匹配? 上一棵樹 新樹
不匹配 <div><Timer /></div> <span><Timer /></span>
匹配 <div>a <Timer /> a</div> <div>b <Timer /> b</div>
不匹配 <div><Timer /></div> <div>first <Timer /></div>
匹配 <div>{false}<Timer /></div> <div>first <Timer /></div>
匹配 <div><Timer key="t" /></div> <div>first <Timer key="t" /></div>

從未使用過 Vue,但這是我的看法。

即使子組件的道具已更改,子組件的 state 是否仍然存在? (在 Vue 中,組件的 state 在其 props 更改時確實存在)

這取決於您如何處理孩子身上的道具。

每次您更改(變異)道具時,這個孩子都會重新渲染。

const Child = (props) => {
    return <div>{ props.username }</div>;
};

由於返回值取決於本地 state,而不是道具,因此當道具更改時,此子項不會重新渲染。

const Child = (props) => {
    const [state, setState] = useState(props.username);
    return <div>{ state }</div>;
};

當道具更改時,這個孩子將重新渲染,因為本地 state 使用新道具更新。

const Child = (props) => {
    const [state, setState] = useState(props.username);

    useEffect(() => {
        // changing props changes the component's state, causing a re-render
        setState(props.username); 
    }, [props]);

    return <div>{ state }</div>;
};

從上面的例子中可以看出,程序員是控制 React 是否觸發子元素的重新渲染的人。

清單呢? 當子組件列表中間的條目被刪除時會發生什么? 對這樣的列表進行更改顯然會生成非常不同的 VDOM 節點,但組件的 state 仍然存在。

當涉及到子列表時(例如,當使用.map時)React 將需要key參數,以便 React 知道在父組件重新渲染之間添加/刪除/更改了什么。 React 要求對相同的組件使用相同的密鑰,以防止不必要的重新渲染(不要使用Math.random()作為密鑰)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM