React學習(4)-理清React的工作方式
- 2019 年 10 月 4 日
- 筆記
前言

在接觸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會將元素和它的子元素與它們之前的狀態進行比較,並只會進行必要的更新,例如:如下示例

具體程式碼如下所示
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模組的,後續單獨拿一篇幅來說也不為過的,涉及到的知識還是挺多的