首發於我的 Blog
閱讀推薦:本人需要您有一定的 React 基礎,並且想簡單瞭解一下 Hook 的工作方式和注意點。但是並不詳細介紹 React Hook,如果想有進一步的瞭解,可以查看官方文檔。因為項目比較簡單,所以我會比較詳細的寫出大部分代碼。建議閱讀文章之前請先閱讀目錄找到您關注的章節。
React Hook + Parcel
幾天前,我女票和我說他們新人培訓需要一個《真心話大冒險》的界面,想讓我幫她寫一個。我說好呀,正好想到最近的 React Hook 還沒有玩過,趕緊來試試,於是花了一個晚上的時間,其實是倆小時,一個小時搭建項目,一個小時寫。
Demo: souche-truth-or-dare.surge.sh (因為女票是大搜車的)

環境搭建
首先我們創建一個文件夾,做好初始化操作。
mkdir truth-or-dare
cd truth-or-dare
npm init -y
安裝好依賴,[email protected]
[email protected]
parcel-bundler
[email protected]
[email protected]
[email protected]
。
React Hook 截止發稿前(2018-12-26)還處於測試階段,需要使用
next
版本。
emotion
是一個比較完備的 css-in-js 的解決方案,對於我們這個項目來講是非常方便合適的。另外因為 [email protected] 的最新版本對parcel
還有一定的兼容性問題,見 issue。所以這裡暫時使用[email protected]
的舊版本。
npm i [email protected] [email protected] [email protected] [email protected]
npm i parcel-bundler [email protected] -D
創建 .babelrc
文件或者在 package.json
中寫入 Babel 配置:
{
"plugin": [
["emotion", {"sourceMap": true}]
]
}
創建 src
文件夾,並創建 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>真心話大冒險</title>
</head>
<body>
<div id="app"></div>
<script src="./index.jsx"></script>
</body>
</html>
和 index.jsx
文件
import * as React from 'react'
import { render } from 'react-dom'
render(<div>First Render</div>, document.getElementById('app'))
最後添加如下 scripts
到 package.json
中
{
"start": "parcel serve src/index.html",
"build": "rm -rf ./dist && parcel build src/index.html"
}
最後我們就可以 npm start
就可以成功啟動開發服務器了。在瀏覽器中打開 localhost:1234
即可。
parcel
已經內建了 Hot Reload,所以不需要進行額外的配置,開箱即用。是不是覺得非常簡單,有了它,手動搭建項目不再困難。當然了,TS 也是開箱即用的,不過這次我這個項目真的很小,就不用 TS 了。
useState 第一個接觸的 Hook
我們創建一個 App.jsx
開始我們真正的編碼。先簡單來看一下
export default function App() {
const [selected, setSelected] = useState('*')
const [started, setStarted] = useState(false)
return (
<div>
<div>{selected}</div>
<button>{started ? '結束' : '開始'}</button>
</div>
)
}
我們就完成了對 Hook 最簡單的使用,當然了現在還沒有任何交互效果,也許你並不明白這段代碼有任何用處。
簡單講解一下 useState,這個函數接受一個參數,為初始值,可以是任意類型。它會返回一個 [any, (v: any) => void]
的元組。其中第一個 State 的值,另一個是一個 Setter,用於對 State 設置值。
這個 Setter 我們如何使用呢?只需要在需要的地方調用他就可以了。
<button onClick={() => setStarted(!started)}>{started ? '結束' : '開始'}</button>
保存,去頁面點擊一下這個按鈕看看,是不是發現他會在 結束
和 開始
之間切換?Setter 就是這麼用,非常簡單,如果用傳統的 Class Component 來理解的話,就是調用了 this.setState({started: !this.state.started})
。不過和 setState 不同的是,Hook 裡面的所有數據比較都是 ===
(嚴格等於)。
useState 還有很多用法,比如說 Setter 支持接收一個函數,用於傳入之前的值以及返回更新之後的值。
useEffect 監聽開始和結束事件
接下來,我們想要點擊開始之後,屏幕上一直滾動,直到我點擊結束。
如果這個需求使用 Class Component 來實現的話,是這樣的:
- 監聽按鈕點擊事件
- 判斷是開始還是結束
- 如果是開始,那麼就創建一個定時器,定時從數據當中隨機獲取一條真心話或大冒險並更新
selected
- 如果是結束,那麼就刪除之前設置的定時器
- 如果是開始,那麼就創建一個定時器,定時從數據當中隨機獲取一條真心話或大冒險並更新
非常直接,簡單粗暴。
用了 Hook 之後,當然也可以這樣做了,不過你還需要額外引入一個 State 來存儲 timer,因為函數組件無法持有變量。但是如果我們換一種思路:
- 監聽
started
變化- 如果是開始,那麼創建一個定時器,做更新操作
- 如果是結束,那麼刪除定時器
好像突然變簡單了,讓我們想象這個用 Class Component 怎麼實現呢?
export default class App extends React.Component {
componentDidUpdate(_, preState) {
if (this.state.started !== preState.started) {
if (this.state.started) {
this.timer = setInterval(/* blahblah*/)
} else {
clearInterval(this.timer)
}
}
}
render() {
// blahblah
}
}
好麻煩,而且邏輯比較繞,而且如果 componentDidUpdate 與 render 之間有非常多的代碼的時候,就更難對代碼進行分析和閱讀了,如果你後面維護這樣的代碼,你會哭的。可是用 useEffect Hook 就不一樣了。畫風如下:
export default function App() {
// 之前的代碼
// 當 started 變化的時候,調用傳進去的回調
useEffect(() => {
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne())
}, 60)
return () => clearInterval(timer)
}
}, [started])
return (
// 返回的 View
)
}
當用了 React Hook 之後,所有的邏輯都在一起了,代碼清晰且便於閱讀。
useEffect 從字面意義上來講,就是可能會產生影響的一部分代碼,有些地方也說做成副作用,其實都是沒有問題的。但是副作用會個人一種感覺就是這段代碼是主動執行的而不是被動執行的,不太好理解。我覺得更好的解釋就是受到環境(State)變化影響而執行的代碼。
為什麼這麼理解呢?你可以看到 useEffect 還有第二個參數,是一個數組,React 會檢查這個數組這次渲染調用和上次渲染調用(因為一個組件內可能會有多次 useEffect 調用,所以這裡加入了渲染限定詞)裡面的每一項和之前的是否變化,如果有一項發生了變化,那麼就調用回調。
當理解了這個流程之後,或許你就能理解為什麼我這麼說。
當然了,第二個參數是可以省略的,省略之後就相當於默認監聽了全部的 State。(現在你可以這麼理解,但是當你進一步深入之後,你會發現不僅僅有 State,還有 Context 以及一些其他可能觸發狀態變化的 Hook,本文不再深入探究)
到現在,我們再來回顧一下關於定時器的流程,先看一下代碼:
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne())
}, 60)
return () => clearInterval(timer)
}
理想的流程是這樣的:
- 如果開始,那麼註冊定時器。——Done!
- 如果是結束,那麼取消定時器。——Where?
咦,else
的分支去哪裡了?為啥在第一個分支返回了取消定時器的函數?
這就牽扯到 useEffect 的第二個特性了,他不僅僅支持做正向處理,也支持做反向清除工作。你可以返回一個函數作為清理函數,當 effect 被調用的時候,他會先調用上次 effect 返回的清除函數(可以理解成析構),然後再調用這次的 effect 函數。
於是我們輕鬆利用這個特性,可以在只有一條分支的情況下實現原先需要兩條分支的功能。
其他 Hook
在 Hook 中,上面兩個是使用非常頻繁的,當然還有其他的比如說 useContext
/useReducer
/useCallback
/useMemo
/useRef
/useImperativeMethods
/useLayoutEffect
。
你可以創建自己的 Hook,在這裡 React 遵循了一個約定,就是所有的 Hook 都要以 use
開頭。為了 ESLint 可以更好對代碼進行 lint。
這些都屬於高級使用,感興趣的可以去研究一下,本片文章只是入門,不再過多講解。
我們來用 Emotion 加點樣式
css-in-js 大法好,來一頓 Duang, Duang, Duang 的特技就好了,代碼略過。
收尾
重新修改 src/index.jsx
文件,將 <div/>
修改為 <App/>
即可。
最後的 src/App.jsx
文件如下:
import React, { useState, useEffect } from 'react'
import styled from 'react-emotion'
const lists = [
'說出自己的5個缺點',
'繞場兩週',
'拍一張自拍放實習生群裡',
'成功3個你說我猜',
'記住10個在場小夥伴的名字',
'大聲說出自己的名字“我是xxx”3遍',
'拍兩張自拍放實習生群裡',
'選擇另一位小夥伴繼續遊戲',
'直接通過',
'介紹左右兩個小夥伴',
]
function chooseOne(selected) {
let n = ''
do {
n = lists[Math.floor(Math.random() * lists.length)]
} while( n === selected)
return n
}
const Root = styled.div`
background: #FF4C19;
height: 100vh;
width: 100vw;
text-align: center;
`
const Title = styled.div`
height: 50%;
font-size: 18vh;
text-align: center;
color: white;
padding: 0 10vw;
font-family:"Microsoft YaHei",Arial,Helvetica,sans-serif,"宋體";
`
const Button = styled.button`
outline: none;
border: 2px solid white;
border-radius: 100px;
min-width: 120px;
width: 30%;
text-align: center;
font-size: 12vh;
line-height: 20vh;
margin-top: 15vh;
color: #FF4C19;
cursor: pointer;
`
export default function App() {
const [selected, setSelected] = useState('-')
const [started, setStarted] = useState(false)
function onClick() {
setStarted(!started)
}
useEffect(() => {
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne(selected))
}, 60)
return () => clearInterval(timer)
}
}, [started])
return (
<Root>
<Title>{selected}</Title>
<Button onClick={onClick}>{started ? '結束' : '開始'}</Button>
</Root>
)
}
總結覆盤 —— 性能問題?
最近剛剛轉正答辯,突然發現覆盤這個詞還挺好用的,哈哈哈。
雖然這麼短時間的使用,還是有一些自己的思考,說出來供大家參考一下。
如果你仔細思考一下會發現,當使用 useEffect 的時候,其實每次都是創建了一個新的函數,但並不是說每次都會調用這個函數。如果你代碼裡面 useEffect 使用的很多,而且代碼還比較長,每次渲染都會帶來比較大的性能問題。
所以解決這個問題有兩個思路:
不要在 Hook 中做太多的邏輯,比如說可以讓 Hook 編寫一些簡單的展示組件,比如 Tag/Button/Loading 等,邏輯不復雜,代碼量小,通過 Hook 寫在一起可以降低整個組件的複雜度。
將 Effect 拆分出去,並通過參數傳入。類似於這個樣子
function someEffect(var1, var2) { // doSomething } export function App() { // useState... useEffect(() => someEffect(var1, var2), [someVar]) // return .... }
雖然這也是創建了一個函數,但是這個函數創建的速度和創建一個幾十行幾百行的邏輯的函數相比,確實快了不少。其次不建議使用
.bind
方法,他的執行效率並沒有這種函數字面量快。這種方式不建議手動來做,可以交給 babel 插件做這部分的優化工作。
其實作為一個開發者來說,不應該太多的關注這部分,但是性能就是程序員的 XX 點,我還是會下意識從性能的角度來思考。這裡只是提出了一點小小的優化方向,希望以後 React 官方也可以進一步做這部分的優化工作。
已經有的優化方案,可以查看官方 FAQ
總結
經過這個簡短的使用,感覺用了 Hook 你可以將更多的精力放在邏輯的編寫上,而不是數據流的流動上。對於一些輕組件來說簡直是再合適不過了,希望早點能夠正式發佈正式使用上吧。
另外 parcel
提供了強大的內置功能,讓我們有著堪比 webpack
的靈活度卻有著比 webpack
高效的開發速度。
好的,一篇 1 小時寫代碼,1 天寫文章的水文寫完了。以後如果有機會再深入嘗試。