React 是如何重新定義前端開發的
原文鏈接:Yes, React is taking over front-end development. The question is why.
作者:Samer Buna
譯者:余博倫
轉載請註明出處。
TL;DR 太長不看(譯者注)
- 最主要的 React 核心 API 非常少,在你對 ES6/7 等新語法 和 Modern JavaScript 技術棧比較熟悉的前提下,React 上手是非常快的。
- React 實現了一層抽象的 「虛擬 DOM」 來表示用戶界面,這也就意味著,只要有合適的 renderer 來渲染,你可以把 React 編寫的用戶界面渲染到任何設備或平台上(Web/Native/VR)
- React 採用聲明式的方法編寫用戶界面,你只需要向 React 「描述」 你想要什麼樣的界面,然後規劃好應用會有哪些狀態數據的改變,剩下的具體如何更新如何渲染 React 會自動幫你完成。
- 注意:所有的框架或庫都不會比原生的 JavaScript 快,React 再快也快不過你直插 DOM ,但是 React 實現的 「虛擬 DOM」 以及 「核心 Diff 演算法」 可以只更新界面必要的部分,而不是整體替換,這是你手工操作 DOM 很難實現的效果。
以下是React如此迅速流行的幾個原因:
- 使用 DOM API 很困難。 React 實現的虛擬 DOM 對開發人員來說更加友好。 就像開發者和真實瀏覽器之間的代理一樣。
- React允許開發人員採用聲明式的方法描述他們的用戶界面,並對這些介面的狀態進行建模。 這意味著開發人員只是根據最終狀態(如函數)來描述介面,而不需要考慮過程和中間環節。 當一些事件觸發應用狀態改變時,React會根據具體的情況自動更新用戶界面(想像我們的界面是一段動畫,React可以讓我們把動畫切分成一幀一幀的片段,我們只需要關注其中某個片段,以及片段有哪些變化,至於怎麼變化,過程如何,React可以自動幫我們處理)。
- React 單純是 JavaScript,你只需要了解非常少的核心API,外加幾個簡單的函數的使用方法。 除此之外,只要你的 JavaScript 技術足夠好,你就能成為一個很棒的 React 開發者。 上手門檻也比較低,只要你熟悉 JavaScript ,僅需幾個小時你就能熟練運用 React 了。
但除了這些表象之外還有更多重要的原因。接下來我們就仔細地來聊聊。首先一點是它實現的 虛擬DOM(Virtual DOM) 以及 核心調度演算法(reconciliation algorithm)。我們可以通過實例來證明這些實現的價值。
React 官方的定義是它是一個用於構建用戶界面的庫。這其中有兩個比較重要的部分:
1.首先React是一個庫,而不是框架。它並不是一個完整的解決方案,我們在使用React的同時還需要加入別的庫來配合(不像Angular那種)。React 只專註於一件事,並且把它做到很好。
2.第二部分也就是 React 專註要做的事了:構建用戶界面。用戶界面就是用戶能夠看到並通過它與程序交互的部分。廣義的用戶界面無處不在,從微波爐上的按鈕到宇宙飛船上的控制面板。而只要這個設備支持 JavaScript,我們就能用 React 為它編寫界面(想起來了《西部世界》里Hosts的人工智慧貌似都在用React)。
瀏覽器都能夠運行 JavaScript ,所以我們自然能夠通過 React 來 描述 Web 的界面。注意到我這裡用到的 描述 一詞,這也就是通常情況下我們運用 React 做的事情,我們只是告訴 React 如何來構建具體的用戶界面。如果沒有 React 的話,我們只能通過原始的 Web APIs 和 JavaScript 來手動編寫了。
所以當你聽到 「React 是聲明式」 的這樣的定義時,它其實就是字面意思,我們通過使用 React 來描述用戶界面(只是告訴 React 是什麼或如何做)。剩下的 React 會幫我們搞定,它會把我們聲明式的描述轉換成瀏覽器當中的用戶界面。React 的這些功能是構建在原生的 HTML 基礎之上的,不過有了 React 除了靜態內容之外我們還可以描述動態的數據。
React 三條主要的設計理念成就了它的走紅:
1——可復用、嵌套、有狀態的組件
在 React 中,我們使用組件描述用戶界面。 您可以將組件視為簡單的函數(不論任何編程語言)。 我們用一些輸入調用函數,並給我們一些輸出。 我們可以根據需要復用函數,並從簡單的函數中構建更複雜的函數。
組件的作用也完全一樣; 我們把調用組件時的輸入稱之為「屬性 properties」和「狀態 state」,組件輸出就是對用戶界面的描述(與瀏覽器的HTML類似)。 我們可以在多個用戶界面中復用單個組件,組件可以嵌套其他組件。
然而,與純函數不同,完整的 React 組件可以具有私有 狀態state 來保存變化的數據。
2——響應式更新的性質
React 這個單詞本身的含義(反應)就是對這個概念的簡單解釋。 當組件(輸入)的狀態發生變化時,它所表示的用戶界面(輸出)也會發生變化。 用戶界面描述中的這種變化同樣會在我們使用的設備中響應。
在瀏覽器中,我們需要重新生成文檔對象模型(DOM)中的 HTML 視圖。 使用 React ,我們就不需要擔心如何響應這些更改,甚至管理什麼時候應用更改到瀏覽器; React會直接對狀態進行更改,並在需要時自動更新 DOM 。
3——在內存中對視圖的虛擬
在 React 中,我們使用 JavaScript 編寫 HTML 。 我們依靠 JavaScript 的功能通過某些數據來生成相應的 HTML,而不是擴展 HTML 的功能。 擴展 HTML 是其他 JavaScript 框架的選擇。 例如,Angular 擴展了 HTML 使用循環,條件和其他的功能。
當我們收到來自伺服器的數據(在後台使用 Ajax )時,我們需要比 HTML 更多的東西來處理該數據。 要麼使用擴展了功能的 HTML ,要麼使用 JavaScript 本身的能力來生成 HTML 。 這兩種方法各有優劣。 React 選擇了後一種瑕不掩瑜的方式。
事實上,使用這種方式有一個最主要的優點; 使用 JavaScript 來渲染 HTML 使 React 能夠直接在內存中保存 HTML 的虛擬表示(通常稱為 虛擬DOM)。 React使用虛擬DOM先渲染一個 HTML樹,然後,每當狀態數據發生變化,我們就可以更新這個代表 DOM 的樹狀結構數據,然後 React 不是重新渲染整個 DOM ,而只會寫入 新樹和之前的區別(因為 React 在內存中保留了用於比較差異的兩個版本)。 這個過程被稱為 Tree Reconciliation ,我認為,這是自 Ajax 以來Web開發中最偉大的發明!
在下面的示例中,我們將重點介紹第三個概念,並且可以看到一個簡單的樹對照過程的實例及其帶來的重大改變。 我們將編寫相同的 HTML 示例兩次,首先使用原生 Web API 和 JavaScript,然後我們再來看看如何使用 React 描述相同的 HTML樹。
為了專註說明這一概念,我們不會使用組件,我們將使用 JavaScript 定時器來模擬狀態數據的改變。 我們也不會使用 JSX(在 React 中使用 JSX 可以極大地提高我們編寫代碼的效率)在這個例子中直接使用 React API,以幫助大家更好地了解這個概念。
React 的調度演算法示例
要嘗試此示例,你需要一個瀏覽器和一個代碼編輯器。 實際上也可以使用在線編碼應用,不過我將使用本地文件並直接在瀏覽器中進行測試(不需要 Web 伺服器):
我們將從零開始編寫這個例子。 創建一個新目錄,並在你習慣的編輯器中打開:
mkdir react-demoncd react-demonatom .n
在該目錄中創建一個 index.html 文件,並在其中編寫一個標準的 HTML 模板。 在該模板中包含一個 script.js 文件,並在腳本中添加了一個 console.log 語句,以測試我們的文件正確引入了:
index.html
<!DOCTYPE html>n<html>n <head>n <meta charset="utf-8">n <title>React Demo</title>n </head>n <body>n <script src="script.js"></script>n </body>n</html>n
script.js
console.log(Included!)n
在瀏覽器中打開 index.html 文件,並確保您可以看到空的模板沒有問題,你可以在 控制台中看到您放在 script.js 中的 console.log 測試消息:
open index.html # Macnexplorer index.html # Windowsn
現在讓我們來引入 React 庫文件,我們可以直接使用它的 CDN ,複製 react 和 react-dom 的腳本標籤添加到 index.html 中:
<script src="https://unpkg.com/react@latest/dist/react.js"></script>n<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>n
我們需要引入兩個 React 庫文件是因為 React 實現的功能是獨立的,也可以在別的平台使用,而為了把 React 描述的界面在 DOM 中渲染出來則需要用到 ReactDOM 這個庫:
現在我們刷新一下頁面,在控制台試著輸出 React 和 ReactDOM ,應該就能夠看到了它們暴露的全局對象了:
通過簡單的配置,我們現在可以訪問 React 和 ReactDOM 的 API 了。
想要在頁面中動態插入 HTML ,我們需要先聲明一個 DOM 標籤作為容器:
<div id="js"></div>n
然後在我們的 script.js 腳本文件中編寫代碼獲取到這個容器:
// 這裡是 ES6 的 const 關鍵字聲明常量nconst jsContainer = document.getElementById("js");n
加入我們想要簡單地插入 HTML 片段的話,只需要使用原生的 innerHTML 方法即可:
// 這裡是 ES6 的字元串模板njsContainer.innerHTML = `n <div class="demo">n Hello JSn </div>n`;n
如果這一切都工作正常,你應該可以在頁面里看到一個 "Hello JS" 的字樣了。
為了更簡潔地說明我們的概念,代碼就保持這麼簡單。注意到我們使用的所有方法都是原生的 JS 方法。而接下來我們使用 React 時則需要調用 React 的 API,再有 React 來進行同 DOM 原生 API 之間的交互(事實上 React 源碼最後把 DOM 插入的時候用的也就是 innerHTML 方法)。
React 的角色就好像是我們與瀏覽器之間的代理,我們只需要告訴 React 該幹什麼,React 會自己去和瀏覽器打交道。我們僅有的需要直接使用原生 JS 的情況可能也就只有獲取 DOM 容器這些操作了。
接下來我們使用 React 實現一下剛才的示例,還是一樣的,新建一個 id 為 react 的頁面容器:
<div id="react"></div>n
同樣的,在我們的 script.js 腳本文件中獲取這個容器:
const reactContainer = document.getElementById("react");n
接下來我們需要使用到 ReactDOM 這可庫中的 render 方法:
ReactDOM.render(n /* TODO: Reacts version of the HTML template */,n reactContainern)n
我們接下來要做的,是你理解 React 至關重要的一點。記得我們之前提到過的,React 是通過 JavaScript 來書寫 HTML 的吧。
我們可以通過調用 React 的 API 來生成一個簡單的 HTML 界面
並不像之前一樣操作字元串,在 React 當中,我們通過定義對象來定義 DOM 的內容,下面的方法可以生成和剛才原生 JS 示例一樣的內容:
ReactDOM.render(n React.createElement(n "div",n { className: "demo" },n "Hello React"n ),n reactContainern );n
React.createElement 方法接受很多個參數:
- 首先是生成 HTML 的標籤名,例如 div
- 然後是標籤的屬性,不過這裡我們使用的是對象來描述,例如 { className: "demo" } 會生成 class="demo"
- 第三個則是標籤中包含的內容,例如我們示例中的 "Hello React" 字元串
我們現在可以稍微添加一點點樣式,在瀏覽器中查看一下:
<style media="screen">n .demo {n border: 1px solid #ccc;n margin: 1em;n padding: 1em;n }n</style>n
現在我們有了兩個節點,一個由原生 DOM Web API 創建,另一個則是通過 React API 創建。最主要的一個區別是,原生的方法用字元串表示節點內容,而 React 則是通過一個方法傳入對象來表示內容。
不論我們的界面多麼複雜,使用 React 時,所有的頁面元素都是通過 React.createElement 方法傳入對象來表示。
接下來我們再來試著添加點其他內容,我們來嘗試一下嵌套結構的 HTML 內容,原生的 JS 還是一樣的,用字元串表示:
jsContainer.innerHTML = `n <div class="demo">n Hello JSn <input />n </div>n`;n
使用 React 也很簡單,我們只需要修改 React.createElement 方法的第三個參數:
ReactDOM.render(n React.createElement(n "div",n { className: "demo" },n "Hello React",n React.createElement("input")n ),n reactContainern);n
看到這一步,你可能會疑惑。使用 React 是不是把簡單的問題複雜化了?表面上看起來是這樣,但我們有這麼做的理由,請接著往下讀:
我們再來添加一個顯示時間戳的標籤:
jsContainer.innerHTML = `n <div class="demo">n Hello JSn <input />n <p>${new Date()}</p>n </div>n`;n
使用 React 時呢,我們則需要傳入 5 個參數來表示:
ReactDOM.render(n React.createElement(n "div",n { className: "demo" },n "Hello React",n React.createElement("input"),n React.createElement(n "p",n null,n new Date().toString()n )n ),n reactContainern);n
現在原生 JS 和 React 在頁面上仍舊顯示相同的內容。
就目前的示例來看,使用 React 看起來要比原生的方法複雜困難許多。那麼 React 究竟有什麼價值能讓我們放棄編寫原生的 HTML 轉而用它的 API 方法來書寫呢?問題的關鍵不在於第一次生成頁面,而是之後如何更新頁面的內容。
接下來我們用示例演示一下,讓時間戳隨著每一秒變化。
最直接的方法是通過 setInterval 函數每秒鐘執行一詞我們定義好的方法:
const jsContainer = document.getElementById("js");nconst reactContainer = document.getElementById("react");nconst render = () => {n jsContainer.innerHTML = `n <div class="demo">n Hello JSn <input />n <p>${new Date()}</p>n </div>n `;n ReactDOM.render(n React.createElement(n "div",n { className: "demo" },n "Hello React ",n React.createElement("input"),n React.createElement(n "p",n null,n new Date().toString()n )n ),n reactContainern );n}nsetInterval(render, 1000);n
運行一下我們的代碼,現在兩個節點中的計時器應該都自動工作了。不過,你現在試一下原生 JS 的節點,你會發現 input 是無法輸入的,因為每一秒,這個節點的 DOM 元素就都被重新構建了。而你會發現 Rect 節點的輸入框是可以正常使用的。
雖然 React 也是重複調用 render 方法,但實際上,React 只會重新渲染計時器數字變化的部分,其他內容會保持不變,輸入框自然也就能正常使用了。
在控制台裡面,你可以很具體的觀察到兩個節點之間的異同,原生節點的所有內容都被重新渲染了,而 React 則只更新了計數器的節點。
React 有一個智能的差分diff演算法,通過這一演算法我們能實現只生成 DOM 節點當中需要被重新渲染的部分。這一演算法之所以能夠行得通,是因為 React 的虛擬 DOM 技術,把 DOM 的結構和內容保存在了 JS 當中。
使用虛擬 DOM ,React 將內存中的最後一個 DOM 版本保留下來,當它有一個新的 DOM 版本生成到瀏覽器時,新的 DOM 版本也將保存在內存中,所以React可以計算新版本和舊版本之間的差異(在我們的例子中,區別是時間戳段落)。
然後React會指示瀏覽器只更新計算出的差異,而不是整個 DOM 節點。無論我們重新生成我們的界面多少次,React將只向瀏覽器添加新的「部分」更新。
這種方法不僅可以提高效率,而且為了更新用戶界面的方式,也會消除一大堆複雜性。有了React來做所有關於我們是否應該更新 DOM 的計算,都使我們能夠專註于思考我們的數據(狀態)和描述用戶界面的方式。
然後,我們根據需要管理我們的數據更新,而不用擔心在瀏覽器中將實際用戶界面反映這些更新所需的步驟(因為我們知道 React 將會自動地並以最高效的方式做到這一點)!
推薦閱讀:
※阿里雲前端周刊 - 第 21 期
※感謝《深入淺出React和Redux》的所有讀者
※基於React.js開發IM即時通訊系統,觸摸大型互聯網公司真實項目
※Webpack傻瓜指南(二)開發和部署技巧