關於我為什麼要寫這篇文章,我有幾句話要講。本身作為一個前端開發人員,我一直有在用 Vue.js Framework
,鑑於其快速構建大型 Web App 的能力,我對其非常喜歡。然而近期我打算進軍 Multi-Platform App 的領域了,這樣其實作為一個 Web 開發人員來講,就沒有多少可以做的選擇了。Vue.js 在跨裝置方面的應用尚且薄弱,而 React 家族的 React Native 和 Google 家的 Flutter 是這方面的絕對主力。
然而,“得益於” Flutter 噁心的樣式互套機制,對開發人員極度不友好,我毫無猶豫地放棄了它。所以,React Native 就成了我唯一的選擇。
所以讀者可以將這篇文章看作是 React Native 的前置知識。
本文的參考來自 React 的官方網站( https://react.dev/ ),感謝社區成員所作出的努力。
React 元件
React 最重要的概念就是“元件”(Component),一個 React 元件就是一個回傳 UI 組合的 JSX 函式。例如:
可以將 React 元件抽離成一個檔案,即所謂的 single-file component
。例如我們可以在 Profile.js
檔案中匯出元件:
然後我們就可以在 Main.js
元件中使用這個元件:
React 元件在使用的時候需以大寫字母開頭,以區別普通的 DOM 元素。例如 <Profile></Profile>
。
JSX
React 使用的是 JS 的語法擴充套件 JSX,這個套件可以允許在 JS 檔案中書寫 HTML 類似的標記。
JSX 有一套嚴格的規則,不同於 HTML,使用時要特別注意:
- 每一個 React 元件的回傳值必須是單根的。即所有元素必須被包含在一個根元素中。React推薦使用
<></>
。
- 所有的標籤必須關閉:使用自閉標籤例如
<img/>
或關閉的標籤<li></li>
。這一點對於有良好開發習慣的開發人員來講不成問題。
使用大括弧 {}
來開啟 JavaScript 程式碼窗
類似於 Vue.js 中的 JavaScript Template Reflect 機制,在 JSX 中,我們也可以在返回的 JSX DOM Tree 中嵌入我們的 JS 程式碼。這時候我們需要使用 {}
來開啟 JS 程式碼窗。
JS 程式碼窗可以用在標籤之間的 HTML Text 上,也可以用在標籤的屬性取值上,就如同上面的例子展示的一樣。
我們還可以在大括弧中傳遞 JS 物件:
我們分析一下 <ul></ul>
這個標籤。其 style
屬性中,第一個大括弧表示開啟 JS 程式碼窗,第二個大括弧則是JS 物件的大括弧。
透過向標籤屬性中傳遞 JS 物件,我們可以實現 CSS 的動態傳遞。
向元件中傳遞 props
Vue.js 中也有 props 傳遞的概念,JSX 類比與那個概念。由於 React 元件屬於 JS 函式,因此,props 可以作為函式的引數來傳遞,這一點確實比 Vue.js 要來得方便。
然後在 main.js
中:
還可以為 props 設定預設值:
props 透傳
如果一個元件希望將自己的所有 props 傳遞給子元件,那麼我們有一個 props 透傳的簡潔語法:
這將把 Profile 元件的所有 props 全部透傳到 Avatar 元件。
props 是不可改變的。當我們要 props 改變的時候,我們其實是需要傳遞一個新的 props,而不是改變舊的。這一點將會在下面講到。
條件渲染
類似於 Vue.js 中的 v-if
和 v-else
。由於 JSX 使用函式渲染的模式,因此我們可以很方便地使用 JS 的 if-else
來進行條件渲染。
可以使用 null
來什麼也渲染空:
列表渲染
類似於 Vue.js 中的 v-for
。官方提供的步驟如下:
- 創建陣列:
- 把陣列 map 到 JSX node 中:
- 在元件中返回 map 的結果:
可以使用 key
屬性在陣列中唯一標示該元素,類似於 Vue.js 中的 :key
。
這個 key 應該是獨一無二的。
事件處理
透過在元件中定義事件處理函式的方法可以實現事件處理:
事件處理函式有幾個注意事項:
- 必須定義在元件中。
- 名稱通常使用
handle
開頭,後接事件名稱。
自然,你也可以直接將函式定義在 JSX node 中:
由於事件處理函式是定義在元件內部的,因此該函式也可以直接使用元件的 props。
客製化事件
在 Vue.js 中,我們可以很方便地客製化事件。使用 defineEmits()
函式配合 emit()
函式,我們可以向父元件丟出一個事件。
在 React 中,這個過程大差不差。 defineEmits()
中的定義被轉成了函式類型的 props,而後直接執行之即可。
事件傳播
我們有這樣一個元件:
我們發現,當我們點按任意一個 Button,不僅會觸發 Button 本身的 onClick()
事件,也觸發了其父 div 的 onClick()
。
要停止事件向其父傳播,我們需要呼叫其預設引數:
與之類似,e.preventDefault()
函式可以避免預設的元素行為。
狀態管理
由於互動的結果,元件通常需要更改螢幕上的內容。在表單中鍵入應更新輸入欄位,單擊影象列上的“Next”應更改顯示的影象,單擊“購買”應將產品放入購物車。元件需要“記住”事情:當前輸入值、當前影象、購物車。在 React 中,這種特定於元件的記憶體被稱為狀態(state)。
在 Vue.js 中,也有類似的概念。例如,當我們需要將某個變數動態渲染到 template 上的時候,我們就需要使用 ref()
或 reactive()
,否則,對變數的任何修改都不會被渲染器意識到。
比如在下面的程式中,點按 Change Index 按鈕就不會發生任何渲染改變:
在 React 中也是如此,不同的是,在 React 中,我們使用的是 useState()
函式。它的使用過程如下:
我們可以看到,原本的變數被變成了一個變數陣列,其中 index
是原來的變數,而配套的 setIndex
則是變數的 setter。
在 React 中,useState 以及以 “use” 開頭的任何其他函式都被稱為 Hook。Hook 是特殊函式,僅在 React 渲染時可用。
與 Vue.js 類似,React 中狀態也是元件依賴的。也就是說,如果你把一個元件渲染兩次,這兩個元件將會有完全不互通的各自狀態。即狀態是“私有的”。
狀態批次處理
我們來看這樣一個例子:
你可能會期望,當我點按 “+3” 按鈕的時候,number 能夠連續相加 3 次,畢竟我使用了三次 setNumber()
函式。
然而你會發現,每點按一次,number 僅 +1。這是因為,在每一輪渲染之前,number 的值都是固定的,因此無論在本輪渲染中呼叫了多少次 setNumber()
函式,其都是一樣的效果。
React 的狀態批次處理機制是:每一輪渲染之前,都要將所有的狀態更新乃至於所有的事件處理函式全部執行完成,才會開始這一輪渲染。所以會出現上面的那個問題。
儘管像上面那樣子寫程式不是一個好主意,上面的需求也算是相當小眾,然而 React 仍舊給我們提供了解決的方案,只需要將 setNumber(number + 1)
替換成 setNumber(n => n + 1)
即可。這告訴 React,用上一輪更新的狀態結果來做事,而不是使用預設固定值。
這裡的 n => n + 1
函式稱為“更新函式”,通常情況下,使用被更新變數的第一個字母作為該函式的引數。
物件狀態的更新
遵循一個原則:不要修改已經持有的物件(儘管它們是 mutable 的),而是創造一個新的物件去替換原有的物件。
以上的更新適用於物件中的每一個鍵的值都發生變化的情況。如果物件中只有少數的鍵發生變化,每一次都要更新整個物件,這顯然太不合理了。解決這個問題,我們可以使用 ...
來拷貝原來的物件變數值。
好,問題來了,假如我們有一個複雜的 Nested 物件,我們應該如何去更新它?比如這個物件:
如果我想要更新 artwork
中的 city
一欄,按照上面說的,我們自然可以這樣子去更新它:
我們不得不使用了兩遍 ...
。這個還算好,如果遇到更加複雜的 nested 情況,這樣做顯然來得效率低。
隆重介紹我們寫 React Native 第一個必備的外部套件——use-immer
。
使用 use-immer
套件,我們可以反直覺地無痛更新物件。
首先我們安裝 use-immer
套件:
然後我們就可以把 useState()
換成 useImmer()
了:
其中的更新語句:
允許我們直接更新物件中的鍵。這很酷,對吧?
陣列狀態的更新
陣列可以被看作特殊的物件,因此對於物件的限制也同樣適用於陣列。我們也依然需要將陣列看作 immutable,儘管其依然是 mutable 的。
對於陣列,我們可以使用一些更加簡單的方式去生成新的陣列。
向陣列中新增元素
push()
方法是 mutable 方法,要實現這個內容,需要:
刪除陣列中的元素
可以曲線救國,使用 filter()
函式篩選出非刪除元素組成新陣列。
修改陣列
使用 map()
函式來客製化修改規則,生成新的陣列。
陣列中插入元素
配合 slice()
函式來使用:
其他修改
其實,對於陣列的修改還有一個最簡單不過的方式:直接拷貝原來的陣列到一個新陣列,然後修改新陣列即可。
然而這樣做對於陣列中的物件來說是一個問題。因為這樣的拷貝很淺,無法將物件的邏輯拷貝到新陣列。因為陣列中的物件實際上是一個指向,而不是一個元素。所以當我們修改新陣列中的物件內容,事實上是在修改原陣列,這是應當被避免的。
可以這樣做:
或者你會更喜歡使用 immer:
狀態管理的高級玩法
對於 React 狀態管理和宣告式程式設計的思考
初學者對於 React 的狀態管理可能會感到非常混亂和迷惑,但是如果習慣起來,會發現 React 的狀態管理功能十分強大。
按照 React 官方的說法,其狀態管理屬於“宣告式程式設計”的典型應用。
說到宣告式,我上次接觸的以它為特點的東西還叫做 NixOS,那個 Linux 發行版給我留下了極其深刻的心理陰影。雖然我承認其一些設計理念確實是超前的且無可比擬的,比如說宣告式設計。然而由於其軟體的缺失和 pack 的過程之複雜,實在是讓我記憶深刻,並且終生不想再碰它。
跑遠了~回到宣告式程式設計這件事上來。什麼叫宣告式呢?React 官方給了一個十分形象的例子:
假如你叫了一輛計程車,想要去到某個地方,使用傳統的命令式程式設計,你就需要一步一步指導司機走哪條路,最後到達你想要到達的地方。如果其中一步錯誤,可能最後就無法到達正確的目的地。
但是宣告式程式設計則不一樣。其相當於你直接告訴司機你要去到哪個地方,然後司機會幫你完成路線規劃,最後去到那裏。
相當於,你只需要宣告結果,過程不需要去管。這在一些大型的程式系統中會顯著提高開發和維護的效率。
回到 React 上來,你只需要宣告頁面的“狀態”,React 就能夠幫助你達到那個狀態,而不需要你親自去操縱頁面上的元件之類的。
事實上,Vue.js 所基於的 template 系統也是一樣。template 預先聲明了頁面的元素,然後我們可以修改 template 的狀態來實現頁面的更新。
在元件之間共享狀態
有的時候我們希望兩個元件的狀態可以同步改變,這時候我們需要進行“提升狀態”作業。
提升狀態,是指將狀態從元件中刪除掉,轉而將其放到共同的最近父元件中的過程。
我們來看這樣一個例子:
顯然,這兩個 Panel 的 isActive
狀態是獨立的。
如果我希望讓它們兩個能夠同時改變,即要展示就共同展示,要隱藏就共同隱藏。這該如何做呢?
- 從子元件中刪除狀態。
- 從父元件中向子元件傳遞硬編碼資料。
- 將狀態給到父元件。
例如上述例子就可以如下修改:
提升狀態的過程本質上是父元件統一管理子元件的過程。
保留和重置狀態
只要元件在 UI tree 的相同地方被渲染,React就會保留其狀態,而當其被移除,或者其他元件代替了它在 UI tree 中的位置,它的狀態就會被丟棄。
相同位置的相同類型元件保留狀態
有如下一個例子:
你會發現當你按下 Click me. 按鈕的時候,即使 Children1 變成了 Children2,元件裡面的狀態 score 也不會改變。這是因為兩個 Test 元件是 UI tree 相同位置的相同類型元件,因此會被 React 當作相同元件,狀態得到保留,
同一位置的不同類型元件重置狀態
根據上面的例子,我們應該十分容易理解它,比如同一位置,把 Test 元件換成其他的什麼元件,再換回來,Test 元件的 score 狀態自然就被重置了。
在同一位置重置狀態
上面我們講到,元件遵循的是在同一位置的相同類型元件將會保留狀態。然而如果我們希望在同一位置的相同元件強制重置狀態,我們該如何做呢?
我們有兩種方法:
- 在不同的位置渲染。
- 使用 key。
我們來看第一種。還是用上面的那個例子。將:
修改成:
即可。
另一種方式是使用 key。key 可以讓 React 區分不同的元件。只需要給到兩個 <Test></Test>
元件兩個不同的 key 即可。
使用 Reducer
當我們的狀態處理邏輯越來越多的時候,難免會顯得非常混亂。這個時候,我們可以將狀態處理邏輯統一提取到元件外面的 reducer 函式中,將針對某一個狀態變數的全部處理邏輯都放到這裡面。然後在元件內部,只需要使用這個 reducer 函式來分發相應的處理邏輯就好。
首先我們需要一個 reducer 函式,它接受兩個引數:state 和 action,state 是要監控的狀態變數,action 是執行的作業。該函式回傳更新後的狀態。
比如我們編寫這樣一個函式:
在元件中使用 reducer:
useReducer()
函式接受兩個引數:taskReducer
是在元件外定義的 reducer 函式,initialTasks 是預設的狀態值。
當需要更新元件的狀態時,我們只需要使用 dispatch()
函式:
傳送給 dispatch()
函式的引數是一個物件,這個物件最後會變成 taskReducer()
函式的第二個引數 action
。
我們還可以使用 immer 來提升我們使用 Reducer 的體驗。(immer 真是個偉大的發明)
使用 immer,我們就可以使用 push
或 array[index] =
這種形式來修改我們的 state。
只需要將 useReducer()
換成 userImmerReducer()
即可,然後你就可以在你的 reducer 函式中大膽使用上面提到的修改方法了。
使用 context 深入傳遞資料
這個場景在客製化元件庫的時候特別有用。在之前使用 Vue.js 開發元件庫的時候,我總會遇到這樣一個問題:客製化的 radio-box 元件需要監聽其父級 radio-box-group 元件的屬性,來確定自己是不是已經被選中了。
context 就可以做到這一點。通俗講,context 可以讓父級(不管多遙遠)的元件可以跨越千山萬水,為子級元件提供一些資料。
我們就用這個例子來做。現在我有一個 RadioBoxGroup 元件,其有一個屬性是 check,類型是 integer,代表選中的 RadioBox 元件的 id。其 children 為 RadioBox 元件。要求 RadioBox 元件能夠根據其父級的 check,自動獲悉自己是否被選中。
我們首先建立這兩個元件,我分為兩個檔案:
然後再建立一個測試元件:
接下來我們就要開始實現上述目的了。
首先我們需要創建一個 context,我們重新建立一個新的檔案用於專門存放 context:
createContext()
函式唯一的引數,表示預設的 context 值。
然後,我們在 RadioBoxGroup 元件中提供這個 context:
這一步是告訴 React:如果我的子元件索要context,請將這個 context 提供給它。 React 將會把距離子元件最近的 context 提供給它。
而後,子元件就可以向父元件索要 context 了:
完成了。接下來,就只要在 App
元件中為 RadioBoxGroup 元件設定 check 屬性即可。
結語
暫時先寫這麼多吧,也很多了。應當注意的是,這些內容僅僅在 React 官方檔案中屬於“中級”,而更高級的功能我暫時用不上,於是就暫時先寫到這裡。以後用到了,以後再說。
另外要再次提醒讀者,React 是一個函式庫,而並非是一個框架。基於 React 的框架,請移步目前最流行的 Next.js。