# React Deep Div 1 ###### tags: `react` ## 自我介紹 哈囉,大家好! 我是 哈囉,目前是在新加坡工作,主要是在寫前端 React。 ## 目標 那我們今天的討論目標就是要把 React 的 API 設計跟背後的機制釐清 但因為涉及的內容有點多,所以可能會分做好幾天。 ## 課程適合對象 這次課程適合所有等級的 React 工程師, 不論你是新手還是老手, 這堂課應該對你都會有些幫助。 如果你是新手的話,今天的話題會幫你之後在學習更能理解到底發生了什麼事。 如果是老手的話,對於底層理解也可以幫助你走得更深,像是做自己的套件或是對 React 做開源貢獻之類的。 都會比較清楚該怎麼做。 ## 為什麼這堂課很重要? 是因為我個人覺得市面上大多數的教材比較偏向如何使用 React, 卻很少提及到 React 的本質跟他的設計概念, 我個人是覺得當你知道他背後的設計跟理念的時候, 會比較清楚該如設計自己的程式碼, 也比較知道到底為什麼自己的 Code 會產生 Bug。 ## 舉例: Question1 所以,這裡先來看一個範例。 如果等等你答對了, 恭喜你,你對於 React 至少有基本的了解, 但如果你答錯了,也沒關係,這就是開這次直撥的目的。 ## Rethinking Best Practice 你可能很難想像。 當初 React 剛開始推出時其實是被很多人撻伐的。 因為他提出的觀點,跟當時主流的 Best Practice 是大相徑庭的。 當時主流的網頁寫法是倡導 HTML, CSS, JS 關注點分離, 但 React 在 2015 年的 JSconf 提出了一段非常經典的演說, 重新思考了一遍所謂網頁 Best Practice 的做法。 這也間接影響了後面的整個前端框架的設計, 在當時是一個很大膽跟很前衛的理念, 這邊也是鼓勵大家在思考所謂 Best Practice 的時候, 重新回想歷史上,所謂 Best Practice 到底被顛覆了幾次。 ## 進入正題:API 理念 接下來的重點就是要解釋 React API 概念。 ### 畫面是資料轉換來的 React 最核心的概念就是,畫面就是資料轉換來的。 給他相同的資料,就應該產生相同的畫面。 如果你有聽過 functional programming 的話,應該會熟悉, 這就是 pure function 的定義, 同樣的 input 會產生 同樣的 output。 ### 抽象化 接下來第二點就是抽象化, 當我們的專案大到一定的程度,或是畫面很複雜的時候, 其實很難單純用 HTML 直接刻完。 其實比較符合人類的思考方式, 應該是按照將畫面拆分成一個個的小元件, 然後根據需要組合起來。 ### 組合 然後接下來的概念是組合, 如果只是單純元件包元件,其實還是會有重複的部分, 因為小元件被寫死在另一個大元件裡面了, 這表示越大的元件重用性會越低, 我們希望大的元件可以透過挖洞的方式然後拋小元件進去, 這方便我們根據情況組合出想要的元件。 ### 狀態 前端不是單純 反應 server 或 business 的狀態而已。 他其實也有很多自己的狀態要處理。 像是 文字輸入匡裡面的文字,或是根據 filter 中的文字去過濾清單要顯示的東西, 這些都不會是 server 或是 database 要處理的事情。 所以我們必須要能處理狀態。 再來後半段我會跳過,大家有興趣自己在去看看。 接下來我們會進到 React 設計的部分。 ## React 設計 ### 架構 這邊先提一下 React 的整個架構跟流程其實是一個大型的遞迴。 我們寫的 Code 部分,你可以想像他是一個大型 function, 就跟一般的 function 一樣他可以輸出一個資料, 然後這個資料就會丟給 React , React 再透過我們給他的資料去更新宿主樹。 所以整流程就會像 Code => React => 畫面 這是段單向且不可逆的流程, 當渲染到畫面的時候,Code 到 React 這段早就結束了。 但因為我們在程式碼裡面有嵌入一些 event handler 跟 effects, 當這些事件導致 React 的狀態異動的時候, 就會觸發 rerender,而所謂的 rerender 其實就只是重跑一遍剛才提到的流程, 只是透過 React 內部的一些機制, 讓整段流程變得非常高效,但本質還是一樣,就重跑一遍而已。 ### 宿主樹 所以第一個我們要談到的東西叫宿主樹。 那宿主樹到底是什麼, 其實就是根據你要寫的東西是什麼, 像是我平常寫 WEB 那宿主樹就是 DOM, 那如是寫 IOS 的話宿主樹就是原生平台。 所以 React 本身是不在乎他要輸出的對象到底是誰。 React 讓你用同樣的方式寫畫面交互邏輯, 所以你不用在需要因為要寫 IOS 所以要重寫 Swift,然後寫 Andriod 又要重寫 Kotlin。 然後再來談到 UI 本身的特性, UI 本來就是要穩定的,你不會希望今天按鈕在左邊,明天就跑到右邊去, 這樣的東西根本就不好用。 然後 UI 本身是有結構性的, 可以被拆成好幾個小部件在組合起來。 React 就是基於上述前提下去設計的。 ### 宿主實例 然後因為宿主樹本身就是透過樹狀結點下去組成的, 所以這些節點就是我們剛剛說的宿主實例。 那在 WEB 的話 宿主實例就是 DOM Node, 就是你用 document.getElementById 拿到的東西。 你不需要在你的程式碼用到任何 DOM 相關的操作, 操作這些 DOM API 是 React 的工作。 ### 渲染器 接下來就是要討論是誰在負責操作, 在寫 React WEB 的時候,你應該會看到有一個套件叫 ReactDOM 但你平常根本就不會用到它。 但其實他就是負責把妳的資料渲染到 DOM 上面的人。 選染器有幾個模式, 但你可能也不需要知道, 除非你想要自己寫一個渲染器, 那我可以推薦你去看 react-test-renderer 在 React 官方網站上。 ### React Element 再來我要討論 React Element, 剛剛提到的,我們的 Code 輸出的資料其實就是透過 React Element 組合而成的, React Element 其實就是單純的 JS 物件而已。 他用來描述宿主實例,像是他是什麼 type,跟他的屬性有什麼,就像範例顯示的那樣。 React Element 非常輕量,沒有綁任何的宿主實例在上面。 再來他們是一次性的,就是用完就丟了。 像前面提到的,你的 Code 是一個大的函式, 等到輸出了 React Element 之後,你的函式就結束了。 React 不會再回頭改你輸出的東西, 就算是你改了狀態,他也只是把你的函式在跑一遍,之後會再詳細談到這塊。 再來,大多數在 React 裡面的東西是 immutable 的, 也就是說,你最好不要直接異動他,而是直接生成一個新的, 我印象中你直接改 React Element 是會直接拋錯給你, 所以要馬你就直接做一個新的,或是透過一些 API 像是 Children.map 來處理。 ### JSX 這邊在順帶提到, 如果你是新人請注意就是,你在 React JS 裏面寫的 HTML 程式碼, 其實只是一個 syntax sugar,他會在 compile 的時候,全部轉換成一般的 JS Code, 就是轉換成,`React.createElement`, 而 `createElement` 函式回傳的其實就是 前面提到的 React Element。 ### Entry Point 每個 Renderer 都會有一個 Entry point, 像是 ReactDOM 的話,就是 `ReactDOM.render`。 當你呼叫 `ReactDOM.render(reactElement, domContainer)` , 你實際上是說:“嘿 React,可以幫我 container 弄的跟我的給你的 react Element 一樣嗎”。 然後 React 就會對照你給他的 element type, 去生成相對應的宿主實例, 像是我給他的 element type 是 button, 那他就會幫我 call `document.createElement("button")`, 然後把我們綁在 element 上的 props assign 到那個 DOM Node 上。 然後他會再一次渲染內,把我們給他的整個樹狀結構全部走過一遍, 這樣就可以產生相對應的 DOM Tree。 最近 18 版的,是把 `ReactDOM.render` 改成 `createRoot().render`,不過今天就不談論 18 太細。 ## Reconciliation 然後接下來我們要談到, React 其中一個核心算法,叫 `Reconciliation`, 基本上他就是一個 diff 算法, 用來比對兩個樹狀結構的差異。 如果他今天比對到的節點是一樣的, 那他就會重用那個節點, 而不是重新建立它。 所以範例中提到,我們 call 了 `render` 兩次會發生什麼事? 這邊直接給結論,他不會建立第一次之後,再把它清掉,再重新建立一次, 因為我們兩次給的結構是一樣的, 所以他就只會單純的把 button 的 class blue 改成 class red, 比較詳細可以再看文章。 這邊要在提醒到的是, 我這邊提的是 React 對 host instance 的操作, 他會重用的是 host instance, 並不是他會重用你的 React Element, 這邊要搞清楚,那是兩個不同的階段,發生在完全不一樣的地方。 ## 條件渲染 然後再來看到條件渲染, 以這個範例來說, 我們第一次是一個 `input`, 第二次是 `input` 前面多了一個 `paragraph`, 我們一樣直接看結論的部分。 在下面提到,我們的 React 程式碼通常是會用函式把它拆成好幾個元件, 然後這些元件輸出的 React Element 並不會異動到他的結構跟順序, 當然這取決於你的程式碼是怎麼寫的, 如果你是直接用 early return 不同結構的話, 他的回傳結構當然還是會改變,所以要注意一下。 因為 React 會走訪整個 element tree,比較前後版本: 所以他就會沿著我們給的結構, 然後他就會開始比對你的 element type,注意是比對 element type: dialog → dialog: 可以重用宿主實例嗎?可以 — type 有對上。 (null) → p: 必須插入新的 p 宿主實例。 input → input: 可以重用宿主實例嗎?可以 — type 有對上。 所以你的 input 不會被重新建立, 也不會遺失像是 selection, focus 之類的狀態。 這邊就是屬於很小的細節,但卻可以帶來很大的優化, 如果你的 input 會因為 rerender 然後遺失掉剛才提到的那些狀態, 可能表示你的寫法上有些問題, 可能回過頭來看這篇對你應該會有些幫助。 ## 列表 接下來是列表的部分,也是直接講結論。 假設妳今天重新排序了 array, React 其實並不知道你的 array 只是順序被改變了, 所以他會全部跑一遍 assign, 如果是小筆資料是沒什麼差,但假如是大筆資料就會有效能問題。 這就是為什麼 React 在你輸出 array 的時候, 會要求你要給 `key`。 因為他就可以透過 `key` 下去判斷,兩次渲染 `key` 是不是一樣, 如果 `key` 一樣的話,他就會重用那個宿主實例,然後重新排位置而已, 但要注意,`key` 的作用域只在同一層父元素下才有用, 他沒有辦法跨不同父元素做 `key` 的比對。 ## 問問題