大家好,我是長林啊!一個 Go、Rust 愛好者,同時也是一名全棧開發者;致力於終生學習和技術分享。
本文首發於微信公眾號《全棧修煉之旅》,歡迎大家點贊、關注和分享!
在上一篇文章《走進 React:打造交互式用户界面的第一步》中,我們已經對 React 以及其環境搭建有了初步的瞭解;React是一個極其強大的JavaScript庫,專為構建用户界面而生。我們將從 React的基石——JSX 語法開始,探索 React 如何處理事件,然後深入研究 React 的狀態管理和生命週期方法。接着我們會進入 React 的一個新領域——Hooks,它為函數組件帶來了前所未有的能力。最後,我們會將所學知識實踐到一個最小的TODO應用上。
JSX
JSX 出現的原因
JSX 出現的主要原因是為了解決 React 中組件渲染的問題。在 React 中,用户界面是由組件構造的,而每個組件都可以看作是一個函數。這些組件或函數需要返回一些需要渲染的內容,而這些內容通常是 HTML 元素。
在早期的 JavaScript 中,如果要創建和操作HTML元素,需要使用一些相對較為複雜的 DOM API,這對開發者來説可能並不友好。而 JSX 就是一個 JavaScript 的語法擴展,它使得我們可以在 JavaScript 中直接寫HTML(或者説看起來很像HTML)的語法,極大地提高了開發效率,也使得代碼更加易讀和易維護。
因此,JSX 的出現使得 React 的組件化開發變得更加簡單和直觀。通過JSX,開發者可以更加專注於組件的邏輯,而不是DOM操作,從而提高開發效率。
React 的一大亮點就是虛擬 DOM:可以在內存中創建虛擬 DOM 元素,由虛擬 DOM 來確保只對界面上真正變化的部分進行實際的 DOM 操作。和真實 DOM 一樣,虛擬 DOM 也可以通過 JavaScript 來創建:
const ele = React.createElement('div', null, 'hello, world')
雖然通過以上的方式,就可以構建成 DOM 樹,但這種代碼可讀性比較差,於是就有了 JSX 。JSX 是 JavaScript 的語法擴展,使用 JSX ,就可以採用我們熟悉的 HTML 標籤形式來創建虛擬 DOM,也可以説 JSX 是 React.createElement 的一個語法糖。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>react</title>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<script type="text/babel">
const container = document.getElementById('app');
const root = ReactDOM.createRoot(container);
root.render(<h1>Hello, world</h1>);
</script>
</body>
</html>
什麼是JSX
JSX(JavaScript XML),即在 JavaScript 語言里加入類 XML 的語法擴展。在 React 中,JSX 是一種 JavaScript 的語法擴展。它看起來很像 HTML,允許你在 JavaScript 中直接寫 HTML 代碼。JSX 能提高代碼的可讀性,使得你的代碼更加直觀和易於維護。實際上,JSX 只是提供了一種創建 React 元素的語法糖,它最終會被轉換到普通的 JavaScript 函數調用和對象。因此,JSX 既是 React 的一個重要特性,也是編寫 React 應用的一種推薦方式。有不少初學者對 React 的第一印象就是 JSX 語法,以至於會有這樣的誤解:
- JSX 就是 React?
- JSX 就是 React 組件?
- JSX 就是另一種 HTML?
- JSX 既能聲明視圖,又能混入 JS 表達式,那是不是可以把所有邏輯都寫在 JSX 裏?
總的來説,React 是一套聲明式的、組件化的前端框架。顧名思義,聲明組件是 React 前端開發工作最重要的組成部分。在聲明組件的代碼中使用了 JSX 語法,JSX 並非 HTML,它也並不代表組件的全部內容。
如何使用JSX
-
JSX 標籤類型
// HTML 類型標籤 const ele = <div>hello, world</div> // react 組件類型標籤 const component = <HelloWrold />在 JSX 語法中,有兩種標籤類型:
- HTML 類型的標籤:這種標籤名需小寫字母開頭;
- 組件類型的標籤(在之後的小節會詳細介紹組件):這種標籤必須以大寫字母開頭。
React 通過標籤首字母的大小寫來區分渲染的是標籤類型。React 中的所有標籤,都必須有閉合標籤
/> -
JSX 中使用 JavaScript 表達式
在 JSX 中,也可以使用 JavaScript 表達式,只需要使用{}將表達式包裹起來就行。通常給標籤屬性傳值或通過表達式定義子組件時會用到。例如下面代碼:// 屬性傳值 const addUser = {id: 1, name: '添加好友'} <PanelItem item={addUser} />// 通過表達式定義子組件 const teams = [ {id: 1, name: '創建高級羣'}, {id: 2, name: '搜索高級羣'} ] <ul> {teams.map(item => <PanelItem item={item} key={item.id}/> )} </ul>JSX 中使用 JavaScript 表達式時,不能使用多行 JavaScript 語句。
JSX 的規則
- 只能返回一個根元素
- 標籤必須閉合
- 自定義 React 組件時,組件本身採用的變量名或者函數名,需要以大寫字母開頭。
- 在 JSX 中編寫標籤時,HTML 元素名稱均為小寫字母,自定義組件首字母務必大寫。
props屬性名稱,在 React 中使用駝峯命名(camelCase),且區分大小寫。
JSX 元素類型
SX 產生的每個節點都稱作 React 元素,它是 React 應用的最小單元;React 元素有四種基本類型:
- React 封裝的 DOM 元素,如
<div></div>、<img />等等元素會最終被渲染為真實的 DOM; - React 組件渲染的元素,如
<App />,這部分元素會調用對應組件的渲染方法; - React Fragment 元素,
<React.Fragment></React.Fragment>或者簡寫成<></>,這一元素沒有業務意義,也不會產生額外的 DOM,主要用來將多個子元素分組。 - React 中內置的一些有實際作用的組件:
<Suspense></Suspense>、<Profiler></Profiler>、<StrictMode></StrictMode>等,他們不會將其直接渲染在 DOM 中。
JSX 的屬性設置
React 對 DOM 元素的封裝實際上是對整個瀏覽器 DOM 的一次 React 化標準化。例如,HTML 中容易引發混淆的 readonly="true",其W3C標準應為 readonly="readonly",而常被誤用的 readonly="false" 實際上沒有效果。但在 React 的 JSX 中,這就統一為 readOnly={true} 或 readOnly={false},這更接近JS的開發習慣。而對於樣式中的 className="container",主要是因為 HTML 標籤中的 class 是 JS 的保留字,所以需要避免使用。
在 React 組件渲染的元素中,JSX 的 props 應與自定義組件定義中的 props 相對應;如果沒有特殊處理,那些沒有對應 props 的元素會被忽略。這也是開發 JSX 時常會遇到的一個錯誤,那就是在組件定義中更改了 props 的屬性名,但忘記了更改對應的 JSX 元素中的 props,導致子組件無法獲取屬性值。對於 Fragment 元素,它是沒有 props 的。
JSX 子元素類型
JSX元素可以定義子元素。這裏有一個重要的概念要理解:並非所有子元素都是子組件,但所有子組件一定都是子元素。
子元素的類型包括:
- 字符串,最終會被渲染成 HTML 標籤裏的字符串;
- 另一段 JSX,會嵌套渲染;
- JS 表達式,會在渲染過程中執行,並讓返回值參與到渲染過程中;
- 布爾值、
null值、undefined值,不會被渲染出來; - 以上各種類型組成的數組。
-
字符串:最終會被渲染成 HTML 標籤裏的字符串。例如:
<div>Hello World!</div> -
另一段JSX:會嵌套渲染。例如:
<div><p>Hello World!</p></div> -
JS 表達式:會在渲染過程中執行,並讓返回值參與到渲染過程中。例如:
<div>{1+2}</div> // 渲染結果為:3 -
布爾值、
null值、undefined值:這些值在 JSX 中不會被渲染出來。例如:<div>{null}</div> // 不會渲染任何內容 <div>{undefined}</div> // 不會渲染任何內容 <div>{false}</div> // 不會渲染任何內容 -
以上各種類型組成的數組:例如:
<div> {['Hello', <p>World</p>, 1+2, null, undefined, false]} </div>以上代碼會渲染出
"Hello",一個包含"World"的段落元素,以及數字3。null、undefined和false不會被渲染。
JSX 中的 JS 表達式
在JSX中,我們可以嵌入JavaScript表達式,這些表達式被大括號 { } 包圍。這主要在兩個方面被應用:
-
作為屬性(Props)的值,也就是緊跟在
=覆符號後的屬性。let myClass = "my-css-class"; <div className={myClass}></div>在這個例子中,我們定義了一個變量myClass,並用大括號把它作為className屬性的值。
-
作為JSX元素的子元素,比如標籤內的文本或者JS表達式結果。
let text = "Hello, JSX!"; <div>{text}</div>在這個例子中,我們定義了一個變量text,並用大括號把它作為div元素的子元素。
JSX是聲明性的,因此其內部不應包含命令式的語句,例如 if ... else ...。當你不確定JSX { } 裏的代碼是否是表達式時,你可以嘗試將這部分代碼直接賦值給一個JS變量。如果賦值成功,那麼它就是一個表達式;如果賦值失敗,那麼你可以從以下四個方面進行檢查:
- 是否有語法錯誤。
- 是否使用了for...of的聲明式變體array.forEach ,這個中招機率比較高。
- 是否沒有返回值。
- 是否有返回值,但不符合 props 或者子元素的要求。
有個 props 表達式的特殊用法:展開語法,<Button {...defaultProps}> 利用的 JavaScript 中的展開語法把 defaultProps 這個對象的所有屬性都傳給 Button 這個組件。
JSX 中使用註釋
如果你嘗試在JSX中使用HTML的註釋方法,你會發現它無法通過編譯。因此,你需要使用 {/ 這是註釋 /} 的格式來添加註釋。在編譯過程中,這種格式的註釋會自動被識別為JS註釋。
const App = () => {
const handleClick = () => {
console.log('click');
};
return (
<div>
{/* 這個是註釋 */}
<button onClick={handleClick}>click me</button>
</div>
);
};
export default App;
React 中的組件
在我們已經初步理解了JSX的基礎上,接下來我們將探討什麼是組件,以及JSX與React組件的關係是什麼。
組件化開發現已經成為前端開發的主流方法,幾乎所有的前端框架都包含了組件的概念。在一些框架中,它被稱為"Component",而在其他一些中則被稱為"Widget"。然而在React中,組件被視為前端應用的核心。
什麼是 react 組件
組件是對視圖以及與視圖相關的邏輯、數據、交互等的封裝。如果沒有組件這層封裝,這些代碼將有可能四散在各個地方,低內聚,也不一定能低耦合,這種代碼往往難寫、難讀、難維護、難擴展。
React 組件層次結構從一個根部組件開始,一層層加入子組件,最終形成一棵組件樹。
這棵樹由節點組成,每個節點代表一個組件。例如,App、FancyText、Copyright 等都是樹中的節點。
在 React 渲染樹中,根節點是應用程序的 根組件。在這種情況下,根組件是 App,它是 React 渲染的第一個組件。樹中的每個箭頭從父組件指向子組件。
JSX 與 React 組件的關係
JSX就是React組件的語法糖,它讓我們可以使用類似於HTML的語法來定義React組件。在React中,我們通常使用JSX來描述組件的UI結構。當我們編寫JSX代碼時,實際上我們是在定義React組件的渲染輸出。
例如,我們可以定義一個名為"HelloWorld"的React組件,使用JSX來描述它的UI:
function HelloWorld() {
return <h1>Hello, world!</h1>;
}
在上述代碼中,<h1>Hello, world!</h1> 就是 JSX。當 React 渲染這個HelloWorld 組件時,它會將 JSX 轉換為相應的 HTML,然後將其插入到 DOM 中。
組件的類型
組件化開發已經成為前端開發的主流趨勢,市面上大部分前端框架都包含組件概念,有些框架裏叫 Component,有些叫 Widget。在React框架中,主要有兩種類型的組件:類組件和函數組件。類組件通常用於需要內部狀態或生命週期方法的複雜情況,而函數組件則適用於無狀態的、更簡單的情況。但是從React 16.8版本開始,藉助React Hooks,函數組件也可以擁有狀態和生命週期方法。
每種組件類型都有其優勢和適用場景,理解它們的作用和差異是成為一名高效的開發者的關鍵
類組件
在React中,類組件是一種可以包含狀態和生命週期方法的組件類型。類組件是ES6的類,它們繼承自 React.Component 或 React.PureComponent。
要定義一個 React 類組件,你需要擴展內置的 Component 類並定義一個 render() 方法。React 會在需要確定屏幕上顯示什麼內容時調用你的 render 方法。
例如:
import { Component } from 'react';
//類組件是由繼承與React的Component基類構建
class Greeting extends Component {
render() {
return <h1>Greeting, friend! How are you today?</h1>;
}
}
類組件在定義是,同樣可以使用屬性:
import { Component } from 'react';
class Greeting extends Component {
render() {
return <h1>Greeting, {this.props.name}!</h1>;
}
}
在類組件中,通過 this 對象訪問其自身 props 屬性對象。
通過類組件構造 React 元素時,也可以為其指定屬性賦值:
function App() {
return (
<div className="App">
<header className="App-header">
<p>hello world!!!</p>
</header>
<Greeting name="Newton" />
</div>
);
}
完整代碼如下(github 中查看源碼):
import { Component } from 'react';
import './App.css';
class Greeting extends Component {
render() {
return <h1>Greeting, {this.props.name}!</h1>;
}
}
function App() {
return (
<div className="App">
<header className="App-header">
<p>hello world!!!</p>
</header>
<Greeting name="Newton" />
</div>
);
}
export default App;
運行效果如下:
類組件還可以跟蹤它們的狀態(state),並使用狀態更新來觸發重新渲染。這使得類組件非常適合用於需要內部狀態管理的複雜組件。
函數組件
將 UI 拆分成獨立的、可複用的代碼片段,並對每個代碼片段進行單獨處理。在 React 中,有兩類常用的組件:函數組件(也叫無狀態組件)和類組件(也叫 class 組件);然而,目前 React 官方以及社區的發展趨勢,已經開始更多地推薦和支持使用函數組件,而不是類組件。因此,我們接下來的學習和探索,將主要圍繞函數組件進行。
React 組件是一段可以使用標籤進行擴展 的 JavaScript 函數。如下所示(你可以編輯下面的示例):
function Profile() {
return (
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/GodfreyKneller-IsaacNewton-1689.jpg/250px-GodfreyKneller-IsaacNewton-1689.jpg"
alt="Katherine Johnson"
/>
);
}
完整代碼如下(github 中查看源碼):
import { Component } from 'react';
import './App.css';
class Greeting extends Component {
render() {
return <h1>Greeting, {this.props.name}!</h1>;
}
}
function Profile() {
return (
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/GodfreyKneller-IsaacNewton-1689.jpg/250px-GodfreyKneller-IsaacNewton-1689.jpg"
alt="Katherine Johnson"
/>
);
}
function App() {
return (
<div className="App">
<header className="App-header">
<p>hello world!!!</p>
</header>
<Greeting name="Newton" />
<Profile />
</div>
);
}
export default App;
效果圖如下:
函數組件在某些方面可以替代類組件,它們的語法更為簡潔明瞭。然而,函數組件面臨着兩個主要的挑戰:
- 缺乏內部狀態(state)
- 缺乏生命週期方法
這兩個問題在某些情況下可能會限制函數組件的使用。但值得注意的是,自從 React 16.8 引入 Hooks 功能後,函數組件現在也可以擁有狀態和生命週期方法,這大大增強了函數組件的功能性和靈活性。
state 與 props
在上述兩個例子中,我們都提到了狀態(state)和 props,並且在類組件中我們還使用了 props;那究竟什麼是state和props呢?
props
React 組件使用 props 相互通信。props 是父組件向子組件傳遞數據的方式。無論是函數組件還是類組件,都可以接收 props。props 是隻讀的,也就是説:子組件不能修改父組件傳遞過來的 props。props 可能會讓您想起 HTML 屬性,可以傳遞任何 JavaScript 值,包括對象、數組和函數。
將 props 傳遞給組件
在下面這段代碼中,Profile 組件沒有向其子組件 Avatar 傳遞任何參數:
function Avatar() {
return (
<img
className="avatar"
src="https://i.imgur.com/1bX5QH6.jpg"
alt="Lin Lanying"
width={100}
height={100}
/>
);
}
export default function Profile() {
return <Avatar />;
}
類組件寫法如下:
class Avatar extends Component {
render() {
return (
<img
className="avatar"
src="https://i.imgur.com/1bX5QH6.jpg"
alt="Lin Lanying"
width={100}
/>
);
}
}
class Profile extends Component {
render() {
return (<Avatar />);
}
}
如果要給 Avatar 組件添加參數,可以經過下面的流程:
-
將 props 傳遞給子組件
可以給
Avatar組件傳遞兩個 props,一個person和size:export default function Profile() { return ( <Avatar person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }} size={100} /> ); }類組件的寫法就是:
class Profile extends Component { render() { return <Avatar person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }} />; } } -
讀取子組件內部的 props
function Avatar({ person, size }) { return ( <img className="avatar" src={getImageUrl(person)} alt={person.name} width={size} height={size} /> ); } export default function Profile() { return ( <div> <Avatar size={100} person={{ name: 'Katsuko Saruhashi', imageId: 'YfeOqp2', }} /> <Avatar size={80} person={{ name: 'Aklilu Lemma', imageId: 'OKS67lh', }} /> <Avatar size={50} person={{ name: 'Lin Lanying', imageId: '1bX5QH6', }} /> </div> ); } function getImageUrl(person, size = 's') { return 'https://i.imgur.com/' + person.imageId + size + '.jpg'; }類組件的寫法:
import React, { Component } from 'react'; class Avatar extends Component { render() { const { person, size } = this.props; return ( <img className="avatar" src={getImageUrl(person)} alt={person.name} width={size} /> ); } } export default class Profile extends Component { render() { return ( <> <Avatar size={100} person={{ name: 'Katsuko Saruhashi', imageId: 'YfeOqp2', }} /> <Avatar size={80} person={{ name: 'Aklilu Lemma', imageId: 'OKS67lh', }} /> <Avatar size={50} person={{ name: 'Lin Lanying', imageId: '1bX5QH6', }} /> </> ); } } function getImageUrl(person, size = 's') { return 'https://i.imgur.com/' + person.imageId + size + '.jpg'; }效果圖如下:
完整代碼可訪問:github 中查看源碼,函數組件的源文件、類組件的源文件。
還可以給 prop 指定默認值
```jsx
function Avatar({ person, size = 100 }) {
// ...
}
```
如果 <Avatar person={...} /> 渲染時沒有 `size` prop,`size` 將被賦值為 100。
默認值僅在缺少 `size` prop 或 `size={undefined}` 時生效。 但是**如果你傳遞了 `size={null}` 或 `size={0}`,默認值將 不 被使用**。
將 JSX 作為子組件傳遞(組件嵌套)
嵌套瀏覽器內置標籤是很常見的:
<main>
<header>
<nav></nav>
</header>
<aside>
<section></section>
</aside>
<article>
<section></section>
</article>
<footer></footer>
</main>
有時你會希望以相同的方式嵌套自己的組件:
<Card>
<Avatar />
</Card>
當您將內容嵌套在 JSX 標籤中時,父組件將在名為 children 的 props 中接收到該內容。例如,下面的 Card 組件將接收一個被設為 <Avatar /> 的 children prop 並將其包裹在 div 中渲染(github 中查看源碼):
function Card({ children }) {
return <div className="card">{children}</div>;
}
function Avatar({ person, size }) {
return (
<img
className="avatar"
src={getImageUrl(person)}
alt={person.name}
width={size}
height={size}
/>
);
}
export default function Profile() {
return (
<Card>
<Avatar
size={100}
person={{
name: 'Katsuko Saruhashi',
imageId: 'YfeOqp2',
}}
/>
</Card>
);
}
function getImageUrl(person, size = 's') {
return 'https://i.imgur.com/' + person.imageId + size + '.jpg';
}
效果如下圖:
state
在 React 中,state 是組件內部管理和存儲數據的一種機制。理解 React 中的 state 非常重要,因為它決定了組件的狀態和行為,直接影響到組件的渲染和交互。
state 是一個 JavaScript 對象,用於存儲組件的內部數據;每個組件可以有自己的 state,用來描述組件當前的狀態。
作用:
- 狀態管理:通過 state 可以跟蹤和管理組件的變化和交互。
- 數據驅動渲染:當 state 發生變化時,React 會重新渲染組件,以反映最新的狀態。
使用場景:
- 存儲和更新組件的動態數據。
- 控制組件的行為和外觀。
- 響應用户輸入和事件。
如何使用 State
初始化 State
-
在類組件中,通過構造函數初始化 state。
class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0, name: 'John', }; } // ... } -
在函數式組件中,使用 useState hook 來初始化 state。
function MyComponent() { const [count, setCount] = useState(0); const [name, setName] = useState('John'); // ... }訪問 State
-
在類組件中,使用 this.state.propertyName 訪問 state 中的屬性。
render() { return <p>Hello, {this.state.name}!</p>; } -
在函數式組件中,直接使用 state 變量。
return <p>Hello, {name}!</p>;更新 State
-
在 React 中,不直接修改 state。而是使用 setState 方法來更新 state。
this.setState({ count: this.state.count + 1 }); -
在函數式組件中,使用 useState hook 返回的更新函數。
setCount(count + 1);異步更新
-
setState是異步的,因此 React 可以批量更新狀態以提高性能。如果需要基於先前的 state 更新,可以使用函數形式的setState。this.setState(prevState => ({ count: prevState.count + 1 }));完整代碼如下:
-
類組件
class MyComponent extends React.Component { constructor(props) { super(props); } state = { count: 0, name: 'John', }; render() { return ( <div> <p>{this.state.count}</p> <p>{this.state.name}</p> </div> ); } } -
函數組件
function MyComponent() { const [count, setCount] = useState(0); const [name, setName] = useState('John'); return ( <div> <p>{count}</p> <p>{name}</p> </div> ); }
生命週期
通過上面的例子,我們知道 React 會把狀態的變更更新到 UI,然後頁面顯示的內容更新,狀態的變更過程必然會經歷組件的生命週期。首先要知道所謂生命週期,就是組件從開始生成到最後消亡的過程, React 通常將組件生命週期分為三個階段:裝載、更新和卸載,我們怎麼能確定組件進入到了哪個階段呢?通過 React 組件暴露給我們的鈎子函數就可以知曉。接下來我們將一起學習 React 組件的生命週期。
16.3 版本之前:
16.3 版本:
16.4 及之後:
通過上面的圖片,我們可以看到 getDerivedStateFromProps 在 React v16.4 中有一定的改動,這個函數會在每次 render 之前被調用,也就意味着即使你的 props 沒有任何變化,由父組件的 state 的改動導致的 render,這個生命週期依然會被調用,使用的時候需要注意。
根據上面的圖片可以看出,在 React v16.4 中,getDerivedStateFromProps 方法有了一些改動。現在,這個生命週期方法在每次組件即將渲染之前都會被調用,不再只在接收新 props 時觸發。這意味着,即使組件的 props 沒有實際變化,只要父組件的 state 發生改變導致重新渲染,這個生命週期方法也會被執行。因此,在使用時需要特別注意這一點。
掛載階段
掛載階段組件被創建,然後組件實例插入到 DOM 中,完成組件的第一次渲染,該過程只會發生一次,在此階段會依次調用以下這些方法:
constructorgetDerivedStateFromPropsrendercomponentDidMount
constructor
組件的構造函數是第一個被執行的部分。如果我們顯式定義了構造函數,就必須在其中調用 super(props),這樣才能確保在構造函數內部正確獲取到 this。這涉及到了 ES6 類的繼承機制,詳細內容可以參考阮一峯的《ECMAScript 6 入門》。
在構造函數裏一般會做兩件事:
- 初始化組件的
state -
給事件處理方法綁定
thisconstructor(props) { super(props); // 不要在構造函數中調用 setState,可以直接給 state 設置初始值 this.state = { counter: 0 }; this.handleClick = this.handleClick.bind(this); }
getDerivedStateFromProps
這是一個靜態方法,因此不能在其內部使用 this。它接收兩個參數:
props接收到的新屬性state當前組件的狀態對象
該方法應返回一個對象,用於更新當前的狀態對象;如果不需要更新,則返回 null。這個方法會在組件掛載時或者接收到新的 props、或調用了 setState 和 forceUpdate 時被調用。例如,當我們接收到新的屬性並希望更新狀態時,可以在此方法內進行處理。
// 當 props.counter 變化時,賦值給 state
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
};
}
static getDerivedStateFromProps(props, state) {
// 如果 props.counter 變化了,那麼就返回新的 state
if (props.counter !== state.counter) {
return {
counter: props.counter,
};
}
return null;
}
handleClick = () => {
this.setState({
count: this.state.count + 1,
});
};
render() {
return (
<div>
<p>Count: {count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
現在我們可以顯式傳入 counter,但出現了一個小問題:如果我們希望通過點擊事件來增加 state.counter 的值,會發現它始終保持着 props 傳入的初始值,沒有發生任何變化。這是因為在 React 16.4 及更高版本中,setState 和 forceUpdate 也會觸發 getDerivedStateFromProps 生命週期方法。因此,當組件內部的狀態發生變化時,會再次調用該方法,並將狀態值重置為 props 的值。為了解決這個問題,我們需要在 state 中添加一個額外的字段來記錄之前的 props 值。
import React from 'react';
export default class GetDerivedStateFromProps extends React.Component {
constructor(props) {
super(props);
this.state = {
// 增加一個 preCounter 來記錄之前的 props 傳來的值
preCounter: 0,
counter: 0,
};
}
static getDerivedStateFromProps(props, state) {
// 如果 props.counter 變化了,那麼就返回新的 state
if (props.counter !== state.preCounter) {
return {
counter: props.counter,
preCounter: props.preCounter,
};
}
return null;
}
handleClick = () => {
this.setState({
count: this.state.count + 1,
});
};
render() {
return (
<div>
<p>Count: {count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
效果如下:
上面示例完整代碼請訪問 github 的 getDerivedStateFromProps 實踐 github lifecycle
render
React 中最核心的方法是 render 方法。一個 React 組件必須包含 render 方法,它根據組件的狀態 state 和屬性 props 來決定返回什麼內容,從而渲染組件到頁面上。
在 render 方法中,通常會返回以下類型中的一個:
- React 元素:包括原生的 DOM 元素或者其他 React 組件。
- 數組和 Fragment(片段):可以返回多個元素作為一個整體。
- Portals(插槽):可以將子元素渲染到不同的 DOM 子樹中。
- 字符串和數字:會被渲染成 DOM 中的文本節點。
- 布爾值或
null:表示不渲染任何內容。
例如:
import React, { Component } from 'react';
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
const { count } = this.state;
return (
<div>
<p>Count: {count}</p>
<button onClick={() => this.setState({ count: count + 1 })}>increment</button>
</div>
);
}
}
export default MyComponent;
componentDidMount
在組件掛載後調用 componentDidMount 方法時,我們可以獲取到 DOM 節點並進行操作,例如對 canvas、svg 進行繪製,或者發起服務器請求等操作。
然而,需要注意的是,在 componentDidMount 中調用 setState 會觸發一次額外的渲染過程,導致多一次 render 方法的執行。儘管這次渲染是在瀏覽器刷新屏幕前進行的,用户通常不會察覺到,但在開發過程中,應儘量避免這種做法以避免潛在的性能問題。為了優化性能,我們應該儘早在 constructor 中初始化組件的 state 對象,而不是在 componentDidMount 中進行狀態的初始化操作。
在組件掛載之後,將計數數字變為10。
import React from 'react';
export default class ComponentDidMount extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
};
}
componentDidMount() {
this.setState({
counter: 10,
});
}
render() {
return <div className="counter">counter值: {this.state.counter}</div>;
}
}
可以看到 counter 的值變為了 10:
更新階段
當組件的 props 改變了,或者組件內部調用了 setState 或 forceUpdate 方法,都會觸發更新和重新渲染的過程。在這個階段,React 組件會按照以下順序依次調用這些方法:
static getDerivedStateFromProps(nextProps, prevState): 當 props 發生變化時調用,用於根據新的 props 更新組件的狀態。這個靜態方法返回一個對象來更新狀態,或者返回null表示不需要更新狀態。shouldComponentUpdate(nextProps, nextState): 在重新渲染之前調用,用於判斷是否需要重新渲染組件。默認返回true,可以根據新的 props 和 state 來進行優化判斷,避免不必要的渲染。render(): 根據最新的 props 和 state 返回需要渲染的 React 元素、數組、Fragment、Portals、字符串、數字或null。getSnapshotBeforeUpdate(prevProps, prevState): 在最終渲染之前調用,用於獲取更新前的 DOM 狀態。它的返回值將作為componentDidUpdate方法的第三個參數傳遞給後者,常用於處理 DOM 更新前後的差異。componentDidUpdate(prevProps, prevState, snapshot): 在組件更新完成後調用,可以執行與更新後的 DOM 交互的操作。通常用於處理網絡請求、手動操作 DOM 或者更新狀態的邏輯。
這些方法協同工作,確保 React 組件能夠響應外部變化,並及時更新用户界面。
getDerivedStateFromProps
這個方法在掛載階段已經説過了,這裏不再贅述,記住在更新階段,無論接收到新的 props,還是調用了 setState 或者 forceUpdate,這個方法都會被觸發。
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
在講這個生命週期函數之前,我們先來探討兩個問題:
-
setState 函數在任何情況下都會導致組件重新渲染嗎?例如下面這種情況:
this.setState({number: this.state.number}) - 如果沒有調用 setState,props 值也沒有變化,是不是組件就不會重新渲染?
我們先探討上面兩個問題:
第一個問題:當 setState 被調用時,React 會更新組件的狀態並重新渲染組件。setState 會合並新的狀態對象到當前狀態,然後觸發 render 方法。 這是 React 的默認行為,用於確保組件反映最新的狀態和 props。
當然也有特殊情況,如果在 setState 中更新的狀態與當前狀態相同,React 可能會跳過重新渲染。 在這種情況下,setState 被調用了,但是狀態對象 { number: this.state.number } 與之前的狀態相同。React 進行狀態合併後,發現新的狀態和之前的狀態沒有變化,默認情況下,React 會優化跳過 render 調用,避免不必要的渲染。這種優化有助於提升性能,避免不必要的計算和 DOM 操作。
第二個問題:如果是父組件重新渲染時,不管傳入的 props 有沒有變化,都會引起子組件的重新渲染。那麼有沒有什麼方法解決在這兩個場景下不讓組件重新渲染進而提升性能呢?
React 通過以下機制來優化和控制組件的重新渲染:
-
淺比較(Shallow Compare):
React 在內部會對新的狀態和舊的狀態進行淺比較。只有當比較結果不同時,React 才會決定更新組件。因此,如果傳遞給 setState 的對象和當前狀態對象是相同的,React 就會跳過渲染。
-
shouldComponentUpdate 方法:
在類組件中,你可以通過覆蓋
shouldComponentUpdate方法來自定義渲染行為。,這個生命週期函數是用來提升速度的,它是在重新渲染組件開始前觸發的,默認返回true,我們可以比較this.props和nextProps,this.state和nextState值是否變化,來確認返回true或者false。當返回false時,組件的更新過程停止,後續的render、componentDidUpdate也不會被調用。import React, { Component } from 'react'; class ShallowCompare extends Component { constructor(props) { super(props); this.state = { number: 1, }; } updateNumber = () => { this.setState({ number: this.state.number }, () => { console.log('updated state:', this.state); }); }; componentDidMount() { console.log('updated number'); } shouldComponentUpdate(nextProps, nextState) { // 僅當 state 中的 number 改變時才重新渲染組件 return nextState.number !== this.state.number; } render() { console.log('render called'); return ( <div> <p>Number: {this.state.number}</p> <button onClick={this.updateNumber}>Update Number</button> </div> ); } } export default ShallowCompare;上面代碼中,每次執行
render和componentDidMount中都打印了一句調試代碼,但是從執行結果來看,每次setState 執行了,但是render和componentDidMount中的打印都沒有執行,效果如下圖:
-
React.PureComponent:
React 提供了
PureComponent,它是Component的一個替代品,並且自動實現了shouldComponentUpdate,對props和state進行淺比較。如果你的組件只依賴於簡單的狀態和props,這可以減少渲染次數,提高性能。import React from 'react'; class PureComponent extends React.PureComponent { constructor(props) { super(props); this.state = { number: 1, }; } updateNumber = () => { this.setState({ number: this.state.number }, () => console.log('set stated number') ); }; render() { console.log('render called'); return ( <div> <p>Number: {this.state.number}</p> <button onClick={this.updateNumber}>Update Number</button> </div> ); } } export default PureComponent;同樣是在
setState和render中打印了調試代碼,然後點擊按鈕後,每次都執行了setState,但是render卻沒有重新渲染,效果圖如下:
-
函數組件和 React.memo
對於函數組件,React 提供了React.memo來進行類似PureComponent的優化。React.memo可以避免函數組件在接收到相同的 props 時重新渲染:import React from 'react'; const Memo = React.memo(() => { const [number, setNumber] = React.useState(1); const updateNumber = () => { setNumber(prevNumber => prevNumber); // 不會觸發重新渲染 }; console.log('render called'); return ( <div> <p>Number: {number}</p> <button onClick={updateNumber}>Update Number</button> </div> ); }); export default Memo;上面代碼中用到了 Hooks,後面的內容會介紹到。
render
同樣更新階段也會觸發該方法,掛載階段已經介紹過,不再重複介紹。
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
這個方法在 render 之後,componentDidUpdate 之前調用,有兩個參數 prevProps 和 prevState,表示更新之前的 props 和 state,這個函數必須要和 componentDidUpdate 一起使用,並且要有一個返回值,默認是 null,這個返回值作為第三個參數傳給 componentDidUpdate。
典型用途
生命週期 getSnapshotBeforeUpdate 通常用於需要在 DOM 更新前獲取一些信息的場景,特別是當你需要在更新前後對比某些狀態時。常見的場景包括:
- 保持滾動位置:在更新內容之前獲取當前的滾動位置,確保內容更新後可以恢復到合適的滾動位置。
- 動畫同步:在組件更新前獲取動畫的狀態或某些元素的位置,以便在
componentDidUpdate中對它們進行調整。 - 測量或計算:需要在 DOM 更新前測量某些元素的尺寸、位置等,以便在
componentDidUpdate中根據這些信息做進一步處理。
以下是一個使用 getSnapshotBeforeUpdate 來保持滾動位置的簡單示例:
import React, { Component } from 'react';
class GetSnapshotBeforeUpdate extends Component {
constructor(props) {
super(props);
// 創建一個引用來訪問 DOM 元素
this.chatContainerRef = React.createRef();
this.state = {
// 初始狀態為空消息列表
messages: [],
};
}
// 假設消息通過某些外部操作或 props 添加到組件中
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
// 使用快照值恢復滾動位置
this.chatContainerRef.current.scrollTop =
this.chatContainerRef.current.scrollHeight - snapshot;
}
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 只有在消息有變化時才獲取快照
if (prevState.messages.length < this.state.messages.length) {
const chatContainer = this.chatContainerRef.current;
// 計算從底部開始的滾動位置
return chatContainer.scrollHeight - chatContainer.scrollTop;
}
return null; // 如果沒有新的消息,返回 null
}
render() {
return (
<div
ref={this.chatContainerRef}
style={{ height: '300px', overflowY: 'auto' }}>
{this.state.messages.map((msg, index) => (
<div key={index}>{msg}</div>
))}
</div>
);
}
}
export default GetSnapshotBeforeUpdate;
上面的代碼就實現了在消息列表更新時,保持聊天窗口的滾動位置的功能。
componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)
生命週期 componentDidUpdate 方法在 getSnapshotBeforeUpdate 方法之後被調用,它接收三個參數:更新前的 props、更新前的 state,以及 getSnapshotBeforeUpdate 的返回值。上面已經詳細介紹過這幾個參數,這裏不再贅述。
在這個方法中我們可以操作 DOM,發起 http 請求,也可以 setState 更新狀態,但注意這裏使用 setState 要有條件,不然就會陷入死循環。
卸載階段
在 React 的卸載階段(Unmounting Phase),只有一個生命週期函數:componentWillUnmount。這個函數在組件即將從 DOM 中移除之前調用。在這一階段,你可以執行一些必要的清理操作,以確保組件被正確地釋放,並防止內存泄漏和其他潛在問題。
典型用法
-
清除定時器
如果組件中使用了
setTimeout或setInterval,你需要在componentWillUnmount中清除這些定時器,以防止在組件卸載後它們繼續運行,導致潛在的資源浪費或試圖訪問已被卸載的組件。componentWillUnmount() { if (this.timerID) { clearTimeout(this.timerID); } } -
取消未完成的網絡請求
在組件卸載之前取消未完成的網絡請求,避免在組件已經卸載後試圖更新組件狀態或訪問組件數據。通常,你可以通過在請求中使用一個標誌或取消令牌來實現這一點。
componentWillUnmount() { if (this.networkRequest) { this.networkRequest.abort(); // 假設我們有一個可以中止的請求 } } -
移除事件監聽器
如果組件在
componentDidMount或其他地方添加了事件監聽器(如窗口的 resize 事件或自定義的事件),你需要在componentWillUnmount中移除這些監聽器,以防止在組件卸載後事件處理函數仍然被調用。componentWillUnmount() { window.removeEventListener('resize', this.handleResize); } -
清理資源和對象
如果組件中使用了某些資源或創建了需要手動清理的對象(如 WebSocket 連接、文件資源等),在 componentWillUnmount 中釋放這些資源是一個好習慣。
componentWillUnmount() { if (this.websocket) { this.websocket.close(); // 關閉 WebSocket 連接 } }
生命週期所演示的代碼都可以在 github 上找到。傳送門
事件處理
在 React 中,事件處理是組件與用户交互的重要方式。React 的事件處理與傳統的 DOM 事件處理有所不同,它採用了自己的合成事件(SyntheticEvent)系統。這個系統使得事件處理在不同瀏覽器中具有一致的行為,並提供了更好的性能和便捷性。
React 事件處理的特點
使用駝峯式命名
在 React 中,事件屬性使用駝峯式命名,而不是全小寫。前面的示例中也用到過,例如,onClick 而不是 onclick,onMouseEnter 而不是 onmouseenter。
<button onClick={this.handleClick}>Update Number</button>
傳遞函數而非字符串
在傳統的 HTML 中,你可以將 JavaScript 代碼作為字符串傳遞給事件屬性。而在 React 中,你需要傳遞一個函數引用,而不是直接的代碼字符串。
傳統 HTML 寫法:
<button onclick="alert('Button clicked')">Click Me</button>
在 React 中的寫法:
<button onClick={() => alert('Button clicked')}>Click Me</button>
合成事件系統
React 使用合成事件系統來封裝原生事件對象,這個系統提供了跨瀏覽器的兼容性和更好的性能。合成事件將原生事件包裝在統一的接口中,使得事件處理函數在所有瀏覽器上表現一致。
handleClick = (event) => {
// 合成事件對象
console.log(event);
// 原生事件對象
console.log(event.nativeEvent);
}
綁定事件處理函數
綁定類方法
在類組件中,事件處理函數通常需要綁定 this 以確保在回調中 this 指向正確的組件實例。你可以在構造函數中使用 .bind 方法來綁定,或者使用箭頭函數來自動綁定。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Button clicked');
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
// 使用箭頭函數自動綁定
class MyComponent1 extends React.Component {
handleClick = () => {
console.log('Button clicked');
};
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
函數組件中的事件處理
在函數組件中,可以直接在 JSX 中定義事件處理函數,通常通過箭頭函數或外部定義的函數來處理事件。
const MyComponent = () => {
const handleClick = () => {
console.log('Button clicked');
};
return <button onClick={handleClick}>Click Me</button>;
};
事件對象(SyntheticEvent)
React 的合成事件 SyntheticEvent 是一個跨瀏覽器的包裝對象,擁有與原生事件相同的接口,但對事件進行了統一的處理。SyntheticEvent 會在事件處理函數執行完之後被回收(即事件池機制),所以你不能異步訪問事件對象,除非通過 event.persist() 方法保留事件對象。
const handleClick = event => {
console.log(event.type); // 'click'
event.persist(); // 保留事件對象
setTimeout(() => {
console.log(event.type); // 'click' (正常訪問)
}, 1000);
};
傳遞參數給事件處理函數
有時候,你可能需要在事件處理函數中使用額外的參數。你可以使用箭頭函數或 .bind 方法來傳遞額外參數。
class MyComponent extends React.Component {
handleClick = (id, event) => {
console.log('Button clicked with id:', id);
console.log('Event:', event.type);
};
render() {
const id = 1;
return (
<button onClick={event => this.handleClick(id, event)}>
Click Me
</button>
);
}
}
停止事件傳播和默認行為
與原生 DOM 事件類似,你可以在合成事件中調用 stopPropagation 和 preventDefault 來阻止事件傳播和默認行為。
class MyComponent extends React.Component {
handleClick = (id, event) => {
event.stopPropagation(); // 阻止事件冒泡
event.preventDefault(); // 阻止默認行為
console.log('Button clicked with id:', id);
console.log('Event:', event.type);
};
render() {
const id = 1;
return (
<button onClick={event => this.handleClick(id, event)}>
Click Me
</button>
);
}
}
事件委託機制
React 的合成事件系統在幕後使用了事件委託機制,即在組件樹的頂層根節點(如 document)上添加一個全局事件監聽器,所有的事件都會在此捕獲。這種機制減少了事件監聽器的數量,從而提升性能。
條件渲染和列表渲染
在 React 中,條件渲染和列表渲染是構建動態和複雜 UI 的核心概念。它們分別允許你根據應用的狀態或數據來靈活地渲染組件或元素。
條件渲染
條件渲染是指在滿足特定條件時才渲染某些組件或元素。在 React 中,可以使用 JavaScript 中的條件語句或邏輯運算符來實現條件渲染。
常見的條件渲染方法
-
使用
if語句,在render方法或函數組件中,你可以使用 if 語句來決定渲染的內容:class Greeting extends React.Component { render() { const isLoggedIn = this.props.isLoggedIn; if (isLoggedIn) { return <h1>Welcome back!</h1>; } else { return <h1>Please sign up.</h1>; } } } -
使用三元運算符,三元運算符是條件渲染的簡潔方式,適用於根據條件渲染簡單的內容。
const Greeting = props => { return ( <div> {props.isLoggedIn ? ( <h1>Welcome back!</h1> ) : ( <h1>Please sign up.</h1> )} </div> ); }; -
使用邏輯與 (
&&) 運算符;如果你只想在某個條件為true時渲染一些元素,可以使用邏輯與運算符。條件為true時渲染後面的元素,為false時不渲染任何內容。const Notification = props => { return ( <div> {props.hasNotifications && <p>You have new notifications.</p>} </div> ); }; -
使用
switch語句;switch語句可以用於多個條件分支的複雜渲染邏輯。class Greeting extends React.Component { render() { const userRole = this.props.userRole; switch (userRole) { case 'admin': return <h1>Admin Dashboard</h1>; case 'user': return <h1>User Dashboard</h1>; default: return <h1>Welcome, Guest!</h1>; } } }
列表渲染
列表渲染是指根據數組的數據,動態地生成一組組件或元素。React 提供了 map 方法來便捷地實現列表渲染。
列表渲染的基本使用
-
使用 map 方法渲染列表;通過
map方法遍歷數組,並返回一個由元素或組件組成的數組。const NumberList = props => { const numbers = props.numbers; return ( <ul> {numbers.map(number => ( <li key={number.toString()}>{number}</li> ))} </ul> ); }; -
使用
key屬性;在列表渲染中,React 需要key屬性來唯一標識每一個列表項,以便高效地更新或重新排序列表項。key通常是列表項的唯一 ID 或數組中的索引。const TodoList = props => { const todos = props.todos; return ( <ul> {todos.map((todo, index) => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); };使用數組的索引作為
key是可以的,但只在列表項的順序不會改變的情況下使用,或者當列表項是靜態的,不會被重新排序或篩選。 -
嵌套列表渲染;當你需要渲染多層嵌套的列表時,可以遞歸地調用渲染函數。
const NestedList = props => { const categories = props.categories; return ( <ul> {categories.map(category => ( <li key={category.id}> {category.name} {category.subcategories && ( <NestedList categories={category.subcategories} /> )} </li> ))} </ul> ); };
結合條件渲染和列表渲染,可以處理更復雜的 UI 邏輯。
例如,顯示一個任務列表,如果沒有任務,則顯示一條消息;代碼如下(完整代碼):
const TodoApp = props => {
const todos = [
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Build a ToDo App', completed: false },
];
// 在沒有 TODO 數據的時候顯示
// if (todos.length === 0) {
// return <p>No tasks available.</p>;
// }
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text} {todo.completed ? '(Completed)' : '(Pending)'}
</li>
))}
</ul>
);
};
export default TodoApp;
效果如下圖:
總結
首先介紹了 JSX 的重要性和背景,它簡化了在 JavaScript 中構建 UI 的過程。接着,我們深入探討了 React 組件的核心概念:類組件和函數組件。類組件通過 ES6 類定義,具有狀態管理和生命週期方法;函數組件更簡潔,適合於無狀態或純展示組件。
隨後,我們詳細討論了組件中的 state 和 props,state 用於管理組件內部的動態數據,而 props 用於在組件之間傳遞數據。然後,我們探索了 React 組件生命週期的不同階段:掛載、更新和卸載階段,以及各生命週期方法的用途和應用場景。
在事件處理方面,我們解釋了 React 的合成事件系統,介紹瞭如何處理事件對象、阻止事件冒泡和默認行為,以及利用事件委託提升性能。最後,我們重點討論了 React 中的渲染技術:條件渲染和列表渲染。條件渲染允許根據條件動態選擇渲染內容,而列表渲染則通過 map 方法動態生成列表元素。
這些核心概念和技術使得開發者能夠利用 React 構建靈活、高效的用户界面,處理複雜的應用邏輯和交互需求。
「❤️關注+點贊+收藏+評論+轉發❤️」,原創不易,分享不易,謝謝你這麼好看還為我點贊點在看!