React官方教程筆記(React 15 4 X)

本筆記基於React官方文檔,當前React版本號爲15.4.0。javascript

1. 安裝

1.1 嘗試

開始以前能夠先去codePen嘗試一下,也能夠下載這份HTML文件並編輯它來嘗試React。html

1.2 Creat React App工具

推薦使用React官方提供的Creat React App工具,來快速新建React單頁面應用項目。前端

npm install -g create-react-app
create-react-app hello-world
cd hello-world
npm start
複製代碼

1.3 推薦工做流

雖然React能夠在沒有任何構建工具的狀況下進行使用,但在生產環境仍是應該使用成套的構建工具來將React用於你的項目。一個現代化的(前端)工做流一般由如下三部分組成:java

  • 包管理器:好比YarnNpm,可讓你更方便使用第三方庫而不用本身造輪子
  • 編譯器:好比Babel,能翻譯使用了最新語法的代碼到瀏覽器兼容較好的版本
  • 打包器 :好比WebpackBrowserify,讓你可以編寫各類風格的模塊化的代碼,由它們打包和壓縮

基於以上工做流,你能夠經過Npm或者Yarn來將React安裝到項目,而後使用Babel來編譯JSX和ES6語法,最終用於生產環境的代碼還須要通過WebpackBrowserify的打包和壓縮才能使用。react

1.4 CDN服務

<!--開發環境-->
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
<!--生產環境-->
<script src="https://unpkg.com/react@15/dist/react.min.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>
複製代碼

2. Hello World

一個最基本的React例子:webpack

ReactDom.render(
  <h1>Hello world!</h1>,
  document.getElementById('root')
)
複製代碼

你能夠在CodePen上嘗試修改這段代碼看看效果。git

React推薦配合ES6語法使用,但僅須要瞭解() => {}constlet`template literals`classes這幾個特性便可github

3. 初識JSX

const element = <h1>hello world</h1>
複製代碼

上面這段既不是字符串又不是HTML的代碼(其實主要指的是<h1>hello world</h1>)就是JSX了。官方推薦搭配使用JSX,有別於模板語言,JSX是全功能的JavaScript。JSX 用於建立「React元素」。web

3.1 JSX是表達式

跟其餘JavaScript表達式同樣,JSX也是表達式,被React編譯後的JSX返回的是普通的JavaScript對象,這意味着你能夠相似對待普通JavaScript表達式那樣對待一個JSX語句:將它賦值給變量、將他做爲函數參數或返回值等等:算法

function getGreating (user) {
  if (user) {
    return <h1>hello {formatName(user)}!</h1>
  }
  return <h1>hello world!</h1>
}
複製代碼

稍微深刻一點,Babel會將JSX轉換成對react.creatElement()的調用,因此下面兩種寫法徹底等價:

// JSX
const mine = (
  <h1 className="greeting"> 這是個人標題 </h1>
)

// javaScript
const yours = react.creatElement(
  'h1',
  { className: 'greeting ' },
  '這是你的標題'
)
複製代碼

然而react.createElement()返回的結果是相似下面這樣的一個對象:

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: '這是誰的標題'
  }
 // ...
}
複製代碼

這就不難理解JSX的用法了——像一個javaScript表達式那樣去使用。

3.2 在JSX中嵌入JavaScript表達式

使用花括號{},能夠在JSX中嵌入任意JavaScript表達式:

const element = (
  <h1> Hello, {formatName(user)}! </h1>
);
複製代碼

爲了提高可讀性能夠對JSX使用縮進和換行,可是爲了不JavaScript自動添加分號的機制給咱們帶來麻煩,應該在換行的JSX外面添加一對小括號。

在JSX的元素中插入用戶輸入的內容是安全的,React默認會對元素內的文本進行轉義以防止XSS攻擊。

3.3 在JSX中聲明屬性

就像在HTML中聲明元素屬性,能夠在「React元素」上直接聲明某個屬性。當但願屬性值是變量或引用時,則就像在在JSX中嵌入JavaScript表達式,使用花括號{}來插入「React元素」的值。

// 簡單屬性值
const element = <div tabIndex="0"></div>;
// 屬性值爲變量或引用
const element = <img src={user.avatarUrl}></img>;
複製代碼

須要注意的是,JSX中元素的屬性名統一使用駝峯寫法(camelCase),而且在React的內置元素上,諸如classfor等屬性還須要換成classNamehtmlFor來使用(自定義元素能夠正常使用)。

3.4 在JSX中聲明子元素

若是「React元素」的標籤內沒有子元素,則能夠像在XML中那樣使用單標籤(包括React內置的HTML元素)。

const element = <img src={user.avatarUrl} />; 複製代碼

若是存在子元素,則就像在HTML中那樣直接包裹在父元素中便可(注意換行的JSX要加小括號()):

const element = (
  <div> <h1>Hello!</h1> <h2>Good to see you here.</h2> </div>
)
複製代碼

4. 渲染元素

元素是React應用的最小組成部分。元素描繪了界面。不一樣於瀏覽器的DOM元素,React元素是簡單對象,建立它們比建立真實的DOM元素要節省太多性能,同時React DOM負責將React元素和真實DOM元素對應起來:

const ele = <h1>Hello World!</h1>
複製代碼

不能將React元素和React組件搞混,React元素是React組件的組成部分,一個React組件由一個或多個React元素組成。同時也要注意區別DOM元素和React元素,DOM元素指的是HTML標準中規定的具體的某個元素,而React元素其實是用於告訴React如何渲染頁面、渲染時用到哪些DOM元素的一個配置對象,它與DOM元素不是一個概念。

4.1 將React元素渲染到DOM中

先建立一個React元素,而後用ReactDOM.render()將其渲染到DOM的某個元素中(就這麼簡單):

const ele = <h1>Hello World!</h1>
ReactDOM.render(
  ele,
  document.getElementById('root') // 假設頁面上有一個id爲root的元素
)
複製代碼

4.2 更新已經渲染的元素

請記住,React元素是不可變的,一旦建立,你就不能再直接改變它的屬性或子元素。假如咱們要更新上面已經渲染到idroot的元素中的React元素,那麼在沒有其餘手段的前提下就只能是像電影膠片同樣一幀一幀進行刷新:

function tick() {
  const element = (
    <div> <h1>Hello World!</h1> <p>{new Date().toLocaleTimeString()}</p> </div>  
  )
  ReactDOM.render(
    ele,
    document.getElementById('root') // 假設頁面上有一個id爲root的元素
  )
}
setInterval(tick, 1000) // 每秒刷新
複製代碼

固然正常狀況下咱們不會這麼作,可是這裏很好的演示了另一個問題——React在渲染頁面時都作了什麼?答案是它只渲染了與上次渲染時DOM中不一樣的部分!React會比較當前渲染與上次渲染時DOM中的不一樣之處,並只刷新這些地方!

[圖片上傳失敗...(image-89394-1535353259915)]

5. 組件和props(輸入屬性)

組件能讓你將UI分割成獨立的可複用的片斷,這些片斷都有各自隔離的做用域,不會互相干擾。你能夠將組件理解成相似函數的概念,組件從它的props屬性接受參數,而後返回React元素來描述UI。

5.1 用函數和類(class)定義組件

最簡單的定義組件的方式就是寫一個構造函數:

function Welcom (props) {
  return <h1>hello, {props.name}</h1>
}
複製代碼

上面這個Welcom構造函數就是一個合法的React組件,由於它接受一個對象做爲參數,而後返回React元素。咱們稱這樣的組件爲「函數式」的組件由於它就是一個JavaScript構造函數。固然也可使用ES6的class特性來定義函數:

class Welcom extends React.Component {
  render () {
    return <h1>hello, {this.props.name}</h1>
  }
}
複製代碼

ES6的class特性實際上是ES5的構造函數和對象繼承特性的一個語法糖,上面的寫法也徹底能夠轉換爲ES5的寫法。React推薦這種寫法存粹是由於寫起來方便,可讀性也更強。但這種寫法的重點是從React.Component繼承一些核心的屬性,後文還會細說。不過目前簡單起見,咱們暫時還只是用簡單函數來建立組件。

5.2 渲染組件

React元素不只僅能夠用於指定須要使用的DOM元素,也能夠用於指代自定義的組件:

// 指代須要使用的DOM元素
const ele1 = <div />
// 指代用戶自定義的組件
const ele2 = <Welcom name="Sara">
複製代碼

當React遇到像<Welcom name="Sara">這種自定義組件時,它會將JSX屬性(也就是React元素屬性)都放在一個對象中(這個對象就是props)並將其傳遞給組件的構造函數,構造函數再返回React元素用於渲染。

5.3 組件的組合

既然React元素可用於指代自定義組件,那麼組件之間就能夠相互嵌套使用:

function Welcom (props) {
  return <h1>Hello, {props.name}</h1>
}
function WelcomList () {
  return (
    <div>
      <Welcom name="Sara" />
      <Welcom name="Lily" />
      <Welcom name="Tom" />
    </div>
  )
}
function App () {
  return <WelcomList />
}
ReactDOM.render(
  <App />,
  document.getElementById('root')
)
複製代碼

5.4 組件的提取

既然組件能夠嵌套組合使用,咱們就能夠將一個大的組件分割成不少小的組件。React官方鼓勵對UI進行切割,分紅不一樣的組件來實現。基本上一組React元素是否要提取成組件,可從如下兩點考慮:

  • 這組元素在別的地方也要使用
  • 這組元素內部的功能相對複雜

這部分實際上是組件化的思路,這裏再也不展開。

5.5 只讀的props

相似於「純函數」的概念(不會改變任何外部的值,包括輸入的參數,即與外部徹底無耦合),無論是使用構造函數仍是類來定義組件,組件都不該該修改它的props,由於這是輸入到組件中的參數。在這一點上,React作了嚴格限定:

全部的React組件必須像「純函數」那樣永遠不修改本身的props屬性

6. state(私有狀態)和生命週期

咱們以上文的時鐘的例子來理解組件的私有狀態和生命週期。

function tick() {
  const element = (
    <div> <h1>Hello World!</h1> <p>{new Date().toLocaleTimeString()}</p> </div>  
  )
  ReactDOM.render(
    ele,
    document.getElementById('root') // 假設頁面上有一個id爲root的元素
  )
}
setInterval(tick, 1000)
複製代碼

首先咱們將時鐘做爲組件提取出來:

// 時鐘組件
function Clock(props) {
 return (
    <div> <h1>Hello World!</h1> <p>{props.date.toLocaleTimeString()}</p> </div>  
  )
}
// 從新渲染
function tick () {
  ReactDOM.render(
    <Clock date={new Date()} />, document.getElementById('root') // 假設頁面上有一個id爲root的元素 ) } // 每秒刷新 setInterval(tick, 1000) 複製代碼

咱們發現對於Clock組件來講,刷新時間的功能其實徹底與外部無關,它不涉及到任何外部的變量,徹底能夠由Clock組件本身來實現而不是讓外部傳遞時間給它。此時Clock組件就須要「私有狀態」來實現這個功能了。

6.1 從React.Component上繼承

到目前爲止,咱們使用簡單的構造函數來建立React組件,無論外部輸入屬性仍是私有狀態,都須要咱們手動建立和管理,諸如修改私有狀態後刷新渲染,外部輸入屬性爲只讀這類功能,若是咱們沒有在構造函數中手動實現則不會存在。

這時咱們能夠從React.Component這個React內置的構造函數上繼承一些有用的方法,這其中就包括對「私有狀態」和「生命週期」實現。咱們可使用ES6的class特性來實現這個繼承(固然這不是必須的,徹底可使用ES5的構造函數和原型的寫法,但那樣會繁瑣不少,可讀性也大大降低):

class Clock extends React.Component {
  render () { // React提供的用於渲染和刷新組件的鉤子函數
    return (
      <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div>
    )
  }
}
複製代碼

6.2 定義組件私有狀態

React.Component提供了propsstate來分別訪問外部輸入屬性和內部私有狀態。咱們能夠在時鐘組件中經過state訪問私有狀態,而後在其構造函數中對該私有狀態進行初始化,最後將它渲染到頁面上:

class Clock extends React.Component {
  constructor (props) {
    super(props) // ES6中類的constructor函數能夠經過super訪問其父類的構造函數
    this.state = { date: new Date() }
  } // 注意,ES6中類的方法之間不須要任何符號
  render () {
    return (
      <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
    )
  }
}
ReactDOM.render(
  <Clock />, // 外部再也不干涉Clock組件的刷新功能 document.getElementById('root) ) 複製代碼

注意Clock類中的constructor構造函數中,調用了父類的構造函數,這是爲了實現徹底的繼承。使用class特性建立React組件時應當老是執行這一步。

6.3 添加生命週期函數

從組件被建立到組件被渲染到頁面到最終被銷燬,React提供了一系列的「生命週期鉤子」,用於在組件的不一樣階段調用回掉函數。爲了讓Clock組件可以本身刷新,咱們但願在組件被建立後當即添加一個計時器進行每秒刷新,同時在組件被銷燬時一併銷燬這個計時器,這樣咱們就須要用到兩個生命週期鉤子函數:

  • componentDidMount:組件被渲染到頁面後執行
  • componentWillUnmount:組件被銷燬前執行
class Clock extends React.Component {
  constructor (props) {
    super(props) // ES6中類的constructor函數能夠經過super訪問其父類的構造函數
    this.state = { date: new Date() }
  } // 注意,ES6中類的方法之間不須要任何符號
  render () {
    return (
      <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
    )
  }
  componentDidMount () {
    this.timerID = setInterval(this.tick.bind(this), 1000)
  }
  componentWillUnmount() {
    clearInterval(this.timerID)
  }
}
ReactDOM.render(
  <Clock />, // 外部再也不干涉Clock組件的刷新功能 document.getElementById('root) ) 複製代碼

注意咱們將定時器存儲在了組件實例上,而不是state中,請先記住一個原則:任何沒有在組件的render()函數中使用的變量,都不該該存放在state

而後再添加tick方法。在這個方法中咱們須要改變組件state中的date的值,這時須要用到方法setState(),該方法會通知React如今state已經改變了,然後React會去從新調用組件的Render()方法刷新DOM。這也是爲何會有**任何沒有在組件的render()函數中使用的變量,都不該該存放在state中 **一說:

class Clock extends React.Component {
  constructor (props) {
    super(props) // ES6中類的constructor函數能夠經過super訪問其父類的構造函數
    this.state = { date: new Date() }
  } // 注意,ES6中類的方法之間不須要任何符號
  render () {
    return (
      <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
    )
  }
  componentDidMount () {
    this.timerID = setInterval(this.tick, 1000)
  }
  componentWillUnmount() {
    clearInterval(this.timerID)
  }
  tick () {
    this.setState({ date: new Date() }) // 該方法會觸發React調用實例的render方法進行重繪
  }
}
ReactDOM.render(
  <Clock />, // 外部再也不干涉Clock組件的刷新功能 document.getElementById('root) ) 複製代碼

6.4 組件生命週期小結

    1. 當把組件傳遞給ReactDOM.render()函數後,React會調用組件的構造函數constructor,進行一些初始化
    1. 而後React會去調用Clock組件的render()方法將組件渲染出來
    1. 當組件渲染完畢後,React會調用componentDidMount()生命週期鉤子函數
    1. setState()函數被調用時,React會從新調用組件的render()方法進行重繪
    1. 當組件被從DOM中移除時,React會調用componentWillUnmount()生命週期鉤子函數

6.5 setState注意事項

  • 不要直接改變state 直接對組件state中的屬性賦值將不會觸發DOM更新,由於React並不知道state被改變了
  • state的更新多是異步的 React會一次處理多個對setState的調用以提升性能,因此調用setState()時不該當直接基於另一些來自stateprops中的屬性進行計算,頗有可能當前計算的值並非最終的值,當用於計算的另外一些值再次變化後,React並不會刷新DOM(由於沒有再次調用setState())。爲了修正這點,React提供另外一種調用setState()函數的方式:傳入一個函數,而不是對象
// 錯誤的用法
this.setState({
  counter: this.state.counter + this.props.increment
})
// 正確的用法
this.setState((prevState, props) => ({ // 接受一個表示前次state的參數和一個當前props的參數
  counter: prevState.counter + props.increment // 這裏其實是返回了一個對象,是ES6箭頭函數的簡寫
}))
複製代碼
  • setState是對象的合併而不是替換 setState方法是將傳入的參數對象或函數返回的對象與現有的state對象進行合併,很是相似於使用Object.assign(prevState, newState)的效果

6.6 單項數據流

在React組件的嵌套中,父組件經過props向子組件傳遞數據,無論傳遞進來的數據是來自於父組件的props仍是state仍是別的地方,子組件不知道也不用關心,由於它不能修改經過props傳遞進來的數據而只能讀取它。這樣,數據就能夠從最外層的父組件一路向內傳遞下去,但反過來卻不行。

這就是傳說中的「單項數據流」("top-down" or "unidirectional" data flow)了:每一個組件只能修改自己和其子組件的數據,而不能修改父組件的數據。這樣的好處不言而喻,數據和狀態的管理會更加方便,但有時候在應用愈來愈複雜的時候,可能須要多個組件共享某些數據或狀態,所以誕生了不少用於管理數據和狀態的庫,redux就是其中最有名的一個。

7. 事件

7.1 基本用法

在React中綁定事件跟直接在HTML中綁定事件很是類似,定義一個事件處理函數,並在JSX中綁定它:

function Greeting () {
  function sayHi(e) {
    e.preventDefault()
    console.log('Hi!')
  }
 return (
    <a onClick={sayHi}>Click me to say hi!</a>
 )
}
複製代碼

全部事件綁定屬性好比onClick均使用駝峯寫法(camelCase),事件綁定屬性的值不是字符串而是事件處理函數名稱,能夠帶上()並傳參,無參數時可省略()

7.2 使用類定義組件時事件處理函數this的指向問題

使用ES6的class特性定義組件時,一般的作法是將事件處理函數看成該類的方法寫在類中。但須要注意的是方法的this指向。

定義在類中的方法的默認的this指向的是當前的類的實例,但事件處理函數由於是綁定到了具體的元素上,就會丟失定義時this的指向。若是你的處理函數中使用了this關鍵字來指向當前組件實例,那麼你須要手動將該方法的this綁定到當前組件實例,有三種方法能夠進行綁定:

1)在類的constructor中調用或在JSX中調用Function.prototype.bind()手動綁定

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
    this.handleClick = this.handleClick.bind(this); // 手動綁定
  }

  handleClick() {
    // console.log(this)
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      // <button onClick={this.handleClick.bind(this)}> // 在這裏綁定也能夠
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('content')
);
複製代碼

2)在JSX的事件綁定屬性中的事件處理函數外層再套一個箭頭函數,在其中返回處理函數調用結果

render() {
  return (
    <button onClick={(e) => this.handleClick(e)}> // 這麼綁定也行 Click me </button>
  );
}
複製代碼

3)Babel提供的一個ES8+的實驗性質的寫法

class LoggingButton extends React.Component {
  handleClick = () => { // 純粹的實驗性質的寫法,須要babel的支持
    console.log('this is:', this);
  }
  render() {
    return (
      <button onClick={this.handleClick}> Click me </button>
    );
  }
}
複製代碼

7.3 事件對象

React的事件對象是一個徹底由React給出的事件對象,該對象對各個瀏覽器作了兼容,同時保留了標準事件對象的接口,詳細信息能夠查看React官網的參考。使用時須要關心的是如何在事件處理函數中使用事件對象。

在事件綁定的JSX中,處理函數接受一個名爲event的參數來表示事件對象,能夠認爲event在事件綁定插值中屬於React的保留字,若是須要往事件處理函數中傳遞更多參數,請使用其餘標識符。

另外,7.2小節中不一樣的事件綁定寫法也對事件對象的處置略有不一樣,主要體如今事件綁定JSX中:

// 無括號
<button onClick={this.handleClick}>
  Click me
</button>

// 帶括號
<button onClick={this.handleClick(event)}>
  Click me
</button>

// 調用了bind()
<button onClick={this.handleClick.bind(this, event)}>
  Click me
</button>
複製代碼
  • 當事件綁定插值中的處理函數省略了()時,處理函數默認接受一個表示事件對象的參數,
  • 當事件綁定插值中的處理函數未省略()時,則須要顯示地使用保留字event來傳入事件對象,未傳入則爲undefined注意,無論有沒有在constructor中綁定this,直接在處理函數名後加()會致使頁面初始化時該函數被當即執行一次,可能會有意想不到的錯誤,好比不能調用setState()方法等,因此強烈不建議用這種寫法
  • 當事件綁定插值中的處理函數調用了bind()時,能夠顯示地使用保留字event來傳入事件對象,不然React會在bind()函數參數序列的末尾默認增長一個表示事件對象的參數

最後,在React中不能經過return false來阻止默認事件,而是須要在事件處理函數中顯式調用event.preventDefault()

8. 條件渲染

全部的JavaScript條件語句均可以用於React條件渲染,由於本質上JSX就是JavaScript的擴展語言。基於此有三種經常使用的條件渲染:

  • if...else...
function UserGreeting () {
  return <h1>Welcom back!</h1>
}
function GuestGreeting () {
  return <h1>Please Sign up.</h1>
}
function App (props) {
  if (!props.isLoggedIn) {
    return <GuestGreeting />
  }
  return <UserGreeting />
}

ReactDOM.render(
  <App isLoggedIn={false} />,
  document.getElementById('root')
)
複製代碼
  • 三元運算符
function App (props) {
  return props.isLoggedIn ? <UserGreeting /> : <GuestGreeting /> } 複製代碼
  • 短路
function App (props) {
  return props.isLoggedIn && <UserGreeting /> // props.isLoggedIn爲true則顯示UserGreeting,不然不顯式 } 複製代碼

若是判斷邏輯比較複雜,不能用三元或者短路表達式編寫,且判斷後的結果須要直接用在JSX中(JSX中只能經過{}插入表達式,而不能使用語句),則可以使用if...else...語句判斷並將結果保存到變量,而後再返回變量或經過{}插值到JSX中:

function UserGreeting () {
  return <h1>Welcom back!</h1>
}
function GuestGreeting () {
  return <h1>Please Sign up.</h1>
}
function Button (props) {
  return <button onClick={ props.handleToggle }>toggle me</button>
}

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      prevState: false
    }
  }
  handleClick () {
    this.setState(prevState => ({ isLoggedIn: !prevState.isLoggedIn }))
  }
  render () {
    let greeting = this.state.isLoggedIn ? <UserGreeting /> : <GuestGreeting />
    return (
      <div>
        <div><Button handleToggle={this.handleClick.bind(this)} /></div> // 注意this的重定向
        { greeting }
      </div>
    )
  }
}

ReactDOM.render(
  <App isLoggedIn={false} />,
  document.getElementById('root')
)
複製代碼

另外,在組件的render函數中返回假值,會阻止組件渲染,結合條件判斷,可以達到隱藏或顯示組件的目的。

9. 列表和key(索引)

9.1 渲染列表

能夠像下面這樣渲染一個列表:

class List extends React.Component {
  constructor (props) {
    super(props)
  }
  render () {
    let list = this.props.number.map(number => ( // 拼裝li
      <li>{number}</li>
    ))
    return (
      <ul>{list}</ul>
    )
  }
}

ReactDOM.render(
  <List number={[1, 2, 3, 4, 5]} />,
  document.getElementById('root')
)
複製代碼

也能夠將map()調用經過{}內聯到JSX中:

class List extends React.Component {
  constructor (props) {
    super(props)
  }
  render () {
    return (
      <ul>{ this.props.number.map(number => ( // 內聯map()方法 <li key={number}>{number}</li> )) }</ul>
    )
  }
}
複製代碼

一般會使用數組的map()方法來從數組拼裝列表,這與使用JavaScript拼裝HTML相似。但上面的代碼運行時會出現警告:

列表渲染報錯

9.2 key

在渲染列表時,React的差別比較算法須要一個在列表範圍內的惟一key來提升性能(一般用於獲知哪一個列表項改變了)。這個惟一的key須要咱們手動提供。React官方建議使用列表數據中可用於惟一性標識的字段來做爲列表項渲染時的key。若是實在沒有,則可以使用數組的index勉爲其難,性能上可能會打折扣。

let list = this.props.number.map(number => ( // 拼裝li
      <li key={number.toString()}>{number}</li>
    ))
複製代碼

key的使用須要注意一下幾點:

  • 只能在數組內指定key:準確地說,只能在map()的回調函數中使用key
  • key須要在列表範圍內保證惟一性:同一個數組中的key須要保證惟一性,但不一樣數組中的key無所謂
  • key不會做爲props傳入組件:能夠認爲key是React在JSX中的保留字,你不能用它來向組件傳遞數據而應該改用其餘詞

10. 表單

在React中存在一個「受控組件(Controlled Component)」的概念,專門指代被React控制了的表單元素。經過onChange事件的處理函數將表單元素值的變化映射到組件的state中,而後再將組件中的這個映射好的值經過{}在JSX中插值給表單元素的value,(兩者缺一不可)這就是一個被React控制了的組件也即「受控組件」了。

class Form extends React.Component {
  constructor (props) {
    super(props)
    this.state ={
      inputTextValue: ''
    }
    this.handleInputTextChange = this.handleInputTextChange.bind(this)
  }
  render () {
    return (
      <form> <input value={this.state.inputTextValue} // 從state中將值綁定到表單元素 onChange={this.handleInputTextChange}/> </form> ) } handleInputTextChange (e) { this.setState({ inputTextValue: e.target.value // 將表單元素的值的變化映射到state中 }) } } ReactDOM.render( <Form />, document.getElementById('root') ) 複製代碼

基本上全部表單元素的使用都跟上例同樣,經過value來「控制」元素,讓state成爲組件惟一的狀態保存地。可是有時候在非React項目中使用React或者一些其餘緣由,咱們不但願使用受控組件時,能夠選擇「非受控組件」技術,這裏再也不展開。

11. 共享狀態提高

考慮下面的需求,頁面上有兩個輸入框,用來輸入貨幣數量,一個輸入美圓,一個輸入人民幣,還有一行提示文字例如:「咱們有1美圓,也就是6.9元」;要求兩個輸入框隨意輸入一個,另外一個輸入框會根據匯率自動顯示轉換後的貨幣數量,而且下方提示文字也跟隨變化。

一般狀況下,咱們會編寫一個用於輸入貨幣數量的組件,而後在頁面上放兩個這樣的組件:

const exchangeRate = 6.9339
const currency = {
  '$': '美圓',
  '¥': '人民幣'
}
class CurrencyInput extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      value: ''
    }
    this.changeHandler = this.changeHandler.bind(this)
  }
  render () {
    return(
      <div>
        <label>
          {currency[this.props.currency]}:
          <input value={this.state.value} onChange={this.changeHandler}/>
        </label>
      </div>
    )
  }
  changeHandler (e) {
    this.setState({
      value: e.target.value
    })
  }
}
class App extends React.Component {
  constructor (props) {
    super(props)
  }
  render () {
    return(
      <div>
        <CurrencyInput currency={'$'}/>
        <CurrencyInput currency={'¥'} />
        <p>咱們有{}美圓,也就是{}元</p>
      </div>
    )
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
)
複製代碼

在上面的代碼中咱們將貨幣種類經過props傳遞給輸入框組件,分別顯示了美圓和人名幣的輸入框。而後在輸入框組件內部,咱們使用了上一節的「受控組件」技術,將輸入框的值交由組件的state控制。但並無完成需求——兩個輸入框並不一樣步,同時組件外部也不知道組件中到底填了什麼值因此下面的提示語句也沒有更新。

不少時候,若干組件須要隱射同一個變化的狀態。咱們推薦將共享的狀態提高至它們最近的共同的祖先上。

就像官方推薦的那樣,這時咱們就須要用到共享狀態提高技術:咱們要將兩個貨幣輸入框組件共享的「數量」狀態,提高到它們最近的祖先組件上,也就是App組件上。

// ...省略的代碼
class CurrencyInput extends React.Component {
  constructor (props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
  }
  render () {
    return(
      <div>
        <label>
          {CURRENCY[this.props.currency]}:
          <input value={this.props.value} onChange={this.handleChange}/> // 須要傳遞額外參數的狀況下只能再包一層
        </label>
      </div>
    )
  }
  handleChange (e) {
    this.props.onValueChange(e.target.value, this.props.currency) // 父級傳遞進來的回調函數
  }
}

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = { // 將共享狀態存放在祖先元素上
      dollar: '',
      yuan: ''
    }
    this.valueChangeHandler = this.valueChangeHandler.bind(this)
  }
  render () {
    return( // 經過props向下傳遞共享狀態和回調函數,不少狀況下子組件共享的狀態父級也須要用到
      <div>
        <CurrencyInput value={this.state.dollar} currency={'$'} onValueChange={this.valueChangeHandler}/>
        <CurrencyInput value={this.state.yuan} currency={'¥'} onValueChange={this.valueChangeHandler}/>
        <p>咱們有{this.state.dollar}美圓,也就是{this.state.yuan}元</p>
      </div>
    )
  }
  valueChangeHandler (value, type) {
    this.setState({
      dollar: type === '$' ? value : this.exchange(value, type),
      yuan: type === '¥' ? value : this.exchange(value, type)
    })
  }
  exchange (value, type) {
    return value * (type === '$' ? EXCHANGERATE : 1 / EXCHANGERATE)
  }
}
// ... 省略的代碼

複製代碼

其實無論是美圓仍是人民幣,其實背後都只有一個數量,這個數量同時表明了必定數量的美圓和必定數量的人民幣,因此更好地,咱們能夠也應該只存放一個狀態在父組件上,而後在渲染子組件時計算子組件的狀態並傳遞給他們:

// ... 省略的代碼
function exchange (value, type) { // 將轉換函數放到全局以便子組件能夠訪問
  return value * (type === '$' ? EXCHANGERATE : 1 / EXCHANGERATE)
}

class CurrencyInput extends React.Component {
 // ... 省略的代碼
  render () {
    // 子組件在渲染時本身計算本身的狀態
    let currentCurrency = this.props.currentCurrency
    let currency = this.props.currency
    let value = ''
    if (currentCurrency.value !== '' && !/^\s+$/g.test(currentCurrency.value)) {
      value = currentCurrency.type === currency ?
        currentCurrency.value : 
        exchange(currentCurrency.value, currentCurrency.type)
    }   
    return(
      <div>
        <label>
          {CURRENCY[currency]}:
          <input value={value} onChange={this.handleChange}/>
        </label>
      </div>
    )
  }
  // ... 省略的代碼
}

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      currentCurrency: { // 存儲一個值,這裏具體作法時存儲當前改變的值
        value: '',
        type: ''
      } 
    }
    this.valueChangeHandler = this.valueChangeHandler.bind(this)
  }
  render () {
    // 將共享的狀態傳遞給組件,同時父組件須要的狀態也本身計算出來
    return(
      <div>
        <CurrencyInput
          currentCurrency={this.state.currentCurrency}
          currency={'$'}
          onValueChange={this.valueChangeHandler}/>
        <CurrencyInput
          currentCurrency={this.state.currentCurrency}
          currency={'¥'}
          onValueChange={this.valueChangeHandler}/>
        <p>咱們有{exchange(this.state.currentCurrency.value, '$')}美圓,也就是{exchange(this.state.currentCurrency.value, '¥')}元</p>
      </div>
    )
  }
  valueChangeHandler (value, type) { // 這裏只須要簡單映射關係便可,再也不須要計算各個組件的具體狀態值
    this.setState({
      currentCurrency: { value, type }
    })
  }
  
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
)
複製代碼

上面的例子很好地貫徹了React官方反覆強調推薦的「單項數據流」模式。雖然多寫了一些代碼,可是好處是能夠減小由於子組件能夠自行修改共享狀態而引發的一些bug,畢竟咱們將共享狀態提高到父級組件上之後,全部對共享狀態的修改就都集中在父級組件上了。

另外,再次強調一個原則:任何能夠由stateprops計算出來的狀態,都不該該放在state。就像上例那樣,應該直接在render()函數中直接計算後使用。

12. 聚合而不是繼承

React官方推薦使用聚合而不是繼承來在組件之間複用代碼。一般有兩種服用的狀況,一種是組件的部分結構或內容不肯定,須要由外部傳入,這時組件就至關於一個容器;另外一種是從更爲抽象的組件建立一個較爲具體的組件,好比「彈層」和「登錄彈層」。

12.1 容器

當組件內有部份內容不肯定須要外部傳入時,可使用一個特殊的props屬性children來傳入。在組件內部訪問props.children能夠獲取使用組件時寫在組件開始和結束標籤內的內容:

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}> {props.children} </div>
  );
}

function WelcomeDialog() {
  return (
    <FancyBorder color="blue"> <h1 className="Dialog-title"> Welcome </h1> <p className="Dialog-message"> Thank you for visiting our spacecraft! </p> </FancyBorder>
  );
}
複製代碼

當組件有多個部份內容不肯定都須要外部傳入時,單靠props.children就不能知足需求了。但時不要忘記React組件的props能夠接受任意類型的參數,因此其實組件的內容也徹底能夠直接使用props來傳遞到組件內部:

function SplitPane(props) {
  return (
    <div className="SplitPane"> <div className="SplitPane-left"> {props.left} </div> <div className="SplitPane-right"> {props.right} </div> </div>
  );
}

function App() { // JSX中使用{}插入另外一個JSX,由於JSX也是表達式
  return <SplitPane left={ <Contacts /> } right={ <Chat /> } /> } 複製代碼

12.2 具象化

有時咱們但願一個組件是另外一個較爲抽象的組件的特例(更爲具象),官方推薦的作法是將抽象組件包裹在具象組件中,並使用props來配置它:

function Dialog(props) {
  return (
    <FancyBorder color="blue"> <h1 className="Dialog-title"> {props.title} </h1> <p className="Dialog-message"> {props.message} </p> </FancyBorder>
  );
}

function WelcomeDialog() {
  return (
    <Dialog title="Welcome" message="Thank you for visiting our spacecraft!" /> ); } 複製代碼

至於繼承...忘掉它吧。

13. Think in React (這部分直接翻譯的原文)

在React官方看來,React是構建大型、高性能web應用的不二之選。它在Fb和Ins表現得很是好。React最棒呆的地方在於它讓你在構建應用時如何看待你的應用。下面給咱們經過編寫一個搜索列表,來帶你體驗這個思惟過程。

13.1 從效果圖開始

假設咱們已經有了一個JSON接口,並有了設計師給咱們的效果圖:

效果圖

JSON接口返回的數據格式以下:

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
]
複製代碼

13.2 第一步:將UI按照組件層級進行分解

要作的第一件事就是在效果圖上的組件(和子組件)周圍畫框框,並命名組件。若是這是你的設計師同事給你的,那麼這部分工做他可能已經作完了,去和他嘮嘮。他的PSD圖層名頗有可能能夠做爲你的組件名。

但具體怎麼劃分組件呢?答案是跟你建立一個函數或對象同樣。這其中的一個原則是「單一職責原則」,具體來講就是一個組件應該只作一件事,不然,它應該被拆分紅更多的子組件。

若是你常常將JSON數據展示給你的用戶,那你應該知道若是你建立了正確的數據模型,你的UI(和組件)將會規劃組織的很是好。由於你的UI和數據模型是同一個信息結構,這也意味着劃分組件是一件比較繁瑣的事情。就將你的組件按照JSON返回的數據結構拆分爲就行了。

組件拆分

(未完待續,這是React官方基礎教程的最後一章,有空我再繼續翻譯吧)

(2018年7月27日更新......天道好輪迴,蒼天饒過誰......最終仍是要寫react,時隔快2年了,我本身也得回來看文章複習一波...索性把坑填上吧!)

你會看到這個簡單的應用中有5個組件。咱們將每一個組件所表明的數據用斜體表示。

  1. FilterableProductTable(橙色):示例的所有內容
  2. SearchBar(藍色):接受 用戶輸入
  3. ProductTable(綠色):基於用戶輸入顯示過濾好的 產品列表
  4. ProductCategoryRow(青色):爲每一個 類目 顯示一個標題
  5. ProductRow(紅色):爲每一個 產品 顯示一行

ProductTable的表頭(包含NamePrice標籤)不是一個單獨的組件。這是個偏好問題,無論哪一種方式都還存在爭議。這個例子中,咱們將其留在了ProductTable中,由於ProductTable的任務是渲染 產品列表 ,而表頭也算列表的一部分。然而,當這個表頭變得複雜了(好比當咱們添加了排序功能),就能夠瓜熟蒂落地將其抽出來成爲ProductTableHeader組件。

既然如今咱們已經將組件都識別出來了,那就把他們結構化一下吧。這很easy。效果圖中出如今另外一個組件內部的組件,在結構上應該做爲前者的子組件:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

13.3 第二步:用React構建一個靜態版本

codepen查看示例代碼

如今你有了組件結構,是時候開始實現你的應用了。最容易的方式是構建一個帶着數據並渲染了UI,可是沒有交互的版本。最好是將這兩步分開作,由於構建靜態版本只須要無腦編寫,而添加交互則須要大量思考和少許編寫。後面咱們會看到這麼作的緣由。

爲了構建你的應用的靜態版本,你會想要複用其它組件來構建新的組件,並利用props來傳遞數據。props是從父組件傳遞數據到子組件的一種方式。若是你熟悉state,那麼構建這個靜態版本就徹底不要使用statestate是專爲交互性預留的,數據會隨時間改變。目前這仍是個靜態版本,你還不須要它。

你能夠自上而下或者自下而上進行構建。也就是說,你能夠從較高層的組件開始構建(好比從FilterableProductTable開始),也能夠從較底層的組件開始(ProductRow)。在簡單的示例中,一般自上而下更容易,而在更大型的項目中,自下而上地構建組件並同時爲其編寫測試會來的更容易。

在這一步驟的結尾,你講會有一個可複用的組件庫來渲染你的數據。因爲目前你的應用仍是靜態版本,組件們將只有render()方法。組件結構最頂層的組件(FilterableProductTable)會將你的數據做爲一個"prop"進行接收。若是你修改了基礎數據並再次調用ReactDOM.render(),UI會跟着更新。很容易就能觀察到你UI的更新和變化的地方,由於到目前爲止尚未什麼複雜的事情發生。React 的單向數據流(又被稱爲單向綁定)使得一切有序而迅捷。

執行這一步時若是須要能夠連接到React文檔尋找幫助。

小插曲:Props與State

在React中有兩種數據模型:propsstate。理解兩種模型的差異很是重要,若是你不瞭解其中差別,能夠跳轉到React官方文檔進行查看。

13.4 第三步:標識出最小(但完整的)UI狀態

爲了讓你的UI可交互,你須要可以觸發變化到你的基礎數據上。React用**state**將這件事變得很簡單。

爲了正確構建出你的應用,首先你須要思考你的應用須要的最小可變state集合。這裏的關鍵是DRY: Don’t Repeat Yourself。搞清楚你的應用須要的最小的state表示,而後按須要計算出其它的一切。好比,你要構建一個TODO LIST,只須要維護一個TODO項數組便可;不須要單獨爲數量保存一個state變量。相反,當你想要渲染TODO的數量時,只須要簡單地讀取TODO項數組的長度便可。

考慮一下咱們示例應用中的全部數據:

  • 原始產品列表
  • 用戶輸入的搜索文字
  • 複選框的值
  • 過濾後的產品列表

咱們一個一個過一遍來搞清楚哪一個纔是state。只要簡單地在每一個數據上問三個問題:

  1. 它是從父組件經過props傳遞進來的嗎?若是是,那它不是state
  2. 它一直不會變化嗎?若是是,那它不是state
  3. 你能基於組建的其它stateprops計算出它來嗎?若是是,那它不是state

原始產品列表是從props傳遞進來的,因此它不是state。用戶輸入的搜索文字和複選框的值好像是state,由於他們會變化,而且不能基於其它數據計算出來。最後,過濾後的產品列表不是state,由於他可以基於原始產品列表、用戶輸入的搜索文字和複選框的值計算出來。

因此最終,咱們的state是:

  • 用戶輸入的搜索文字
  • 複選框的值

13.5 第四步:肯定你的state的位置

codepen查看示例代碼

因此咱們如今已經get到了應用須要的最小state集合,接下來咱們須要搞清楚這些狀態應該放到哪些組件裏。

記住:React的一切都是關於沿着組件層次結構鄉下的單項數據流。可能一開始你無法立刻搞清楚哪一個組件應該擁有哪些狀態。這一般對於新手來講是最難的部分了,但你能夠按下面的步驟來搞定:

對於你應用裏的每個state來講:

  • 肯定基於該state渲染的全部組件
  • 找到這些組件的一個共同的全部者組件(在層級結構中找到一個包含全部須要該state的組件的單一組件)
  • 要麼是共同的全部者組件,要麼是更高層的祖先組件應該擁有該state
  • 若是你沒辦法找到一個合適的組件來控制這個state,那就在共同的全部者組件之上再爲其建立一個父組件來控制這個state

讓咱們按照這個策略來看一下咱們的應用:

  • ProductTable須要基於state來過濾產品列表,SearchBar須要展現搜索文字和複選框狀態
  • 他們共同的全部者組件是FilterableProductTable
  • 那麼理論上把過濾文字和複選框的值放到FilterableProductTable上是比較合適的

好,咱們已經決定將咱們的state放在FilterableProductTable上了。首先,在FilterableProductTableconstructor中添加一個實例屬性this.state = { filterText: '', inStockOnly: false }來初始化你的應用的state。而後將filterTextinStockOnly做爲props傳遞給ProductTableSearchBar。最後,用這倆props來過濾ProductTable的行,以及設置SearchBar的值。

你已經能夠看到你的應用的一些表現了:將filterText設置爲ball並刷新你的應用,你將看到數據表格正確地更新了。

13.6 第五步:添加反向數據流

codepen查看示例代碼

目前咱們已經構建了一個應用,propsstate沿着層級結構自上而下流轉並正確渲染出來。如今是時候支持另外一種方式的數據流了:層級結構深處的的表單組件須要更新FilterableProductTable上的state

React將這種數據流作的很是直白,讓你能更好理解地你的程序是如何運轉的。但相較於傳統的雙向數據綁定,它的確會須要多寫一點代碼。

若是你嘗試在當前這個版本的示例中輸入或點擊複選框,React將會忽略你的輸入。這是故意的,由於咱們已經將inputvalue設置爲老是等於從FilterableProductTable傳遞進來的state

讓咱們思考一下咱們想作什麼。咱們但願不管什麼時候用戶修改了表單,都要更新state來反映用戶輸入。由於組件應該只更新它們本身的stateFilterableProductTable會給SearchBar傳遞迴調函數,當state須要更新時執行該回調函數便可。咱們可使用inputonChange事件來通知其更新。FilterableProductTable傳遞的回調函數中會調用setState(),應用就會更新了。

雖然這聽起來很複雜,但實際上卻只有幾行代碼而已。而且你的數據如何在應用中流轉已經很是明確了。

13.7 終於搞定了

但願這篇文章能給你一些如何用 React思考構建組件和應用的啓發。雖然這可能比你原來的代碼多一些,但請記住,讀代碼的次數要遠遠多於寫代碼的次數,並且讀這種模塊化的、直白的代碼很是容易。當你開始構建大型組件庫時,你會感激這種明晰性和模塊化,而且隨着代碼的重用,你的代碼會越寫越少。:)

相關文章
相關標籤/搜索