從同一功能的八種實現,談談react中的邏輯複用進化過程

NO IMAGE

需求:我們現在有一個獲取驗證碼的按鈕,需要在點擊後禁用,並且在按鈕上顯示倒計時60秒才可以進行第二次點擊。

本篇文章通過對這個需求的八種實現方式來討論在 react 中的邏輯複用的進化過程

從同一功能的八種實現,談談react中的邏輯複用進化過程

代碼例子放在了 codesandbox 上。

方案一 使用 setInterval

import React from 'react'
export default class LoadingButtonInterval extends React.Component {
state = {
loading: false,
btnText: '獲取驗證碼',
totalSecond: 10
}
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearInterval(this.timer)
this.setState({
loading: false,
totalSecond: 10
})
}
setTime = () => {
this.timer = setInterval(() => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState(() => ({
totalSecond: totalSecond - 1
}))
}, 1000)
}
onFetch = () => {
this.setState(() => ({ loading: true }))
const { totalSecond } = this.state
this.setState(() => ({
totalSecond: totalSecond - 1
}))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
)
}
}

方案二 使用 setTimeout

import React from 'react'
export default class LoadingButton extends React.Component {
state = {
loading: false,
btnText: '獲取驗證碼',
totalSecond: 60
}
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)
this.setState({
loading: false,
totalSecond: 60
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
)
}
}

我們可能很快就寫出來兩個這樣的組件。使用 setTimeout 還是 setInterval 區別不是特別大。 但是我會更推薦 setTimeout 因為 萬物皆遞歸(逃)

不過,又有更高的要求了。可以看到剛剛我們的獲取驗證碼。如果說再有一個頁面有相同的需求,只能將組件完全再拷貝一遍。這肯定不合適嘛。

那咋辦嘛?

方案三 參數提取到 Props 1

import React from "react";
class LoadingButtonProps extends React.Component {
constructor(props) {
super(props);
this.initState = {
loading: false,
btnText: this.props.btnText || "獲取驗證碼",
totalSecond: this.props.totalSecond || 60
};
this.state = { ...this.initState };
}
timer = null;
componentWillUnmount() {
this.clear();
}
clear = () => {
clearTimeout(this.timer);
this.setState({
...this.initState
});
};
setTime = () => {
const { totalSecond } = this.state;
if (totalSecond <= 0) {
this.clear();
return;
}
this.setState({
totalSecond: totalSecond - 1
});
this.timer = setTimeout(() => {
this.setTime();
}, 1000);
};
onFetch = () => {
const { loading } = this.state;
if (loading) return;
this.setState(() => ({ loading: true }));
this.setTime();
};
render() {
const { loading, btnText, totalSecond } = this.state;
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
);
}
}
class LoadingButtonProps1 extends React.Component {
render() {
return <LoadingButtonProps btnText={"獲取驗證碼1"} totalSecond={10} />;
}
}
class LoadingButtonProps2 extends React.Component {
render() {
return <LoadingButtonProps btnText={"獲取驗證碼2"} totalSecond={20} />;
}
}
export default () => (
<div>
<LoadingButtonProps1 />
<LoadingButtonProps2 />
</div>
);

對於上面的需求,不就是複用嘛,看我 props 提取到公共父組件一把梭搞定!

想想好像還挺美的。。

結果這時候需求變更來了:

第一點:兩個地方獲取驗證碼的api不一樣。第二點:我需要在獲取驗證碼之前做一些別的事情

撓了撓頭,那咋辦嘛?

方案四 參數提取到 Props 2

import React from 'react'
class LoadingButtonProps extends React.Component {
// static defaultProps = {
//   loading: false,
//   btnText: '獲取驗證碼',
//   totalSecond: 10,
//   onStart: () => {},
//   onTimeChange: () => {},
//   onReset: () => {}
// }
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)
this.props.onReset()
}
setTime = () => {
const { totalSecond } = this.props
console.error(totalSecond)
if (this.props.totalSecond <= 0) {
this.clear()
return
}
this.props.onTimeChange()
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
if (this.loading) return
this.setTime()
this.props.onStart()
}
render() {
return <div onClick={this.onFetch}>{this.props.children}</div>
}
}
class LoadingButtonProps1 extends React.Component {
totalSecond = 10
state = {
loading: false,
btnText: '獲取驗證碼1',
totalSecond: this.totalSecond
}
onTimeChange = () => {
const { totalSecond } = this.state
this.setState(() => ({ totalSecond: totalSecond - 1 }))
}
onReset = () => {
this.setState({
loading: false,
totalSecond: this.totalSecond
})
}
onStart = () => {
this.setState(() => ({ loading: true }))
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<LoadingButtonProps
loading={loading}
totalSecond={totalSecond}
onStart={this.onStart}
onTimeChange={this.onTimeChange}
onReset={this.onReset}
>
<button disabled={loading}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
</LoadingButtonProps>
)
}
}
class LoadingButtonProps2 extends React.Component {
totalSecond = 15
state = {
loading: false,
btnText: '獲取驗證碼2',
totalSecond: this.totalSecond
}
onTimeChange = () => {
const { totalSecond } = this.state
this.setState(() => ({ totalSecond: totalSecond - 1 }))
}
onReset = () => {
this.setState({
loading: false,
totalSecond: this.totalSecond
})
}
onStart = () => {
this.setState(() => ({ loading: true }))
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<LoadingButtonProps
loading={loading}
totalSecond={totalSecond}
onStart={this.onStart}
onTimeChange={this.onTimeChange}
onReset={this.onReset}
>
<button disabled={loading}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
</LoadingButtonProps>
)
}
}
export default () => (
<div>
<LoadingButtonProps1 />
<LoadingButtonProps2 />
</div>
)

嗯?等等。。所以說這樣的操作只共用了時間遞歸減少的部分吧?好像重複代碼有點多哇,感覺和老版本也沒什麼太大的區別嘛。

那咋辦嘛?

方案五 試試 HOC

import React from 'react'
function loadingButtonHoc(WrappedComponent, initState) {
return class extends React.Component {
constructor(props) {
super(props)
this.initState = initState || {
loading: false,
btnText: '獲取驗證碼',
totalSecond: 60
}
this.state = { ...this.initState }
}
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)
this.setState({
...this.initState
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
const { loading } = this.state
if (loading) return
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<WrappedComponent
{...this.props}
onClick={this.onFetch}
loading={loading}
btnText={btnText}
totalSecond={totalSecond}
/>
)
}
}
}
class LoadingButtonHocComponent extends React.Component {
render() {
const { loading, btnText, totalSecond, onClick } = this.props
return (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
)
}
}
const LoadingButtonHocComponent1 = loadingButtonHoc(LoadingButtonHocComponent, {
loading: false,
btnText: '獲取驗證碼Hoc1',
totalSecond: 20
})
const LoadingButtonHocComponent2 = loadingButtonHoc(LoadingButtonHocComponent, {
loading: false,
btnText: '獲取驗證碼Hoc2',
totalSecond: 12
})
export default () => (
<div>
<LoadingButtonHocComponent1 />
<LoadingButtonHocComponent2 />
</div>
)

我們使用 高階組件再次重寫了整個邏輯。好像基本上需求都滿足了?

這個地方思路在於,將 onClick 或者叫做 onStart 事件暴露出來了,最終的執行,

都是由外部組件自行決定執行時機,那麼其實不管怎麼搞都可以了

方案六 renderProps

import React from 'react'
class LoadingButtonRenderProps extends React.Component {
constructor(props) {
super(props)
this.initState = {
loading: false,
btnText: this.props.btnText || '獲取驗證碼',
totalSecond: this.props.totalSecond || 60
}
this.state = { ...this.initState }
}
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)
this.setState({
...this.initState
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
const { loading } = this.state
if (loading) return
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return this.props.children({
onClick: this.onFetch,
loading: loading,
btnText: btnText,
totalSecond: totalSecond
})
}
}
class LoadingButtonRenderProps1 extends React.Component {
render() {
return (
<LoadingButtonRenderProps btnText={'獲取驗證碼RP1'} totalSecond={15}>
{({ loading, btnText, totalSecond, onClick }) => (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
)}
</LoadingButtonRenderProps>
)
}
}
class LoadingButtonRenderProps2 extends React.Component {
render() {
return (
<LoadingButtonRenderProps btnText={'獲取驗證碼RP1'} totalSecond={8}>
{({ loading, btnText, totalSecond, onClick }) => (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
)}
</LoadingButtonRenderProps>
)
}
}
export default () => (
<div>
<LoadingButtonRenderProps1 />
<LoadingButtonRenderProps2 />
</div>
)

嘿嘿,我們使用了 render Props 重寫了在 Hoc 上實現的功能。個人角度看,其實比Hoc 會簡潔也優雅很多!

方案七 React Hooks

import React, { useState, useEffect, useRef, useCallback } from 'react'
function LoadingButtonHooks(props) {
const timeRef = useRef(null)
const [loading, setLoading] = useState(props.loading)
const [btnText, setBtnText] = useState(props.btnText)
const [totalSecond, setTotalSecond] = useState(props.totalSecond)
const countRef = useRef(totalSecond)
const clear = useCallback(() => {
clearTimeout(timeRef.current)
setLoading(false)
setTotalSecond(props.totalSecond)
countRef.current = props.totalSecond
})
const setTime = useCallback(() => {
if (countRef.current <= 0) {
clear()
return
}
countRef.current = countRef.current - 1
setTotalSecond(countRef.current)
timeRef.current = setTimeout(() => {
setTime()
}, 1000)
})
const onStart = useCallback(() => {
if (loading) return
countRef.current = totalSecond
setLoading(true)
setTime()
})
useEffect(() => {
return () => {
clearTimeout(timeRef.current)
}
}, [])
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
)
}
LoadingButtonHooks.defaultProps = {
loading: false,
btnText: '獲取驗證碼',
totalSecond: 10
}
export default () => (
<div>
<LoadingButtonHooks
loading={false}
btnText={'獲取驗證碼hooks1'}
totalSecond={10}
/>
<LoadingButtonHooks
loading={false}
btnText={'獲取驗證碼hooks2'}
totalSecond={11}
/>
</div>
)

我們使用 hooks 重寫了整個程序, 它讓我們把ui和狀態更明確的區分開,也去解決了一些 renderProps 在多層嵌套時的jsx 嵌套地獄問題, 當然個人感覺在這個例子上好像 Hooks 與 renderProps 版本是差別不大的。

方案八 uesHooks

import React, { useState, useEffect, useRef, useCallback } from 'react'
function useLoadingTimer(initState) {
const timeRef = useRef(null)
const [loading, setLoading] = useState(initState.loading)
const [btnText, setBtnText] = useState(initState.btnText)
const [totalSecond, setTotalSecond] = useState(initState.totalSecond)
const countRef = useRef(totalSecond)
const clear = useCallback(() => {
clearTimeout(timeRef.current)
setLoading(false)
setTotalSecond(initState.totalSecond)
countRef.current = initState.totalSecond
})
const setTime = useCallback(() => {
if (countRef.current <= 0) {
clear()
return
}
countRef.current = countRef.current - 1
setTotalSecond(countRef.current)
timeRef.current = setTimeout(() => {
setTime()
}, 1000)
})
const onStart = useCallback(() => {
if (loading) return
countRef.current = totalSecond
setLoading(true)
setTime()
})
useEffect(() => {
return () => {
clearTimeout(timeRef.current)
}
}, [])
return {
onStart,
loading,
totalSecond,
btnText
}
}
const LoadingButtonHooks1 = () => {
const { onStart, loading, totalSecond, btnText } = useLoadingTimer({
loading: false,
btnText: '獲取驗證碼UseHooks1',
totalSecond: 10
})
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
)
}
const LoadingButtonHooks2 = () => {
const { onStart, loading, totalSecond, btnText } = useLoadingTimer({
loading: false,
btnText: '獲取驗證碼UseHooks2',
totalSecond: 10
})
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `請等待${totalSecond}秒..`}
</button>
)
}
export default () => (
<div>
<LoadingButtonHooks1 />
<LoadingButtonHooks2 />
</div>
)

當然,更解耦的做法是,把 hooks 完全獨立的提取出來成 useHooks ,最後我們再編寫組件去組合 uesHooks。

在上述的例子中我們在 react 中用了 8 種 不同的方案,去描述了同一個功能的編寫過程。有一點 “回” 字的多種寫法的意味。不過他也代表著 react 社區在選擇實現上的思想的變化過程,我覺得談不上某一個方案,一定就完全比另外一個好。社區也有比如 HOC vs renderProps 的很多討論。

僅以此希望大家能夠辯證的去看這個過程,也希望能夠在大家編寫 React 組件時帶來更多的新思路。

參考鏈接:

相關文章

http常被問到的知識總結

刪庫了,我們一定要跑路嗎?

Flutter混合開發實戰問題記錄(五)1.9.1hotfix打包aar差異

Flutter完整開發實戰詳解(二十、AndroidPlatformView和鍵盤問題)