學習 React.js 比你想象的要簡單

sunshine小小倩發表於2017-08-14

學習 React.js 比你想象的要簡單

通過 Medium 中的一篇文章來學習 React.js 的基本原理

你有沒有注意到在 React 的 logo 中隱藏著一個六角星?只是順便提下...
去年我寫了一本簡短的關於學習 React.js 的書,有 100 頁左右。今年,我要挑戰自己 —— 將其總結成一篇文章,並向 Medium 投稿。

這篇文章不是講什麼是 React 或者 你該怎樣學習 React。這是在面向那些已經熟悉了 JavaScript 和 DOM API 的人的 React.js 基本原理介紹

本文采用嵌入式 jsComplete 程式碼段,所以為了方便閱讀,你需要一個合適的螢幕寬度。

下面所有的程式碼都僅供參考。它們也純粹是為了表達概念而提供的例子。它們中的大多數有更好的實踐方式。

您可以編輯和執行下面的任何程式碼段。使用 Ctrl+Enter 執行程式碼。每一段的右下角有一個點選後可以在 jsComplete/repl 進行全屏模式編輯或執行程式碼的連結。


1 React 全部都是元件化的

React 是圍繞可重用元件的概念設計的。你定義小元件並將它們組合在一起形成更大的元件。

無論大小,所有元件都是可重用的,甚至在不同的專案中也是如此。

React 元件最簡單的形式,就是一個普通的 JavaScript 函式:

function Button (props) {
  // 這裡返回一個 DOM 元素,例如:
  return <button type="submit">{props.label}</button>;
}
// 將按鈕元件呈現給瀏覽器
ReactDOM.render(<Button label="Save" />, mountNode)複製程式碼

例 1:編輯上面的程式碼並按 Ctrl+Enter 鍵執行(譯者注:譯文暫時沒有這個功能,請訪問原文使用此功能,下同)

括號中的 button 標籤將稍後解釋。現在不要擔心它們。ReactDOM 也將稍後解釋,但如果你想測試這個例子和所有接下來的例子,上述 render 函式是必須的。(React 將要接管和控制的是 ReactDOM.render 的第 2 個引數即目標 DOM 元素)。在 jsComplete REPL 中,你可以使用特殊的變數 mountNode

例 1 的注意事項:

  • 元件名稱首字母大寫,Button。必須要這樣做是因為我們將處理 HTML 元素和 React 元素的混合。小寫名稱是為 HTML 元素保留的。事實上,將 React 元件命名為 “button” 然後你就會發現 ReactDOM 會忽略這個函式,僅僅是將其作為一個普通的空 HTML 按鈕來渲染。
  • 每個元件都接收一個屬性列表,就像 HTML 元素一樣。在 React 中,這個列表被稱為屬性。雖然你可以將一個函式隨意命名。
  • 在上面 Button 函式元件的返回輸出中,我們奇怪地寫了段看上去像 HTML 的程式碼。這實際上既不是 JavaScript 也不是 HTML,老實說,這甚至不是 React.js。然而它非常流行,以至於成為 React 應用程式中的預設值。這就是所謂的 JSX,這是一個JavaScript 的擴充套件。JSX 也是一個折中方案!繼續嘗試並在上面的函式中返回其他 HTML 元素,看看它們是如何被支援的(例如,返回一個文字輸入元素)。

2 JSX 輸出的是什麼?

上面的例 1 可以用沒有 JSX 的純 React.js 編寫,如下:

function Button (props) {
  return React.createElement(
    "button",
    { type: "submit" },
    props.label
  );
}

// 要使用 Button,你可以這麼做
ReactDOM.render(
  React.createElement(Button, { label: "Save" }),
  mountNode
);複製程式碼

例 2:不使用 JSX 編寫 React 元件

在 React 頂級 API 中,createElement 函式是主函式。這是你需要學習的 7 個 API 中的 1 個。React 的 API 就是這麼小。

就像 DOM 自身有一個 document.createElement 函式來建立一個由標籤名指定的元素一樣,React 的 createElement 函式是一個高階函式,有和 document.createElement 同樣的功能,但它也可以用於建立一個表示 React 元件的元素。當我們使用上面例 2 中的按鈕元件時,我們使用的是後者。

不像 document.createElement,React 的 createElement 在接收第二個引數後,接收一個動態引數,它表示所建立元素的子元素。所以 createElement 實際上建立了一個

這裡就是這樣的一個例子:

const InputForm = React.createElement(
  "form",
  { target: "_blank", action: "https://google.com/search" },
  React.createElement("div", null, "Enter input and click Search"),
  React.createElement("input", { className: "big-input" }),
  React.createElement(Button, { label: "Search" })
);

// InputForm 使用 Button 元件,所以我們需要這樣做:
function Button (props) {
  return React.createElement(
    "button",
    { type: "submit" },
    props.label
  );
}

// 然後我們可以通過 .render 方法直接使用 InputForm
ReactDOM.render(InputForm, mountNode);複製程式碼

例 3:React 建立元素的 API

上面例子中的一些事情值得注意:

  • InputForm 不是一個 React 元件;它僅僅是一個 React 元素。這就是為什麼我們可以在 ReactDOM.render 中直接使用它並且可以在呼叫時不使用 <InputForm /> 的原因。
  • React.createElement 函式在前兩個引數後接收了多個引數。從第3個引數開始的引數列表構成了建立元素的子項列表。
  • 我們可以巢狀 React.createElement 呼叫,因為它是 JavaScript。
  • 當這個元素不需要屬性時,React.createElement 的第二個引數可以為空或者是一個空物件。
  • 我們可以在 React 元件中混合 HTML 元素。你可以將 HTML 元素作為內建的 React 元件。
  • React 的 API 試圖和 DOM API 一樣,這就是為什麼我們在 input 元素中使用 className 代替 class 的原因。我們都希望如果 React 的 API 成為 DOM API 本身的一部分,因為,你知道,它要好得多。

上述的程式碼是當你引入 React 庫的時候瀏覽器是怎樣理解的。瀏覽器不會處理任何 JSX 業務。然而,我們更喜歡看到和使用 HTML,而不是那些 createElement 呼叫(想象一下只使用 document.createElement 構建一個網站!)。這就是 JSX 存在的原因。取代上述呼叫 React.createElement 的方式,我們可以使用一個非常簡單類似於 HTML 的語法:

const InputForm =
  <form target="_blank" action="https://google.com/search">
    <div>Enter input and click Search</div>
    <input className="big-input" name="q" />
    <Button label="Search" />
  </form>;

// InputForm “仍然”使用 Button 元件,所以我們也需要這樣。
// JXS 或者普通的表單都會這樣做
function Button (props) {
  // 這裡返回一個 DOM 元素。例如:
  return <button type="submit">{props.label}</button>;
}

// 然後我們可以直接通過 .render 使用 InputForm
ReactDOM.render(InputForm, mountNode);複製程式碼

例 4:為什麼在 React 中 JSX 受歡迎(和例 3 相比)

注意上面的幾件事:

  • 這不是 HTML 程式碼。比如,我們仍然可以使用 className 代替 class
  • 我們仍在考慮怎樣讓上述的 JavaScript 看起來像是 HTML。看一下我在最後是怎樣新增的。

我們在上面(例 4)中寫的就是 JSX。然而,我們要將編譯後的版本(例 3)給瀏覽器。要做到這一點,我們需要使用一個前處理器將 JSX 版本轉換為 React.createElement 版本。

這就是 JSX。這是一種折中的方案,允許我們用類似 HTML 的語法來編寫我們的 React 元件,這是一個很好的方法。

“Flux” 在頭部作為韻腳來使用,但它也是一個非常受歡迎的 應用架構,由 Facebook 推廣。最出名的是 Redux,Flux 和 React 非常合適。

JSX,可以單獨使用,不僅僅適用於 React。

3 你可以在 JavaScript 的任何地方使用 JSX

在 JSX 中,你可以在一對花括號內使用任何 JavaScript 表示式。

const RandomValue = () =>
  <div>
    { Math.floor(Math.random() * 100) }
  </div>;

// 使用:
ReactDOM.render(<RandomValue />, mountNode);複製程式碼

例 5:在 JSX 中使用 JavaScript 表示式

任何 JavaScript 表示式可以直接放在花括號中。這相當於在 JavaScript 中插入 ${} 模板

這是 JSX 內唯一的約束:只有表示式。例如,你不能使用 if 語句,但三元表示式是可以的。

JavaScript 變數也是表示式,所以當元件接受屬性列表時(不包括 RandomValue 元件,props 是可選擇的),你可以在花括號裡使用這些屬性。我們在上述(例 1)的 Button 元件是這樣使用的。

JavaScript 物件也是表示式。有些時候我們在花括號中使用 JavaScript 物件,這看起來像是使用了兩個花括號,但是在花括號中確實只有一個物件。其中一個用例就是將 CSS 樣式物件傳遞給響應中的特殊樣式屬性:

const ErrorDisplay = ({message}) =>
  <div style={ { color: 'red', backgroundColor: 'yellow' } }>
    {message}
  </div>;

// 使用
ReactDOM.render(
  <ErrorDisplay
    message="These aren't the droids you're looking for"
  />,
  mountNode
);複製程式碼

例 6:一個物件傳遞特殊的 React 樣式引數

注意我解構的只是在屬性引數之外的資訊。這只是 JavaScript。還要注意上面的樣式屬性是一個特殊的屬性(同樣,它不是 HTML,它更接近 DOM API)。我們使用一個物件作為樣式屬性的值並且這個物件定義樣式就像我們使用 JavaScript 那樣(我們可以這樣做)。

你可以在 JSX 中使用 React 元素。因為這也是一個表示式(記住,一個 React 元素只是一個函式呼叫):

const MaybeError = ({errorMessage}) =>
  <div>
    {errorMessage && <ErrorDisplay message={errorMessage} />}
  </div>;

// MaybeError 元件使用 ErrorDisplay 元件
const ErrorDisplay = ({message}) =>
  <div style={ { color: 'red', backgroundColor: 'yellow' } }>
    {message}
  </div>;

// 現在我們使用 MaybeError 元件:
ReactDOM.render(
  <MaybeError
    errorMessage={Math.random() > 0.5 ? 'Not good' : ''}
  />,
  mountNode
);複製程式碼

例 7:一個 React 元素是一個可以通過 {} 使用的表示式

上述 MaybeError 元件只會在有 errorMessage 傳入或者另外有一個空的 div 才會顯示 ErrorDisplay 元件。React 認為 {true}{false}
{undefined}{null} 是有效元素,不呈現任何內容。

我們也可以在 JSX 中使用所有的 JavaScript 的集合方法(mapreducefilterconcat 等)。因為他們返回的也是表示式:

const Doubler = ({value=[1, 2, 3]}) =>
  <div>
    {value.map(e => e * 2)}
  </div>;

// 使用下面內容 
ReactDOM.render(<Doubler />, mountNode);複製程式碼

例 8:在 {} 中使用陣列

請注意我是如何給出上述 value 屬性的預設值的,因為這全部都只是 JavaScript。注意我只是在 div 中輸出一個陣列表示式。React 是完全可以的。它只會在文字節點中放置每一個加倍的值。

4 你可以使用 JavaScript 類寫 React 元件

簡單的函式元件非常適合簡單的需求,但是有的時候我們需要的更多。React 也支援通過使用 JavaScript 類來建立元件。這裡 Button 元件(在例 1 中)就是使用類的語法編寫的。

class Button extends React.Component {
  render() {
    return <button>{this.props.label}</button>;
  }
}

// 使用(相同的語法)
ReactDOM.render(<Button label="Save" />, mountNode);複製程式碼

例 9:使用 JavaScript 類建立元件

類的語法是非常簡單的:定義一個擴充套件的 React.Component 類(另一個你需要學習的 React 的頂級 API)。該類定義了一個單一的例項函式 —— render(),並使函式返回虛擬 DOM 物件。每一次我們使用基於類的 Button 元件(例如,通過 <Button ... />),React 將從這個基於類的元件中例項化物件,並在 DOM 樹中使用該物件。

這就是為什麼上面的例子中我們可以在 JSX 中使用 this.props.label 渲染輸出的原因,因為每一個元件都有一個特殊的稱為 props例項 屬性,這讓所有的值傳遞給該元件時被例項化。

由於我們有一個與元件的單個使用相關聯的例項,所以我們可以按照自己的意願定製該例項。例如,我們可以通過使用常規 JavaScript 建構函式來構造它:

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.id = Date.now();
  }
  render() {
    return <button id={this.id}>{this.props.label}</button>;
  }
}

// 使用
ReactDOM.render(<Button label="Save" />, mountNode);複製程式碼

例 10:自定義元件例項

我們也可以定義類的原型並且在任何我們希望的地方使用,包括在返回的 JSX 輸出的內部:

class Button extends React.Component {
  clickCounter = 0;

  handleClick = () => {
    console.log(`Clicked: ${++this.clickCounter}`);
  };

  render() {
    return (
      <button id={this.id} onClick={this.handleClick}>
        {this.props.label}
      </button>
    );
  }
}

// 使用
ReactDOM.render(<Button label="Save" />, mountNode);複製程式碼

例 11:使用類的屬性(通過單擊儲存按鈕進行測試)

注意上述例 11 中的幾件事情

  • handleClick 函式使用 JavaScript 新提出的 class-field syntax 語法。這仍然是 stage-2,但是這是訪問元件安裝例項(感謝箭頭函式)最好的選擇(因為很多原因)。然而,你需要使用類似 Babel 的編譯器解碼為 stage-2(或者僅僅是類欄位語法)來讓上述程式碼工作。 jsComplete REPL 有預編譯功能。
// 錯誤:
onClick={this.handleClick()}

// 正確:
onClick={this.handleClick}複製程式碼

5 React 中的事件:兩個重要的區別

當處理 React 元素中的事件時,我們與 DOM API 的處理方式有兩個非常重要的區別:

  • 所有 React 元素屬性(包括事件)都使用 camelCase 命名,而不是 lowercase。例如是 onClick 而不是 onclick
  • 我們將實際的 JavaScript 函式引用傳遞給事件處理程式,而不是字串。例如是 onClick={handleClick} 而不是 onClick="handleClick"

React 用自己的物件包裝 DOM 物件事件以優化事件處理的效能,但是在事件處理程式內部,我們仍然可以訪問 DOM 物件上所有可以訪問的方法。React 將經過包裝的事件物件傳遞給每個呼叫函式。例如,為了防止表單提交預設提交操作,你可以這樣做:

class Form extends React.Component {
  handleSubmit = (event) => {
    event.preventDefault();
    console.log('Form submitted');
  };

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <button type="submit">Submit</button>
      </form>
    );
  }
}

// 使用
ReactDOM.render(<Form />, mountNode);複製程式碼

例 12:使用包裝過的物件

6 每一個 React 元件都有一個故事:第 1 部分

以下僅適用於類元件(擴充套件 React.Component)。函式元件有一個稍微不同的故事。

  1. 首先,我們定義了一個模板來建立元件中的元素。
  2. 然後,我們在某處使用 React。例如,在 render 內部呼叫其他的元件,或者直接使用 ReactDOM.render
  3. 然後,React 例項化一個物件然後給它設定 props 然後我們可以通過 this.props 訪問。這些屬性都是我們在第 2 步傳入的。
  4. 因為這些全部都是 JavaScript,constructor 方法將會被呼叫(如果定義的話)。這是我們稱之為的第一個:元件生命週期方法
  5. 接下來 React 計算渲染之後的輸出方法(虛擬 DOM 節點)。
  6. 因為這是 React 第一次渲染元素,React 將會與瀏覽器連通(代表我們使用 DOM API)來顯示元素。這整個過程稱為 mounting
  7. 接下來 React 呼叫另一個生命週期函式,稱為 componentDidMount。我們可以這樣使用這個方法,例如:在 DOM 上做一些我們現在知道的在瀏覽器中存在的東西。在此生命週期方法之前,我們使用的 DOM 都是虛擬的。
  8. 一些元件的故事到此結束,其他元件得到解除安裝瀏覽器 DOM 中的各種原因。在後一種情況發生時,會呼叫另一個生命週期的方法,componentWillUnmount
  9. 任何 mounted 的元素的狀態都可能會改變。該元素的父級可能會重新渲染。無論哪種情況,mounted 的元素都可能接收到不同的屬性集。React 的魔力就是這兒,我們實際上需要的正是 React 的這一點!在這一點之前,說實話,我們並不需要 React。
  10. 組價的故事還在繼續,但是在此之前,我們需要理解我所說的這種狀態

7 React 元件可以具有私有狀態

以下只適用於類元件。我有沒有提到有人叫表象而已的部件 dumb

狀態類是任何 React 類元件中的一個特殊欄位。React 檢測每一個元件狀態的變化,但是為了 React 更加有效,我們必須通過 React 的另一個 API 改變狀態欄位,這就是我們要學習的另一個 API —— this.setState

class CounterButton extends React.Component {
  state = {
    clickCounter: 0,
    currentTimestamp: new Date(),
  };

  handleClick = () => {
    this.setState((prevState) => {
     return { clickCounter: prevState.clickCounter + 1 };
    });
  };

  componentDidMount() {
   setInterval(() => {
     this.setState({ currentTimestamp: new Date() })
    }, 1000);
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>Click</button>
        <p>Clicked: {this.state.clickCounter}</p>
        <p>Time: {this.state.currentTimestamp.toLocaleString()}</p>
      </div>
    );
  }
}

// 使用
ReactDOM.render(<CounterButton />, mountNode);複製程式碼

例 13:setState 的 API

這可能是最重要的一個例子因為這將是你完全理解 React 基礎知識的方式。這個例子之後,還有一些小事情需要學習,但從那時起主要是你和你的 JavaScript 技能。

讓我們來看一下例 13,從類開始,總共有兩個,一個是一個初始化的有初始值為 0clickCounter 物件和一個從 new Date() 開始的 currentTimestamp

另一個類是 handleClick 函式,在渲染方法中我們給按鈕元素傳入 onClick 事件。通過使用 setStatehandleClick 方法修改了元件的例項狀態。要注意到這一點。

另一個我們修改狀態的地方是在一個內部的定時器,開始在內部的 componentDidMount 生命週期方法。它每秒鐘呼叫一次並且執行另一個函式呼叫 this.setState

在渲染方法中,我們使用具有正常讀語法的狀態上的兩個屬性(沒有專門的 API)。

現在,注意我們更新狀態使用兩種不同的方式:

  1. 通過傳入一個函式然後返回一個物件。我們在 handleClick 函式內部這樣做。
  2. 通過傳入一個正則物件,我們在在區間回撥中這樣做。

這兩種方式都是可以接受的,但是當你同時讀寫狀態時,第一種方法是首選的(我們這樣做)。在區間回撥中,我們只向狀態寫入而不讀它。當有問題時,總是使用第一個函式作為引數語法。伴隨著競爭條件這更安全,因為 setstate 實際上是一個非同步方法。

我們應該怎樣更新狀態呢?我們返回一個有我們想要更新的的值的物件。注意,在呼叫 setState 時,我們全部都從狀態中傳入一個屬性或者全都不。這完全是可以的因為 setState 實際上 合併 了你通過它(返回值的函式引數)與現有的狀態,所以,沒有指定一個屬性在呼叫 setState 時意味著我們不希望改變屬性(但不刪除它)。

8 React 將要反應

React 的名字是從狀態改變的反應中得來的(雖然沒有反應,但也是在一個時間表中)。這裡有一個笑話,React 應該被命名為Schedule

然而,當任何元件的狀態被更新時,我們用肉眼觀察到的是對該更新的反應,並自動反映了瀏覽器 DOM 中的更新(如果需要的話)。

將渲染函式的輸入視為兩種:

  • 通過父元素傳入的屬性
  • 以及可以隨時更新的內部私有狀態

當渲染函式的輸入改變時,輸出可能也會改變。

React 儲存了渲染的歷史記錄,當它看到一個渲染與前一個不同時,它將計算它們之間的差異,並將其有效地轉換為在 DOM 中執行的實際 DOM 操作。

9 React 是你的程式碼

您可以將 React 看作是我們用來與瀏覽器通訊的代理。以上面的當前時間戳顯示為例。取代每一秒我們都需要手動去瀏覽器呼叫 DOM API 操作來查詢和更新 p#timestamp 元素,我們僅僅改變元件的狀態屬性,React 做的工作代表我們與瀏覽器的通訊。我相信這就是為什麼 React 這麼受歡迎的真正原因;我們只是不喜歡和瀏覽器先生談話(以及它所說的 DOM 語言的很多方言),並且 React 自願傳遞給我們,免費的!

10 每一個 React 元件都有一個故事:第 2 部分

現在我們知道了一個元件的狀態,當該狀態發生變化的時候,我們來了解一下關於這個過程的最後幾個概念。

  1. 當元件的狀態被更新時,或者它的父程式決定更改它傳遞給元件的屬性時,元件可能需要重新渲染。
  2. 如果後者發生,React 會呼叫另一個生命週期方法:componentWillReceiveProps
  3. 如果狀態物件或傳遞的屬性改變了,React 有一個重要的決定要做:元件是否應該在 DOM 中更新?這就是為什麼它呼叫另一個重要的生命週期方法 shouldComponentUpdate 的原因 。此方法是一個實際問題,因此,如果需要自行定製或優化渲染過程,則必須通過返回 true 或 false 來回答這個問題。
  4. 如果沒有自定義 shouldComponentUpdate,React 的預設事件在大多數情況下都能處理的很好。
  5. 首先,這個時候會呼叫另一生命週期的方法:componentWillUpdate。然後,React 將計算新渲染過的輸出,並將其與最後渲染的輸出進行對比。
  6. 如果渲染過的輸出和之前的相同,React 不進行處理(不需要和瀏覽器先生對話)。
  7. 如果有不同的地方,React 將不同傳達給瀏覽器,像我們之前看到的那樣。
  8. 在任何情況下,一旦一個更新程式發生了,無論以何種方式(即使有相同的輸出),React 會呼叫最後的生命週期方法:componentDidUpdate

生命週期方法是逃生艙口。如果你沒有做什麼特別的事情,你可以在沒有它們的情況下建立完整的應用程式。它們非常方便地分析應用程式中正在發生的事情,並進一步優化 React 更新的效能。


信不信由你,通過上面所學的知識(或部分知識),你可以開始建立一些有趣的 React 應用程式。如果你渴望更多,看看我的 Pluralsight 的 React.js 入門課程

感謝閱讀。如果您覺得這篇文章有幫助,請點選下面的 ?。請關注我的更多關於 React.js 和 JavaScript 的文章


PluralsightLynda 建立了線上課程。我最新的文章在Advanced React.jsAdvanced Node.jsLearning Full-stack JavaScript中。我也做小組的線上和現場培訓,覆蓋初級到高階的 JavaScript、 Node.js、 React.js、GraphQL。如果你需要一個導師,請來找我 。如果你對此篇文章或者我寫的其他任何文章有疑問,通過這個聯絡我,並且在 #questions 中提問。


感謝很多檢驗和改進這篇文章的讀者,Łukasz Szewczak、Tim Broyles、 Kyle Holden、 Robert Axelse、 Bruce Lane、Irvin Waldman 和 Amie Wilt.

特別要感謝“驚人的” Amie,經驗是一個實際的 Unicorn。謝謝你所有的幫助,Anime,真的非常感謝你。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章