Reactjs 入門綜合教程 (上)

艾倫先生發表於2017-12-14

按照慣例,先給ReactJS背書

React是一個Facebook開發的UI庫,於2013年5月開源,並迅速的從最早的UI引擎演變成一整套前後端通吃的 Web App 解決方案。衍生的 React Native 專案,目標更是巨集偉,希望用寫 Web App 的方式去寫 Native App。

使用這個庫可以很方便的開發互動式的、具有表達力的和可重用的UI元件。它本身並不是一個MVC框架,就是一個檢視層,並且是一個以元件為基礎的高效檢視層。

React 的核心思想是:封裝元件,各個元件維護自己的狀態和 UI,當狀態變更,自動重新渲染整個元件。基於這種方式的一個直觀感受就是我們不再需要不厭其煩地來回查詢某個 DOM 元素,然後操作 DOM 去更改 UI。

推薦一個腳手架程式

git clone https://github.com/hellobeifeng/reactDemo.git

gulp serve
localhost:5000
複製程式碼

說說我理解的一些概念

  • JSX
    • 真要使用JSX?
    • JSX的使用
  • 虛擬DOM
  • 單向事件流
  • 元件
    • 什麼是元件?
    • 元件的狀態與屬性
    • 元件的生命週期

JSX

真要使用JSX?

JSX 並不是一門全新的語言,僅僅是一個語法糖,允許開發者在JavasSript中編寫XML語言。

作為React的核心部分,JSX使用XML標記的方式直接宣告頁面。在JavaScript程式碼裡直接寫XML的語法,每一個XML標籤都會被JSX轉換工具轉換成純JavaScript程式碼。(學習React的第一個坑)

注意:使用JSX寫的程式碼,需要編譯輸出成JS程式碼才能使用。將 JSX 語法轉為 JavaScript 語法,這一步很消耗時間。現在前端專案,都會使用前端工程化,不會直接在html頁面中直接寫js程式碼,寫好的js程式碼都會使用工具進行編譯壓縮等。這樣的話,我們的JSX也會通過編譯直接轉化成js語法,讓瀏覽器直接使用。

好訊息是你不用使用這個JSX也可以直接建立元件,但是壞訊息是,不用JSX你必須使用原聲JavaScript通過大段的API來建立。

例如,不使用JSX建立一個標題的函式大概是這樣

React.createElement('h1',{className:'question'},'Questions')
複製程式碼

如果使用JSX,上述呼叫就變成了下面這種更熟悉且簡練的標籤

<h1 className="question"/>Questions</>
複製程式碼

而且,一旦巢狀元素就可能出現如下的樣子

var child = React.createElement('li', null, 'Text Content');
var root = React.createElement('ul', { className: 'my-list' }, child);
React.render(root, document.body);
複製程式碼

所以,當你放下心裡負擔之後會發現,使用JSX的好處就是,抽象了React Element的建立過程,允許使用熟悉的語法來定義HTML元素樹,提供更加語義化且易懂的標籤,程式結構更容易被直觀化,所以下定決心使用JSX吧~

如此使用JSX

JSX定義屬性&&樣式使用

在HTML中,可以通過標籤上的屬性來改變當前元素的樣式,當然,這在JSX中也是可以的,只不過JSX在使用行內樣式的時候是有缺陷的。使用{{}}而不是引號的方式。

React.render(
    <div className="text-c1" style={{color:'red',marginTop:'20px'}}>
        xxxxx
    </div>,
    document.body
);
複製程式碼

直接在標籤上使用style屬性時,要寫成style={{}}是兩個大括號,外層大括號是告知JSX這裡是js語法,和真實DOM不同的是,屬性值不能是字串而必須為物件,需要注意的是屬性名同樣需要駝峰命名法。即margin-top要寫成marginTop,屬性之間以逗號間隔。

使用變數:

JSX將兩個花括號中的內容{...}渲染成動態值,花括號指明瞭一個JavaScript上下文環境————你在花括號中放置的任何內容都會被求值。

  • 使用簡單變數

      var msgs = 'Hi all~';
      <h2>{msgs}</h2>
    複製程式碼
  • 使用函式

      function dateToString(date){
          return [
            date.getFullYear(),
            date.getMonth()+1,
            date.getDate()
          ].join('-')
      };
    
      <h2>{dateToString(new Date())}</h2>
    複製程式碼

JSX中的延展屬性

在JSX中我們可以使用ES6中的最新語法...來遍歷物件

  var HelloMessage = React.createClass({
    render: function() {
      return <h1>Hello {this.props.name}  get {this.props.votes} votes</h1>;
    }
  });

  var Lucy = {
    name: "feng",
    votes: "23"
  }

  ReactDOM.render(
  <HelloMessage {...Lucy} />,
    document.getElementById('example')
  );
複製程式碼

JSX中的事件繫結

JSX支援事件的繫結,語法類似於HTML中事件的繫結,不同的是這裡事件的名稱必須按照駝峰式,例如change變成onChange,click變成onClick等。

例項1:點選計數

var Counter = React.createClass({
    incrementCount: function(){
        this.setState({
            count: this.state.count + 1
        });
    },
    getInitialState: function(){
        return {
            count: 0
        }
    },
    render: function(){
        return (
            <div class="my-component">
                <h1>Count: {this.state.count}</h1>
                <button type="button" onClick={this.incrementCount}>Increment</button>
            </div>
        );
    }
});
React.render(<Counter />,document.getElementById('example'))  
複製程式碼

例項2:事件傳遞引數

var HelloComponent = React.createClass({
    testClick: function (str) {
      alert('hello ' +  str)
    },
    render: function() {
      return (
          <p
              onClick={this.testClick.bind(this, 'feng')} 
            style={{color:'#ff6600',width:'200px', height:'100p'}}
          >
          Click me
          </p>
          )
        
    }
  })

  
  ReactDOM.render(
  <HelloComponent  />,
    document.getElementById('example')
  );
複製程式碼

條件判斷

JSX中,不能使用 if/else語句,替代方案:三目運算子

        var MessageBox = React.createClass({
            getInitialState:function(){
              return {
                isComplete:true
              }
            },
            getIsComplete:function(){
              return this.state.isComplete ?'is-complete':''
            },
            render: function(){
            
              return (
                <div className={this.getIsComplete()}>
                  hi all
                </div>
              )
            }
          });
複製程式碼

注意,這裡說的是在HTML中不能使用if/else語句,在函式中依然可以使用。

使用子節點

React將開始標籤和結束標籤中的所有子節點儲存在一個叫this.props.children 的特殊元件屬性內。

  var SubMessageComp = React.createClass({
    render: function(){
      return (
        <div>
          {this.props.children}
        </div>
      )
    }
  });
  var MessageBox = React.createClass({
    render: function(){
      return (
        <SubMessageComp>
            <li>a</li>
            <li>b</li>
        </SubMessageComp>
      )
    }
  });
複製程式碼

註釋:

JSX本質上就是JavaScript,因此可以再標籤內新增原聲的JavaScript註釋

        render: function(){
          //普通javascript註釋
          return (
            //普通javascript註釋
            <div /*內聯註釋*/ className={this.getIsComplete()}>
              {/*這裡節點內註釋*/}
              hi all
            </div>
          )
        }
複製程式碼

//註釋不能放置在return 結構內

/*內聯註釋*/ 內聯註釋不要放置到節點內(會自動被當成一個span標籤)

{/*節點內註釋*/} 節點內註釋不要和內聯註釋替換位置

虛擬DOM

Virtual DOM 用於優化檢視的渲染和重新整理。以前我們更新檢視時,需要先清空DOM容器中的內容,然後將最新的DOM和資料追加到容器中(重繪與重排),現在React將這一操作放到了記憶體中。

React 會在記憶體中維護一個虛擬 DOM 樹,當我們對這個樹進行讀或寫的時候,實際上是對虛擬 DOM 進行的。當資料變化時,然後 React 會自動更新虛擬 DOM,然後拿新的虛擬 DOM 和舊的虛擬 DOM 執行Diff演算法,找到有變更的部分,得出一個Patch,然後將這個 Patch 放到一個佇列裡,最終批量更新這些 Patch 到 DOM 中。

虛擬DOM

這樣的機制可以保證即便是根節點資料的變化,最終表現在 DOM 上的修改也只是受這個資料影響的部分,可以保證非常高效的渲染。

單向事件流

在jquery時代,我們都是基於事件驅動,對於簡單的互動需求而言,這確實足夠了,而且開發起來非常迅速。但業務一旦複雜,這種基於事件驅動的東西就會變得很亂:某個事件,更新的DOM逐漸變多,不好管理,就容易出錯。

React中有單向資料流的概:更新 DOM 的資料總是從頂層流下來,使用者事件不直接操作 DOM,而是操作頂層資料。這些資料從頂層流下來同時更新了DOM。你的程式碼就很少會直接處理DOM,而是隻處理資料的變更。這樣會很大程度上簡化程式碼和邏輯。

舉個例子:我點選一個button,然後頁面上一個span裡數字+1,原有的思考邏輯是“點選發生,然後資料變化,然後UI跟著變化+1”。而現在的思考邏輯是我的資料變化了,那麼我的UI會自動更新,那麼我只用考慮“點選發生,資料變化”。甚至可以把UI和資料變化分層然後處理。

具體地說:

在一個多元件結構裡,一個父元件需要負責管理狀態,並把資料通過props向下發放。

元件的狀態通過setState方法更新。資料通過設定子元件的屬性來傳遞給子元件。

子元件通過this.props來獲取這些資料,通過資料和自身狀態繫結事件UI,觸發UI的改變。

例子: 實現一個動態查詢框

var FilteredList = React.createClass({
filteredList: function (event) {
    var updateList = this.state.initialItems;
    updateList = updateList.filter(function (item) {
        return item.toLowerCase().search(event.target.value.toLowerCase()) !== -1;
    });
    this.setState({items: updateList});
},
getInitialState: function () {
    return {
        initialItems: ["javascript", "java", "c", "c++", "python", "php", "nodejs","react"],
        items: []
    }
},
componentWillMount: function () {
    this.setState({items: this.state.initialItems});
},
render: function () {
    return (
        <div className="filter-class">
            <input type="text" placeholder="Search" onChange={this.filteredList} />  
            <List items={this.state.items} />
        </div>
    )
}
})
 
var List = React.createClass({
    render: function () {
        //List元件中通過this.props來獲取到FilteredList傳遞進來的值
        return (
            <ul>
                {
                    this.props.items.map(function (item) {
                        return <li key={item}>{item}</li>
                    })
                }
            </ul>
        )
    }
});
 
React.render(<FilteredList/>, document.getElementById("example"));
複製程式碼

元件

建立元件

對於React應用而言,你需要分割你的頁面,使其成為一個個的元件。也就是說你的應用是由一個個元件構成的。

使用React.createClass方法建立元件。

    var HelloMessage = React.createClass({
      render: function() {
        return <div>Hello {this.props.name}</div>;
      }
    });
    
    // 載入元件到 DOM 元素 mountNode
    React.render(<HelloMessage name="feng" />, mountNode);
複製程式碼

元件首字母必須大寫,用來區分是元件標籤還是HTML標籤

props

props是一個物件,是元件用來接收外面傳來資料的容器。元件內部是不允許修改自己的props屬性的,只能夠通過父元件來修改。具體的使用方法如下

  • props的使用

      var HelloWorld = React.createClass({
       render:function () {
            console.log(this.props)
           return <p>Hello,{this.props.name ? this.props.name : " World"}</p>;
       },
      });
      var HelloUniverse = React.createClass({
         getInitialState:function () {
             return {
                 name:''
             };
         },
         handleChange: function (event) {
             this.setState({name: event.target.value});
         },
         render: function () {
             return (<div>
             <HelloWorld name={this.state.name}></HelloWorld>
             <br/>
             <input type="text" onChange={this.handleChange} />
             </div>
             );
         }
      });
      ReactDOM.render(<HelloUniverse />,example);
    複製程式碼

Reactjs 入門綜合教程 (上)

  • propTypes

    元件的屬性可以接受任意值,字串、物件、函式等等都可以。有時,我們需要一種機制,驗證別人使用元件時,提供的引數是否符合要求。(一般用於開發階段使用)

      var MyTitle = React.createClass({
        propTypes: {
          title: React.PropTypes.string.isRequired,
        },
        render: function() {
           return <h1> {this.props.title} </h1>;
         }
      });
    複製程式碼

    上面的Mytitle元件有一個title屬性。PropTypes 告訴 React,這個 title 屬性是必須的,而且它的值必須是字串。現在,我們設定 title 屬性的值是一個數值。

      var data = 123;
      ReactDOM.render(
        <MyTitle title={data} />,example
      );
    複製程式碼

    這樣一來,title屬性就通不過驗證了。控制檯會顯示一行warning資訊。

Reactjs 入門綜合教程 (上)

  • getDefaultProps

    用來設定元件屬性的預設值

      var MyTitle = React.createClass({
        getDefaultProps : function () {
          return {
            title : 'Hello World'
          };
        },
      
        render: function() {
           return <h1> {this.props.title} </h1>;
         }
      });
      ReactDOM.render(<MyTitle />,document.body);
    複製程式碼
  • this.props.children

    this.props 物件的屬性與元件的屬性一一對應,但是有一個例外,就是 this.props.children 屬性。它表示元件的所有子節點

      var NotesList = React.createClass({
        render: function() {
          return (
            <ol>
            {
              React.Children.map(this.props.children, function (child) {
                return <li>{child}</li>;
              })
            }
            </ol>
          );
        }
      });
      ReactDOM.render(
        <NotesList>
          <span>hello</span>
          <span>world</span>
        </NotesList>,example
      );
    複製程式碼

    上面程式碼的 NoteList 元件有兩個 span 子節點,它們都可以通過 this.props.children 讀取。這裡需要注意, this.props.children 的值有三種可能:

    1. 如果當前元件沒有子節點,它就是 undefined;
    2. 如果有一個子節點,資料型別是 object;
    3. 如果有多個子節點,資料型別就是 array

    React 提供一個工具方法 React.Children 來處理 this.props.children 。我們可以用 React.Children.map 來遍歷子節點,而不用擔心 this.props.children 的資料型別是 undefined 還是 object。

state屬性的用法

React把元件當成狀態機,狀態(state)是子元件內部維護的資料,一旦使用者互動導致狀態發生變化,觸發不同的鉤子函式(詳見下文中生命週期),元件也會進行更新,重新渲染UI。

  • 工作原理

    常用的通知 React 資料變化的方法是呼叫 setState(data, callback)。這個方法會合並data 到 this.state,並重新渲染元件。渲染完成後,呼叫可選的 callback 回撥。大部分情況下不需要提供 callback,因為 React 會負責把介面更新到最新狀態。

  • getInitialState

    object getInitialState()

    getInitialState 方法用於定義初始狀態,也就是一個物件,這個物件可以通過 this.state 屬性讀取。在元件掛載之前呼叫一次。返回值將會作為 this.state 的初始值。

  • setState

    setState(object nextState[, function callback])

    this.setState 方法用於修改狀態值,每次修改以後,自動呼叫 this.render 方法,再次渲染元件。

    注意:

    1. 絕對不要直接改變 this.state,因為在之後呼叫 setState() 可能會替換掉你做的更改。把 this.state 當做不可變的。
    2. setState()不會立刻改變 this.state,而是建立一個即將處理的state轉變。在呼叫該方法之後獲取 this.state 的值可能會得到現有的值,而不是最新設定的值。但是,可以通過非同步(setTimeout(func, 0))的形式來獲取this.state的值。。
    3. setState () 將總是觸發一次重繪,除非在 shouldComponentUpdate()中實現了條件渲染邏輯,僅在新 state 和之前的 state 存在差異的時候呼叫 setState()可以避免不必要的重新渲染。

    常用的模式是建立多個只負責渲染資料的無狀態(stateless)元件,在它們的上層建立一個有狀態(stateful)元件並把它的狀態通過 props 傳給子級。這個有狀態的元件封裝了所有使用者的互動邏輯,而這些無狀態元件則負責宣告式地渲染資料。

生命週期

生命週期各階段

在整個ReactJS的宣告週期中,主要會經歷這四個階段:建立階段、例項化階段、更新階段、銷燬階段

  var OneComponent = React.createClass({
    clickHandle: function(){
        this.setState({
            count: this.state.count + 1
        })
    },
    //1.建立階段
    getDefaultProps: function() {
      //在建立類的時候被呼叫
      console.log('getDefaultProps');
      return {
        count: 0
      }
    },

    //2.例項化階段
    getInitialState: function() {
      //獲取this.state的預設值
      console.log('getInitialState');
      return {};
    },
    componentWillMount: function() {
      //在render之前呼叫此方法
      //業務邏輯的處理都應該放在這裡,比如對state的操作等
      console.log('componentWillMount')
    },
    render: function() {
      //渲染並返回一個虛擬DOM
      console.log('render');
      return (
          <div> hello
               <b> {this.props.name} 
               </b>
               <button onClick={this.clickHandle}>Click me</button>
           </div>
        )
    },
    componentDidMount: function() {
      //該方法發生在render方法之後
      //在該方法中,ReactJS會使用render方法返回的虛擬DOM物件來建立真實DOM結構
      console.log('componentDidMount');
      console.log('###end###')
    },

    //3.個更新階段
    componentWillReceieveProps: function() {
      //該方法發生在this.props被修改或父元件呼叫setProps()方法之後
      console.log('componentWillRecieveProps');
    },
    shouldComponentUpdate: function() {
      //是否需要更新
      console.log('shouldComponentUpdate');
      return true;
    },
    componentWillUpadate: function() {
      //將要更新
      console.log('componentWillUpadate');
    },
    componentDidUpdate: function() {
      //更新完畢
      console.log('componentDidUpdate');
    },
    //4.銷燬階段
    componentWillUnmout: function() {
      //銷燬時被呼叫
      console.log('componentWillUnmout');
    }
  })

  
  ReactDOM.render(
  <OneComponent  name="World "/>,
    document.getElementById('example')
  );
複製程式碼

建立階段:

該階段主要發生在建立元件類的時候,即在呼叫React.createClass的時候。這個階段只會觸發一個getDefaultProps方法,該方法會返回一個物件,並快取下來。然後與父元件制定的props物件合併,最後賦值給this.props作為該元件的預設屬性。

例項化階段

該階段主要發生在例項化元件類的時候,也就是元件類被呼叫的時候。

  ReactDOM.render(
  <OneComponent  name="World "/>,
    document.getElementById('example')
  );
複製程式碼

該元件被呼叫的時候,這個階段會觸發一系列的流程,具體的執行順序如下所示。

  • getInitialState。

    初始化元件的state的值,其返回值會賦值給元件的this.state屬性。

  • componentWillMount 在渲染(掛載)之前呼叫一次。

    此時this.refs物件為空。如果在此時呼叫this.setState()則會更新this.state物件

  • render

    根據state的值,生成頁面需要的虛擬DOM結構,並返回該結構

  • componentDidMount

    在渲染之後呼叫一次。根據虛擬DOM結構而生成的真實DOM進行相應的處理,元件內部可以通過this.getDOMNode()來獲取當前元件的節點,然後就可以像在web開發中那樣操作裡面的DOM元素了。

      componentDidMount () {
        const textbox = React.findDOMNode(this.refs.text)
        if (this.props.autoFocus) textbox.focus()
      }
    複製程式碼

更新階段

主要發生在使用者操作或者元件有更新的時候,此時會根據使用者的操作行為進行相應的頁面結構的調整。該階段也會觸發一系列的流程,具體的執行順序如下所示。

  • componentWillReceiveProps

    在將要接收props時呼叫。在該函式中,通常可以呼叫this.setState方法來完成對state的修改。

    props是父元件傳遞給資組建的。父元件發生render的時候子元件就會呼叫componentWillReceiveProps(不管props有沒有更新,也不管父子元件之間有沒有資料交換)。

      componentWillReceiveProps (nextProps) {
        if (this.props.disabled !== nextProps.disabled) {
          // disabled這個屬性改變了
        }
      }
    複製程式碼
  • shouldComponentUpdate

    該方法用來攔截新的props或state,然後根據事先設定好的判斷邏輯,返回值決定元件是否需要update。

    元件掛載之後,每次呼叫setState後都會呼叫shouldComponentUpdate判斷是否需要重新渲染元件。預設返回true,需要重新render。在比較複雜的應用裡,有一些資料的改變並不影響介面展示,可以在這裡做判斷,優化渲染效率。

      shouldComponentUpdate (nextProps, nextState) {
          // 比較props或者states,返回true則更新照常,返回false則取消更新,且不會呼叫下面的兩個生命週期函式
      }
    複製程式碼
  • componentWillUpdate :

    元件更新之前呼叫一次。當上一部中shouldComponentUpdate方法返回true時候,就可以在該方法中做一些更新之前的操作。

  • render

    根據一系列的diff演算法,生成頁面需要的虛擬DOM結構,並返回該結構。實際表明,在render中,最好只做資料和模板的結合,而不進行state等邏輯的修改,這樣元件結構更清晰。

  • componentDidUpdate 元件的更新已經同步到DOM中後出發。

    除了首次render之後呼叫componentDidMount,其它render結束之後都是呼叫componentDidUpdate。

銷燬階段

  • componentWillUnmount

    會在元件即將從掛載點移去時呼叫,用來取出去除即將被銷燬的DOM節點的引用,或者是清除計時器,取消監聽的時間等等。

componentWillMount、componentDidMount和componentWillUpdate、componentDidUpdate可以對應起來。區別在於,前者只有在掛載的時候會被呼叫;而後者在以後的每次更新渲染之後都會被呼叫。

元件生命週期流程圖

Reactjs 入門綜合教程 (上)

元件之間的通訊

先建立一個父類元件Parent,它內部呼叫一個叫做Child的子元件,並將接收到的外部引數name傳遞給子元件Child。

var Parent = React.createClass({
    handleClick: function() {
      this.refs.myTextInput.focus();
    },
    render: function() {
      return (
          <div onClick={this.handleClick}>
            <input type="text" ref="myTextInput" />
            Parent is:
              <Child name={this.props.name}></Child>
          </div>
        )
    }
});
複製程式碼

在建立一個子類元件Child

  var Child = React.createClass({
    render: function() {
      return <span>{this.props.name}</span>
    }
  });
複製程式碼

自後通過React.render方法將元件渲染到頁面上

  ReactDOM.render(
    <Parent name="React" />,document.getElementById('example')
  );
複製程式碼

執行結果

Reactjs 入門綜合教程 (上)

  • 子元件呼叫父元件的方法。從上面的例子可以看出,子元件要拿到父元件的屬性,需要通過this.props方法。所以,如果子元件想要呼叫父元件的方法,只需要父元件把要被呼叫的方法以屬性的方式放在子元件上,子元件內部便可通過"this.props.被呼叫的方法"這樣的方式拿到name屬性的。然後,每次父元件修改了傳入的name屬性,子元件便會得到通知,然後自動獲取新的name屬性
  • 父元件呼叫子元件的方法。子元件呼叫父元件是通過prop屬性,而反過來,父元件呼叫子元件通過的就是 ref 屬性。子元件被設定了ref屬性之後,父元件便可以通過this.ref.xxx來獲取到子元件了,其中xx為子元件的ref值。