嚐鮮用 React Hook + Parcel 構建真心話大冒險簡單頁面

首發於個人 Blogcss

閱讀推薦:本人須要您有必定的 React 基礎,而且想簡單瞭解一下 Hook 的工做方式和注意點。可是並不詳細介紹 React Hook,若是想有進一步的瞭解,能夠查看官方文檔。由於項目比較簡單,因此我會比較詳細的寫出大部分代碼。建議閱讀文章以前請先閱讀目錄找到您關注的章節。html

React Hook + Parcel

幾天前,我女票和我說他們新人培訓須要一個《真心話大冒險》的界面,想讓我幫她寫一個。我說好呀,正好想到最近的 React Hook 尚未玩過,趕忙來試試,因而花了一個晚上的時間,實際上是倆小時,一個小時搭建項目,一個小時寫。react

Demo: souche-truth-or-dare.surge.sh (由於女票是大搜車的)webpack

Demo

環境搭建

首先咱們建立一個文件夾,作好初始化操做。git

mkdir truth-or-dare
cd truth-or-dare
npm init -y
複製代碼

安裝好依賴,react@next react-dom@next parcel-bundler emotion@9 react-emotion@9 babel-plugin-emotion@9程序員

React Hook 截止發稿前(2018-12-26)還處於測試階段,須要使用 next 版本。github

emotion 是一個比較完備的 css-in-js 的解決方案,對於咱們這個項目來說是很是方便合適的。另外由於 emotion@10 的最新版本對 parcel 還有必定的兼容性問題,見 issue。因此這裏暫時使用 emotion@9 的舊版本。web

npm i react@next react-dom@next emotion@9 react-emotion@9
npm i parcel-bundler babel-plugin-emotion@9 -D
複製代碼

建立 .babelrc 文件或者在 package.json 中寫入 Babel 配置:npm

{
  "plugin": [
    ["emotion", {"sourceMap": true}]
  ]
}
複製代碼

建立 src 文件夾,並建立 index.htmljson

<!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'))
複製代碼

最後添加以下 scriptspackage.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 來實現的話,是這樣的:

  1. 監聽按鈕點擊事件
  2. 判斷是開始仍是結束
    • 若是是開始,那麼就建立一個定時器,定時從數據當中隨機獲取一條真心話或大冒險並更新 selected
    • 若是是結束,那麼就刪除以前設置的定時器

很是直接,簡單粗暴。

用了 Hook 以後,固然也能夠這樣作了,不過你還須要額外引入一個 State 來存儲 timer,由於函數組件沒法持有變量。可是若是咱們換一種思路:

  1. 監聽 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 使用的不少,並且代碼還比較長,每次渲染都會帶來比較大的性能問題。

因此解決這個問題有兩個思路:

  1. 不要在 Hook 中作太多的邏輯,好比說可讓 Hook 編寫一些簡單的展現組件,好比 Tag/Button/Loading 等,邏輯不復雜,代碼量小,經過 Hook 寫在一塊兒能夠下降整個組件的複雜度。

  2. 將 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 天寫文章的水文寫完了。之後若是有機會再深刻嘗試。

相關文章
相關標籤/搜索