React的平凡之路,或許我們才剛剛上路

chenhongdong發表於2018-05-13

大家一起來

React是一個用於構建使用者介面的JS庫,核心專注於檢視,目的實現元件化開發

所謂元件化開發,其實就像堆積木一樣,每個元件都包含了自己的邏輯和樣式,然後再組合到一起完成一個複雜的頁面

元件化的主要特點就是:可組合、可複用、可維護

那麼廢話不多說,讓我們直接進入今天的主題,運用官方推薦的腳手架來搭建一個react專案開始學習吧

create-react-app啟動react專案

第一步:全域性安裝create-react-app腳手架

npm i create-react-app -g
複製程式碼

第二步:建立react專案

create-react-app 專案名  
// 如:create-react-app react123
複製程式碼

React的平凡之路,或許我們才剛剛上路
通過以上操作就會自動建立一個名為react123的react專案了,在建立的過程中,腳手架會自動為你安好react的核心包,react和react-dom,在此過程完成後即可進入第三步了

第三步:進入專案並啟動服務

cd react123 && npm start
複製程式碼

React的平凡之路,或許我們才剛剛上路
通過上面的三步曲,就會自動彈出瀏覽器訪問一個localhost:3000(預設為3000埠)的react頁面了

現在,讓我們通過進入建立好的react專案中,先去看一下搭建好的結構是什麼樣的?

下面就為大家展示一番:

React的平凡之路,或許我們才剛剛上路
根據上圖畫圈所示,public目錄下的index.html表示的是專案的主靜態檔案(包含依賴的節點),開啟後發現一大堆內容,其實都可以刪掉,只需要留一個root即可

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
複製程式碼

說完public後,再說一下src目錄,這裡面的檔案也都可以刪掉(除了index.js主入口檔案),這裡就和webpack4裡預設的入口檔案比較類似了

先來了解下jsx

在src下的index.js中,我們就可以開啟React之旅了,React主要用的是Facebook自己開發的jsx語法,這是一種js和xml的集合體,直接上程式碼

// 首先引入react的核心包
// 這裡的命名必須叫React,如果不是的話會報錯
import React from 'react';  
// 這裡主要用到render方法,所以直接將其解構出來使用
import { render } from 'react-dom';   

// jsx語法通過babel來轉義,通過預設的babel-preset-react來進行轉義
// 將jsx語法解析成js來使用
let ele = (
    <h1 className="big">
        hi, <span>baby</span>
    </h1>
);
複製程式碼

我們可以將寫好的jsx語法直接放到babel官網來解析一下

// 解析後的程式碼如下
React.createElement(
  "h1",
  { className: "big" },
  "hi, ",
  React.createElement(
    "span",
    null,
    "baby"
  )
);
複製程式碼

通過React.createElement來建立一個虛擬DOM,其中包含三個引數,分別是type、props和children

  • type: 對應的標籤h1
  • props: 對應的屬性{ className: "big" }
  • children: 對應的子節點 -> 字串'hi, ' 和一個新的React.createElement( "span", null, "baby" )

其實按照React.createElement建立的物件來看,列印出來後的結構是這樣滴

React的平凡之路,或許我們才剛剛上路
根據上面列印的結構來看,其實它們都是react.element上的例項,那麼就來針對列印後的物件進行一個主要的抽離,來看看是何方神聖吧

{
    type: 'h1', 
    props: {
        children: [
            'hi',
            {
                type: 'span',
                props: {
                    children: 'baby'
                }
            }
        ],
        className: 'big'
    }
}
複製程式碼

在我們轉化完物件後,就可以通過ReactDOM提供的render方法去渲染這樣的一個物件,最後將其渲染到頁面中

上面寫好的ele程式碼塊,其實就是React.createElement的語法糖,畢竟每次寫那麼一大坨React.createElement的話,十有八九會寫出錯的(我覺得可能是100%會出錯,哈哈)

所以我們看到這裡可以簡單的梳理一下整個渲染的執行順序

jsx語法 -> createElement格式 -> 轉化成物件 -> 物件轉換成真實dom - > render方法渲染

import React from 'react';  
import { render } from 'react-dom';   

// React.createElement的語法糖
let ele = (
    <h1 className="big">
        hi, <span>baby</span>
    </h1>
);
render(ele, window.root); // 將ele渲染到root上
複製程式碼

下圖是將虛擬dom渲染到root節點下的情況

React的平凡之路,或許我們才剛剛上路
上面寫的程式碼不多,主要就是簡單寫了個jsx的語法,然後通過render進行了渲染而已。

在寫ele的時候,我們發現和正常的html還是很相像的,但是我在這裡要說一下, 其實它們不一樣,接下來就看看有哪些地方不一樣吧!

jsx和html寫法的區別

  1. className 它會轉化成 class
  2. htmlFor 它要轉化成for屬性 label for
  3. jsx元素可以巢狀
  4. 相鄰的react元素,必須要加一層包裹起來
  5. jsx裡面可以寫js,{}裡面可以寫js語句
  6. 只支援多行註釋,{/* ... */},不過很少寫註釋
  7. style標籤必須寫成一個物件,如:{ background: 'skyblue' }

說了這幾點區別,直接開擼,沒有程式碼更讓人直觀的喜愛了,hurry up go go go

import React from 'react';
import { render } from 'react-dom';
// 提示:下面註釋標記的數字對應上面區別的序號,序號5和6就不寫了

let singer = '周杰倫';
let style = { backgroundColor: '#0cc', color: '#fff', fontSize: '14px' };
let album = '跨時代';
let arr = ['晴天', '陰天', '下雨天'];

let ele = (
    /* 4.相鄰的react元素外層必須包起來(h1,label,input,p它們都是相鄰的),
        vue是用一個template標籤包起來,
        可以寫一個div包起來,不過會產生一個多餘的div標籤
        react直接採用React.Fragment來包裹最佳
    */
    <React.Fragment>
        { /*1.渲染後會轉成<h1 class="song">煙花易冷</h1>*/ }
        <h1 className="song">
            煙花易冷-
            {/* 3.jsx元素可以巢狀 */}
            <span>{singer}</span>
        </h1>
        {/* 2.htmlFor會轉成<label for="inp"></label>
            點選label標籤會自動獲取到input焦點 */}
        <label htmlFor="inp">獲得焦點</label>
        <input type="text" id="inp" />
        {/* 7.style必須是物件的形式,如style={{color: '#fff'}} */}
        <p style={style}>{album}</p>
        {arr.map((item, index) =>{ 
            return <li key={index}>{item}</li>
        })}
    </React.Fragment>
);

render(ele, window.root);
複製程式碼

React的平凡之路,或許我們才剛剛上路
按照上面的程式碼可以直觀的看到一些具體的區別,那麼心急吃不了臭豆腐,趕緊開始繼續往下寫吧。就這點東西才不是react的兩把刷子呢,下面有請react的元件閃亮登場!

元件

在react中元件分為兩種,一種是函式元件,一種是類元件,那麼如何區分是不是元件呢?這裡有一個標準,首字母要大寫

what?是的,沒錯,小寫的就是jsx元素了,哈哈,不信你來看

函式元件

import React from 'react';
import { render } from 'react-dom';

// 此為函式元件
function Song(props) {
    return (
        <div className="wrap">
            <h1>晴天-{props.singer}</h1>
            <p>為你翹課的那一天,花落的那一天</p>
            <p>教室的哪一間,我怎麼看不見</p>
        </div>
    );
}
// 元件可以通過屬性傳遞資料
// render方法不會一直渲染,只會渲染一次
render(<Song singer="周杰倫" />, window.root)
複製程式碼

當然有些人可能不信,如果我把Song寫成song,然後再渲染,能有什麼問題呢?

其實沒有問題,那是不可能的,因為人家react瞭解你,會給你一個報錯告訴你是不是想使用一個react元件進行渲染,那就把首字母大寫

官方說的,童叟無欺了,哈哈

繼續回到函式元件的探討中,函式元件是個無this,無生命週期,無狀態的三無產品。所以可想而知使用頻率也是不如類元件的。

舉個例子:

import React from 'react';
import { render } from 'react-dom';
// 函式元件Clock
function Clock(props) {
    return <p>{props.date}</p>
}

setInterval(() => {
    render(<Clock date={new Date().toLocaleString()} />, window.root);
}, 1000);
複製程式碼

可以實時的更新時間,不過由於函式元件沒有狀態,而且只會渲染一次,所以還要在外層加一個setInterval定時器,就有點不倫不類了

更重要的是,狀態的更改都是通過屬性由我們傳遞過去的,沒有自己的狀態,重新整理的時候都依賴傳遞,這樣很不好,元件的特點就是複用

所以我們就要隆重的請出元件中的巨無霸,類元件閃亮登場

類元件

還是上面的栗子,我們改成類元件的形式

// 由於類元件需要繼承react的Component,所以直接解構出來使用
import React, { Component } from 'react';
import { render } from 'react-dom';

class Clock extends Component { // 繼承Component
    constructor() {
        super();    // 繼承後可以使用setState方法
        // 設定元件的預設狀態
        this.state = { date: new Date().toLocaleString(), pos: '北京' }
    }
    // 需要提供一個render方法返回dom物件
    render() {
        return (
            <React.Fragment>
                <p>當前時間:{this.state.date}</p>
                <p>座標:
                    <span style={{color: 'skyblue'}}>{this.state.pos}</span>
                </p>
            </React.Fragment>
        );
    }
    // 一不小心就展示了目前的第一個生命週期了
    // componentDidMount生命週期
    // 當元件渲染完成後呼叫此生命週期
    componentDidMount() {
        setInterval(() => {
            // 重新設定date的狀態值,
            // this.setState可以更改狀態重新整理頁面
            this.setState({date: new Date().toLocaleString()});
        }, 1000);
    }
}

render(<Clock />, window.root);
複製程式碼

上面通過類元件完成了同樣的效果,接下來我們好像還少些什麼?事件?沒錯,我們寫js怎麼能不新增事件呢,下面就來說一下如何新增事件

接著上面的程式碼,我們繼續寫,再敲一遍,不怕累,熟能生巧,是勝利

import React, { Component } from 'react';
import ReactDOM, { render } from 'react-dom';

class Clock extends Component {
    constructor() {
        super();
        this.state = { date: new Date().toLocaleString(), pos: '北京' };
        // 強制繫結this
        this.handleClick = this.handleClick.bind(this);
    }
    /* 繫結方法有幾種方式 方法中可能會用到this
        1.箭頭函式 (不提倡,會產生個新函式,樓下2層也是)
        2.bind繫結 this.handleClick.bind(this) 
        3.在建構函式中繫結this (官方推薦)
        4.ES7語法可以解決this指向 handleClick = () => {} (更推薦,哈哈)
    */
    handleClick = () => {
        console.log(this) // 直接綁給一個函式的話,此時的this是undefined
        
        // 在方法裡,我們來移除一下這個元件
        // 移除元件我們就用到了ReactDOM的方法
        ReactDOM.unmountComponentAtNode(window.root);
    }
    render() {
        // 這裡繫結個click事件,用的是駝峰寫法onClick
        // 後面要跟js語法,onClick={}
        return (<React.Fragment>
            <p onClick={this.handleClick}>當前時間:{this.state.date}</p>
            <p>座標:
                <span style={{color: 'skyblue'}}>{this.state.pos}</span>
            </p>
        </React.Fragment>)
    }
    componentDidMount() {
        this.timer = setInterval(() => {
            this.setState({ date: new Date().toLocaleString() });
        }, 1000);
    }
    componentWillUnmount() {
        clearInterval(this.timer);
    }
}

render(<Clock />, window.root);
複製程式碼

執行以上程式碼後,點選當前時間的p標籤後,就會在root節點下移除掉該元件(dom)了,不過可能大家會忽略掉一些常見的問題,下面我就來解釋一下

  • 當點選移除元件後,定時器仍在執行,setState還在更新著date的狀態,所以會有報錯的
  • 下面就著重解釋一下這個問題
class Clock extends Component {
    // 主要看這裡(氣質)
    componentDidMount() {
        // 和以往清除定時器的做法一樣,我們直接將timer掛到this例項上
        this.timer = setInterval(() => {
            this.setState({date: new Date().toLocaleString()})
        });
    }
    // 這個生命週期是元件將要被解除安裝的時候呼叫
    componentWillUnmount() {
        // 一般解除安裝元件後要移除定時器和繫結的方法
        clearInterval(this.timer);  
    }
}
複製程式碼

好了,上面的報錯問題就這樣迎刃而解了。不過,說的總比唱的好聽,為什麼要將timer放到this上?放到this.state裡的timer狀態不行嗎?

放到this.state上的話說明,當前頁面要依賴這個狀態,狀態變了檢視可以重新整理,setState是可以更新頁面的

然而將timer直接放到當前例項上(this),即時刪掉(清空)了,也不影響頁面

元件講了這麼多,讓我們做個小結吧

  • 元件有兩個資料來源
    • 一個是屬性 外界傳遞的
    • 一個是狀態 自己擁有的

小結是完畢了,那就喝杯水繼續往下看吧,我們發現react的狀態很有分量啊,state狀態改變後可以重新整理檢視改變,屌不屌!吊炸天啊簡直,那麼我們就單獨針對這個state來說上一說吧

state狀態

蒼白的文字,不如真實的程式碼看的帶勁,上程式碼

// 這是一個很常見很常見的栗子,計數器

import React, { Component } from 'react';
import { render } from 'react-dom';

class Counter extends Component {
    constructor() {
        super();
        this.state = { count: 1 };
    }
    handleClick = () => {
        // 寫到這裡就可以實現點選一次button就加1一次了
        // this.setState({ count: this.state.count + 1 });
        
        // this.setState也可以接受一個函式,它的第一個引數就是上一次的狀態
        // 
        this.setState(prevState => ({ count: prevState.count + 1 }));
        this.setState(prevState => ({ count: prevState.count + 1 }));
        this.setState(prevState => ({ count: prevState.count + 1 }));
    }
    render() {
        return (
            <div>
                <span>計數器: </span>
                {this.state.count}
                <button onClick={this.handleClick}>點我</button>
            </div>
        );
    }
}

render(<Counter></Counter>, window.root);
複製程式碼

state有個特點:多個狀態可以批量更新?就是說如果我上面寫了三個this.setState去改變count,那按常理來說,狀態就應該改變三次,點一次button就加3了

其實不然,react不會這麼做,如果狀態改變一次,就重新整理一次頁面的話,那就瘋狂了,react是把狀態(count)先記住,最後再一起重新整理頁面

於是乎就用上面重新設定的setState來保留count的狀態,最後再一次性的改變。說白了就是如果更新時下一次的狀態依賴於上一次的狀態,就得寫成函式的形式

好了,下面再來說說元件間的通訊吧,先來個父子元件直接的通訊

元件間的通訊

元件間的通訊,父子元件之間就是通過屬性傳遞的,父 -> 子 -> 孫子

單向資料流,資料方向是單向的,子不能改父的屬性 下面來看個小demo

父元件 songs.js


import React, { Component } from 'react';
import { render } from 'react-dom';
import axios from 'axios';  // axios用於請求
import List from './list';  // 子元件
import Audio from './audio';  // 子元件

import './css/songs.css';

class Songs extends Component {
    constructor() {
        super();
        this.state = { songs: [], mp3Url: '', isPlay: false };
    }
    // 子傳父通訊 -> 父提供一個方法,子呼叫後將引數回傳
    // chooseSong為選擇歌曲地址填入audio的src的方法
    chooseSong = (url, isPlay) => { 
        // 這裡子元件回傳了屬性更改了isPlay的狀態
        // 對應修改了List的同級元件Audio接收的played屬性值
        this.setState({mp3Url: url, isPlay});
    }
    render() {
        return (
            <div className="songs-box">
                <ul>
                    {this.state.songs.map((item, index) => (
                        /* 
                            通過傳遞屬性的方式進行通訊
                            這裡將item解構出來,然後List子元件按照需要去拿資料
                        */ 
                        <List key={index} {...item} choose={this.chooseSong}></List>
                    ))}
                </ul>
                <Audio url={this.state.mp3Url} played={this.state.isPlay}></Audio>
            </div>
        );
    }
    async componentDidMount() {
        // 在react中傳送ajax請求 現在我們用axios
        // axios封裝了RESTFul 基於promise的 不支援jsonp 可用在服務端
        let { data } = await axios.get('http://musicapi.leanapp.cn/search?keywords=林俊杰');
        this.setState({ songs: data.result.songs });
    }
}

render(<Songs></Songs>, window.root);
複製程式碼

未播放效果

React的平凡之路,或許我們才剛剛上路
播放效果
React的平凡之路,或許我們才剛剛上路
剩餘的程式碼我就不一一展示了,僅僅是個簡單的小demo,功能很多都未完善,有興趣的同學可以去研究一下繼續寫寫吧,貼個地址吧

通過上面的小demo,其實可以發現元件間的通訊主要有三種方式

  • 第一種方式是通過屬性傳遞,父 -> 子 -> 孫子 (父傳子)
    • 單向資料流,資料方向是單向的,子不能改父的屬性
  • 第二種方式是父寫好了一個方法,傳遞給兒子 (子傳父)
    • 兒子呼叫這個方法,在這個方法中可以去更改狀態
  • 第三種方式是同級元件傳遞
    • 同級元件想要傳遞資料,可以找到共同的父級,沒有父級就創造父級

受控元件和非受控元件

接下來了解一下受控元件與非受控元件的兩個概念,他們指的是表單元素

既然是涉及到了表單元素,那我們期待的雙向資料繫結應該也是該登場的了

雙向資料繫結

import React, { Component } from 'react';
import { render } from 'react-dom';

class Input extends Component{
    constructor(){
        super();
        this.state = {val: '你好'};
    }
    // 通過onChange事件呼叫該方法後,每次更改輸入後val的狀態值
    handleChange = (e) =>{ //e是事件源
        let val = e.target.value;
        this.setState({val});
    };
    render(){
        return (<div>
            <input type="text" value={this.state.val} onChange={this.handleChange}/>
            {this.state.val}
        </div>)
    }
}
render(<Input />, window.root);
複製程式碼

雙向資料繫結主要通過的是監聽onChange事件,然後每次都將新輸入的value值更改對應val的狀態,從而再賦給input的value屬性,做到了資料雙向傳遞的實現,果然很高階,哈哈

受控元件

受狀態控制的元件,必須要有onChange方法,否則不能使用

受控元件可以賦予預設值

import React, { Component } from 'react';
import { render } from 'react-dom';

class App extends Component{
    constructor(){
        super();
        this.state = {a: '破風', b: '激戰'};
    }
    // name表示的就是當前狀態改的是哪一個
    // e表示的是事件源
    handleChange = (e) => { //處理多個輸入框的值對映到狀態的方法
        let name = e.target.name;
        this.setState({ [name]: e.target.value });
    }
    render(){
        return (
            <form>
                 <input type="text"
                    required={true}
                    value={this.state.a}
                    onChange={this.handleChange}
                    name="a"
                />
                <input type="text"
                    required={true}
                    value={this.state.b}
                    onChange={this.handleChange}
                    name="b"
                />
                <input type="submit" />
                <p>{this.state.a}</p>
                <p>{this.state.b}</p>
            </form>
        )
    }
}

render(<App></App>, window.root);
複製程式碼

React的平凡之路,或許我們才剛剛上路
最後再來看一下非受控元件

非受控元件

所謂非受控元件有三個特點:

  1. 可以操作dom,獲取真實dom
  2. 可以和第三方庫結合
  3. 不需要對當前輸入的內容進行校驗,也不需要預設值
import React, { Component } from 'react';
import { render } from 'react-dom';

// 1.函式的方式 ref
// 2.React.createRef()  v16.3+
class App extends Component {
    constructor() {
        super();
        this.aaa = React.createRef();
    }
    componentDidMount() {
        // this.aaa.focus();    // 對應1
        this.aaa.current.focus(); // 對應2
    }
    render() {
        return (<div>
            {/* 1.<input type="text" ref={input=>this.aaa = input} />*/}
            {/* 2.會自動的將當前輸入框 放在this.aaa.current */}
            <input type="text" ref={this.aaa} />
        </div>)
    }
}
render(<App />, window.root);
複製程式碼

上面的栗子呢,就是一進入頁面後就會自動獲取輸入框的焦點了,之後大家可以試著敲敲看吧

時間不早,節目剛好

雖然react還有很多的內容要講,而且還沒有涉及到生命週期等內容

不過,大家放心,該來的總會來,一個都不能少,在之後的文章裡,我會繼續學習和分享的

那麼今天就到這裡了,謝謝大家不辭辛勞的觀看,各位,安啦!!!

相關文章