一篇文章講清楚React的基礎概念
譯文,原址:https://medium.freecodecamp.org/all-the-fundamental-react-js-concepts-jammed-into-this-single-medium-article-c83f9b53eac2
這篇文章不是講什麼是React,也不是為什麼學習React。 而是一篇介紹React的一些基本概念和原理,我假定你們都熟悉js也了解基本的DOM API。
文章中的所有代碼例子都只是作為參考,它們可能有別的更好的寫法,但這裡純粹的為了理解React概念。
#1: React皆組件
React按照可復用組件的概念來設計的。定義一個個的小組件,然後組裝成大組件。
所有的組件不論大小都是可復用的,即使跨項目也一樣。
一個組件,從形式上看就是一個普通的JS函數:
// 例1n// https://jscomplete.com/repl?j=Sy3QAdKHWnfunction Button (props) {n // Returns a DOM element here. For example:n return <button type="submit">{props.label}</button>;n}n// To render the Button component to the browsernReactDOM.render(<Button label="Save" />, mountNode)n
關於Button裡面的花括弧,還有ReactDOM會在後面介紹。這裡只是一個熱身例子。
ReactDOM.render 的第二個參數是React會覆蓋和監控的目標元素。
關於 例1 的個要點:
- 為了和HTML標籤區別開來,組件的名字首字母大寫。小寫是為HTML元素預留的,事實上如果你把該組件命名為button,ReactDOM將會忽略此函數直接渲染一個正常的空的HTML button。
- 和HTML元素一樣每個組件也會接收一個屬性列表,在React裡面這個列表叫Props。由於是一個函數組件你可以隨便定義屬性。
- 在Button函數組件上,我們寫出了一個類HTML的輸出。這種輸出既不JS也不是HTML或者React.js。但它是這麼的受歡迎以至於變成了React的默認寫法。她叫做JSX,一個JS的擴展。JSX也是一種折衷的方案。自己動手試試吧,比如試試讓她返回一個input元素。
#2: JSX的 「flux」 是多少?
上面的例子例1可以用純react.js實現,而不用jsx:
// 例2 - React component without JSXn// https://jscomplete.com/repl?j=HyiEwoYB-nfunction Button (props) {n return React.createElement(n "button",n { type: "submit" },n props.labeln );n}n// To use Button, you would do something likenReactDOM.render(n React.createElement(Button, { label: "Save" }),n mountNoden);n
createElement函數是React頂級API的主要函數。也是7大API中所需要學習的一個。這也說明了ReactAPI很小。
就類似DOM有自己的document.createElement 函數來創建特定標籤名的元素。React的createElement函數是一個高級函數能做到document.createElement能做的事,他也能創建代表React組件的元素。我們在上面的例子2中做過後者。
不一樣的還有,React的createElement函數接受一個可變的多個參數做為第二個參數後面的參數來代表創建的元素的子元素。所以CreatElement實際是創建了一個樹
舉個例子
// 例3 - React』s createElement APIn// https://jscomplete.com/repl?j=r1GNoiFBbnconst InputForm = React.createElement(n "form",n { target: "_blank", action: "https://google.com/search" },n React.createElement("div", null, "Enter input and click Search"),n React.createElement("input", { className: "big-input" }),n React.createElement(Button, { label: "Search" })n);n// InputForm uses the Button component, so we need that too:nfunction Button (props) {n return React.createElement(n "button",n { type: "submit" },n props.labeln );n}n// Then we can use InputForm directly with .rendernReactDOM.render(InputForm, mountNode);n
從上面列子需要知道:
- 因為InputForm不是React組件,只是React元素。所以我們直接用ReactDOM.render來調用InputForm,而不是(注意和例1的對比)。
- React.createElement 函數在前兩個參數後面接收了多個參數. 他從第三個起的參數列表 由一個該組件的子組件構成的列表.
- 由於 React.createElement 都是 JavaScript所以我們可以嵌套調用.當該元素沒有屬性或者Props的時候,該函數的第二個參數可以是null或者是一個空的對象.
- 我們可以將HTML元素和React組件一起混用,你就把HTML想像為React的內置組件就可以了.
- React 的API設計會儘可能的河DOM API接近,這也是為什麼在 Input 元素中我們要用className而不是class的原因. 私下裡,我們都希望React API可以變成DOM API的一部分,那就太好了.
上面的代碼當引入了React庫後瀏覽器是可以理解的,瀏覽器不能直接解析JSX. 然而我們開發者喜歡跟HMTL打交道而不是createElment(想像一下,整個頁面用document.createElement來創建的情景,辣眼睛). 這就是JSX存在的意義,以其用React.createElement來構建頁面,我們更願意使用一種和HTML更相近的語法:
// 例4 - JSX (compare with 例3)n// https://jscomplete.com/repl?j=SJWy3otHWnconst InputForm =n <form target="_blank" action="https://google.com/search">n <div>Enter input and click Search</div>n <input className="big-input" name="q" />n <Button label="Search" />n </form>;n// InputForm "still" uses the Button component, so we need that too.n// Either JSX or normal form would donfunction Button (props) {n // Returns a DOM element here. For example:n return <button type="submit">{props.label}</button>;n}n// Then we can use InputForm directly with .rendernReactDOM.render(InputForm, mountNode);n
上面例子你需要知道:
- 它們不是HTML,你可以看到我們還在用className而不是class.
- 我們一直認為這種類HTML的語法是一種JS,所以可以看到我們在最後加了分號。
上面的代碼(例4)就是JSX了。到目前為止,我們呈現給瀏覽器的是(例3)版本編譯後的代碼。為了達到被瀏覽器理解的目的,我們需要使用與處理器來講JSX版本(例4)的代碼轉換成為React.createElement(例3)版本。
這就是JSX,它是為了讓我們能夠用一種更接近HTML語法來寫React組件的一種折衷的辦法,也是一個不錯的方案。
開頭說的「Flux」這個單詞,其實只是為了押韻所以用了這個詞。Flux同時也是一個Facebook推出的非常受歡迎的應用框架的名字。其中最出名的實現是Redux。Flux非常適用於React的reactive特性.
另外,JSX也不是非得和React搭配用的,他也可以獨立使用.
#3: 你可以在 JSX任何位置使用JS表達式
在JSX裡面,你可以使用帶有花括弧的JS表達式:
// 例5 - Using JavaScript expressions in JSXn// https://jscomplete.com/repl?j=SkNN3oYSWnconst RandomValue = () => n <div>n { Math.floor(Math.random() * 100) }n </div>;n// To use it:nReactDOM.render(<RandomValue />, mountNode);n
任何JS表達式都可以放到花括弧裡面,這有點像JS字元串模版裡面的${} 插值語法.
唯一的約束是:只能是JS表達式,比如:if 語句不能使用了,但是你可以用三元表達式來代替.
JS變數是表達式,所以當組件接收到Props列表(除了隨機數外,props是可選的)時,我們可以將其使用到花括弧裡面。我們在(例1)裡面的Button組件使用過。
JS對象也同樣是表達式。有時候我們在花括弧裡面使用JS對象,從表現上來看是一個雙括弧,但這實際上就是一個放在花括弧裡面的對象而已。比如,我們可以在React的特殊樣式屬性中傳入一個CSS樣式對象:
// 例6 - An object passed to the special React style propn// https://jscomplete.com/repl?j=S1Kw2sFHbnconst ErrorDisplay = ({message}) =>n <div style={ { color: red, backgroundColor: yellow } }>n {message}n </div>;n// Use it:nReactDOM.render(n <ErrorDisplay n message="These arent the droids youre looking for" n />,n mountNoden);n
注意這裡我是怎麼構props裡面的message參數的,再次證明它是JS。另外注意看一下style屬性的特殊性(它不是HTML,它只是接近DOM API)。我們使用一個對象作為style屬性。這樣的定義樣式就像在寫js一樣,當然我們就是在寫JS。
由於React元素也是一個表達式,所以同樣我們可以在JSX中使用。別忘了React元素其實就是一個函數調用:
// 例7 - Using a React element within {}n// https://jscomplete.com/repl?j=SkTLpjYr-nconst MaybeError = ({errorMessage}) =>n <div>n {errorMessage && <ErrorDisplay message={errorMessage} />}n </div>;n n// The MaybeError component uses the ErrorDisplay component:nconst ErrorDisplay = ({message}) =>n <div style={ { color: red, backgroundColor: yellow } }>n {message}n </div>;n// Now we can use the MaybeError component:nReactDOM.render(n <MaybeErrorn errorMessage={Math.random() > 0.5 ? Not good : }n />,n mountNoden);n
上面的 MaybeError 組件,如果有errorMessage傳入將展示ErrorDisplay組件. React 會將 {true}, {false}, {undefined}, 和 {null} 視為有效的子元素,只不過不顯示出來罷了.
你還可以在JSX里使用函數式集合方法(map, reduce, filter, concat等等),因為它們返回的也是一個表達式:
// 例8 - Using an array map inside {}n// https://jscomplete.com/repl?j=SJ29aiYH-nconst Doubler = ({value=[1, 2, 3]}) =>n <div>n {value.map(e => e * 2)}n </div>;n// Use itnReactDOM.render(<Doubler />, mountNode);n
上面的例子,我給了屬性一個默認值,同樣還在div裡面輸出了一個數組表達式。React接受這種操作,他將把每一個乘以二後的數字用text node 展示出來.
#4: 你可以用JS class 來寫React組件
簡單函數組件只能滿足簡單需求,但實際開發中我們需要更複雜的組件。 React 支持通過class語法來寫組件. 這裡給出上面的Button例子的改寫:
// 例9 - Creating components using JavaScript classesn// https://jscomplete.com/repl?j=ryjk0iKHbnclass Button extends React.Component {n render() {n return <button>{this.props.label}</button>;n }n}n// Use it (same syntax)nReactDOM.render(<Button label="Save" />, mountNode);n
class語法很簡答. 定義一個繼承自React.Component(另一個頂級React API)的class. 這個 class 定義了一個單例函數 render(), 該函數返回一個 virtual DOM 對象. 當我們使用這個class-based 的 Button組件的時候 (例如, <Button ... />), React 將從這個 class-based 組件實例化一個對象並將該對象放入 DOM 樹中.
這就是為什麼我們要在render函數里的JSX上使用this.props.label. 當組件初始化的時候每一個組件實例都會有一個專有的實例屬性props傳給組件實例.
既然我們有一個和單一組件使用有關的實例,那麼我們可以按照意願修改該實例。例如:
// 例10 - Customizing a component instancen// https://jscomplete.com/repl?j=rko7RsKS-nclass Button extends React.Component {n constructor(props) {n super(props);n this.id = Date.now();n }n render() {n return <button id={this.id}>{this.props.label}</button>;n }n}n// Use itnReactDOM.render(<Button label="Save" />, mountNode);n
我們也可以自定義屬性方法並在組件任何地方使用它:
// 例11 — Using class propertiesn// https://jscomplete.com/repl?j=H1YDCoFSbnclass Button extends React.Component {n clickCounter = 0;nhandleClick = () => {n console.log(`Clicked: ${++this.clickCounter}`);n };n n render() {n return (n <button id={this.id} onClick={this.handleClick}>n {this.props.label}n </button>n );n }n}n// Use itnReactDOM.render(<Button label="Save" />, mountNode);n
在上面的例子里:
- handleClick 函數 使用的是新的類域語法. 它還處在 stage-2階段,但是這是最好的使用組件實例的方式(感謝尖頭函數的出現).但是,你需要用Babel這樣的編譯器,來讓瀏覽器認識這些新語法. 網上有babel相關的資料大家可以看看.
- 我們也同樣用了類域語法定義了clickCounter變數,這讓我們省略了類構造函數的使用。
- 當我們指定handleClick函數作為React專有屬性onClick的屬性值,我們沒有調用該函數,我們只是傳了一個該函數的引用. 在那個級別調用方法是寫React一個常犯的錯誤.
// Wrong:nonClick={this.handleClick()}n// Right:nonClick={this.handleClick}n
#5: React的事件: 兩大不同
在遇到React事件的時候,我們需要知道兩個不同於DOM API的點:
- 所有的react元素屬性都適用駝峰命名而不是小寫. 如:onClick非onclick.
- 我們傳遞的是函數的應用作為事件的處理器而不是字元串。如:onClick={handleClick}, 非onClick="handleClick".
React 把DOM的事件對象包裝到了自己的對象裡面,並對事件處理的性能做了優化。但是在事件處理器裡面我們同樣可以使用DOM事件對象裡面的所有可用方法。每一次事件調用React都會將該包裝後的事件對象傳遞過去. 例如,禁用form表單的默認提交事件可以這麼做:
// 例12 - Working with wrapped eventsn// https://jscomplete.com/repl?j=HkIhRoKBbnclass Form extends React.Component {n handleSubmit = (event) => {n event.preventDefault();n console.log(Form submitted);n };n n render() {n return (n <form onSubmit={this.handleSubmit}>n <button type="submit">Submit</button>n </form>n );n }n}n// Use itnReactDOM.render(<Form />, mountNode);n
#6: 每個組件都有一個生命周期
下面幾點僅適用於class-based組件(繼承自 React.Component). function-based的組件稍有不同.
- 定義一個讓React創建元素的模版.
- 然後, 我們委託React來使用它。比如,ReactDOM.render里,或者別的組件的render方法里。
- 接著,React實例化一個元素並傳入一個props集合,這個集合我們可以通過this.props來訪問. 這些props實際上是通過上面第二步傳入的.
- 由於都是JS,其構造函數將會被調用(如果定義了的話). 這也是我們所說的第一個組件生命周期函數.
- React會計算出render函數輸出的結果 ( virtual DOM 節點).
- 這個時候React開始渲染元素, React 將會和瀏覽器通信 (對於我們來說就是開始使用DOM API) 將元素展示到瀏覽器里. 這個過程被叫做 mounting.
- 接著React開始啟用另一個生命周期方法 componentDidMount. 我們可以利用這個方法,例如,可以在這裡操作DOM. 早於這個方法的DOM都是虛擬DOM.
- 有些組件生命到這裡就結束了.其他的組建將會由於各種各樣的原因收到瀏覽器DOM的unmounted狀態 . 如果後者發生,此時React 將啟用componentWillUnmount.
- 裝在後的元素的狀態可能發生改變. 父元素也可能重新渲染.同時,裝在後的element也可以接受到一個不同的props.React的神奇就在這裡! 這也是我們為什麼需要React的原因.
- 組件生命繼續,在此之前我們先來理解一下什麼是state.
#7: 每個組件都有一個私有狀態state
下面的內容也僅適用於class-based組件。
State 類域在React class組件都是特有的。React監視這個每一個組件的state的改變。為了高效的實現這個機制,我們需要通過另外一個頂級的React API來修改state,那就是this.setState:
// 例13 - the setState APIn// https://jscomplete.com/repl?j=H1fek2KH-nclass CounterButton extends React.Component {n state = {n clickCounter: 0,n currentTimestamp: new Date(),n };n n handleClick = () => {n this.setState((prevState) => {n return { clickCounter: prevState.clickCounter + 1 };n });n };n n componentDidMount() {n setInterval(() => {n this.setState({ currentTimestamp: new Date() })n }, 1000);n }n n render() {n return (n <div>n <button onClick={this.handleClick}>Click</button>n <p>Clicked: {this.state.clickCounter}</p>n <p>Time: {this.state.currentTimestamp.toLocaleString()}</p>n </div>n );n }n}n// Use itnReactDOM.render(<CounterButton />, mountNode);n
這是一個需要理解的很重要的例子。這將完善你的React的知識。理解了這個例子後,你只需要在學習一點JS技巧方面的知識就可以入門了。
從class域開始讓我們回顧一下例13,有兩個類域。
- 第一個是私有state,它包含兩個屬性clickCounter和currentTimestamp.
- 另一個是handleClick函數,該函數的引用被傳給button的onClick 事件屬性。它會通過setState來改變組件的狀態。
另外我們還在componentDidMount方法裡面調用一個定時輪詢來改變狀態,每秒調用一下this.setState,在render函數里我們就使用了一下這兩個屬性,沒別的特殊API。
不知道你注意到了沒有?我們使用了兩種不同的方法來更新state。
- 通過傳遞返回對象的函數.
- 直接傳遞一個普通的對象.
兩種方式都可行,但第一種適用於當你需要同時讀和寫state的時候(就像我們這樣)。在定時器函數裡面,我們只需要對state進行寫操作不用讀。當你實在分不清楚的時候那你就用函數作為參數的用法。在競態條件(譯者註:當兩個線程競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件)下更加安全,因為setState實際上是一個非同步方法。
那麼我們怎麼更新state的呢?我們在函數里返回一個我們需要更新的新對象。還有一點,可以看到我們在更新state的時候僅僅只用傳遞state的一個屬性就好了,而不用都傳遞。這完全可行的,因為setState實際上對傳入的新值是一個merge操作。所以沒有傳的那部分值說明我們不希望它改變而不是把它刪掉。
#8: React 將會響應你的變化
React 之所以叫React 是基於他會響應state的變化(即使不響應也在響應的路上)。還有一種說法是React應該起名叫Schedule。
然而,當任何組件的狀態更新時,我們用肉眼看到的是React對該更新做出反應,並自動反映瀏覽器DOM中的更新(如果需要更新到DOM).
這裡思考render函數的兩種輸入:
- 通過父組件傳入的props
- 可以隨時更新的內部狀態state
當render函數的輸入改變時,它的輸出可能也會發生改變。
React保留了渲染歷史的記錄,當它看到一個渲染與前一個渲染不同時,它將計算它們之間的差異,並有效地將其轉換為在DOM中執行的實際DOM操作。
#9: React是你的代理人
您可以將React視為我們聘請的與瀏覽器通信的代理。以上面的顯示當前時間戳為例。我們不是去手動的用DOM API來操作p#timestamp讓其每秒更換一下時間戳,而是更組件狀態的屬性值,然後讓React代表我們去和瀏覽器溝通. 我相信著就是react為什麼這麼受歡迎的原因. 我們討厭和瀏覽器先生(還說著各種帶有口音的DOM方言)打交道,React志願為我們做這些事情,還是免費的~
#10: 組件周期 ( 2)
現在我們已經知道了一個組件的狀態,當它改變的時候會有神奇的事情發生。接下來讓我們繼續把後面的生命周期里的概念給理解清楚吧。
- 當一個組件的state或者其父組件傳遞的props發生改變的時候組件就會重新渲染.
- 如果是後者即props發生改變時 React 會調用另外一個周期函數componentWillReceiveProps.
- 如果兩者都發生改變,React會做一個重要決策,該組件是否需要在瀏覽器里被更新?這也是為什麼會調用另外一個周期函數shouldComponentUpdate的原因. 這個方法實際上也是在問一個問題,所以如果你想自定義或者優化你的渲染過程,你就需要通過返回一個true或者false來回答這個問題。
- 如果沒有手動指定shouldComponentUpdate, React 會默認作出聰明的決策,多數情況下也是足夠良好的.
- 首先, 這時候React會調用componentWillUpdate方法. 然後計算新的渲染產出把它和上一次的渲染產出進行比較.
- 如果沒什麼改變,那麼就什麼也不做.
- 如果有改變則把差異反應到瀏覽器上.
- 無論什麼情況,儘管更新會發生在任何地方(甚至計算出來的產出是相同的),React 最終都會調用另一個周期方法componentDidUpdate.
生命周期函數實際上就是一個逃逸艙口。如果你不做什麼特別的事情,你可以不用他們也可以創建一個完整的應用。它們會非常方便地分析應用程序中發生的情況,並進一步優化了React更新的性能。
最後
看完這篇文章我相信你可以開始創建一個有趣的React應用了,如果想了解更多,可以關注我的React課程,同時我強烈推薦這本書Learning React book Alex 和 Eve寫的。
謝謝您的品讀,如果你覺得文章有用就分享給身邊的人吧。關注我的專欄獲取更多關於React 和 JS新知識。
推薦閱讀:
※實戰 | 用原生js寫一個"多動症"的簡歷
※閱讀vue.js源碼可以從哪幾方面入手?
※Angular UI 框架:Element Angular 發布 0.0.4-alpha.3 版本