惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
C
Check Point Blog
GbyAI
GbyAI
Jina AI
Jina AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 司徒正美
Hacker News - Newest:
Hacker News - Newest: "LLM"
V2EX - 技术
V2EX - 技术
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
F
Full Disclosure
S
Secure Thoughts
WordPress大学
WordPress大学
博客园 - Franky
N
News and Events Feed by Topic
雷峰网
雷峰网
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Hacker News: Front Page
N
Netflix TechBlog - Medium
Forbes - Security
Forbes - Security
TaoSecurity Blog
TaoSecurity Blog
C
CERT Recently Published Vulnerability Notes
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Vercel News
Vercel News
T
The Blog of Author Tim Ferriss
MyScale Blog
MyScale Blog
Security Latest
Security Latest
Attack and Defense Labs
Attack and Defense Labs
The Register - Security
The Register - Security
Spread Privacy
Spread Privacy
H
Help Net Security
宝玉的分享
宝玉的分享
L
LangChain Blog
MongoDB | Blog
MongoDB | Blog
I
Intezer
Schneier on Security
Schneier on Security
Latest news
Latest news
Y
Y Combinator Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
I
InfoQ
A
About on SuperTechFans
T
Tor Project blog
Microsoft Security Blog
Microsoft Security Blog
M
MIT News - Artificial intelligence
Google DeepMind News
Google DeepMind News
Microsoft Azure Blog
Microsoft Azure Blog
T
Threat Research - Cisco Blogs
S
Schneier on Security
Cisco Talos Blog
Cisco Talos Blog
H
Heimdal Security Blog

三省吾身丶丶

在 vue 中使用 jsx 与 class component 的各种姿势 使用 generic-pool 优化 puppeteer 并发问题 再谈中文字体的子集化与动态创建字体 《张乌梅的日记》摘录 一个使用 react 的思想去使用 vue 的方式 React 项目构建 从一个简单的实例看 JavaScript 的异步编程进化历 webpack入坑之旅(零)简介与升级 中文播客推荐 学习 Promise,掌握未来世界 JS 异步编程基础 2017 杭州 nodeParty 记录 一次canvas中文字转化成图片后清晰度丢失的探索 从零学习 canvas (一) 浅谈 electron 中的 session 管理(隔离) FlexBox 布局详解 HTML5常用标签分类 ES6 简单特性学习记录 一张图学习 ES6 中的 React 生命周期与流程 2017,一切才刚刚开始。
从一个简单功能的实现,谈谈 react 中的逻辑复用进化过程
guowenfh · 2019-09-17 · via 三省吾身丶丶

需求:我们现在有一个获取验证码的按钮,需要在点击后禁用,并且在按钮上显示倒计时60秒才可以进行第二次点击。
本篇文章通过对这个需求的八种实现方式来讨论在 react 中的逻辑复用的进化过程

代码例子放在了 codesandbox 上。

方案一 使用 setInterval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import React from 'react'

class LoadingButtonProps extends React.Component {








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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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 组件时带来更多的新思路。

参考链接: