react實戰系列 —— react 的第一個元件

2022-08-08 06:04:49

其他章節請看:

react實戰 系列

react 的第一個元件

寫了 react 有一個半月,現在又有半個月沒寫了,感覺對其仍舊比較陌生。

本文分兩部分,首先聊一下 react 的相關概念,然後不使用任何語法糖(包括 jsx)或可能隱藏底層技術的便利措施來構建 React 元件

Tip:從一項新技術的底層元素起步有利於使用者更好的長期使用它

跨平臺

大部分 react 應用是在 Web 平臺上。而 React NativeReact VR 這樣的專案則創造了 react 應用在其他平臺上執行的可能

React 應用主要成分

元件

元件是 React 中最基本單元

元件通常對應使用者介面的一部分,比如導航。也可以擔任資料格式化等職責。

可以將任何東西作為元件,儘管並不是所有東西作為元件都有意義。

如果將整個介面作為元件,並且沒有子元件或進一步的細分,那麼對自己並沒有什麼幫助。倘若,將介面不同部分拆解成可以組合,複用的部分,卻很有幫助。

元件具有良好的封裝性複用性組合性。有助於為使用者提供一個更簡單的方式來思考和構建使用者介面。使用 React 構建應用就像使用積木來搭建專案,而構建應用時有取之不盡的「積木」

將 UI 分解成元件可以讓人更輕鬆的處理應用不同的部分。

元件需要一起工作,也就是說元件可以組合起來形成新的元件。元件組合也是 React 最強大的部分之一。

如果身處一箇中大型團隊,可以將元件釋出到私有註冊中心(npm 或者其他)

React 元件還有一個方面就是生命週期方法。當元件經過其生命週期的不同時期時(掛在、更新、解除安裝等),可以使用可預測、定義良好的方法。

React 庫

React 核心庫與 react-dom 和 react-native 緊密配合,側重元件的規範和定義。能讓開發者構建一個元件樹,該元件樹能夠被瀏覽器和其他平臺所使用。

react-dom 就是一個渲染器。針對瀏覽器環境和伺服器端渲染。

比如我們要將元件渲染到瀏覽器,就得用到 react-dom。

React Native 庫專注於原生平臺,能夠為 ios、android 和其他平臺建立 react 應用。

第三方庫

React 不自帶 http 等其他前端常用工具庫。開發者可以自由的選擇對於工作最好的工具。

react 的權衡

react 屬於 專一型,主要關注 UI 試圖方面。

而 angular 屬於 通用型,其內建了許多解決方案,例如 http 呼叫、路由、國際化、字串和數位格式化...

Tip:通常一些優秀的團隊會用這兩種方式。

React 的建立主要用於 Facebook 的 UI 需求。雖然大多數的 web 應用在此範圍之內,但也有一些應用不在。

React 是一種抽象,也存在抽象的代價。React 以特定的方式構建並通過 api 向外暴露,開發者會失去對底層的可見性。當然 React 也提供了緊急出口,讓開發者深入較低的抽象層級,仍然可以使用 jQuery,不過需要以一種相容 React 的方式使用。

有時還需要為 React 的行事方式買單。或許會影響應用的小部分(即不太適合用 React 的方式來工作)

使用 React 時所做的權衡有助於使用者成為更好的開發者。

虛擬 Dom

React 旨在將複雜的任務簡單化,把不必要的複雜性從開發者身上剝離出來。

鼓勵開發者使用宣告式的程式設計而非命令式,也就是開發者宣告元件在不同狀態下的行為和外觀即可,React 負責渲染以及更新 UI,並將效能做到恰到好處。從而讓研發人員騰出時間思考其他方面。

驅動這些的主要技術之一就是虛擬dom

Tip:有關虛擬dom 的介紹可以參考 vue 快速入門-虛擬dom

虛擬 Dom 不是我們關注的重點。這正是 React 簡單 的地方:開發者被解放出來,去關注最關注的部分。

React 的簡單、非固化

什麼使 React 成為大型團隊的寵兒?首先是簡單,其次是非固化

簡單的技術讓人更容易理解和使用。

React 是一個非常輕量的庫,只關注應用的檢視。更加容易與使用者當前的技術整合,並在其他方面為使用者留下了選擇的空間。一些功能固化的框架和庫要求使用者要麼全盤接受要麼徹底不用。

簡單和非固化的特性,以及恰到好處的效能,讓它非常適合大大小小的專案。

元件間的關係

元件可以獨立存在,也可用來建立其他元件。人們認為元件可以建立很多不同類的關係,從某種意義這是對的。

但元件更多的是以靈活的方式被使用,應該關注其獨立性和常常不帶任何負擔,可組合使用。所以元件只關注其父母和孩子,兄弟關係可以不管。

建立元件關係的過程對每個團隊或專案都不盡相同,元件關係也可能會隨時間而改變,我們可以不期望一次就建立完美,也無需太過擔心,因為 React 會讓我們的 UI 迭代沒那麼困難。

搭建元件的框架

首先我們將元件的框架寫好:

<body>
    <div id="root">
        <!-- 此元素的內容將替換為您的元件 -->
    </div>

    <!-- react 庫  -->
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <!-- 用於處理 Dom 的 react 包 -->
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <!-- 使用 PropTypes 進行型別檢查 -->
    <script src="https://unpkg.com/[email protected]/prop-types.js"></script>

    <script>
        const reactElem = React.createElement(
            'h1',
            {title: 'i am h1'},
            'Hello, world!'
        )
        ReactDOM.render(
            reactElem,
            document.getElementById('root')
        );
    </script>
</body>

Tip:這是一個普通的 html 頁面,直接通過 vscode 的 Live Server 外掛執行即可

執行後的網頁顯示 Hello, world!。生成的元素結構如下:

<div id="root">
  <h1 title="i am h1">Hello, world!</h1>
</div>

下面我們稍微分析一下這個頁面:

首先定義了一個 div 元素,接著引入三個包,作用如下:

  • react.js,React 的核心庫,用於定義元件的規範
  • react-dom.js,渲染器,用於瀏覽器和伺服器端渲染,用於建立元件和管理元件
  • prop-types.js,傳遞給元件的資料做型別檢查

接著通過 React.createElement 建立一個 react 元素。

React.createElement(
  type,
  [props],
  [...children]
)

Tip:react 元素是什麼?

  • react 元素是建立起來開銷極小的普通物件
  • react 元素是構成 React 應用的最小磚塊。react 元素之於React如同DOM元素之於DOM,react 元素組成了虛擬 DOM
  • react 元件是由 react 元素構成的

最後使用 ReactDOM.render 將 React 元素渲染到 div#root 中。

// 在提供的 container 裡渲染一個 React 元素,並返回對該元件的參照
ReactDOM.render(element, container[, callback])

Tip:呼叫 react-dom 的 render() 方法來讓 React 將組建渲染出來,並對元件進行管理。

React 元素

React 元素是你想讓 React 渲染的東西的輕量表示。它可以表示為一個 Dom 元素,上文我們已經用其建立了一個 h1 的 dom 元素。

有必要再來分析一下 createElement() 的引數:

// 建立並返回指定型別的新 React 元素。其中的型別引數既可以是標籤名字串(如 'div' 或 'span'),也可以是 React 元件 型別 (class 元件或函陣列件),或是 React fragment 型別。
React.createElement(
  type,
  [props],
  [...children]
)
const reactElem = React.createElement(
    'h1',
    {title: 'i am h1'},
    'Hello, world!'
)
  • type,一個 html 標籤("div"、"h1")或 React 類
  • props,指定 html 元素上要定義哪些屬性或元件類的範例上可以使用哪些屬性
  • children,還記得 React 元件是可以組合的嗎?

一句話:React.createElement() 在問:

  • 我在建立什麼? ,是 Dom 元素,是 React 元件,還是React fragment。
  • 我怎麼設定它?
  • 它包含什麼?

假如我們需要在頁面顯示如下元素:

<div>
    <h2>i am h2</h2>
    <a href="www.baidu.com">go baidu</a>
    <p>
        <em>i am em element</em>
    </p>
</div>

可以這麼寫:

const c = React.createElement
const reactElem2 = React.createElement(
    'div',
    {},
    c('h2', {}, 'i am h2'),
    c('a', {href: 'www.baidu.com'}, 'go baidu'),
    c('p', {}, 
        c('em', {}, 'i am em element')
    )
)

虛擬 DOM 樹

React 是怎麼把那麼多 React.createElement 轉換成螢幕上看到的東西的?這裡得用到虛擬 dom。

虛擬 dom 和真實 dom 有著相似的結構。

為了從 React 元素中形成自己的虛擬 DOM 樹,React 會對 React.createElement 的全部 children 屬性進行求值,並將結果傳遞給父元素。就像一個小孩反覆再問 X是什麼?,直到理解 X 的每個細節,直到他能形成一棵完整的樹。

React 元件

看看這段程式碼,我們建立了一個 React 元素並將其放入 dom 中:

<script>
    const c = React.createElement
    const reactElem = React.createElement(
        'div',
        {},
        c('h2', {}, 'i am h2'),
        c('a', {href: 'www.baidu.com'}, 'go baidu'),
        c('p', {}, 
            c('em', {}, 'a am em element')
        )
    )
    ReactDOM.render(
        reactElem,
        document.getElementById('root')
    );
</script>

如果我們需要擴充套件 reactElem 的功能、樣式以及其他UI相關?這時可以使用元件

元件可以將這些有效的組織在一起。

所以,要真正構建東西,不僅僅需要 React 元素,還需要元件。

React 元件就像是 React 元素,但 React 元件擁有更多特性。React 元件是幫助將 React 元素和函陣列織到一起的類

我們可以使用函數或 js 類建立元件。

使用 es6 的 class 來定義元件。就像這樣:

class MyComponent extends React.Component {
    // 必須定義 render()。否則會報錯:
    // MyComponent(...): No `render` method found on the returned component instance: you may have forgotten to define `render`.
    render() {
        // 返回單個 React 元素或 React 元素的陣列
        return reactElem
    }
}

通常需要至少定義一個 render() 方法,幾乎任何向螢幕顯示內容的元件都帶有 render 方法

Tip:那些不直接顯示任何東西而是修改或增強其他元件的元件(稱高階元件),後續再討論。

我們將上面範例改成元件形式:

<script>
    class MyComponent extends React.Component{
        render(){
            const c = React.createElement
            return React.createElement(
                'div',
                {},
                c('h2', {}, this.props.cnt1),
                c('a', {href: 'www.baidu.com'}, 'go baidu'),
                // class 屬性在需要改為 className 
                c('p', {className: this.props.aClass}, 
                    c('em', {}, this.props.cnt2)
                )
            )
        }
    }
</script>
<script>
    // createElement 第一個引數可以是標籤名字串(如 'div' 或 'span'),也可以是 React 元件 型別 (class 元件或函陣列件),或是 React fragment 型別
    const App = React.createElement(MyComponent, {
        cnt1: 'i am h2',
        aClass: 'p-class',
        cnt2: 'a am em element'
    })
    ReactDOM.render(
        App,
        document.getElementById('root')
    );
</script>

生成的 html 如下:

<div>
    <h2>i am h2</h2>
    <a href="www.baidu.com">go baidu</a>
    <p class="p-class">
        <em>a am em element</em>
    </p>
</div>

React 中通過 this.props 就能獲取傳遞給元件的屬性。

this.props 是怎麼來的

MyComponent 中沒有初始化 props 的程式碼,既然自己沒做,那麼肯定是父類別幫忙做了。

就像這樣:

<script>
    // 父類別
    class Rectangle {
        constructor() {
            // 子類接收的引數,這裡 arguments 都能接收到
            const args = Array.from(arguments)
            // args= (3) [{…}, 'b', 'c']
            console.log('args=', args)
            this.props = args[0]
        }
    }
    // 子類
    class Square extends Rectangle {
        render(){
            // name= pjl
            console.log('name=', this.props.name)
        }
    }

    let square = new Square({name: 'pjl'}, 'b', 'c')
    square.render()
</script>

如果需要自己寫 constructor ,則需要手動呼叫 super(),否則會報錯。就像這樣:

// 控制檯輸入如下程式碼,報錯
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
// Uncaught ReferenceError:在存取「this」或從派生建構函式返回之前,必須在派生類中呼叫超級建構函式
class A{}
class B extends A{
    constructor(){
       
    }
}
let b = new B()

Tip: 有關 super 更多介紹請看 這裡

constructor(props) 如果不初始化 state 或不進行方法系結,則不需要為 React 元件實現建構函式。通常,建構函式僅用於以下兩種情況:

  • 通過給 this.state 賦值物件來初始化內部 state。
  • 為事件處理常式繫結範例

型別檢測

類元件能使用自定義屬性。

通過元件好像能建立自定義 html 元素,而且還能做得更多。

能力越大,責任也越大,我們需要使用一些方法來驗證所使用的屬性,防止缺陷、規劃元件所使用的資料種類。

在上面範例基礎上,我們增加型別檢測:

<script>
    class MyComponent extends React.Component {
        ...
    }

    // 屬性型別
    MyComponent.propTypes = {
        cnt1: PropTypes.number,
        // 注:函數不是 function,而是 func
        // 函數型別,並且必填
        func: PropTypes.func.isRequired
    }
    // 預設值
    MyComponent.defaultProps = {
        cnt1: 'defaultName'
    }
</script>

型別檢測生效了。控制檯報錯如下:

Warning: Failed prop type: Invalid prop `cnt1` of type `string` supplied to `MyComponent`, expected `number`
警告:失敗的屬性型別:提供給「MyComponent」的型別為「string」的屬性「cnt1」無效,應為「number」`

Warning: Failed prop type: The prop `func` is marked as required in `MyComponent`, but its value is `undefined`.
警告:失敗的屬性型別:屬性「func」在「MyComponent」中標記為必需,但其值為「undefined」。

除了控制檯發出 Warning,頁面顯示仍舊正常。

這裡其實就是按照特定規定,給 MyComponent 類增加了兩個靜態成員,用於型別檢測。我們可以自己模擬一下,請看範例:

<script>
    class Rectangle {
        constructor() {
            this.props = arguments[0]
            // 模擬型別驗證
            this.validate(this.constructor)
        }
        validate(subClass) {
            Object.keys(subClass.propTypes).forEach(key => {
                const propType = subClass.propTypes[key]
                const type = typeof this.props[key]
                if (type !== propType) {
                    console.error(`Warning: ${key} 屬性 - 期待型別是 ${propType},所傳入的型別確是 ${type}`)
                }
            })
        }
    }

    class Square extends Rectangle {
        render() {

        }
    }

    Square.propTypes = {
        name: 'string',
        age: 'number'
    }
    let square = new Square({ name: 18, age: 'pjl' })
    square.render()
</script>

型別檢測結果如下:

// 瀏覽器控制檯輸出:
Warning: name 屬性 - 期待型別是 string,所傳入的型別確是 number
Warning: age 屬性 - 期待型別是 number,所傳入的型別確是 string

現在這麼寫有些零散,我們可以使用 static 語法來對其優化。就像這樣:

class Square extends Rectangle {
    static propTypes = {
        name: 'string',
        age: 'number'
    }
    render(){}
}

Tip: 有關 static 更多介紹可以百度 mdn static

巢狀元件

我們已經建立了一個類元件,並傳入了一些屬性,現在我們可以嘗試巢狀元件。

前面我們已經提到,元件組合是 React 中非常強大的功能。比如一個頁面,我們可以通過元件進行拆分,單獨開發,最終卻是需要將元件組合成一個頁面,否則就不好玩了。

將上面元件拆成兩個,稍作變動,程式碼如下:

<script>
    class MyComponent extends React.Component {
        render() {
            const c = React.createElement
            return React.createElement(
                'div',
                { className: 'parent-class' },
                c('h2', {}, this.props.cnt1),
                // 核心是:this.props.children。
                this.props.children
            )
        }
    }

    class MySubComponent extends React.Component {
        render() {
            const c = React.createElement
            return React.createElement(
                'div',
                { className: 'sub-class' },
                c('a', { href: 'www.baidu.com' }, 'go baidu'),
                c('p', { className: this.props.aClass },
                    c('em', {}, this.props.cnt2)
                )
            )
        }
    }

</script>
<script>

    // createElement 第一個引數可以是標籤名字串(如 'div' 或 'span'),也可以是 React 元件 型別 (class 元件或函陣列件),或是 React fragment 型別
    const App = React.createElement(MyComponent, {
            cnt1: 'i am h2',
        },
        React.createElement('p', {}, 'i am p element'),
        React.createElement(MySubComponent, {
            aClass: 'p-class',
            cnt2: 'a am em element'
        }
        ))
    ReactDOM.render(
        App,
        document.getElementById('root')
    );
</script>

核心是 this.props.children,每個元件都可以獲取到 props.children。最終渲染 html 結構如下:

<div id="root">
    <div class="parent-class">
        <h2>i am h2</h2>
        <!-- this.props.children -->
        <p>i am p element</p>
        <div class="sub-class">
            <a href="www.baidu.com">go baidu</a>
            <p class="p-class">
                <em>a am em element</em>
            </p>
        </div>
        <!-- /this.props.children -->
    </div>
</div>

動態元件

現在我們已經為元件新增了 render 方法和一些 propTypes。上面範例也僅僅顯示一些靜態文案,但要建立動態元件,遠不止這些。

React 提供了某些特殊方法,當 React 管理虛擬 dom 時,react 會按順序呼叫它們,render 方法只是其中之一。

狀態

狀態可以讓元件互動並鮮活起來。

Tip: 狀態其他特性如下:

  • 狀態可以理解成事物在某一時刻的資訊。可以分成可變狀態和不可變狀態。簡單區分就是事物建立之後是否能變化?如果可以,則是可變狀態
  • 通過 this.state 存取的是可變狀態;通過 this.props 存取的是不可變狀態
  • state 和 props 是資料的傳輸工具,這些資料構成應用並使其有用。
  • State 與 props 類似,但是 state 是私有的,並且完全受控於當前元件 —— 官網
  • react 中的 props 用來接收父元件傳來的屬性,state 是私有屬性
  • 應該在什麼時候使用 state?想要改變儲存在元件中的資料時。

下面我們使用一下 state,既然 state 是可變狀態,那麼我們就建立一個表單元件,裡面有一個 input,一個提交按鈕。

程式碼如下:

<script>
    class MyComponent extends React.Component {
        render() {
            const c = React.createElement
            return React.createElement(
                'div',
                { className: 'parent-class' },
                this.props.children
            )
        }
    }

    class MySubComponent extends React.Component {
        constructor(props){
            // 如果不需要使用 this.props,則可以是 super()
            super(props)
            // 在建構函式中存取 this 之前,一定要呼叫 super(),它負責初始化 this
            this.state = {age: 18}
        }
        render() {
            const c = React.createElement
            return React.createElement(
                'form',
                { className: 'sub-class' },
                c('p', { }, 
                    c('input', {type: 'text', value: this.state.age})
                ),
                c('p', { }, 
                    c('input', {type: 'submit', value: 'submit'})
                ),
            )
        }
    }

</script>

生成的表單也很簡單,狀態資料 age 也已經在 input 元素中成功顯示:

<div id="root">
    <div class="parent-class">
        <form class="sub-class">
            <p><input type="text" value="18"></p>
            <p><input type="submit" value="submit"></p>
        </form>
    </div>
</div>

現在需要專門的方法更新 state 中的資料。不能直接修改(例如 this.state.age = 19),因為 React 需要跟蹤狀態,並保證虛擬 dom 和真實 dom 的同步。得通過 React 提供的特殊通道(this.setState()) 來更新 React 類元件中的狀態。

setState 不會立即更新元件,React 會根據狀態變化批次更新以便使效率最大化,也就是說 React 會以它最高效的方法基於新狀態更新 dom,做到儘可能快。

Tip: 不要直接修改 state 的範例請看 這裡

事件與 React 如何共同作業

以前我們直接操作 dom,於是可以通過 addEventListener 註冊事件;現在不直接操作 dom,而是和 React 元素打交道,那麼 React 應該提供對應的事件機制,最好和我們之前的習慣相同,而 React 確實是這樣做的。

React 實現了一個合成事件系統作為虛擬 Dom 的一部分,它會將瀏覽器中的事件轉為 React 應用的事件。可以設定響應瀏覽器事件的事件處理器,就像通常用 js 那樣做就好。區別是 React 的事件是設定在 React 元素或元件自身上,而不是用 addEventListener。

React 能監聽瀏覽器中很多不同事件,涵蓋了幾乎所有的互動(點選、提交、捲動等)

接下來我們就可以用來自這些事件(比如文字變化時的事件 onchange)的資料來更新元件狀態。

接著上面範例,需求是:更改 input[type=text] 的值,對應 state 中的 age 也會同步,點選 submit 能提交。

程式碼如下:

<script>
    class MyComponent extends React.Component {
        render() {
            const c = React.createElement
            return React.createElement(
                'div',
                { className: 'parent-class' },
                this.props.children
            )
        }
    }

    class MySubComponent extends React.Component {
        constructor(props) {
            // 如果不需要使用 this.props,則可以是 super()
            super(props)
            // 在建構函式中存取 this 之前,一定要呼叫 super(),它負責初始化 this
            this.state = { age: 18 }

            // 自定義的方法,react 沒有幫我們處理 this。所以這裡需要我們自己繫結一下
            this.handleTextChange = this.handleTextChange.bind(this)
            this.handleSubmit = this.handleSubmit.bind(this)
        }
        handleTextChange(evt) {
            // 不手動繫結 this,則為 undefined
            // console.log('this', this)

            const v = evt.target.value

            // 用來自事件的資料更新元件狀態。否則介面是看不到 age 的最新值的
            this.setState({ age: v })
        }
        // React 要阻止預設行為,必須顯式的使用 preventDefault
        handleSubmit(evt) {
            evt.preventDefault()
            // 提交表單。state {age: 18}
            console.log('提交表單。state', this.state)
        }
        render() {
            const c = React.createElement
            return React.createElement(
                'form',
                {
                    className: 'sub-class',
                    onSubmit: this.handleSubmit
                },
                c('p', {},
                    c('input', {
                        type: 'text',
                        value: this.state.age,
                        // React 事件的命名採用小駝峰式(camelCase),而不是純小寫。例如在 html 中通常都是小寫(onclick)
                        onInput: this.handleTextChange
                    })
                ),
                c('p', {},
                    c('input', { type: 'submit', value: 'submit' })
                ),
            )
        }
    }
</script>

函數傳遞資料

利用函數可以將子元件的資料傳遞給父元件。核心程式碼如下:

class MyComponent extends React.Component {
    constructor(props) {
        super(props)
        this.handleSubmit = this.handleSubmit.bind(this)
    }
    handleSubmit(data) {
        console.log('提交表單 data=', data)
    }
    render() {
        const c = React.createElement
        return React.createElement(
            'div',
            { className: 'parent-class' },
            React.createElement(MySubComponent, {
                aClass: 'p-class',
                cnt2: 'a am em element',
                onFormSubmit: this.handleSubmit
            })
        )
    }
}

class MySubComponent extends React.Component {
    handleSubmit(evt) {
        evt.preventDefault()
        this.props.onFormSubmit(this.state)
    }
}

資料流向

在 React 中,資料自頂向下流動,可以通過 props 向子元件傳遞資訊並在子元件中使用這些資訊。表明可以將子元件的資料儲存在父元件中,並從那裡將資料傳遞給子元件。做個範例來驗證一下,定義三個元件(A、B、C),結構如下:

<div class='componentA'>
    <p class='componentB'> apple </p>
    <button class='componentC'>add apple</button>
</div>

資料存在 AComponent 中,每點選一次 CComponent 元件,就會要求 AComponent 增加一個 apple,渲染到頁面的 BComponent 元件也相應增加。

全部程式碼如下:

<script>
    class AComponent extends React.Component {
        constructor(props) {
            super(props)
            this.state = {apples: this.props.apples}
            this.handleAddApple = this.handleAddApple.bind(this)
        }
        handleAddApple(data) {
            this.setState({apples: [data, ...this.state.apples]})
        }
        render() {
            const c = React.createElement
            return c(
                'div',
                { className: 'componentA' },
                this.state.apples.map((item, index) => c(BComponent, {
                    content: item,
                    // 需要增加 key 屬性,否則報錯:
                    // Warning: Each child in a list should have a unique "key" prop.
                    key: index
                })),
                c(CComponent, {
                    onHandleAddApple: this.handleAddApple
                })
            )
        }
    }

    class BComponent extends React.Component {
        render() {
            const c = React.createElement
            return React.createElement(
                'p',
                {
                    className: 'componentB',
                    // key: this.props.key
                },
                this.props.content
            )
        }
    }

    class CComponent extends React.Component {
        constructor(props) {
            super(props)

            this.handleAdd = this.handleAdd.bind(this)
        }
        
        // React 要阻止預設行為,必須顯式的使用 preventDefault
        handleAdd(evt) {
            this.props.onHandleAddApple('apple')
        }
        render() {
            const c = React.createElement
            return React.createElement(
                'button',
                {
                    className: 'componentC',
                    onClick: this.handleAdd
                },
                'add apple'
            )
        }
    }
</script>

<script>
    const App = React.createElement(AComponent, {
            apples: ['apple']
        },
    )
    ReactDOM.render(
        App,
        document.getElementById('root')
    );
</script>

jsx

JSX 僅僅只是 React.createElement(component, props, ...children) 函數的語法糖 —— react 官網-深入 JSX

我們建議在 React 中配合使用 JSX,JSX 可以很好地描述 UI 應該呈現出它應有互動的本質形式 —— react 官網-JSX 簡介

jsx 讓人編寫類似於(但不是) HTML 的程式碼。

將上面增加 apple 的例子改為 jsx。全部程式碼如下:

<body>
    <div id="root"></div>

    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/[email protected]/prop-types.js"></script>

    <!-- Babel 能夠轉換 JSX 語法 -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <script type="text/babel">
        class AComponent extends React.Component {
            constructor(props) {
                super(props)
                this.state = { apples: this.props.apples }
                this.handleAddApple = this.handleAddApple.bind(this)
            }
            handleAddApple(data) {
                this.setState({ apples: [data, ...this.state.apples] })
            }
            render() {
                return <div className='componentA'>
                    {
                        this.state.apples.map(
                            (item, index) =>
                                (<BComponent content={item} key={index} />)

                        )
                    }
                    <CComponent onHandleAddApple={this.handleAddApple} />
                </div>
            }
        }

        class BComponent extends React.Component {
            render() {
                return <p className='componentB'>{this.props.content}</p>
            }
        }

        class CComponent extends React.Component {
            constructor(props) {
                super(props)

                this.handleAdd = this.handleAdd.bind(this)
            }

            handleAdd(evt) {
                this.props.onHandleAddApple('apple')
            }
            render() {
                return <button className="componentC" onClick={this.handleAdd}>add apple</button>
            }
        }
    </script>

    <script type="text/babel">
        const App = <AComponent apples={['apple']} />

        ReactDOM.render(
            App,
            document.getElementById('root')
        );
    </script>

</body>

jsx 除了類似於 HTML 且語法簡單,另一個好處是宣告式封裝。通過將組成檢視的程式碼和相關聯的方法包含在一起,使用者建立了一個功能組。本質上,需要知道的有關元件的所有資訊都匯聚在此,無關緊要的東西都被隱藏起來,意味著使用者更容易的思考元件,並且更加清楚他們作為一個系統是如何工作的。

主要注意,JSX 不是 html,只會轉譯成常規 React 程式碼。它的語法和慣例也不完全相同,需要關注一些細微的差異(偶爾有些「破費思量之處」),比如:

  • 自定義元件使用大寫字母開頭。用於區分自定義元件和原生 html
  • 屬性表示式寫在大括號內。例如 <AComponent apples={['apple']} />
  • 省略一個屬性的值,jsx 會視為 true。要傳 false,必須使用屬性表示式
  • 要在元素內部插入表示式的值,也必須使用大括號

Tip:比如class 得換成 className,更多 jsx 語法規則請看 這裡

其他章節請看:

react實戰 系列