其他章節請看:
生命周期&受控和非受控組件&Dom 元素&Diffing 算法
生命周期
首先回憶一下 vue 中的生命周期:
vue 對外提供了生命周期的鈎子函數,允許我們在 vue 的各個階段插入一些我們的邏輯,比如:created、mounted、beforeDestroy等。

react 中的生命周期是否也類似?請接着看:
每個組件都包含 “生命周期方法”,你可以重寫這些方法,以便於在運行過程中特定的階段執行這些方法 —— react 官網-組件的生命周期
請看一張 react 的生命周期圖譜:

從這張圖我們知道:
- 既然沒有勾選”展示不常用的生命周期“,這里顯示的 5 個方法就是常用的生命周期方法。
- 組件的生命周期可以分三個階段:掛載、更新、卸載
- 掛載時的順序是:
constructor()、render()、componentDidMount()
Tip:
- componentDidMount() 會在組件掛載后(插入 DOM 樹中)立即調用。常做定時器、網絡請求
componentDidUpdate()會在更新后會被立即調用。首次渲染不會執行此方法componentWillUnmount()會在組件卸載及銷毀之前直接調用。在此方法中執行必要的清理操作,例如,清除 timer,取消網絡請求或清除在componentDidMount()中創建的訂閱等
掛載和卸載
以 Clock 組件為例:
當 Clock 組件第一次被渲染到 DOM 中的時候,就為其設置一個計時器。這在 React 中被稱為“掛載(mount)”。
同時,當 DOM 中 Clock 組件被刪除的時候,應該清除計時器。這在 React 中被稱為“卸載(unmount)”。
請看實現:
class Clock extends React.Component {
state = { date: new Date() }
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
)
}
// 組件卸載前會被調用
componentWillUnmount() {
clearInterval(this.timerID) // {1}
}
tick() {
this.setState({
date: new Date()
});
}
handleUnmount = () => {
// 從 DOM 中卸載組件
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
<button onClick={this.handleUnmount}>卸載</button>
</div>
);
}
}
頁面顯示:
Hello, world!
It is 11:34:16.
卸載
時間每秒都會更新,點擊按鈕”卸載“,頁面將不再有任何信息,對應的 html 為 <div id="root"></div>
Tip:unmountComponentAtNode() 從 DOM 中卸載組件,會將其事件處理器(event handlers)和 state 一並清除。
注:倘若將 clearInterval(this.timerID)(行{1})注釋,點擊”卸載“將報錯如下:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
警告:無法對卸載的組件執行 React 狀態更新。 這是一個空操作,但它表明您的應用程序中存在內存泄漏。 要修復,請取消 componentWillUnmount 方法中的所有訂閱和異步任務。
不要將定時器放入 render()
倘若將上面例子中的定時器放在 render() 中。就像這樣:
render() {
console.log(1)
// 定時器
this.timerID = setInterval(
() => this.tick(),
1000
)
return (
// ...不變
);
}
之前 render() 每秒執行一次,現在很快就會執行過萬,因為每次執行都會生成一個定時器。
過時的生命周期方法
以下生命周期方法標記為“過時”。這些方法仍然有效,但不建議在新代碼中使用它們 —— 官網-過時的生命周期方法
-
componentWillMount,現在改名為UNSAFE_componentWillMount(),在掛載之前被調用 -
componentWillReceiveProps,現在改名為UNSAFE_componentWillReceiveProps(),在已掛載的組件接收新的 props 之前被調用。第一次傳的不算,以后傳的才算,有人說應該叫componentWillReceiveNewProps -
componentWillUpdate,現在改名為UNSAFE_componentWillUpdate(),當組件收到新的 props 或 state 時,會在渲染之前調用。
倘若用了重命名之前的方法,控制台會有詳細的警告信息。請看示例:
class Clock extends React.Component {
componentWillMount() {
}
UNSAFE_componentWillReceiveProps() {
}
}
控制台輸出:
Warning: componentWillMount has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.
* Move code with side effects to componentDidMount, and set initial state in the constructor.
* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.
Please update the following components: Clock
Tip:既然這幾個方法不建議使用,所以不打算深入研究
UNSAFE_ 不是指安全性
這里的 “unsafe” 不是指安全性,而是表示使用這些生命周期的代碼在 React 的未來版本中更有可能出現 bug,尤其是在啟用異步渲染之后 —— 官網-異步渲染之更新
shouldComponentUpdate
shouldComponentUpdate() 默認返回 true。用法如下:
class Clock extends React.Component {
state = { date: new Date() }
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
)
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
// 返回 false
shouldComponentUpdate() {
return false
}
}
Clock 的時間不會再變化。render() 方法也不會再執行。請看下圖:

調用 setState(),如果 shouldComponentUpdate() 返回 false 則中斷,不再執行 render()。
Tip:此方法僅作為性能優化的方式而存在。不要企圖依靠此方法來“阻止”渲染,因為這可能會產生 bug —— 官網-shouldComponentUpdate()
forceUpdate
根據上圖說明,調用 forceUpdate() 將致使組件調用 render() 方法,此操作會跳過該組件的 shouldComponentUpdate()。
通常應該避免使用 forceUpdate()
新增生命周期方法
相對舊的生命周期,新增如下兩個方法,但都屬於不常見的情形,所以不做詳細研究。
getDerivedStateFromProps
getDerivedStateFromProps() 會在調用 render 方法之前調用,並且在初始掛載及后續更新時都會被調用。它應返回一個對象來更新 state,如果返回 null 則不更新任何內容。
此方法適用於罕見的用例,即 state 的值在任何時候都取決於 props。
getDerivedStateFromProps 的存在只有一個目的:讓組件在 props 變化時更新 state —— 官網-什么時候使用派生 state
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate() 在最近一次渲染輸出(提交到 DOM 節點)之前調用。它使得組件能在發生更改之前從 DOM 中捕獲一些信息(例如,滾動位置)。
此用法並不常見,但它可能出現在 UI 處理中,如需要以特殊方式處理滾動位置的聊天線程等。
在函數組件中使用生命周期
我們可以在函數組件中使用 useEffect 來模擬常見的生命周期鈎子:componentDidMount()、componentDidUpdate()、componentWillUnmount()。
體驗 useEffect
首先我們運行一個例子:
function MyButton() {
const [count, setCount] = React.useState(0)
const add = () => {
setCount(count + 1)
}
const unMount = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
// React.useEffect() 將寫在此處 {1}
return (
<div>
<button onClick={add}>{count}</button> <button onClick={unMount}>卸載</button>
</div>
);
}
ReactDOM.render(
<MyButton />,
document.getElementById('root')
)
頁面顯示兩個按鈕:
0 卸載
第一個按鈕顯示一個數字,每點擊一次就會自增 1,點擊第二個按鈕,此組件就會被卸載。
我們接下來在行{1}處添加 React.useEffect() 相關代碼。請看示例:
// 相當於 componentDidMount()、componentDidUpdate()
React.useEffect(() => {
console.log('a')
})
頁面渲染后就會輸出 a,之后每點擊第一個按鈕都會輸出 a,點擊卸載沒有輸出。
可以給 useEffect 傳遞第二個參數,它是 effect 所依賴的值數組 —— 官網-effect 的條件執行
倘若給 useEffect 第二個參數傳遞一個空數組,表明沒有依賴值:
// 相當於 componentDidMount()
React.useEffect(() => {
console.log('a')
}, [])
頁面渲染后就會輸出 a,但點擊第一個按鈕就不會再有輸出。
通常,組件卸載時需要清除 effect 創建的諸如訂閱或計時器 ID 等資源。要實現這一點,useEffect 函數需返回一個清除函數 —— 官網-清除 effect
倘若給 useEffect 函數返回一個函數。請看示例:
React.useEffect(() => {
console.log('a')
return () => {
console.log('b')
}
}, [])
頁面渲染后就會輸出 a,但點擊第一個按鈕就不會再有輸出,點擊卸載輸出 b。
優化函數組件 Clock 中的定時器
在函數組件中使用 state中我們寫過這么一個例子:
function Clock() {
const [name] = React.useState('pjl')
const [date, setDate] = React.useState(new Date())
setInterval(() => {
console.log('setInterval')
setDate(new Date())
}, 1000)
return (
<div>
<h1>Hello, world! {name}</h1>
<h2>It is {date.toLocaleTimeString()}.</h2>
</div>
);
}
十秒就會輸出一千多次 setInterval。定時器應該只執行一次,放在 componentDidMount 生命鈎子中比較合適。以下是優化后的增強版:
function Clock() {
// console.log('Clock')
const [name] = React.useState('pjl')
const [date, setDate] = React.useState(new Date())
React.useEffect(() => {
console.log('useEffect')
const timerId = setInterval(() => {
// console.log('setInterval')
setDate(new Date())
}, 1000)
return () => {
clearInterval(timerId)
}
}, [name])
const unMount = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
return (
<div>
<h1>Hello, world! {name}</h1>
<h2>It is {date.toLocaleTimeString()}.</h2>
<button onClick={unMount}>卸載</button>
</div>
);
}
受控組件和非受控組件
在大多數情況下,我們推薦使用 受控組件 來處理表單數據。在一個受控組件中,表單數據是由 React 組件來管理的。另一種替代方案是使用非受控組件,這時表單數據將交由 DOM 節點來處理 —— 官網-非受控組件
這里我們能接收兩個信息:
- 推薦使用受控組件
- 受控組件和非受控組件的區別在於:表單數據由誰來處理 —— 是 react 組件管理,還是 dom 來處理。
受控組件
將表單寫為受控組件:
class NameForm extends React.Component {
state = { value: '' }
// 值若改變,則將其更新到 state 中
handleChange = event => {
this.setState({ value: event.target.value });
}
// 提交表單
handleSubmit = event => {
console.log('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
ReactDOM.render(
<NameForm />,
document.getElementById('root')
);
頁面顯示
名字:[ 輸入框 ] 提交
在輸入框中輸入”123“,點擊”提交“按鈕,控制台將輸出 ”提交的名字: 123“。
非受控組件
重寫 NameForm 組件,改為功能相同的非受控組件:
class NameForm extends React.Component {
input = React.createRef()
handleSubmit = event => {
console.log('提交的名字: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
勿過度使用 Refs —— 官網
Tip:倘若發生事件的元素,是你要操作的元素時,可以通過 event.target 取得 dom。
高階函數和函數柯里化優化受控組件
按照受控組件中的寫法,如果我們定義多個 input,我們就得寫多個 handleXxxx 處理方法。就像這樣:
class NameForm extends React.Component {
state = { name: '', age: '' }
// 2 個 input 對應 2 個處理方法
handleName = event => {
this.setState({ name: event.target.value });
}
handleAge = event => {
this.setState({ age: event.target.value });
}
handleSubmit = event => {
console.log({ name: this.state.name, age: this.state.age });
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.name} onChange={this.handleName} />
</label>
<label>
年齡:
<input type="text" value={this.state.age} onChange={this.handleAge} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
如果我們有10個,豈不是要寫10個處理方法!我們可以用高階函數和函數柯里化來對其優化。請看實現:
class NameForm extends React.Component {
state = { name: '', age: '' }
// saveFormField 既是`高階函數`,也使用了`函數柯里化`
saveFormField = (stateName) => {
return (event) => {
this.setState({ [stateName]: event.target.value }) // {1}
}
}
handleSubmit = event => {
console.log({ name: this.state.name, age: this.state.age });
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.name} onChange={this.saveFormField('name')} />
</label>
<label>
年齡:
<input type="text" value={this.state.age} onChange={this.saveFormField('age')} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
Tip:this.setState({ [stateName]: event.target.value }) 使用的語法是 可計算屬性名。
高階函數
高階函數是處理函數的函數,只要滿足其中一個條件即可:
- 參數是函數
- 返回函數
js 內置的高階函數有:Array.forEach、setInterval、Promise等。
函數柯里化
通過函數調用繼續返回函數,實現多次接收參數最后統一處理的函數編碼形式。
最二的一個示例是將:
function sum(a,b,c){
return a + b + c
}
改成 sum(1)(2)(3) 的形式。就像這樣:
const sum = (a) => {
return (b) => {
return (c) => {
return a + b + c
}
}
}
// 6
console.log(sum(1)(2)(3))
DOM 元素
React 實現了一套獨立於瀏覽器的 DOM 系統,兼顧了性能和跨瀏覽器的兼容性。我們借此機會完善了瀏覽器 DOM 實現的一些特殊情況 ——官網-DOM 元素。
在 React 中,所有的 DOM 特性和屬性(包括事件處理)都應該是小駝峰命名的方式。例如,與 HTML 中的 tabindex 屬性對應的 React 的屬性是 tabIndex。
注:例外的情況是 aria-* 以及 data-* 屬性,一律使用小寫字母命名。比如, 你依然可以用 aria-label 作為 aria-label。
React 與 HTML 之間有很多屬性存在差異,下面以 onChange 為例。
Tip:比如 react 中用 htmlFor 代替 for,其他更多介紹請看 DOM 元素。
onChange
onChange 事件與預期行為一致:每當表單字段變化時,該事件都會被觸發。我們故意沒有使用瀏覽器已有的默認行為,是因為 onChange 在瀏覽器中的行為和名稱不對應,並且 React 依靠了該事件實時處理用戶輸入 —— 官網-onChange
change 事件並不是每次元素的 value 改變時都會觸發 —— mdn-change 事件
原生 html 中 change 事件是這樣的:
<body>
名字:<input name="name" />
<script>
document.querySelector('input').
addEventListener('change', e => console.log(e.target.value))
</script>
</body>
在輸入框中輸入 123,點擊他處讓 input 失去焦點,控制台輸出 123。
在上面受控組件 NameForm 中增加一行:
class NameForm extends React.Component {
state = { value: '' }
handleChange = event => {
+ console.log(event.target.value)
this.setState({ value: event.target.value });
}
}
在輸入框中輸入 123,控制台依次輸出:
1
12
123
每當表單字段變化時,該事件都會被觸發。事件名和行為相對應。
Diffing 算法
根節點
當對比兩棵樹時,React 首先比較兩棵樹的根節點 —— 官網-Diffing 算法
對比不同類型的元素
當根節點為不同類型的元素時,React 會拆卸原有的樹並且建立起新的樹
舉個例子,當一個元素從 <a> 變成 <img>,從 <Article> 變成 <Comment>,或從 <Button> 變成 <div> 都會觸發一個完整的重建流程
當卸載一棵樹時,對應的 DOM 節點也會被銷毀。組件實例將執行 componentWillUnmount() 方法。
在根節點以下的組件也會被卸載,它們的狀態會被銷毀。比如,當比對以下更變時:
<div>
<Counter />
</div>
<span>
<Counter />
</span>
React 會銷毀 Counter 組件並且重新裝載一個新的組件。
對比同類型的元素
當對比兩個相同類型的 React 元素時,React 會保留 DOM 節點,僅比對及更新有改變的屬性
比如:
<div className="before" title="stuff" />
<div className="after" title="stuff" />
通過對比這兩個元素,React 知道只需要修改 DOM 元素上的 className 屬性。
在處理完當前節點之后,React 繼續對子節點進行遞歸。
對比同類型的組件元素
當一個組件更新時,組件實例會保持不變,因此可以在不同的渲染時保持 state 一致。React 將更新該組件實例的 props 以保證與最新的元素保持一致,並且調用該實例的 componentDidUpdate() 方法。
下一步,調用 render() 方法,diff 算法將在之前的結果以及新的結果中進行遞歸
對子節點進行遞歸
默認情況下,當遞歸 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表
在子元素列表末尾新增元素時,更新開銷比較小。比如:
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React 會先匹配兩個 <li>first</li> 對應的樹,然后匹配第二個元素 <li>second</li> 對應的樹,最后插入第三個元素的 <li>third</li> 樹。
如果只是簡單的將新增元素插入到表頭,那么更新開銷會比較大。比如:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React 並不會意識到應該保留 <li>Duke</li> 和 <li>Villanova</li>,而是會重建每一個子元素。這種情況會帶來性能問題。
Keys
為了解決上述問題(新增元素插入表頭開銷大),React 引入了 key 屬性。以下示例在新增 key 之后,使得樹的轉換效率得以提高:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
現在 React 知道只有帶着 '2014' key 的元素是新元素,帶着 '2015' 以及 '2016' key 的元素僅僅移動了。
正確使用 key
倘若用元素在數組中的下標作為 key,有時不僅會造成上面所說的性能問題,有時還會造成程序的錯誤。請看示例:
function Demo() {
const [todos, setTodos] = React.useState(['a', 'b'])
const unshift = () => {
setTodos([++seed, ...todos])
}
return (
<div>
<ul>
{
todos.map((item, index) => {
return <li key={index} data-index={index}> {item} <input type="text" /></li>
})
}
</ul>
<button onClick={unshift}>頭部插入</button>
</div>
)
}
頁面顯示:
a [ /* input 輸入框 */ ]
b [ /* input 輸入框 */ ]
頭部插入
在第一個輸入框中輸入 a,在第二個輸入框中輸入 b,然后點擊按鈕“頭部插入”,界面錯亂如下:
1 [a ]
a [b ]
b [ ]
頭部插入
倘若將 key 改成唯一值,使用相同的操作,界面就正常:
{
todos.map((item, index) => {
return <li key={item} data-index={index}> {item} <input type="text" /></li>
})
}
1 [ ]
a [a ]
b [b ]
頭部插入
在 Codepen 有兩個例子,分別為 展示使用下標作為 key 時導致的問題,以及不使用下標作為 key 的例子的版本,修復了重新排列,排序,以及在列表頭插入的問題 —— 官網-Keys
Tip:如果僅做簡單展示,用元素在數組中的下標作為 key 也是可以的。
其他章節請看:
