尝鲜用 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 天写文章的水文写完了。之后若是有机会再深刻尝试。

相关文章
相关标签/搜索