React學習(4)-理清React的工作方式

  • 2019 年 10 月 4 日
  • 筆記

前言

理清React的工作方式

在接觸React之前,我們也許習慣了DOM編程,那它相比於原生JS,JQ編程方式,究竟有什麼區別?React的工作方式是什麼樣子的?所謂的虛擬DOM又指的是什麼?以及React的工作方式的優點有哪些?

那麼本篇就是你想要知道的

如果想閱讀體驗更好,可戳React學習(4)-理清React的工作方式,內有影片

從一個簡單的React組件開始

我們先看一個加減數字框組件,具體效果如下所示,分別通過原生JS和JQ是怎麼實現的

數字加減

原生JS實現

DOM結構

       <div>  		   	<button id = "reduce">-</button>  		   	<input  id = "input" type="text" value="0">  		   	<button id = "add">+</button>  	   </div>

CSS層疊樣式

   *{  		padding: 0;  		margin: 0;  	}    	div{  		width: 100%;  		display: flex;  		display: -webkit-flex;  		position:fixed;  		left: 40%;  		top:10%;  	}    	button {  		padding: 10px;  	}    	input {  		text-align:center;  	}

對應的JS

            // 獲取DOM元素  	   	     var oBtnReduce = document.querySelector("#reduce"),  	   	         oInput = document.querySelector("#input"),  	   	         oBtnAdd = document.querySelector("#add");       			 // 添加事件               oBtnAdd.onclick = function() {               	oInput.value++;               }                 oBtnReduce.onclick = function() {               	oInput.value--;               }

JQ實現:

               var  $reduce = $('#reduce'),       			    $input = $('#input'),       			    $add = $('#add'),       			    $nowVal = $("#input").val();         			    $reduce.click(function() {       			    	$input.val($nowVal--);       			    });         			    $add.click(function() {       			    	$input.val($nowVal++);       			    })

當然,你把事件添加在內聯元素身上,可以在行內元素裡面添加事件,通過傳參的方式去控制,如下程式碼所示,也是可以的

        <div>  		   	<button onclick = "handleClick('-')" id = "reduce">-</button>  		   	<input  id = "input" type="text" value="0">  		   	<button onclick = "handleClick('+')" id = "add">+</button>  	   </div>         <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>           <script type="text/javascript">                 function handleClick(flag) {  					var nowVal =  $("#input").val();  				    if(flag == '+') {  						$("#input").val(parseInt(nowVal) +1);  					}else if(flag == '-' ) {  						$("#input").val(parseInt(nowVal) -1);  					}  			 }         </script>

對於在原生JS,JQ中,通過內聯方式添加事件,是不推薦的,然而在如今的一些面向數據編程,例如React,Vue等框架中,這一方式卻得到了支援與延續,要從面向DOM編程轉移到面向數據編程

React實現

import React, {  Component } from 'react';  import ReactDOM from 'react-dom';      class CountNum extends Component {      constructor(props) {          super(props);            this.state = {              inputVal: 0          }          }        render() {          return (              <div style = {{ textAlign: "center", marginTop: "50px"}}>                   <button onClick = { this.handleClickReduce.bind(this) }>-</button>                   <input style = {{ textAlign: "center"}} value = { this.state.inputVal } onChange = { this.handleInputChange.bind(this) } />                   <button onClick = { this.handleCLickAdd }>+</button>              </div>          );      }        handleCLickAdd = () => {        this.setState({          inputVal: this.state.inputVal+1        });      }        // handleCLickAdd(){      //    this.setState({      //        inputVal: this.state.inputVal+1      //    })      // }       handleClickReduce() {        this.setState({            inputVal: this.state.inputVal-1        })     }       handleInputChange(e) {        let changeVal = e.target.value;        this.setState({          inputVal: changeVal++        });     }  }      const container = document.getElementById('root');    ReactDOM.render(<CountNum />, container);

從上面一看,對於剛接觸React的小夥伴來說,可能覺得用原生JS,JQ實現起來很簡單呀,React寫起來的程式碼,什麼玩意的,那麼一大堆的,JS裡面還寫HTML程式碼,簡直噁心到不行,並未達到,內容結構,層疊樣式,邏輯的分離,如果對於這部分內容有疑惑的,可以閱讀之前兩篇JSX的文章的

對於JS,JQ的實現方式,主要工作是在操作DOM,獲取元素,添加事件,執行操作。對於簡單的業務實現,是沒有什麼問題的,但是當DOM結構層級比較深,要進行一些複雜的邏輯操作時,此時,不斷的操作DOM就變得非常噁心了的,這裡並不是忽視原生JS,即使有了一些上層的框架簡化了操作,但核心的邏輯程式碼編寫仍然是要寫的,只是關注點不一樣了的

而在React中,我們可以發現,並沒有操作DOM的過程,一切以數據為中心,數據是什麼,頁面就顯示什麼

並沒有像JS,JQ一樣獲取元素,添加事件然後執行一些操作的動作.

對於大型項目迭代開發,這種方式編寫的程式碼會更容易的管理,因為React只是用作於視圖UI層的渲染工作,我們關心的是渲染成什麼樣子,而不需要關心如何實現渲染,怎麼進行DOM操作

這就好比在業界里有這麼一句話,優秀的程式設計師關心數據結構,平凡的程式設計師操心程式碼一樣,如果把JQ,與React做這樣一個對比,前者就是React,在這裡沒有任何貶低JQ的意思.

JQ仍然是無比強悍的,每個技術都有與之對應的應用場景.

況且也沒有JQ實現不了的,只不過是略繁瑣一些而已.

至少在沒有出現React,vue,Angular等這些框架之前,它仍然是霸主統治性地位存在的,然而現在真的不得不說,它的確是在走向落寞.

從上面的React程式碼中,我們可以歸結出,React的理念可以用這麼一個公式表示:

UI = render(data)

這個等號左邊UI用戶介面的顯示取決於等號右邊的render函數,這個render函數接收一個數據data作為參數,這個函數是一個純函數,也可以稱為是無狀函數(函數式組件)

換而言之,類似這種只用作UI顯示的函數,我們可以用無狀態函數去定義,這在後續若使用了redux做公共數據管理時,把組件裡面的state數據抽離到store當中時,可以使用無狀態組件的

因為它只負責頁面的渲染,沒有去做任何邏輯操作的時候,UI組件我們一般都可以用無狀態組件來定義,UI組件只負責頁面的渲染,當然這並不是絕對的,有時候,也可以做一些簡單邏輯的操作

使用無狀態組件(函數組件),它的性能是高於普通組件的,因為它是函數,而用class類定義的組件,類生成的對象裡面有生命周期函數,所以它執行起來肯定沒有函數組件(UI組件)快

對於我們開發來說,最重要的是區分哪些是屬於data,哪些是屬於render,想要更新用戶介面,要做的是更新data,用戶的介面自然會做出響應,所以把React稱為響應式編程(面向數據編程)

注意:render函數返回的值,組件生成的 HTML 結構只能有一個單一的根節點

Virtual(虛擬) DOM

元素(JSX)是構成React應用的最小磚塊,它描述了你在在螢幕上看到的UI內容

與瀏覽器的DOM元素不同,React元素時創建開銷極小的普通對象,並不會跟原生操作DOM一樣,影響整個DOM的重繪渲染,React DOM會負責更新DOM與React元素保持一致

React只更新它需要更新的部分,React DOM會將元素和它的子元素與它們之前的狀態進行比較,並只會進行必要的更新,例如:如下示例

虛擬DOM的更新

具體程式碼如下所示

import React, {  Fragment, Component } from 'react';  import ReactDOM from 'react-dom';    function tick() {    const element = (      <Fragment>        <div style = {{ textAlign: 'center' }}>          <h1>歡迎關注微信itclancoder公眾號</h1>          <p>現在北京時間是 { new Date().toLocaleTimeString() }</p>        </div>      </Fragment>    );    const container = document.getElementById('root');    ReactDOM.render(element, container);    }    setInterval(tick, 1000);    

當然,我們可以對它進一步的優化,寫成一個組件,如下所示:

import React, {  Fragment, Component } from 'react';  import ReactDOM from 'react-dom';    class Clock extends Component {    constructor(props) {      super(props);        this.state = {         date: new Date()      }      }      render() {      return (         <Fragment>              <div style = {{ textAlign: "center" }}>                  <h1>歡迎關注微信itclanCoder公眾號</h1>                  <p>現在是北京時間:{ this.state.date.toLocaleTimeString() }</p>              </div>         </Fragment>      );    }     // 生命周期函數,組件掛載時自動執行這個方法,組件已經被渲染到 DOM 中後運行    componentDidMount() {      this.timer = setInterval(() => {        this.tick()      }, 1000)    }    // 組件卸載時,清除定時器    componentWillUnmount(){      clearInterval(this.timer);    }      tick() {      this.setState({        date: new Date()      })    }  }    const container = document.getElementById('root');      ReactDOM.render(<Clock  />, container);

對於上面的程式碼,涉及到初始化state狀態數據,以及componentDidMount和componentWillUnmount兩個生命周期函數,在組件掛載時設置一個定時器函數,自動更新時間,在組件卸載時,清除定時器,通過setState這個方法,實時更新state數據。更多相關state以及props,生命周期的知識,暫時知道這麼用就可以了,後續會有更詳細的內容介紹的

儘管每一秒我們都會新建一個描述整個 UI 樹的元素,但是React DOM 只會更新實際改變了的內容,也就是上面中的文本節點

這是因為React利用Virtual DOM,讓每次渲染都只重新渲染最少的DOM元素

而操作DOM會引起瀏覽器對網頁進行重排重繪。

DOM樹是對HTML的抽象,而vitrtual DOM就是對DOM樹的抽象,虛擬DOM不會觸及瀏覽器,虛擬DOM本質上就是javascript對象,還記得前面說過的JSX是React.createElement()方法的一個語法糖?

它是存在於javascript空間樹形結構,每次自上而下渲染React組件時,會對比這一次產生的virtual DOM和上一次渲染的virtual DOM,對比就會發現差別,然後修改真正的DOM樹時就只需要修改中的部分就可以了的

React的工作方式及優點

在沒有組件化React,Vue,Angular之前,毫無疑問,JQ是最直觀易懂的,但是當項目逐漸變得複雜龐大時,用JQ寫出來的程式碼耦合度就沒那麼高了的,正是這樣,也就誕生了一些requirejs以及Seajs解決一些問題,但是使用JQ寫出來的程式碼往往互相糾纏

如下圖所示

使用React的方式,就可以避免構建這樣複雜的程式結構,無論何種事件,引發的都是React組件的重新渲染,它只會修改數據變化的的DOM部分,並不需要去關心怎麼去操作DOM

如下圖所示

在React中,對JSX元素上添加事件,是通過on*EventType 這種內聯方式添加的,不需要手動調用瀏覽器原生的 addEventListener 進行事件監聽,在React中,它已經幫我們封裝好了一些事件類型屬性,當需要給某個元素監聽事件的時候,只需要通過內聯方式,React元素上加on*EventType就可以了,注意這裡事件類型的寫法,駝峰式命名法

也無需考慮瀏覽器的兼容性,這裡要格外注意的是,這些 on*EventType的事件監聽只能用在普通的 HTML 的標籤上(div,input,p,a等原生瀏覽器支援的標籤),而不能用在組件標籤上。也就是說,<Button onClick={…} /> 這樣的寫法是不起作用的

如果想要做到這一點,在組件標籤上監聽事件起作用,也可以做到,就是結合第三方模組styled-components樣式組件進行使用,是可以做到的,更多內容,可以參考styled-components官方文檔

這裡簡單提一下:

  • 終端里,安裝styled-components: npm install –save styled-components
import React, {  Fragment, Component } from 'react';  import ReactDOM from 'react-dom';  import styled from 'styled-components';    export const Button = styled.button`     outline: none;  `    // class Button extends Component {    //      render() {  //         return (  //           <button>按鈕</button>  //         );    //      }    // }    class CountNum extends Component {      constructor(props) {          super(props);            this.state = {              inputVal: 0          }          }        render() {          return (              <div style = {{ textAlign: "center", marginTop: "50px"}}>                   <button onClick = { this.handleClickReduce.bind(this) }>-</button>                   <input style = {{ textAlign: "center"}} value = { this.state.inputVal } onChange = { this.handleInputChange.bind(this) } />                   <button onClick = { this.handleCLickAdd }>+</button>                   <Button onClick = { this.handleBtnClick.bind(this) }>按鈕</Button>              </div>          );      }        handleBtnClick() {        alert("我是樣式組件,簡直帥呆了");      }        handleCLickAdd = () => {        this.setState({          inputVal: this.state.inputVal+1        });      }        // handleCLickAdd(){      //    this.setState({      //        inputVal: this.state.inputVal+1      //    })      // }       handleClickReduce() {        this.setState({            inputVal: this.state.inputVal-1        })     }       handleInputChange(e) {        let changeVal = e.target.value;        this.setState({          inputVal: changeVal++        });     }  }      const container = document.getElementById('root');    ReactDOM.render(<CountNum />, container);

具體效果如下所示

  • 在文件中引入styled-components模組
  • 樣式組件定義使用,如下所示

React的編程模式是函數式編程來解決用戶介面渲染問題的,也稱為面向數據編程,一切皆是JS,基於組件開發模式

結語

本文主要從一個簡單的React數字框組件應用開始,分別用原生JS,JQ,React進行了實現,在React中UI視圖取決於render函數返回的內容,數據是什麼,就讓頁面顯示什麼,無需關注DOM操作,並且React引入了虛擬DOM

它是對DOM樹的一種抽象,本質上就是一js對象,當進行視圖的改變時,當React的子元素內容發生改變時,並不會引起整個瀏覽器的重繪和重排,只會更改變化的數據部分,並且在給JSX添加事件監聽時,使用on*EnentType的方式

並且這種事件的監聽,它只作用於原生HTML元素上,若放在自定義的組件上時,是不起作用的,具體解決辦法,可以引入第三方styled-components模組的,後續單獨拿一篇幅來說也不為過的,涉及到的知識還是挺多的