React系統性學習(下)

船頭尺發表於2021-09-09

$ 前言

  在中我們主要學習了

  1. 什麼是React

  2. JSX語法

  3. 元素渲染

  4. 元件(Component) 和 屬性 (Props)

  5. 狀態(State) 和 生命週期(lifeCircle)

  6. 處理事件

  7. 條件渲染

本文我們將繼續學習

  1. 列表(List) 和 鍵(keys)

  2. 表單(Forms)

  3. 狀態提升(Lifting State Up)

  4. 組合 VS 繼承 (Composition vs inheritance)

$ 版本宣告

  本文使用版本 React v16.2.0

$ 列表 和 鍵

  列表(List), 鍵(Key)
  回顧一下在javascript中如何轉換列表:在陣列中使用map()函式對numbers陣列中的每個元素依次執操作

const numbers = [1, 2, 3, 4, 5];const doubled = numbers.map((number) => number * 2);console.log(doubled)  // 2, 4, 6, 8, 10

  React 基本借鑑了以上寫法,只不過將陣列替換成了元素列表

多元件渲染

  可以建立元素集合,並用一對大括號 {} 在 JSX 中直接將其引用即可

  下面,我們用 JavaScript 的 map() 函式將 numbers 陣列迴圈處理。對於每一項,我們返回一個

  • 元素。最終,我們將結果元素陣列分配給 listItems

    const numbers = [1, 2, 3, 4, 5];const listItems = numbers.map((number) => 
        
  • {number}
  • )

      再把整個 listItems 陣列包含到一個

      元素,並渲染到DOM
    ReactDOM.render(    
      {listItems}
    ,    document.getElementById('root') )
    基本列表元件

      通常情況下,我們會在一個元件中渲染列表而不是直接放到root上。重構一下上例

    function NumberList(props) {    const numbers = props.number;    const listItems = numbers.map((number) => 
            
  • {number}
  •     )    return (         
      {listItems}
        ) }const numbers = [1, 2, 3, 4, 5]; ReactDOM.render(     ,     document.getElementById('root') )

      當執行上述程式碼的時候,將會受到一個警告:a key should be provided for list items,要求應該為元素提供一個鍵(注:min版本react無提示)。要去掉這個警告也簡單,只需要在listItem的每個li中增加key屬性即可,增加後的每個

  • 如下

    function NumberList(props) {  const numbers = props.numbers;  const listItems = numbers.map((number) =>
        
  •       {number}     
  •   );  return (     
      {listItems}
      ); }

      當建立元素列表時,“key” 是一個你需要包含的特殊字串屬性,那為什麼要包含呢?

    鍵(Keys)

      鍵Keys 幫助React標識那個項被修改、新增或者移除了。陣列中的每一個元素都應該有一個唯一不變的鍵來標識。

     挑選key最好的辦法是使用一個在它的同輩元素中不重複的表示字串。多數情況下可以使用資料中的IDs來作為Keys。但是還是會遇到沒有id欄位的資料,這種情況你可以使用資料項的索引值

    cosnt todoItems = todos.map((todo, index) => 
        // 資料項沒有IDs時使用該辦法    
  •         {todo.text}    
  • )

      如果列表項可能被重新排序,這種用法存在一定的效能問題,React會產生時間複雜度為O(n^3)的演算法執行。因此優先使用資料項本身的欄位內容來設定鍵

    使用 Keys 提取元件

    Keys只有在陣列的上下文中存在意義。例如,如果你提取了一個ListItem元件,應該把key放置在陣列處理的元素中,而不能放在ListItem元件自身的

  • 根元素上。

     以下的用法就是錯誤的

    function ListItem(props) {    const value = props.value;    return (        // 錯誤!不需要再這裡指定 key
            
  •  {value}
  •     ) }function NumberList(props) {  const numbers = props.numbers;  const listItems = numbers.map((number) =>    // 錯誤!key 應該在這裡指定:        );  return (     
       {listItems} 
      ); }

      應該寫成如下

    function ListItem(props) {  // 正確!這裡不需要指定 key :
      return 
  • {props.value}
  • ; }function NumberList(props) {  const numbers = props.numbers;  const listItems = numbers.map((number) =>     // 正確!key 應該在這裡被指定        );  return (    
            {listItems}    
      ); }const numbers = [1, 2, 3, 4, 5]; ReactDOM.render(  ,   document.getElementById('root') );
    keys 在同輩元素中必須唯一

      在陣列中使用的 keys 必須在它們的同輩之間唯一。然而它們並不需要全域性唯一。我們可以在操作兩個不同陣列的時候使用相同的 keys :

    function Blog(props) {
      const sidebar = (    
            {props.posts.map((post) =>        
    •           {post.title}        
    •       )}    
      );   const content = props.posts.map((post) =>    
          

    {post.title}

          

    {post.content}

        
      );   return (    
          {sidebar}      
          {content}    
      ); } const posts = [   {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},   {id: 2, title: 'Installation', content: 'You can install React from npm.'} ]; ReactDOM.render(  ,   document.getElementById('root') );

      【注意】鍵是一個內部對映,他不會作為props傳遞給元件內部,如果你需要在元件中使用到這個值,可以自定義一個屬性名將該值傳入到props中,如下例中我們定義了一個id屬性傳入給props.

    const content = posts.map((post) =>  );

      在這個例子中,我們能讀取props.id,但是讀取不了props.key

    直接在JSX中使用map()

      在上例中我們先宣告瞭一個listItem然後在jsx中引用,然而我們也能在JSX中直接引用,稱之為 內聯map()

    function NumberList(props) {  const numbers = props.numbers;  return (    
            { numbers.map((number) =>               )}    
      ); }

      至於選用哪種風格編寫,只要遵循程式碼清晰易讀原則即可

    $  表單

    &esmp; HTML 表單元素與 React 中的其他 DOM 元素有所不同,因為表單元素自然地保留了一些內部狀態。例如,這個純 HTML 表單接受一個單獨的 name:

    
    
             

      該表單和 HTML 表單的預設行為一致,當使用者提交此表單時瀏覽器會開啟一個新頁面。如果你希望 React 中保持這個行為,也可以工作。但是多數情況下,用一個處理表單提交併訪問使用者輸入到表單中的資料的 JavaScript 函式也很方便。實現這一點的標準方法是使用一種稱為“受控元件(controlled components)”的技術。

    受控元件(Controlled Components)

      在 HTML 中,表單元素如 表單元素通常保持自己的狀態,並根據使用者輸入進行更新。而在 React 中,可變狀態一般儲存在元件的 state(狀態) 屬性中,並且只能透過 setState()更新。

      透過使 React 的 state 成為 “單一資料來源原則” 來結合這兩個形式。然後渲染表單的 React 元件也可以控制使用者輸入之後的行為。這種形式,其值由 React 控制的輸入表單元素稱為“受控元件”。

    class NameForm extends React.Component {    constructor(props) {        super(props);        this.state = {value: ''};
        }
        
        handleChange(event) {        this.setState({value:event.target.value})
        }
    
        handleSubmit(event) {
            alert('A name was submitted: ' + this.state.value);
            event.preventDefault();
        }
    
        render() {        return (
                
                                                  
            )     } }

      設定表單元素的value屬性之後,其顯示值將由this.state.value決定,以滿足react狀態的同一個資料理念。每次鍵盤敲擊之後會執行handleChange方法以便更新React狀態,顯示只也將隨著使用者的輸入而改變。

      由於value屬性設定在我們的表單元素上,顯示的值總是this.state.value,以滿足state 狀態的同意資料理念。由於 handleChange 在每次敲擊鍵盤時執行,以更新React state,顯示的值將更新為使用者的輸入

      對於受控元件來說,每一次state的變化都會伴有相關聯的處理函式。這使得可以直接修改或驗證使用者的輸入。比如,我們希望強制name的輸入都是大寫字母,可以如下實現

    handleChange(event) {    this.setState({value: event.target.value.toUpperCase()});
    }
    textarea標籤

      在 HTML 中, 元素透過它的子節點定義了它的文字值:

      在 React 中, 的賦值使用 value 屬性替代。這樣一來,表單中 的書寫方式接近於單行文字輸入框 :

    class EssayForm extends React.Component {    constructor(props) {        super(props);        this.state = {            value: 'Please write an esay about your favorite DOM element.'
            }
        }    // ...
        render() {        return (
                
                                                  
            )     } }

      注意,this.state.value 在建構函式中初始化,所以這些文字一開始就出現在文字域中。

    select 標籤

       在 HTML 中, 建立了一個下拉選單用法如下

       html利用selected預設選中,但在React中,不使用selected,而是給標籤中增加一個value屬性,這使得受控元件使用更加方便,因為你只需要更新一處變數即可。

    class FlavorForm extends React.Component {  // ...
      render() {    return (      
                            
        );   } }

      總的來說,這使 都以類似的方式工作 —— 它們都接受一個 value 屬性可以用來實現一個受控元件。

    多選select

      使用多選select時,需要給select標籤增加value屬性,同時給value屬性賦值一個陣列

    # 利用e.target合併多個輸入元素的處理事件

      當您需要處理多個受控的 input 元素時,您可以為每個元素新增一個 name 屬性,並且讓處理函式根據 event.target.name 的值來選擇要做什麼。

    class Reservation extends React.Component {  constructor(props) {    super(props);    this.state = {      isGoing: true,      numberOfGuests: 2
        };    this.handleInputChange = this.handleInputChange.bind(this);
      }
    
      handleInputChange(event) {    const target = event.target;    const value = target.type === 'checkbox' ? target.checked : target.value;    const name = target.name;    this.setState({ [name]: value });
      }
    
      render() {    return (
          
                                     
        );   } } ReactDOM.render(   ,   document.getElementById('root') );

      注意這裡使用ES6語法來更新與給定輸入名稱相對應的 state(狀態) 鍵的辦法

    this.setState({ [name]: value })

    var partialState = {};
    partialState[name] = value;this.setState(partialState);

      由於 setState() 自動,所以我們只需要呼叫更改的部分即可。

    受控 Input 元件的 null 值

     在 受控元件上指定值 prop 可防止使用者更改輸入,除非您希望如此。 如果你已經指定了一個 value ,但是輸入仍然是可編輯的,你可能會意外地把 value 設定為undefined  或 null

      以下程式碼演示了這一點。 (輸入首先被鎖定,但在短暫的延遲後可以編輯。)

    ReactDOM.render(, mountNode);
    
    setTimeout(function() {
      ReactDOM.render(, mountNode);
    }, 1000);

    $ 狀態提升 (Lifting State Up)

      通常情況下,同一個資料的變化需要幾個不同的元件來反映。我們建議提升共享的狀態到它們最近的祖先元件中。為了更好的理解,從一個案例來分析

    溫度計算器

       在本案例中,我們採用自下而上的方式來建立一個溫度計算器,用來計算在一個給定溫度下水是否會沸騰(水溫是否高於100C)

    (1)建立一個 BoilingVerdict 元件,用來判水是否會沸騰並列印

    function Bioling Verdict(props) {  if (props.celsius >= 100) {    return 

    The water would boil.

      }  return 

    The water would not boil.

    }

    (2)有了判斷溫度的元件之後,我們需要一個Calculator元件,他需要包含一個提供我們輸入文圖,並在this.state.temperature中儲存值。另外,以上BoilingVerdict 元件將會獲取到該輸入值並進行判斷

    class Caculator extends React.Component {  constructor(props) {    super(props);    this.state = { temperature: '' };    
      }
    
      handleChange(e) {    this.setState({ temperature: e.target.value });
      }
    
      render() {    const temperature = this.state.temperature;    return (
          
            Enter temperature in Celsius;                         
        )   } }

    (3)現在我們實現了基礎的父子元件通訊功能,假設我們有這樣的需求:除了一個設施文圖的輸入之外,還需要有一個華氏溫度輸入,並且要求兩者保持自動同步

      我們從Calculator中提取出TemperatureInput,然後增加新的scale屬性,值可能是cf

    const scaleNames = {  c: 'Celsius',  f: 'Fahrenheit'}class TemperatureInput extends React.Component {  constructor(props) {    super(props);    this.state  = { temperature: e.target.value }
      }
      
      handleChange(e) {    this.setState({ temperature: e.target.value });
      }
    
      render() {    const temperature = this.state.temperature;    const scale = this.props.scale;    return (      
            Enter temperature in {scaleNames[scale]}                
        )   } }

      抽出TemperatureInput之後,Calculator元件如下

    class Calculator extends React.Component {
      render() {    return (
          
                            
        );   } }

      現在有了兩個輸入框,但這兩個元件是獨立存在的,不會互相影響,也就是說,輸入其中一個溫度另一個並不會改變,與需求不符

      我們不能再Calculator中顯示BoilingVerdict, Calcultor不知道當前的溫度,因為它是在TemperatureInput 中隱藏的, 因此我們需要編寫轉換函式

    (4)編寫轉換函式
      我們先來實現兩個函式在攝氏度和華氏度之間轉換

    function toCelsius(fahrenheit) {  return (fahrenheit - 32) * 5 / 9;
    }
    
    functin toFahrenheit(celsius0 {  return (celsius * 9 / 5) + 32;
    }

     接下來,編寫函式用來接收一個字串temperature 和一個轉化器函式作為引數,並返回一個字串,這個函式在兩個輸入之間進行相互轉換。為了健壯性,對於無效的temperature值,返回一個空字串,輸出保留三位小數

    function tryConvert(temperature, convert) {  const input = parseFloat(temperature);  if (Number.isNaN(input)) {    return '';
      }  const output = convert(input);
      cosnt rounded = Math.round(output * 1000) / 1000;  return rounded.toString();
    }

      其中,convert取值為 toCelsiustoFahrenheit

    狀態提升

      目前,兩個 TempetureInput 元件都將其值保留在本地狀態中,但是我們希望這兩個輸入時相互同步的。但我們更新攝氏溫度輸入時,華氏溫度輸入應該反映並自動更新,反之亦然。

      在React 中,共享state(狀態)是透過將其移動到需要的的元件的最接近的共同祖先元件來實現的,這被稱之為狀態提升(Lifting State Up)。我們將從TemperatureInput中移除相關狀態本地狀態,並將其移動到Calculator

      如果Calculator擁有共享狀態,那麼他將成為兩個輸入當前溫度的單一資料來源。他可以指示他們具有彼此一致的值 。由於兩個TemperatureInput的元件的props來自於同一個父級Calculator元件,連個輸入將始終保持同步

      讓我們來一步步實現這個過程

    (1)將值挪出元件,用props傳入

    render() {  // const temperature = this.state.temperature;
      const temperature = this.props.temperature;
    }

      我們知道,props是隻讀的,因此我們不能根據子元件呼叫this.setState()來改變它。這個問題,在React中通常使用 受控的方式來解決。就像DOM一樣接收一個valueonChange prop, 所以可以定製Temperature 接受來自其腹肌 CalculatortemperatureonTemperatureChange:

    thandleChange(e) {   // 之前是:this.setState({ temperature: e.target.value });
      this.props.onTemperatureChange(e.target.value);
    }

    請注意,之定義元件中的 templatureonTemperatureChange prop(屬性)名稱沒有特殊的含義。我們可以命名為任何其他名字,就像命名他們為valueonChange。是一個和常見的慣例

    onTemperatureChange proptemperature prop 一起由父級的Calculator元件提供,他將透過修改自己的本地state來處理資料變更,從而透過新值重新渲染兩個輸入。現在我們的程式碼如下

    class TemperatureInput extends React.Component {  constructor(props) {    super(props);
      }
    
      handleChange(e) {    this.props.onTemperatureChange(e.target.value);
      }
    
      render() {    const  temperature = this.props.temperature;    const scale
            Enter temperature in {scalenames[scale]}
            
          
        )
      }
    }

      我們將當前輸入的 temperaturescale儲存在本地的state中,這是我們衝輸入“提升”的state(狀態),他將作為連個輸入的“單一資料來源”。為了渲染這兩個輸入,我們需要知道的所有資料的最小表示,如攝氏度輸入37,這時Calculator元件狀態將是:

    {  temperature: '37',
      scale: 'c'}

      我們確實可以儲存兩個輸入框(攝氏度和華氏度)的值,但事實證明是不必要的。我們只要儲存最近更改的輸入框的值,以及他們所表示的度量衡(scale)就足夠了。然後推斷出另一個值。這也是我們實現兩個輸入框保持同步的途徑

    class Calculator extends React.Component {  constructor(props) {    super(props);    this.state = { temperature: '',  scale: 'c' }
      }
    
      handleCelsiusChange(temperature) {    this.setState({ scale: 'c', temperature });
      }
      
      handleRahrenheitChange(temperature) {    this.setState({scale: 'f', temperature })
      }
    
      render() {    const scale = this.state.scale;    const temperature = this.state.temperature;    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;    return (
          
                                     
        )   } }

      現在,無論你編輯哪個輸入框,Calculator 中的 this.state.temperaturethis.state.scale 都會更新。其中一個輸入框獲取值,所以任何使用者輸入都被保留,並且另一個輸入總是基於它重新計算值。

      讓我們回顧一下編輯輸入時會發生什麼:

    • React 呼叫在 DOM 上的 onChange 指定的函式。在我們的例子中,這是 TemperatureInput 元件中的 handleChange 方法。

    • TemperatureInput 元件中的handleChange 方法使用 新的期望值 呼叫 this.props.onTemperatureChange()TemperatureInput 元件中的props(屬性) ,包括 onTemperatureChange,由其父元件 Calculator 提供。

    • 當它預先呈現時, Calculator 指定了攝氏 TemperatureInputonTemperatureChangeCalculatorhandleCelsiusChange 方法,並且華氏 TemperatureInputonTemperatureChangeCalculatorhandleFahrenheitChange   方法。因此,會根據我們編輯的輸入框,分別呼叫這兩個 Calculator 方法。

    • 在這些方法中, Calculator 元件要求 React 透過使用 新的輸入值 和 剛剛編輯的輸入框的當前度量衡 來呼叫 this.setState() 來重新渲染自身

    • React 呼叫 Calculator 元件的 render 方法來了解 UI 外觀應該是什麼樣子。基於當前溫度和啟用的度量衡來重新計算兩個輸入框的值。這裡進行溫度轉換

    • React 使用 Calculator 指定的新 props(屬性) 呼叫各個 TemperatureInput 元件的 render 方法。 它瞭解 UI 外觀應該是什麼樣子

    • React DOM 更新 DOM 以匹配期望的輸入值。我們剛剛編輯的輸入框接收當前值,另一個輸入框更新為轉換後的溫度。

    ^狀態提升經驗總結

      在一個 React 應用中,對於任何可變的資料都應該遵循“單一資料來源”原則,通常情況下,state首先被新增到需要它進行渲染的元件,然後如果其他的元件也需要它,你可以提升狀態到他們最近的祖先元件。你應該依賴從上到下的資料流向,而不是試圖在不同的元件中同步狀態。

      提升狀態相對於雙向繫結方法需要寫更多的"模板"程式碼,但是有個好處,他可以更方便的找到和隔離bugs。由於熱河state(狀態)都"存活"若干個元件中,而且可以分別對其獨立修改,所以發生錯誤的可能性大大減少。另外,你可以實現任何定製的邏輯來拒絕或者轉換使用者輸入。

      如果某個東西可以從props(屬性)或者state(狀態)得到,那麼他可能不應該在state中。例如我們只儲存最後編輯的temperaturescale,而不是儲存celsiusValuefahrenheitValue。另一個輸入框的值總是在render()中計算得到。這是我們對其進行清除和四捨五入到其他欄位的同事不會丟失其精度

      當你看到UI中的錯誤,你可以使用React開發者工具來檢查props,並向上遍歷樹,知道找到負責更新狀態的元件,這是你可以跟蹤到bug的源頭:Monitoring State in React DevTools

    $ 組合 VS 繼承

      組合 Composition vs 繼承Inheritance

      React 擁有一個強大的組合模型,建議使用組合而不是繼承以實現程式碼的重用

      接下來同樣從案例觸發來考慮幾個問題,新手一般會用繼承,然後這裡推薦使用組合

    包含

      一些元件在設計前無法或者自己要使用什麼子元件,尤其是在 SidebarDialog 等通用的 “容器” 中比較常見

      這種元件建議使用特殊的children prop 來直接傳遞子元素到他們的輸出中:

    function FancyBorder(props) {  return (    
             // children 表示來自父元件中的子元素         {props.children}    
      ) }

      這允許其他元件透過巢狀JSX傳遞任意子元件給他們,比如在父元件中有h1p子元素

    function WelcomeDialog() {    return (        
                

    Welcome

                

    Thank you for your visitiong

                 ) }

      在 JSX 標籤中的任何內容被傳遞到FancyBorder 元件中,作為一個 children prop(屬性)。由於 FancyBorder 渲染{props.children} 到一個

    中,傳遞的元素會呈現在最終的輸出中。

      這是一種簡單的用法,這種案例並不常見,有時候我們需要在一個元件中有多個“佔位符”,這種情況下,你可以使用自定義的prop屬性,而不是children:

    function Contacts() {  return 
    ; }function Chat() {  return 
    ; }function SplitPane(props) {  return (     
          
    {props.left}
          
    {props.right}
        
      ) }function App() {  return (            }       right={                }     />   ) }

      如 React 元素本質上也是物件,所以可以將其像其他資料一樣作為 props(屬性) 傳遞使用。

    特例

      有時候,我們考慮元件作為其它元件的“特殊情況”。例如,我們可能說一個 WelcomeDialogDialog 的一個特殊用例。
      在React中,也可以使用組合來實現,一個偏“特殊”的元件渲染出一個偏“通用”的元件,透過 props(屬性) 配置它:

    function FancyBorder(props) {  return (    
          {props.children}    
      ); }function Dialog(props) {    return (                     

     {props.title} 

                

     {props.message} 

            
        ) }function WelcomeDialog() {    return (             ) }

      這對於類定義的元件組合也同樣適用

    function FancyBorder(props) {  return (    
          {props.children}    
      ); }function Dialog(props) {  return (           

            {props.title}      

          

            {props.message}      

          {props.children}    
      ); }class SignUpDialog extends React.Component {  constructor(props) {    super(props);    this.handleChange = this.handleChange.bind(this);    this.handleSignUp = this.handleSignUp.bind(this);    this.state = {login: ''};   }   render() {    return (                                    );   }   handleChange(e) {     this.setState({login: e.target.value});   }   handleSignUp() {     alert(`Welcome aboard, ${this.state.login}!`);   } }

    如何看待繼承?

    在 Facebook ,在千萬的元件中使用 React,我們還沒有發現任何用例,值得我們建議你用繼承層次結構來建立元件。

      使用 props(屬性) 和 組合已經足夠靈活來明確、安全的定製一個元件的外觀和行為。切記,元件可以接受任意的 props(屬性) ,包括原始值、React 元素,或者函式

      如果要在元件之間重用非 U I功能,我們建議將其提取到單獨的 JavaScript 模組中。元件可以匯入它並使用該函式,物件或類,而不擴充套件它。

    $ 後語

      React的顛覆性思想不同於之前的任何一個框架,掌握React這門技術,會幫助你自己思考如何更高效能、高效率的程式設計,這可能影響你方方面面和以後的任意一次程式設計經歷。

      本文中如有錯誤之處,歡迎指正。



    作者:果汁涼茶丶
    連結:


    來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3016/viewspace-2813624/,如需轉載,請註明出處,否則將追究法律責任。

    相關文章