React 之我見:JSX,虛擬 DOM,Diff 算法,setState 梳理

本文主要梳理一下我對 React 框架基礎內容的認識,後面也會總結一些深度內容的認識。固然,筆者水平也有限,若是你發現不妥之處,望斧正!html

React VS Vue

不少人都說 React 上手要難一些,Vue 上手要簡單一些。也有不少人在問 React 和 Vue 的區別是什麼。我想說一下個人理解。前端

首先,在框架設計層面,React 是 MVC 軟件架構中的 View,它只負責視圖層的東西,而對於數據和路由等,則由 Redux 和 Router 等來完成。這也就是爲何 React 官方文檔說 「React 是一個用於構建用戶界面的 JavaScript 庫」。而 Vue 則不一樣,Vue 是基於 MVVM 架構設計的一個框架,因此在架構層面,Vue 和 React 的思想就是不一樣的。react

React 的設計哲學很簡單,不可變(Immutable)思想貫穿了整個框架的設計。React 能夠說沒引進什麼新的概念,讓開發者可以以相似寫原生 JavaScript 代碼的方式來使用 React 進行開發。這也就是不少人說 React 難的緣由,即 JavaScript 基礎知識。git

而不少人說 Vue 容易上手,可能有些同窗就迷惑了,我以爲 Vue 不簡單啊。筆者認爲,說 Vue 簡單是由於 Vue 將不少底層邏輯封裝好了,直接給你對應的 API,你調用對應 API 就能夠完成對應的工做。因此,你不須要有很紮實的 JavaScript 基礎,你只要記住或者可以查閱到對應 API 就能夠用 Vue 完成一些基礎性的開發。因此有些後端的同窗能夠看看 Vue 官方文檔就能上手基礎的前端開發。github

React 之我見,那必定要說個人見解,我喜歡 React。我喜歡其簡潔的設計理念和類原生的開發體驗,讓我可以感受到本身是在寫代碼。那麼 Vue 我沒有深刻學習,但這是一個很是優秀的前端框架,目前也已經安排進了下一階段的學習任務中。算法

React JSX,虛擬 DOM 和 ReactDOM.render

JSX 和虛擬 DOM

JSX 和虛擬 DOM 是必說內容。JSX 也就是 JavaScript 的一個語法擴展,能夠經過 Babel 對 JSX 進行轉譯。後端

const title = <h1 className="title">Hello, world!</h1>;
複製代碼

例如這樣一段 JSX 代碼會被 Babel 轉譯成:瀏覽器

const title = React.createElement(
    'h1',
    { className: 'title' },
    'Hello, world!'
);
複製代碼

從這裏咱們能夠看出,React.createElement 方法的參數是這樣的:性能優化

createElement( tag, attrs, child1, child2, child3 );
複製代碼

那麼咱們能夠實現一下 createElement前端框架

function createElement( tag, attrs, ...children ) {
    return {
        tag,
        attrs,
        children
    }
}
複製代碼

而後這樣就能夠對其進行調用:

// 將上文定義的 createElement 方法放到對象 React 中
const React = {
    createElement
}

const element = (
    <div> hello<span>world!</span> </div>
);
console.log( element );
複製代碼

此處引用 hujiulong從零開始實現一個 React(一):JSX 和虛擬 DOM,詳細內容推薦看其原文。

也就是說,createElement 方法返回的東西就是所謂的虛擬 DOM。說到虛擬 DOM 就必說 diff 算法,這個咱們以後再講。

ReactDOM.render

render 其實沒有太多好說的,其做用就是將虛擬 DOM 渲染成真實的 DOM,只是在實現的過程當中有很是多的細節狀況要處理。

React 組件和生命週期

在 React 中,咱們能夠經過 Class 和 Function 兩種方式來寫組件。以 Class 的方式寫組件的時候,咱們須要繼承 React.Component。這裏主要涉及 React.Component 的模擬實現和 React 的生命週期。模擬實現推薦看 從零開始實現一個 React(二):組件和生命週期。至於 React 的生命週期,以後我會總結一篇文章,推薦你們去看 React 官方文檔。

Diff 算法

爲何要 diff?由於 DOM 操做十分昂貴,也就是很是耗時耗性能。可是 DOM 操做就必定很慢很耗性能嗎?其實不必定,只是廣泛來說是這樣的,詳細內容推薦看 JJC 的知乎回答:前端爲何操做 DOM 是最耗性能的呢。簡單講,瀏覽器中 DOM 對象是使用 C++ 開發的,性能確定不慢,有些狀況下性能甚至高於操做 JavaScript 對象。而大多數時候操做 DOM 是很耗性能的,這是由於 JavaScript 對象的屬性(properties)映射到的是 DOM 對象的特性(attributes)上。

咱們操做 JavaScript 對象只是修改一個對象,而修改 DOM 對象時還會修改對象的 attributes,那麼性能就消耗在 JavaScript 對象和 DOM 對象的轉換和同步上。而且,操做 DOM 還會影響頁面的渲染,進而會再一次下降了性能。雖然 DOM 的性能有可能比操做普通對象還要快,但 99% 的場景下,操做 DOM 對象仍是昂貴的。因此,高效的 diff 勢在必行。

咱們能夠經過實現高效的 diff 算法,對比出操做先後有差別的 DOM 節點,而後只對更改了的 DOM 進行更新。這樣能夠大大減小沒有必要的 DOM 操做,進而大幅提高性能。

React 的 diff 算法其實很簡單:對比當前真實 DOM 和虛擬 DOM,在對比過程當中直接更新真實 DOM。而且這個比對是同級比對,當發現這一級的 DOM 不一樣時,會直接更新該節點及其全部子節點。這種 diff 策略其實是出於性能上的取捨。首先,DOM 操做不多出現跨層級的狀況,因此只須要同級比對就能夠知足大多數狀況,也就沒有必要對比全部的 DOM 節點,由於那樣須要 O(n^3) 的時間複雜度,代價過高。

React diff 算法的模擬實現推薦參見:從零開始實現一個 React(三):Diff 算法

所謂異步的 setState

基本知識

首先明確,所謂異步的 setState 並非真的異步,只是其行爲相似於異步操做的行爲。下面咱們來詳細說說。

首先,將 setState 的行爲設置爲所謂異步的形式的出發點依舊是性能優化,由於若是每次經過 setState 更改了狀態都對組件進行一次更新,會很浪費性能。而經過將屢次 setState 合併在一塊兒,進行一次更新渲染,則會大大提高性能。

將多個 setState 的調用合併成一個來執行,也就意味着 state 並不會被當即更新。例以下面這例子:

class App extends Component {
    constructor() {
        super();
        this.state = {
            num: 0
        }
    }
    componentDidMount() {
        for ( let i = 0; i < 100; i++ ) {
            this.setState( { num: this.state.num + 1 } );
            console.log( this.state.num );    // 會輸出什麼?
        }
    }
    render() {
        return (
            <div className="App"> <h1>{ this.state.num }</h1> </div>
        );
    }
}
複製代碼

這裏定義了一個組件 APP,在組件掛載後,會循環 100 次。但當咱們真的對這個組件進行渲染會發現,渲染結果爲 1,而且控制檯中輸出了 100 次 0。也就是說,每次循環中拿到的 state 都仍是更新以前的。也就是說 React 對 setState 進行了優化,但若是咱們就是想要馬上得到更新後的 state 怎麼辦呢?React 提供了一種解決方案,setState 接收的參數還能夠是一個函數,經過這個函數咱們能夠拿到更新以前的狀態,而後經過這個函數的返回值獲得下一個狀態:

componentDidMount() {
    for ( let i = 0; i < 100; i++ ) {
        this.setState( prevState => {
            console.log( prevState.num );
            return {
                num: prevState.num + 1
            }
        } );
    }
}
複製代碼

合併 setState

對於這個地方,有兩個要點:一是如何對多個 setState 調用進行合併。二是我怎麼斷定一次合併哪些 setState 呢?接下來咱們一一解答。

setState 的合併是經過隊列實現的。經過建立一個隊列來保存每次 setState 的數據,而後每隔一段時間,清空和這個隊列並渲染組件。此處的模擬實現推薦閱讀:從零開始實現一個 React(四):異步的 setState

接下來就是一次到底合併哪些 setState。換句話說,咱們會將一段時間內的 setState 調用合併成在一塊兒執行,那麼這段時間的長短取決於什麼?其實此處使用的是 JavaScript 的事件隊列機制,也就是事件循環(Event Loop)。關於事件循環的詳細內容可參見阮一峯的 JavaScript 運行機制詳解:再談 Event Loop。事件循環的核心概念就是同步任務,異步任務,微任務以及宏任務。

在 React 的 setState 中,利用 JavaScript 的事件循環機制對多個 setState 調用進行合併。首先建立一個隊列保存每次 setState 的數據,在一次事件循環的全部同步任務以後,清空着隊列,將隊列中的全部 setState 進行合併,並進行一次性更新渲染。這樣在一次事件循環的,最多隻會執行一次合併操做,而且只會渲染一次組件。

因此呢,這也就是爲何我說,所謂的 setState 的異步行爲並非真正的異步,只是不會對每個 setState 操做進行實時更新,而是經過隊列的方式對一次事件循環中的全部同步任務的 setState 調用進行合併,合併成一個以後進行一次更新和渲染,因此效果上看上去是異步的,但並非 setTimeout 或 setInterval 這種真正的異步操做。

至此,React 的基本概念已經梳理完畢,感謝閱讀。筆者水平有限,若是你發現任何問題,歡迎評論指正!

TODO

  • 梳理 React 生命週期
  • 梳理 React Fiber 的更新內容

以上內容會以新文章的方式發佈。

參考資料

相關文章
相關標籤/搜索