1. 程式人生 > >120分鐘React快速掃盲教程

120分鐘React快速掃盲教程

  在教程開端先說些題外話,我喜歡在學習一門新技術或讀過一本書後,寫一篇教程或總結,既能幫助消化,也能加深印象和發現自己未注意的細節,寫的過程其實仍然是一個學習的過程。有個記錄的話,在未來需要用到相關知識時,也方便自己查閱。

  React既不是一個MVC框架,也不是一個模板引擎,而是Facebook在2013年推出的一個專注於檢視層,用來構建使用者介面的JavaScript庫。它推崇元件式應用開發,而元件(component)是一段獨立的、可重用的、用於完成某個功能的程式碼,包含了HTML、CSS和JavaScript三部分內容。React為了保持靈活性,只實現了核心功能,提供了少量的API,一些DOM方法都被剝離到了

react-dom.js中。這麼做雖然輕巧,但有時候要完成特定的業務場景,還是需要與其他庫結合,例如Redux、Flux等。React不僅讓函數語言程式設計愈加流行,還引入了JSX語法(能把HTML嵌入進JavaScript中)和Virtual DOM技術,大大提升了更新頁面時的效能。在React中,每個元件的呈現和行為都由特定的資料所決定,而資料的流動都是單向的,即單向資料流。在編寫React時,推薦使用ES6語法,官方的文件也使用了ES6語法,因此,在學習React之前,建議對ES6有所瞭解,避免不必要的困惑。

  下面是一段較為完整的React程式碼,本文大部分的示例程式碼來源於此,閱讀下面的程式碼可以對React有一個感性的認識。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
class Search extends Component {
    //static defaultProps = {
    //    url: "http://jane.com"
    //};
    constructor(props) {
        super(props);
        this.state = {
            txt: 
"請輸入關鍵字" }; } componentWillMount() { console.log("component will mount"); } componentDidMount() { console.log("component did mount"); this.refs.txt.addEventListener("blur", (e) => { this.getValue(e); }); console.log(this.refs) } handle(keyword, e) { console.log(keyword); console.log(this); console.log(this.select.value); } getValue(e) { console.log(e.target.value); } refresh(e) { this.setState({ type: e.target.value }); } render() { console.log("render"); let {type} = this.state; console.log(type); return ( <div> {this.props.children} <select value={type} onChange={this.refresh.bind(this)}> <option value="1">標題</option> <option value="2">內容</option> </select> <select defaultValue={2} ref={(select) => this.select = select}> <option value="1">標題</option> <option value="2">內容</option> </select> <input placeholder={this.state.txt} ref="txt" defaultValue="教程" style={{marginLeft:10, textAlign:"center"}}/> <button className="btn" data-url={this.props.url} onClick={this.handle.bind(this, "REACT")}>{"<搜尋>"}</button> </div> ); } } Search.defaultProps = { url: "http://jane.com" }; ReactDOM.render( <Search url="http://www.pwstrick.com"> <h1>React掃盲教程</h1> </Search>, document.getElementById("container") );

一、JSX語法

  JSX是對JavaScript語法的一種擴充套件,它看起來像HTML,同樣擁有清晰的DOM樹狀結構和元素屬性,如下程式碼所示。但與HTML不同的是,為了避免自動插入分號時出現問題,在最外層得用圓括號包裹,並且必須用一個元素包裹(例如下面的<div>元素)其它元素,所有的元素還必須得閉合。

(<div>
  <input placeholder={this.state.txt} />
  <button className="btn">{"<搜尋>"}</button>
</div>)

1)元素

  JSX中的元素分為兩種:DOM元素和元件元素(也叫React元素),DOM元素就是HTML文件對映的節點,首字母要小寫;而元件元素的首字母要大寫。無論是DOM元素還是元件元素,最終都會通過React.createElement()方法轉換成JSON物件,如下所示,JSON物件是簡化過的。

//React.createElement()方法
React.createElement("div", null, [
  React.createElement("input", { placeholder: `${this.state.txt}` }, null),
  React.createElement("button", { className: "btn" }, "<搜尋>")
]);
//簡化過的JSON物件
{
  type: "div",
  props: {
    children: [
      {
        type: "input",
        props: {
          placeholder: `${this.state.txt}`
        }
      },
      {
        type: "button",
        props: {
          className: "btn",
          children: "<搜尋>"
        }
      }
    ]
  }
}

  由於JSX中的元素能夠被編譯成物件,因此還可以把它們應用到條件、迴圈等語句中,或者作為一個值應用到變數和引數上。

2)屬性

  JSX中的屬性要用駝峰的方式表示,例如maxlength要改成maxLength、readonly要改成readOnly。還有兩個比較特殊的屬性:class和for,由於這兩個是JavaScript中的關鍵字,因此要改名,前者改成className,後者改成htmlFor。

  JSX中的屬性值不僅可以是字串,還可以是表示式。如果是表示式,那麼就要用花括號(“{}”)包裹。而在JSX中,任何位置都可以使用表示式。

  有一點要注意,為了防止XSS的攻擊,React會把所有要顯示到DOM中的字串進行轉義,例如“<搜尋>”轉義成“&lt;搜尋&gt;”。

3)Virtual DOM

  眾所周知,DOM操作會引起重繪和重排,而這是非常消耗效能的。React能把元素轉換成物件,也就是說可以用物件來表示DOM樹,而這個存在於記憶體中的物件正是Virtual DOM。Virtual DOM相當於一個快取,當資料更新後,React會重新計算Virtual DOM,再與上一次的Virtual DOM通過diff演算法做比對(如下圖所示),最後只在頁面中更新修改過的DOM。由於大部分的操作都在記憶體中進行,因此效能將會有很大的提升。

二、元件

  元件的構建方式有3種:React.createClass()、ES6的類和函式。當用ES6的類來構建時,所有的元件都繼承自抽象基礎類React.Component,該抽象類聲明瞭state、props、defaultProps和displayName等屬性,定義了render()、setState()和forceUpdate()等方法。注意,在元件的建構函式constructor()中要呼叫super()函式,用於初始化this和執行抽象類的建構函式。

import React, { Component } from 'react';
class Search extends Component {
    constructor (props) {
        super(props);
    }
    render() {
        return ();
    }
}

  元件中的render()方法是必須的,它會返回一個元素、數字或字串等各種值。render()是一個純函式,即輸出(返回值)只依賴輸入(引數),並且執行過程中沒有副作用(不改變外部狀態)。

  元件之間可以相互巢狀,而它們的資料流是自頂向下流動的(如下圖所示),即父元件將資料傳給子元件。此處傳遞的資料就是元件的配置引數,由props屬性控制,而元件的內部狀態儲存在state屬性中。

1)props

  如果一個元件要做到可複用,那麼它應該是可配置的。為此,React提供了props屬性,它的使用如下所示。

class Search extends Component {
    render() {
        return (
            <div>
                <button className="btn" data-url={this.props.url}>{"<搜尋>"}</button>
            </div>
        );
    }
}
<Search url="http://www.pwstrick.com" />

  先給Search元件定義一個名為url的屬性,然後在元件內部,可以通過引用props屬性來獲取url的值。有一點要注意,props是隻讀屬性,因此在元件內部無法修改它。

  React為元件提供了預設的配置,可以呼叫它的靜態屬性defaultProps。總共有兩種寫法實現預設配置,如下程式碼所示,其中寫法一用到了ES6中的static關鍵字。

//寫法一
class Search extends Component {
    static defaultProps = {
        url: "http://jane.com"
    };
}
//寫法二
Search.defaultProps = {
    url: "http://jane.com"
};
<Search />

  此時,即使元件不定義url屬性,在元件內部還是會有值。

  props還有一個特殊的屬性:children,它的值是元件內的子元素,如下程式碼所示,children屬性的值為“<h1>React掃盲教程</h1>”。

class Search extends Component {
    render() {
        return (
            <div>
                {this.props.children}
            </div>
        );
    }
}
<Search>
  <h1>React掃盲教程</h1>
</Search>

2)state

  元件的呈現會隨著內部狀態和外部配置而改變,通常會在元件的建構函式中初始化需要的內部狀態,如下程式碼所示,為文字框新增預設提示。

class Search extends Component {
    constructor (props) {
        super(props);
        this.state = {
            txt: "請輸入關鍵字"
        };
    }
}

  React還提供了setState()方法,用於更新元件的狀態。注意,不要通過直接為state賦值的方式來更新狀態,因為setState()方法在更新狀態後,還會呼叫render()方法,重新渲染元件。此外,React為了提升效能,會把多次setState()呼叫合併成一次,像下面這樣寫打印出的txt屬性的值仍然是前一次的值,因此狀態更新是非同步的。

this.setState({
  txt: "React"
});
console.log(this.state.txt);    //"請輸入關鍵字"

3)生命週期

  元件的生命週期(life cycle)可簡單的分為4個階段:初始化(Initialization)、掛載(Mounting)、更新(Updation)和解除安裝(Unmounting),具體如下圖所示。每個階段都會對應幾個方法,其中包含will的方法會在某個方法之前被呼叫,而包含did的方法會在某個方法之後被呼叫。

1、在初始化階段,會設定props、state等屬性。

2、在掛載階段,兩個掛載方法將以元件的render()為分界點。

3、更新階段發生在傳遞props或執行setState()的時候。

4、當一個元件被移除時,就會呼叫componentWillUnmount()方法。

當元件在頁面中輸出時,在控制檯將依次輸出“will mount”、“render”和“did mount”。

class Search extends Component {
    componentWillMount() {
        console.log("will mount");
    }
    componentDidMount() {
        console.log("did mount");
    }
    render() {
        console.log("render");
    }
}

三、React和DOM

1)ReactDOM

  如果要把元件新增到真實的DOM中,那麼就需要使用ReactDOM中的render()方法,如下程式碼所示,其實在前面已經呼叫過幾次這個方法了。

ReactDOM.render(
    <Search />,
    document.getElementById("container")
);

  此方法可接收三個引數,第一個是要渲染(即新增)的元素,第二個是容器元素(即新增的位置),第三個是可選的回撥函式,會在渲染或更新之後執行。

  ReactDOM還提供了另外兩個方法:unmountComponentAtNode()unmountComponentAtNode(),具體可參考官方文件。

2)事件

  React實現了一種合成事件(SyntheticEvent),合成事件只有冒泡傳播,並且它的註冊方式、事件物件和事件處理程式中的this物件都與原生事件不同。

1、合成事件會通過設定元素的屬性來註冊事件,但與原生事件不同的是,屬性的命名要用駝峰的寫法而不是全部小寫,並且屬性值可以是任意型別而不再僅是字串,如下程式碼所示。React已經封裝好了一系列的事件型別(原生事件型別的一個子集),並且已經處理好它們的相容性,提供的事件型別可以參考官網

class Search extends Component {
    handle(e) {
        console.log("click");
    }
    render() {
        return (
            <div>
                <button onClick={this.handle}>搜尋</button>
            </div>
        );
    }
}

2、合成事件中的事件物件(event object)是一個基於W3C標準的SyntheticEvent物件的例項,它不但與原生的事件物件擁有相同的屬性和方法(例如cancelable、preventDefault()、stopPropagation()等),還完美解決了相容性問題。

3、React的事件處理程式中的this物件預設是undefined,因為註冊的事件都是以普通函式的方式呼叫的。如果要讓this指向當前元件,那麼可以用bind()方法或ES6的箭頭函式。

class Search extends Component {
    //bind()方法
    handle1(e) {
        console.log(this);
    }
    //箭頭函式
    handle2 = (e) => {
        console.log(this);
    };
    render() {
        return (
            <div>
                <button onClick={this.handle1.bind(this)}>搜尋</button>
                <button onClick={this.handle2}>搜尋</button>
            </div>
        );
    }
}

4、在向事件處理程式傳遞引數時,要把事件物件放在最後,如下程式碼所示。

class Search extends Component {
    handle(keyword, e) {
        console.log(keyword);
        console.log(this);
    }
    render() {
        return (
            <div>
                <button onClick={this.handle1.bind(this, "REACT")}>搜尋</button>
            </div>
        );
    }
}

5、如果要為元件中某個元素註冊原生事件,那麼可以利用元素的ref屬性和元件的refs物件實現。例如實現一個文字框在失去焦點時,列印輸出它的值,如下程式碼所示。注意,原生事件的註冊要在componentDidMount()方法內執行。

class Search extends Component {
    componentDidMount() {
        this.refs.txt.addEventListener("blur", (e) => {
            this.getValue(e);
        });
    }
    getValue(e) {
        console.log(e.target.value);
    }
    render() {
        return (
            <div>
                <input placeholder={this.state.txt} ref="txt" />
            </div>
        );
    }
}

  在上面的程式碼中,ref屬性的值被設為了“txt”,此時,在refs物件中就會出現一個名為“txt”的屬性,關於這個它們的具體用法可以參考官網

3)表單

  HTML中的表單元素(例如<input>、<select>和<radio>等)在React都有相應的元件實現,React還把表單中的元件分為受控和非受控。

  受控元件(controlled component)的狀態由React元件控制,它的每個狀態的改變都會有一個與之對應的事件處理程式,並且在程式內部會呼叫setState()方法更新狀態。React推薦使用受控元件,下面是一個受控元件,注意,選擇框(<select>元素)中的value屬性表示選中項。

class Search extends Component {
    refresh(e) {
        this.setState({
            type: e.target.value
        });
    }
    render() {
        let {type} = this.state;
        return (
            <div>
                <select value={type} onChange={this.refresh.bind(this)}>
                    <option value="1">標題</option>
                    <option value="2">內容</option>
                </select>
            </div>
        );
    }
}

  非受控元件(uncontrolled component)的狀態不受React元件控制,也不用為每個狀態編寫對應的事件處理程式,但可以通過元素的ref屬性獲取它的值,非受控元件的寫法更像是傳統的DOM操作。在使用非受控元件時,如果要為其設定預設值,可以使用屬性defaultValue或defaultChecked,具體如下所示。

class Search extends Component {
    handle(e) {
        console.log(this.select.value);
    }
    render() {
        return (
            <div>
                <select defaultValue={2} ref={(select) => this.select = select}>
                    <option value="1">標題</option>
                    <option value="2">內容</option>
                </select>
                <button onClick={this.handle.bind(this)}>搜尋</button>
            </div>
        );
    }
}

4)樣式

  在React問世的初期,由於它推崇元件模式,因此會要求HTML、CSS和JavaScript混合在一起,這與過去的關注點分離正好相反。React已將HTML用JSX封裝,而對CSS的封裝,則丟擲了CSS in JS的解決方案,即用JavaScript寫CSS。

  在React中的元素都包含className和style屬性,前者可設定CSS類,後者可定義內聯樣式。style的屬性值是一個物件,其屬性就是CSS屬性,但屬性名要用駝峰的方式命名,例如margin-left改成marginLeft,具體如下所示。

class Search extends Component {
    render() {
        return (
            <div>
                <input style={{marginLeft:10, textAlign:"center"}}/>
            </div>
        );
    }
}

  注意,屬性名不會自動補全瀏覽器字首,並且React會自動給需要單位的數字加上px。在MDN上給出了CSS屬性用JavaScript命名的對應關係,可在此處參考

  由於React處理CSS的功能並不強大,因此市面上出現了很多與CSS in JS相關第三方類庫,例如classnamespolished.js等,有外國網友還專門蒐集了40多種相關的類庫。

  雖然這種方式能讓元件更方便的模組化,但同時也徹底拋棄了CSS,既不能使用CSS的特性(例如選擇器、媒體查詢等),也無法再用CSS前處理器(例如SASS、LESS等)。為了解決上述問題,又有人提出了CSS Modules

  如果要在React中製作動畫,官方推薦使用React Transition GroupReact Motion。不過,你也可以使用普通的動畫庫(例如animejs),只要在DOM渲染好以後呼叫即可。

四、React進階

1)跨級通訊

  React資料流動是單向的,元件之間通訊最常見的方式是父元件通過props向子元件傳遞資訊,但這種方式只能逐級傳遞,如果要跨級通訊(即父元件與孫子元件通訊),那麼可以利用狀態提升實現,但這樣的話,程式碼會顯得很不優雅並且很臃腫。好在React包含一個Context特性,可以滿足剛剛的需求,不過官方不建議大量使用該特性,因為它不但會增加元件之間的耦合性,還會讓應用變得混亂不堪,下圖演示了兩種資料傳遞的過程。在理解Context特性後,能更合理的使用狀態管理容器Redux。

   當一個元件設定了Context後,它的子元件就能直接訪問Context中的內容,Context相當於一個全域性變數,但作用域僅限於它的子元件中。總共有兩種Context的實現方式,都基於生產者消費者模式。首先來看第一種,具體程式碼如下所示。

import PropTypes from 'prop-types';
class Grandpa extends Component {
    getChildContext() {
        return { name: "strick" };
    }
    render() {
        return (<Son />);
    }
}
Grandpa.childContextTypes = {
    name: PropTypes.string
};
class Son extends Component {
    render() {
        return (<Grandson />);
    }
}
class Grandson extends Component {
    render() {
        let { name } = this.context;
        return (<div>爺爺叫{name}</div>);
    }
}
Grandson.contextTypes = {
    name: PropTypes.string
};

  在上面的程式碼中,建立了三個元件,Grandpa是最上層的父元件(生產者),Son是中間的子元件,Grandson是最下層的孫子元件(消費者)。首先在Grandpa中,聲明瞭一個靜態屬性childContextTypes和一個getChildContext()方法,這兩個是必須的,否則無法實現資料傳遞。其中childContextTypes是一個物件,它的屬性名就是要傳遞的變數名,而屬性值則通過PropTypes指明瞭該變數的資料型別,getChildContext()方法返回的物件就是要傳遞的一組變數和它們的值。然後在Son中渲染Grandson元件。最後為Grandson宣告一個靜態屬性contextTypes,同樣是個物件,並且屬性名和屬性值與childContextTypes中的相同。

  第二種方式是在React 16.3的版本中引入的,比起第一種方式,寫法更加簡潔,並且Context的生產者和消費者都以元件的方式實現,如下所示。

let NameContext = React.createContext({ name });
class Grandpa extends Component {
    render() {
        return (
            <NameContext.Provider value={{name: "strick"}}>
                <Son />
            </NameContext.Provider>
        );
    }
}
class Son extends Component {
    render() {
        return (<Grandson />);
    }
}
class Grandson extends Component {
    render() {
        return (
            <NameContext.Consumer>
                {(context) => (
                    <div>爺爺叫{context.name}</div>
                )}
            </NameContext.Consumer>
        );
    }
}

  上面的程式碼依然建立了三個元件,名字也和第一種方式中的相同。除了中間元件Son之外,另外兩個元件的內容發生了變化。首先,通過React.createContext()方法建立一個Context物件,此物件包含兩個元件:Provider和Consumer,前者是生產者,後者是消費者。然後在Grandpa的render()方法中設定Provider元件的value屬性,此屬性相當於getChildContext()方法。最後在Grandson元件中呼叫Context物件,注意,Consumer元件的子節點只能是一個函式。

2)高階元件

  高階元件(higher-order component,簡稱HOC)不是一個真的元件,而是一個函式,它的引數中包含元件,其返回值是一個功能增強的新元件。高階元件是一個沒有副作用的純函式,它遵循了裝飾者模式的設計思想,不會修改傳遞進來的原元件,而是對其進行包裝和拓展,不僅增強了元件的複用性和靈活性,還保持了元件的易用性。下面演示了高階元件是如何控制props和state的。

class Button extends Component {
    render() {
        return (
            <div>
                <button>{ this.props.txt }</button>
            </div>
        );
    }
}
//高階元件
function HOC(Mine) {
    class Wrapped extends Component {
        constructor() {
            super();
            this.state = {
              txt: "提交"
            };
        }
        render() {
            return <Mine {...this.state} />;
        }
    }
    return Wrapped;
}
let Wrapped = HOC(Button);

  高階元件HOC()的函式體中建立了一個名為Wrapped的元件,在它的建構函式中初始化了state狀態。然後在其render()方法中使用了{...this.state},這是JSX的一種語法,在state物件前新增擴充套件運算子,可把它解構成元件的一組屬性。最後在Button元件中呼叫傳遞進來的屬性。

  高階元件還有遷移重複程式碼、劫持render()方法和引用refs等功能。

五、後記

  就先整理這些了,如有錯誤,歡迎指正,後面還會陸續加入漏掉的知識點。

  最後,我想說下,其實自己也是一個React初學者,通過這樣的梳理後,對React有了更為深刻的理解,在後續的學習中能容易的吸收新的知識點。

 

原始碼下載:

https://github.com/pwstrick/react

 

參考資料:

React中文文件

深入React技術棧

React.js 小書

React WIKI

深入淺出React和Redux》 

React入門例項教程 阮一峰

React入門教程

全棧React: 第1天 什麼是 React?

深度剖析:如何實現一個 Virtual DOM 演算法

從零開始學 ReactJS

react component lifecycle

CSS Modules 入門及 React 中實踐

CSS in JS 簡介

聊一聊我對 React Context 的理解以及應用