React 初探

同時發表於 AlloyTeamcss

React 簡單介紹

先說 React 與 React Native

他們是真的親戚,可不像 Java 和 Javascript 同樣。html

其實第一次看到 React 的語法我是拒絕的,由於這麼醜的寫法,你不能讓我寫我就寫。前端

但當我發現 React Native 橫空出世後,它學習一次處處運行的理念很是誘人。React Native 能夠寫出原生體驗的 iOS/Android 應用?那不就多了一門裝逼技能?因此咱們調研小組試了一下,感受 "Duang" 一下,很爽很舒服。寫 React Native 須要兩門基礎技能:React 語法 和 iOS 基礎知識。node

很爽很舒服,索性就研究一下,算是入門。
瞭解以後發現,React 真是有另外一番天地,值得學習。react

接下來總結如下我對 React 的理解,分享給你們。webpack

至於 React Native,有機會再好好探究下。git

這部分廢話太多,喜歡實戰的能夠直接看代碼部分。es6

React 是 Facebook 出品的一套顛覆式的前端開發類庫。github

爲何說它是顛覆式的呢?web

內存維護虛擬 DOM

對於傳統的 DOM 維護,咱們的步驟多是:

  1. 初始化 DOM 結構
  2. 從服務器獲取新數據
  3. 使用新數據更新局部 DOM
  4. 綁定各類事件

首先,咱們操做 DOM 是最昂貴的開銷,對於 須要反覆更新 DOM 的網頁,無疑是噩夢。其次,對 DOM 局部的更新以及事件綁定加大了維護的難度。

而 React 引入了一個全新的概念:虛擬 DOM。

虛擬 DOM 是躺在內存裏的一種特殊的結構,咱們能夠理解爲這是真實 DOM 在內存裏的映射。

除告終構上的映射外,這個虛擬的 DOM 還包括了渲染
真實所須要的數據以及事件綁定。

全量更新真實 DOM

虛擬 DOM 在建立時,首先是使用 JSX 的語法生成一個真實 DOM 樹的映射,其次是從服務器端拉取遠程數據,接着注入到這個虛擬 DOM 樹中,同時綁定事件。

好了,有了虛擬 DOM、數據、事件,萬事俱備。

接下來,調用 render() 方法一次性渲染出真實的 DOM,而後全量插入到網頁中。

虛擬 DOM 靜靜地躺在內存裏,等待數據更新。

新數據來臨,調用 setState() 方法更新數據到虛擬 DOM 中,而後自動調用 render() 再一次性渲染出真實的 DOM ,而後全量更新到網頁中。

一個虛擬 DOM,對應一個真實 DOM
一份數據更新,從新生成虛擬 DOM ,全量更新真實 DOM

就這麼簡單。
除了帶來性能上的提高以外,很顯然這種寫法簡化了咱們維護 DOM 的成本 -- 咱們只須要維護一份數據。

只是 View,可配合其餘類庫使用

能夠看到,React 裏最重要的概念有虛擬 DOM、單向數據注入(虛擬 DOM 到真實 DOM)。
這裏並無引入太多其餘的東西,因此我對 React 的理解是一個類庫,而非框架。
若是要使用 MVC、MVVM 等技術的吧,徹底能夠把 React 當作其中的 V,即 View, 配合其餘類庫使用。

組件化

我雖然是個前端菜鳥,但日觀天象也是能嗅到下一代 Web 將是組件化、組件複用共享的時代。

React 編寫起來,就是編寫一個個的組件。

我對一個 React 組件的理解是:

  • 模板 HTML (JSX 語法格式)
  • 樣式 CSS (仍是獨立的樣式文件)
  • 交互 JS (與HTML一塊兒,揉和到 JSX 語法中)

以上三者能夠打包複用,甚至是無縫接入,我腳得它就多是將來了。

HTML 與 JS 使用 JSX 語法糅合到一塊兒的方式是見仁見智,恐怕會引發戰爭。

我剛接觸到 JSX 的時候,一開口也是『我*,好醜』。

但慢慢地卻發現,這種方式一開始寫起來彆扭,但用得卻很爽。

接下來,我經過編寫一個簡單的應用來入門 React。

看完若是大呼不過癮,建議直飛 React 官方看文檔,那纔是寶藏!

React 簡單示例

示例代碼放置在 demo/目錄下,每一個文件夾爲一個獨立的示例。

先看下這個 demo 最終的樣子吧:

demo - 速度與激情

每一個示例的入口文件 index.html 結構大致相同:

<!-- React 真實 DOM 將會插入到這裏 -->


<div id="demo"></div>



<!-- 引入 React -->
<script src="../../bower_components/react/react.js"></script>
<!-- 引入 JSX 語法格式轉換器 -->
<script src="../../bower_components/react/JSXTransformer.js"></script>

<!-- 注意:script 須要註明 type 爲 text/jsx 以指定這是一個 JSX 語法格式 -->
<script type="text/jsx" src="demo.js"></script>
</body>

渲染一個虛擬 DOM 爲真實 DOM

使用 render() 方法生成真實 DOM 並插入到網頁中。

// 使用 React.createClass 建立一個組件
var DemoComponent = React.createClass({
    // 使用 render 方法自動渲染 DOM
    render: function () {
        return (


<div className="component-hello">
                <h1 className="hello-title">Hello React</h1>
                <p  className="hello-desc">React 初探</p>
                <div className="hello-movies">
                    <p2>我喜歡的電影</p2>
                    <ul>
                        <li className="movie-item">
                            <span className="movie-name">速度與激情7</span>
                            -
                            <span className="movie-date">2015</span>
                        </li>
                    </ul>
                </div>
            </div>


        )
    }
});

// 將組件插入到網頁中指定的位置
React.render(<DemoComponent />, document.getElementById('demo'));

在線演示 demo/render

示例代碼 demo/render

設置初始數據

第一次渲染真實 DOM 時將使用 getInitialState() 返回的數據。

// 使用 React.createClass 建立一個組件
var DemoComponent = React.createClass({
    // getInitialState 中返回的值將會做爲數據的默認值
    getInitialState: function () {
        return {
            title: '我喜歡的電影',
            movies: [
                {
                    id: 7,
                    name: '速度與激情7',
                    date: 2015
                },
                {
                    id: 6,
                    name: '速度與激情6',
                    date: 2013
                }
            ]
        }
    },
    // 使用 render 方法自動渲染 DOM
    render: function () {
        // this.state 用於存儲數據
        var title  = this.state.title;
        var movies = this.state.movies.map(function (movie) {
            return (
                <li className="movie-item" key={movie.id}>
                    <span className="movie-name">{movie.name}</span>
                    -
                    <span className="movie-date">{movie.date}</span>
                </li>
            )
        });

        return (
            <div className="component-hello">
                <h1 className="hello-title">Hello React</h1>
                <p  className="hello-desc">React 初探</p>

                <div className="hello-movies">
                    <p2>{title}</p2>
                    <ul>{movies}</ul>
                </div>
            </div>
        )
    }
});

// 將組件插入到網頁中指定的位置
React.render(<DemoComponent />, document.getElementById('demo'));

在線演示 demo/get-initial-state

示例代碼 demo/get-initial-state

動態更新數據

第二次更新渲染真實 DOM 時將使用 setState() 設置的數據。

// 使用 componentDidMount 在組件初始化後執行一些操做
    componentDidMount: function () {
        // 拉取遠程數據
        // 開啓假數據服務器:
        // cd fake-server && npm install && node index.js
        this.fetchData();
    },

    // 使用自定義的 fetchData 方法從遠程服務器獲取數據
    fetchData: function () {
        var self = this;
        // 發起 ajax 獲取到數據後調用 setState 方法更新組件的數據
        var url = '../../fake-data/movies.json';
        $.getJSON(url, function (movies) {
            // 本地模擬返回太快了,模擬一下網絡延遲
            setTimeout(function() {
                self.setState({
                    movies: movies
                });
            }, 2000);
        });
    },

在線演示 demo/set-state

示例代碼 demo/set-state

綁定事件

綁定事件時,咱們可使用 ref="name" 屬性對一個 DOM 節點進行標記,同時能夠經過 React.findDOMNode(this.refs.name) 獲取到這個節點的原生 DOM。

// 使用 render 方法自動渲染 DOM
    render: function () {
        var self = this;
        // this.state 用於存儲數據
        var title  = this.state.title;
        var movies = this.state.movies.map(function (movie) {
            return (
                <li className="movie-item" key={movie.id}>
                    <span className="movie-name">{movie.name}</span>
                    -
                    <span className="movie-date">{movie.date}</span>
                    <a href="#" onClick={self.onRemove.bind(null, movie)}>刪除</a>
                </li>
            )
        }.bind(this));// 注意這裏 bind(this) 修正了上下文

        return (
            <div className="component-hello">
                <h1 className="hello-title">Hello React</h1>
                <p  className="hello-desc">React 初探</p>

                <div className="hello-movies">
                    <p2>{title}</p2>
                    <form onSubmit={this.onAdd}>
                        {/* 注意這裏指定 ref 屬性,而後咱們就可使用 this.refs.xxx 訪問到 */}
                        <input type="text" ref="name" placehlder="輸入你喜歡的電影"/>
                        <input type="text" ref="date" placeholder="上映時間"/>
                        <input type="submit" value="提交"/>
                    </form>
                    <ul>{movies}</ul>
                    {this.state.loading ? <div>你們好我是菊花, 我如今在轉</div> : null}
                </div>
            </div>
        )
    }
onRemove: function (movie) {
        var id = movie.id;
        console.log(movie)
        // 刪除這個 item
        var movies = this.state.movies;
        var len = movies.length;
        var index = -1;
        for(var i = 0; i < len; i++) {
            var _movie = movies[i];
            if (_movie.id === id) {
                index = i;
                break;
            }
        }
        if (index > 0) {
            movies.splice(index, 1);
            this.setState({
                movies: movies
            });
        }
    },

    onAdd: function (e) {
        e.preventDefault();
        var refs = this.refs;
        var refName = React.findDOMNode(refs.name);
        var refDate = React.findDOMNode(refs.date);
        if (refName.value === '') {
            alert('請輸入電影名');
            return;
        } else if (refDate === '') {
            alert('請輸入上映時間');
            return;
        }
        var movie = {
            // 使用 findDOMNode 獲取到原生的 DOM 對象
            name: refName.value,
            date: refDate.value,
            id: Date.now() // 粗暴地以時間數字做爲隨機 id
        };

        var movies = this.state.movies;
        movies.push(movie);
        this.setState(movies);

        refName.value = '';
        refDate.value = '';
    },

在線演示 demo/events

示例代碼 demo/events

多組件與組件嵌套

一個組件就包含了 JSX 模板、數據維護、事件綁定的話,代碼量已經夠多了,這時候能夠採用 AMD/CMD 的方式,將組件進行更細粒度的劃分,能夠以文件即組件的方式來編寫,這裏就不上 demo 了。

組件間通訊

在 React 中,數據流是單向的,且組件之間能夠嵌套,咱們能夠經過對最頂層組件傳遞屬性方式,向下層組件傳送數據。

  • 嵌套組件間,使用 this.props 屬性向下傳遞數據

  • 獨立組件之間,自行維護數據則須要自行維護一個全局數據存儲,或者使用發佈訂閱地方式通知數據的更新。

全局數據存儲怎麼作呢?能夠理解爲不一樣的組件獲取的數據源一致,在組件的外部維護這個數據集合,或者乾脆直接從服務器端獲取。

有人會說了,這樣很不方便。

但我以爲,既然是一個組件,那就配套有獲取組件所需數據的方式,獨立組件間有很強的數據依賴時,要麼使用上述方式,要麼能夠簡單粗暴,將獨立組件用一個頂層組件包裹起來,轉化爲嵌套組件的關係,便可數據互通。

// 將子組件抽離出來
var LiWrapper = React.createClass({
    render: function () {
        // 使用 this.props 得到傳入組件的數據
        var movie = this.props.movie;
        return (
            <li>{/* ... */}</li>
        )
    }
});

// 使用 React.createClass 建立一個組件
var DemoComponent = React.createClass({
    // 使用 getInitialState 的返回值做爲數據的默認值
    getInitialState: function () {
      // ...
    },

    // 使用 render 方法自動渲染 DOM
    render: function () {
        // this.state 用於存儲數據
        var movies = this.state.movies.map(function (movie) {
            return (
               <LiWrapper movie={movie}/>
            )
        }.bind(this));// 注意這裏 bind(this) 修正了上下文

        return (
            <div className="component-hello">
                {/* ... */}
                <div className="hello-movies">
                    <ul>{movies}</ul>
                </div>
            </div>
        )
    }
});

// 將組件插入到網頁中指定的位置
// 在使用組件時傳入 movies 數據
var movies = [// ...];
React.render(<DemoComponent movies={movies}/>, document.getElementById('demo'));

在線演示 demo/comunications

示例代碼 demo/comunications

打造絲滑的構建 使用 ES6 + gulp + webpack

ES6 和 gulp 的話就很少介紹啦。

webpack 是一款新生的前端構建工具,兼容 AMD/CMD 等寫法,支持 Browser 和 Node 端共享代碼,在瀏覽器端能夠像寫 Node 同樣方便的進行模塊化的劃分。

在這裏主要用 webpack 的兩個插件:

  • 使用 jsx-loader 這個插件支持 jsx 語法解析

  • 使用 esx-loader 這個插件支持 es6 語法解析

來看下簡單目錄結構:

  • js/main.js 爲入口文件,引入了兩個組件。
var React = require('react');

var MovieListComponent = require('./components/movie-list');
var HelloMessageComponent = require('./components/hello');

var movies = [
    {
        id: 5,
        name: '速度與激情5',
        date: 2011
    },
    {
        id: 4,
        name: '速度與激情4',
        date: 2009
    }
];

var wording = '保羅';

var MainComponent = React.createClass({
    render: function () {
        return (


<div className="component-hello">
                <HelloMessageComponent name={wording}/>
                <MovieListComponent movies={movies} />
            </div>


        )
    }
});

React.render(<MainComponent />, document.getElementById('demo'));
  • js/components/movie-list.js 組件爲 JSX 語法編寫
var React = require('react');

// 引入子組件
var MovieComponent = require('./movie');

// 使用 React.createClass 建立一個組件
var MovieListComponent = React.createClass({
    // 使用 getInitialState 的返回值做爲數據的默認值
    getInitialState: function () {
        return {
            loading: true,
            title: '我喜歡的電影',
            // 注意這裏將 外部傳入的數據賦值給了 this.state
            movies: []
        }
    },

    // 使用 render 方法自動渲染 DOM
    render: function () {
        // this.state 用於存儲數據
        var title  = this.state.title;
        // this.props 用於從組件外部傳入數據
        var movies = this.props.movies;
        movies = movies.map(function (movie) {
            return (
                <MovieComponent movie={movie}/>
            )
        }.bind(this));// 注意這裏 bind(this) 修正了上下文

        return (
            <ul>{movies}</ul>
        )
    }
});

module.exports = MovieListComponent;
  • js/components/hello.js 組件爲 ES6 + JSX 語法編寫
var React = require('react');

class HelloComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {wording: '你好呀, '};
    }
    render() {
        return 

<div>{this.state.wording} {this.props.name}</div>

;
    }
}

module.exports = HelloComponent;
  • webpack.config.js 指定 jsx-loader 和 es6-loader
module.exports = {
    entry: ['./js/main.js'],
    output: {
        path: __dirname,
        filename: 'js/bundle.js'
    },
    module: {
        loaders: [
            { test: /\.js$/, loader: 'es6-loader' },
            { test: /\.js$/, loader: 'jsx-loader' }
        ]
    }
};
  • gulpfile.js 在這裏配置 webpack 任務,啓動文件監聽
var gulp = require('gulp');
var livereload = require('gulp-livereload');
var webpack = require("gulp-webpack");

var webpackConfig = require('./webpack.config');

gulp.task("webpack", function() {
    return gulp.src('./js/main.js')
        .pipe(webpack(webpackConfig))
        .pipe(gulp.dest('.'));
});

gulp.task('watch', function() {
    livereload.listen();
    gulp.watch(['js/**/*.js', '!js/bundle.js'], ['webpack']);
});

gulp.task('default', [
    'webpack',
    'watch'
]);
  • index.html 示例頁面,引入 webpack 打包後的 js/bundle.js
<!-- React 真實 DOM 將會插入到這裏 -->
<div id="demo"></div>
<script src="./js/bundle.js"></script>

在 js/main.js 中引入兩個不一樣的組件,而後在 webpack.config.js 中指定編譯 JSX 和 ES6 的 loader 工具,使用 gulp 監聽 js/ 中文件變化,自動編譯出的 js/bundle.js 將被 index.html 引用。

嗯,再在 webpack 中加入各類你喜歡的 loader,在 gulp 中加上各類 css、js、img 的處理任務,編寫代碼,自動從新編譯,縱享絲滑。

示例代碼

零碎總結

文章到這裏應該就算結束了,接下來是一些在學習過程當中記下來的幾個小點,也分享給你們。

簡單理解 JSX 語法

JSX 把 JS 和 HTML 糅合起來了,這麼理解是否是感受比較簡單:

遇到 {} 包裹的是 JS,遇到 <> 包裹的是 HTML

render() 中 返回的的 JSX 模板須要一個根元素包裹起來

好比:

// 錯誤的寫法
var MyComponent = React.createClass({
    render: function () {
        return (
            <h1>速度與激情7</h1>
            <p>致敬保羅</p>
        )
    }
});

應該寫成:

// 正確的寫法
var MyComponent = React.createClass({
    render: function () {
        return (
            <div>
                <h1>速度與激情7</h1>
                <p>致敬保羅</p>
            </div>
        )
    }
});

幾個重要方法

  • render() 返回的是一系列嵌套的組件
  • this.props 獲取父組件傳遞給子組件的數據
  • this.setState({data: data}); 用於動態更新狀態,設置數據(設置後UI會自動刷新)
  • getInitialState() 在整個組件的生命週期中只會執行一次,用於初始化數據
  • componentDidMount 會在 render 後自動調用,用於異步獲取數據,更新數據

操做數據的流程

  1. gitInitialState() 初始化數據
  2. render() 渲染初始化數據
  3. componentDidMount() 異步獲取數據
  4. setState() 更新數據

理解一個組件的狀態轉換

每個組件均可以理解爲有一個簡單的狀態機。

調用 setState(data, callback) 後,data 將會混入 this.state 中,數據獲得了更新,render() 就會被調用,UI 就能被更新。

組件之間如何通訊

<Parent><Child /></Parent>

父組件能夠獲取到子組件:this.props.children

render() 永遠不要手動調用

render() 在 React 建立時會調用一次,在數據更新時調用 setState() 方法則會繼續調用它來更新網頁中的真實 DOM。

使用 getInitial() 設置默認值

這個方法返回的值會在組件初始化第一次調用 render() 時就被使用

class 是關鍵字,改用 className

// 錯誤的寫法
var MyComponent = React.createClass({
    render: function () {
        return (
            <div class="movie">
                <h1>速度與激情7</h1>
                <p>致敬保羅</p>
            </div>
        )
    }
});

應該寫成:

// 正確的寫法
var MyComponent = React.createClass({
    render: function () {
        return (
            <div className="movie">
                <h1>速度與激情7</h1>
                <p>致敬保羅</p>
            </div>
        )
    }
});

組件名大寫,否則不被識別

// 錯誤的寫法
var myComponent = React.createClass({
    render: function () {
        return (


<div class="movie">
                <h1>速度與激情7</h1>
                <p>致敬保羅</p>
            </div>


        )
    }
});

React.render(<myComponent />, document.getElementById('demo'));

應該寫成:

// 正確的寫法
var MyComponent = React.createClass({
    render: function () {
        return (


<div className="movie">
                <h1>速度與激情7</h1>
                <p>致敬保羅</p>
            </div>


        )
    }
});

React.render(<MyComponent />, document.getElementById('demo'));

怎麼隱藏或顯示菊花

var MyComponent = React.createClass({
    getInitialState: function () {
        return {
            loading: true
        }
    },
    showLoading: function () {
        this.setState({loading: true})
    },
    hideLoading: function () {
        this.setState({loading: false})
    },
    render: function () {
        return (
            {
                this.state.loading ?
                <div>你們好我是菊花,我在轉</div>
                :
                null
            }
        )
    }
});

插入原生的 HTML 片斷的方式

React 會爲咱們過濾 XSS,要讓一段 HTML 片斷直接顯示出來,須要這樣:

<div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />

讓 React 支持移動觸摸實踐

React.initializeTouchEvents(true);

處理表單

表單由於會因用戶交互而變化,因此有特定的一些屬性

  • input 和 textarea 組件具備 value
  • input[type=checkbox]input[type=radio] 具備 checked
  • option 具備 selected,若是要支持多選,能夠傳入數組:
<select multiple={true} value={['B', 'C']}>

表單項具備 onChange 事件

注意若是這麼寫:

render: function() {
    return <input type="text" value="Hello!" />;
  }

那每次 render 的時候 input 的 value 都會被重置爲 "Hello!",因此須要這麼控制:

getInitialState: function() {
    return {value: 'Hello!'};
  },
  handleChange: function(event) {
    this.setState({value: event.target.value});
  },
  render: function() {
    var value = this.state.value;
    return <input type="text" value={value} onChange={this.handleChange} />;
  }

利用這點,能夠無縫地接入一些驗證規則,好比限制文字爲 140 字:

handleChange: function(event) {
   this.setState({value: event.target.value.substr(0, 140)});
 }

若是不想這麼被控制呢?那就在返回 input 的時候,不要設置 value 屬性,這樣隨着用戶輸入,value 不會被重置:

render: function() {
    return <input type="text" />;
  }

也能夠設置默認值:

render: function() {
    return <input type="text" defaultValue="Hello!" />;
  }

除了 defaultValue 以外,還支持 defaultChecked

理解虛擬 DOM

React 會在內存裏維護一個表明 DOM 的結構,調用
render 方法時才生成真正的 DOM 插入到網頁中。

理解組件的生命週期

一個組件的聲明週期能夠理解爲三個階段:

  1. mounting 組件正在插入到 DOM 中
  2. updating 組件正在從新注入新數據後更新到 DOM 中
  3. unmounting 組件從 DOM 中移除

mounting 階段

  • getInitialState() 被調用,返回原始數據
  • componentWillMount() 在組件 mounting 前調用
  • componentDidMount() 在組件 mounting 完成後調用

updating 階段

  • componentWillReceiveProps(nextProps) 在接收到新的 props 時調用
  • shouldComponentUpdate(nextProps, nextState) 在組件須要更新 DOM 時調用,若這個函數返回 false 則告訴 React 不要更新
  • componentWillUpdate(nextProps, nextState) 在更新發生時調用,能夠在這裏調用 this.steState() 刷新數據
  • componentDidUpdate(prevProps, prevState) 在更新完成後調用

unmounting 階段

  • componentWillUnmount() 在組件移除時被調用,在這裏能夠對數據進行清理

強制使用數據更新組件

forceUpdate() 強制使用數據更新組件,而不用調用 this.setState()

獲取原生 DOM 元素

React.findDOMNode(component) 返回原生的 DOM 元素
注意要獲取原生的 DOM 元素,必須在 render 被調用, 真正的 DOM 已經被插入到頁面中時。

理解 refs

能夠把 refs 理解爲咱們在 HTML 中的id,用於定位到指定的組件。

<form onSubmit={this.onAdd}>
    {/* 注意這裏指定 ref 屬性,而後咱們就可使用 this.refs.xxx 訪問到 */}
    <input type="text" ref="name" placehlder="輸入你喜歡的電影"/>
    <input type="text" ref="date" placeholder="上映時間"/>
    <input type="submit" value="提交"/>
</form>

ref 屬性能夠是一個回調函數而不是名字,這個回調會在組件 mounted 後被調用。回調函數使用被引用的組件做爲參數。

<input ref={ function(component){ React.findDOMNode(component).focus();} } />

注意不要在 render 方法中訪問 refs 屬性。

相關文章
相關標籤/搜索