—— 基礎知識、JSX介紹、React 元素、組件和屬性、狀態和生命周期
此文檔來自 React 官方文檔,在英文原文的基礎上進行了增刪改,用于我本人的研究與學習,暫不支持轉載。因為本人的水平問題,來取經的同學也請慎用。
React 特點
- 組件化,適用于高交互的大型系統
- 單向數據流,數據的變化易于追蹤,缺點就是:實現復雜,需要寫各種 action 來應對 UI 的變化。
JSX介紹
先來看一句代碼。
const element = <h1>Hello, world!</h1>;
這就是 JSX,是 JavaScript 的一種格式擴展。我們推薦在 React 中使用它來描述 UI。
JSX 很像是一種模板語言,但是它其實都是由 JavaScript 實現。
JSX 會創建 React 元素,在下節中我們將學習其加入 DOM 中。
在后面你可以獲得必要的 JSX 基礎知識。
在 JSX 中嵌入表達式
你可以在 JSX 中嵌入任意 JavaScript 代碼,只要將其放入大括號中。
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
JSX 也是一種表達式
經過編譯之后,JSX 會變為常規的 JavaScript 代碼。 所以,你可以將 JSX 放入 if 或者 for 表達式中,將其分配為變量,將其作為參數,或者作為函數的返回值。
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX 中給屬性賦值
可以通過雙引號來賦值字符串屬性。
const element = <div tabIndex="0"></div>;
也可以嵌入 JavaScript 代碼來為屬性賦值。
const element = <img src={user.avatarUrl}></img>;
兩個不要同時使用。
注意:
因為 JSX 與 JavaScript 的關系比 HTML 更近一些,所以 React DOM 使用駝峰法命名。比如 class 使用 className,tabindex 使用 tabIndex。
在 JSX 中聲明子節點
如果節點沒有子節點,即可用 /> 立刻關閉節點,就像 XML 一樣。
const element = <img src={user.avatarUrl} />;
如果 JSX 包含子節點。
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX 可以防范腳本注入攻擊
在 JSX 中嵌入用戶輸入是很安全的。
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
通常情況下,React DOM 會在處理 JSX 之前將所有輸入內容轉換為字符串從而忽略其中的所有值。因此可以確保無法注入攻擊代碼。
JSX 相當于 Objects
Babel 通過調用方法 React.createElement() 來編譯 JSX。
以下兩個例子是相同的。
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement() 會提供一些簡單的檢查來幫助你編寫無 bug 的代碼。但是本質上通過下面的方法來創建對象。
// Note: this structure is simplified
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
};
這些對象叫做 React 元素。你可以將它們看作 你想在屏幕上看到的東西的 描述( You can think of them as descriptions of what you want to see on the screen.)。
React 會讀取這些對象并把它們添加到 DOM 中并且讓其始終保持最新。
Tip:
我們推薦你使用 Babel 給你的語言添加標簽,可以讓 ES6 和 JSX 代碼都可以正確的高亮顯示。
元素渲染
元素是React應用中的最小組成單位。
元素描述的是你在屏幕上看到的內容。
const element = <h1>Hello, world</h1>;
不同于 DOM 節點,React 元素更加輕量,并且易于創建。React DOM 會更新 DOM 來匹配 React 元素。
在 DOM 中渲染元素
假設你的 HTML 文檔里有一個 <div> 節點。
<div id="root"></div>
我們把這個節點命名為 root,因為在其中的所有東西都被 React DOM 接管。
React 應用通常都有一個 root DOM 節點。如果你是在已存在的應用中添加 React,你可以指定多個 root 節點。
要在 root DOM 節點里渲染 React 元素,需要使用下面方法。
方法:
ReactDOM.render(React Element, HTML DOM);
const element = <h1>Hello, world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
它會在頁面上展示“Hello, world”。
已渲染元素的更新
React 元素是不可改變的。一旦創造了 React 元素,其元素屬性和子元素就不可以改變了。一個元素就像是電影里的一幀,它表現了某個特定時間節點的 UI。
所以更改 UI 的唯一方法就是創造一個新的元素,并將其通過ReactDOM.render() 方法添加到 DOM 中。
來看下面的秒表案例:
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
每過一秒都會在 setInterval 的回調函數中調用 ReactDom.render() 。
React 只會更新發生改變的部分
React 在更新元素時會與之前的元素及其子元素進行比較,只會更新兩者的不同之處。
所以在 React 中我們只需要考慮下一時刻我們想要的 UI 效果是什么樣的,而不是考慮如何通過目前存在的 UI 效果來改變。
在上面的例子中,即使看起來我們每秒都創建了一個描述整個 UI 樹的元素,但是其實每次都只有一個文字節點每次在被 React DOM 所更新。
在我們的經驗看來,思考每一刻 UI 該是什么樣子 會比 思考每一刻如何對它進行改變 少很多 bug。
組件和屬性
組件會將 UI 分成彼此之間互不依賴,且可重用的部分,并分別思考每一部分的實現。
從概念上來看,組件就像是 JavaScript 的 functions。接受任意的輸入數據(稱為“props”)然后返回要在屏幕上顯示的 React 元素。
函數式的和類式的組件
最簡單的定義組件的方法是寫一個JavaScript 函數。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
這個函數是一個有效的 React 組件,因為它接受一個屬性對象作為輸入參數,返回一個React 元素。我們把它稱作函數式的是因為它確實是一個 JavaScript 函數。
你也可以利用 ES6 來定義一個組件。
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
在React 的角度來看,上面兩個組件是完全等價的。
組件渲染
之前我們只遇到過帶 HTML 標簽的元素。
const element = <div />;
然而元素也可以使用用戶自定義標簽的組件。
const element = <Welcome name="Sara" />;
當 React 看到用戶自定的組件出現在了元素中,就會將組件中 JSX 屬性值作為一個對象傳遞給組件,我們把這個對象叫做 props。
例如:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
上述代碼的執行步驟如下:
- 我們調用 ReactDOM.render() 來處理元素。
- React 將 {name: 'Sara'} 作為 props 調用 Welcome 組件。
- Welcome 組件 返回一個 <h1>Hello, Sara</h1> 元素作為結果。
- React DOM 會高效地更新 DOM 來匹配 <h1>Hello, Sara</h1>。
注意:
組件 要以大寫字母開頭。
組件構成
組件可以在返回值中返回其他組件。這就讓我們在每層的內容中都使用組件這一共同概念。一個按鈕,一個表單,一段對話,整個屏幕都被統稱為組件。
例如,我們來創建一個多次使用 Welcome 組件的 App。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
通常,在一個React App 中使用一個 App 組件作為應用的最頂層。然而,如果你是將 React 融入一個已存在的 App 中,則需要自下而上,比如一個按鈕開始逐漸到達應用的最頂層。
注意:
組件必須返回單個元素,如果要返回多個元素,必須用單個元素包裹起來。
組件提取
不要害怕將組件分割為多個更小的組件。
來看下面這個例子:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
它接受 author(一個對象),text(一個字符串)和 date(一個 date 類型)作為屬性,在一個網站上作為評論區存在。
這個組件里的數據很難進行更新,因為這個組件的結構較復雜,而且重用其中的部件也比較麻煩,所以我們來著手分離一些組件出來。
首先,我們將頭像 Avatar 分離出來。
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
Avatar 組件不需要知道它是被用在 Comment 組件之中。這就是為什么我們在給其屬性命名時使用了 user 而不是 author。
我們建議在命名組件的屬性時站在這個屬性的角度命名即可,不必要考慮其所應用的上下文環境。
現在我們對 Comment 組件進行了一點精簡:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
接下來我們我們繼續提取一個 UserInfo 組件,包括 Avatar 組件和用戶名:
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
對 Comment 組件進行了進一步簡化:
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
分開多個組件在剛開始看來是一個麻煩的工作,但是在大型應用中可以創造很多可重用的組件。劃分組件的一個較好的標準是如果一個 UI 運用很多次(比如按鈕,面板,頭像)或者一個組件足夠復雜,都可以將其作為獨立組件。
屬性是只讀的
無論你聲明一個函數或類作為組件,都必須保持其參數不被改變。
看下面這個 sum 函數:
function sum(a, b) {
return a + b;
}
上面的函數叫做純函數(指不依賴于且不改變它作用域之外的變量狀態的函數),因為它沒有修改其輸入,而且在相同的輸入下總是返回相同的值。
作為對比,下面的函數式非純函數,因為它修改了自己的輸入值:
function withdraw(account, amount) {
account.total -= amount;
}
React 是很靈活的,但是它有一個很嚴格的規則:
所有的 React 組件必須是一個不能修改輸入的純函數。
當然,應用的 UI 是動態的而且時刻在改變。在下一節中,我們會介紹一個新的概念“state(狀態)”。狀態提供了一種在用戶操作,網絡請求或其他情況下修改組件輸出而不違背上述規則的方法。
狀態和生命周期
回想一下前幾節的秒表案例。
到目前為止我們只學習了一種更新 UI 的方法,即調用 ReactDOM.render () 來更改渲染的結果。
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在本節中,我們會學習如何這個封裝 Clock 組件,將會創造一個它的專屬定時器然后讓它每秒更新自己。
首先來看一下秒表應該是什么樣的:
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);
然而,這樣忽略了一個很重要的需求:實際上設置一個定時器并且每秒都進行更新應該是屬于 Clock 內部的實現細節。
理想情況下,我們只想要寫一次,然后讓 Clock 進行自我更新。
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
要想實現上述想法,我們需要將 狀態 引入 Clock 組件。
狀態與屬性類似,但狀態是私有的,而且被組件完全控制。
之前提到過,用類的方法定義組件會有額外的特性,就是局部狀態了——只能通過類定義來獲得。
由函數向類轉變
你可以用下面五步將一個函數式的組件轉換為類。
- 創建一個同名的 ES6 類,作為 React.Component 的擴展。
- 添加一個空方法 render()。
- 將函數的內容放入 render 方法中。
- 將 render 方法中的 props 用 this.props 代替。
- 刪除遺留的函數聲明。
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Clock 現在是以類定義的而非函數了。
在這我們就可以使用額外的特性,像局部狀態和生命周期方法。
在類中添加局部狀態
我們將用下面的步驟來將 date 由 屬性 轉化為 狀態。
- 在 render() 方法中使用 this.state.date 替換 this.props.date:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
- 添加一個 類的構造函數來分配 this.state:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
注意我們是如何向底層構造函數傳遞 props 的。
constructor(props) {
super(props);
this.state = {date: new Date()};
}
類組件應該調用底層的構造函數。
- 在 <Clock /> 元素中移除 date 屬性:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
稍候我們會在組件中添加定時器相關代碼。
現在的結果如下所示:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
接下來,我們會在 Clock 中添加計時器讓它每秒更新自己。
在類中添加生命周期方法
在包含很多組件的應用中,當組件銷毀時釋放其所占用的資源是十分重要的。
我們想在 Clock 第一次渲染時在 DOM 中時設置一個定時器,這在 React 中叫做掛載(Mounting)。
我們也想在 Clock 在 DOM 中移除時清除定時器,這個在 React 中叫做解掛(Unmounting)。
我們可以聲明一些特殊的方法在組件掛載和解掛時執行。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
}
componentWillUnmount() {
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
這些方法叫做“生命周期鉤子”(很不習慣 鉤子 這個詞,以后就稱方法了)。
componentDidMount() 這個方法會在 組件輸出渲染在 DOM 上之后運行。這像是一個放置定時器的好地方:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
注意我們是怎樣將 timerID 保存給 this 的。
當 this.props 已經給 React 使用并且 this.state 有特殊意義,你可以手動添加另外的變量如果你需要存儲東西而且不將它們進行可視化輸出。
如果變量不在 render() 中使用,它就不應該出現在狀態里。
我們要在 componentWillUnmount() 里對計時器進行銷毀。
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我們要實現一個 tick () 方法來讓 Clock 組件每秒更新。
方法中會使用 this.setState() 來更新組件的局部狀態。
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({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
現在秒表可以讀秒了。
我們來快速總結一下我們做了什么,調用了哪些方法:
- 當 <Clock /> 被傳遞給 ReactDOM.render(),React 調用了 Clock 組件的構造函數。因為 Clock 需要使用當前時間,所以使用包含當前事假你的對象對 this.state 進行了初始化。稍后會更新這個狀態。
- 接下來 React 調用了 Clock 組件的 render() 方法。從這個方法 React 可以知道應該在屏幕上顯示什么東西。React 會根據 Clock render() 的輸出來調整 DOM。
- 當 Clock 輸出被添加到 DOM 中,React 調用 componentDidMount() 生命周期方法。在這個方法里,Clock 組件要求瀏覽器設置一個定時器來每秒調用組件里的 tick() 方法。
- 每秒瀏覽器都會調用 tick() 方法。在這個方法里,Clock 組件通過將一個包含當前時間的對象傳入 setState() 方法來安排 UI 的更新。感謝 setState(),React 知道 state 已經改變,然后又調用 render() 方法來知曉屏幕上應該顯示什么。這次,render() 方法中的 this.state.date 發生了改變,render 的輸出也會包含更新后的時間。因此 React 更新了 DOM。
- 如果 Clock 組件被移出 DOM,React 調用 componentWillUnmount() 生命周期方法來停止定時器。
正確使用 State
關于 setState() 你要明白三件事。
不要直接修改 State
如下的方式不會重新渲染組件:
// Wrong
this.state.comment = 'Hello';
相反的,使用 setState():
// Correct
this.setState({comment: 'Hello'});
唯一可以分配 this.state 的地方是構造函數中。
State 的更新可能是異步的
在單次的更新中 React 可能有很多 setState() 的調用。
因為 this.props 和 this.state 可能是異步更新,所以你不應該根據這兩個值來計算之后的 state 值。
例如,下面的代碼會無法更新 counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
可以使用 setState() 的另一種形式來達成目的。setState() 可以接受一個函數,這個函數會接收之前的 state 值作為第一個參數,接收在此刻更新的 props 值作為第二個參數:
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
State 更新被合并
當你調用 setState(),React 將你提供給當前 state 的對象合并起來。
假如,你的屬性值有多個獨立的變量:
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({comment}) 不會影響 this.state.posts,但是會完全替代 this.state.comments。
數據順流而下
對于一個確定的組件來說,無論是其子組件還是父組件都無法知道它是否含有狀態 state,而且也不應該關注它到底是以函數定義還是以類定義的。
這就是為什么 狀態 state 總是被稱為 局部 或者 被封裝。狀態 state 對于定義使用它的組件之外的都是透明的。
一個組件可以選擇將其狀態值作為屬性傳遞給它的子組件。
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
對于用戶自定組件來說同樣適用。
<FormattedDate date={this.state.date} />
FormattedDate 組件會受到 date 作為它的屬性值,而且也不會知道它到底來自 Clock 組件的狀態還是屬性,亦或是手動傳入:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
這通常叫做 自上而下 或者 單向數據流。任何的 屬性值 state 都是屬于特定的組件,而且它所產生任何數據或 UI 也都只能影響下游的組件。
如果你把一個組件樹設想成一個由 屬性 props 組成的瀑布,每個組件的 狀態 state 都像在任意節點加入的額外水流,隨著瀑布一起流下。
為了展示每個組件都是相互獨立的,我們可以創建一個 App 組件渲染三個 <Clock> 組件:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每個 Clock 設置了獨立的定時器,獨立地更新時間。
在 React 應用中,無論一個組件有無狀態,其內部實現細節都可以隨時改變。你可以在一個有狀態組件中使用一個無狀態的組件,反之亦然。