在上兩個章節:初探 State 與 Class Component State 語法中,我們了解了 state 的概念與用法,先來重新複習一下吧!
State 代表 React component 的狀態,會是一個完全由創建的 component 維護 / 控制的 JavaScript object,常用於存放 component 自己需要的資料。
操作 State 分為兩個階段:
constructor
中賦值,並將 this.state
賦值(assign)為任意的 JavaScript object 即可。setState
API。到目前為止,這個段落回顧了 state 的基本介紹與使用方式,接著就深入的抽絲剝繭吧!
接下來將會說明使用 state 的正確方式,有些內容在上一篇中已經介紹過,但在這段落中,我們將會補充更多的細節。
setState
修改 stateReact 的 state 必須要用 setState
這個 API 才可以修改。除了初始化以外,直接賦值與修改 state 是錯誤的行為,如下所示:
// Wrong
this.state.comment = 'Hello';
// Correct
this.setState({comment: 'Hello'});
也許會有讀者好奇為何 React 必須要呼叫 setState
才能以更新 state 的。這是因為 React 無法自動偵測 State object 何時被改動與賦值,因此才規定一定要使用 React 提供的 API 來觸發更新。
另一個需要注意的地方是,setState
可能會非同步的更新 state。這是因為 React 因為要增進效能,所以會把一段時間內的多個 setState
批次(Batch)執行。也就是,this.setState
後立即拿 this.state
取得的值有機會是舊的。
再繼續延伸下去,因為 state 有機會被當作子層 component 的 props 傳入(換句話說就是上層 component 的 state 可能會等於下層 component 的 props),因此相當於 props 的內容也是有機會被非同步更新的,這代表 this.props
也不能直接拿來 setState
。
這也是為何我們必須在需要當前 state 或 props 的值時使用 setState
updater
參數的原因:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment,
}));
最後,更新 state 時,新的 state object 中的屬性會與舊的 state object 的屬性淺層合併(Shallow merge),意思是只要把要更新的資料帶入新的 state object 就好,不需要把沒有更動的資料也一併帶入。舉例來說:
const fetchPosts = () => { /* ... */ };
const fetchComments = () => { /* ... */ };
class Articles extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
comments: [],
};
}
componentDidMount() {
fetchPosts().then((response) => {
this.setState({
posts: response.posts,
});
});
fetchComments().then((response) => {
this.setState({
comments: response.comments,
});
});
}
}
範例中,更新 this.setState({ posts })
時會把 posts
的內容合併為舊有 state object,而 comments
屬性則原封不動,反之亦然。
必須知道的是,由於 setState
只會淺層合併(Shallow merge),posts
內不會再進行合併,因此就算原本的 posts
內有內容也會被完全覆蓋為 response.posts
。
以上,我們已經學習完 state 的基本知識與正確用法了,是時候鑽進內部運作步驟段落了!
我們將以時鐘 component 的例子來分析 state 的內部運作。
首先,開發者編寫了一段 React 程式來實作一個小時鐘。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = { date: new Date() };
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState((state, props) => ({
date: new Date(state.date.getTime() + 1000)
}));
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
this.setState
中使用 new Date(state.date.getTime() + 1000)
而非 new Date()
只是為了方便說明機制而已。如果往後有做時鐘的需求,還是用 new Date()
來取得最正確的時間比較好。接著,在 bundle 時期 Babel 會先把 JSX 轉譯成 React.createElement()
。
ReactDOM.render(React.createElement(Clock, null), document.getElementById('root'));
執行階段,Clock
React element 創建出來後被傳進 ReactDOM.render()
執行。
React 接著呼叫 Clock
component 的 constructor
。
constructor
中會去初始化 this.state
的 date
內容為目前的時間,以讓 React 知道 Clock
要顯示的初始狀態為何,以及讓 component 知道之後的狀態要基於什麼資料更新。
constructor(props) {
super(props);
this.state = { date: new Date() };
}
Clock
初始化後,React 便會去執行 Clock
component 的 render
函式,建制出 <div>...</div>
React element。其中,this.state.date
的內容因為有被放在 expression 中,所以也會是 React element 的一部分。
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
React 會把 Virtual DOM snapshot 產出來,試圖跟上一個的 Virtual DOM snapshot 做比較,但因為這是第一次產出 Virtual DOM snapshot,並沒有上一刻 Virtual DOM snapshot 可以做比較,所以 React 就會把所有的內容都 render 到實際的畫面(DOM)上。
當 Clock
被成功渲染到 DOM 上之後,React 就會呼叫 lifecycle 方法:componentDidMount
,在這個函式中,Clock
component 會 setInterval
,讓程式每一秒都觸發一次 tick()
。
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
在每秒觸發一次的 tick()
中,Clock
component 會去執行 setState
。setState
中使用的是 function 參數,React 會將當前的 state 與 props 傳到這個 function 中,而 Clock
component 就會把 state 更新為 目前時間 + 1000 毫秒
(就相當於當前時間)作為新的狀態。
tick() {
this.setState((state, props) => ({
date: new Date(state.date.getTime() + 1000)
}));
}
React setState
後,知道了 state 已改變,因此重新觸發一次 render
。
同樣的,Virtual DOM snapshot 會被產生出來,並跟上一個的 Virtual DOM snapshot 做比較。
這次,React 發現 snapshot 中只有 h2
的內容:{this.state.date.toLocaleTimeString()}
改動了,因此就只將這部分的內容更新到 DOM 上。如下圖所示:
最後,如果某刻 Clock
component 要被移除時,React 就會呼叫 lifecycle 方法:componentWillUnmount
把之前設定的 setInterval
清掉。
componentWillUnmount() {
clearInterval(this.timerID);
}
有興趣的讀者也可以到 CodePen 中親自試試。
現在,對於 React 內部是怎麼處理 state 的已經有了進階的認識了,最後,就再近一步說明一些 state 的特性與要留心的地方。
State 是封裝在創建自己的 React component instance 中的。任何的其他 Element(不論是上層、下層、還是同層)都無權改變它也不需要知道它的存在。
為了證明 state 是封裝的,稍微修改一下 State 內部運作步驟 的時鐘範例。我們把三個 Clock
React component instance 包裹進新的 component App
中再交給 React render,來看看狀態是否為各個 React component instance 獨立所有吧:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
範例中創建了三個 Clock
的 React component instance,從畫面上可以看到,每個 Clock
component instance state 的累加都是獨立的,不會出現任何 Clock
element 的 state 一次被加了兩秒或三秒的問題,這也證明了 state 的獨立性與封裝性。
讀者也可以從 CodePen 查看結果。
因為 state 的封裝性,一個 component 也無法知道另一個 component 是有狀態的(Stateful)還是無狀態的(Stateless)。對於 React 來說,component 是否有 state 只是實作細節而已,這也代表一個 stateless 的 component 中可以有一個 stateful 的 component,反之亦然。
具下面的例子來說:
// Inside a class component
<FormattedDate date={this.state.date} />
<FormattedDate date={new Date()} />
// Will never know where the props come from, it just know the data just came.
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
FormattedDate
永遠不知道資料是來自 state 還是來自任意的變數,它只會知道有資料傳入了而已。
下層的 component 只單純接收上層 component 傳下去的值,且上層 component 的 state 也只能影響到它之下的 component。這種資料由上往下(top-down)的概念就像水流一樣,我們把它稱為單向資料流(Unidirectional data flow)。
可以想像 React 元件樹充滿了資料。資料是由樹的上方往下流入,每次多了一個 state 都像是多了一個額外的資料流分支,這個分支也會是樹的一部分,可以變成下層 component 的 props 繼續往下傳遞資料。
在 setState
執行後,React 會重新觸發該 component 的 render
函式以渲染出新的畫面,這代表每次的狀態改動都會重新執行 render
。
相較於其他框架都是在資料面上做變更偵測,React 則選擇做畫面的變更偵測(Virtual DOM)。這是因為 React 認為資料面的變更偵測遠比畫面層面的偵測來得複雜,使用偏向畫面向的偵測可以讓程式變得可測試,也能有效的減少 bug。更詳細的說明可參考 Peter Hunt 的演講。
不過每次都會觸發 render
也代表開發時需要更注意 setState
的次數以及 render
的效能。不當的操作 setState
或 render
將對效能有顯著的負面影響。
在這個章節中,我們學習到了正確使用 state 的方式,包括:
setState
修改 state另外,我們也學習了 state 的內部運作原理以及其特性:
下個章節中,我們將開始介紹 component 的 Lifecycle。
這邊是不是應該這樣
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment,
}));
感謝您的勘誤,已修正!