React 实现 Vue 中 watch 和 computed

计算属性 computed 和数据监听 watch 两者的区别

  • 计算属性适合做值得转换,基于一个值生成另一个值
  • 数据监听更适合一些动作响应,即监听某个值变化后执行相应动作

React Class Component 实现 Vue 的 computed (计算属性)

对于类组件,直接使用 get 方法即可实现计算属性 computed:
React 中的 getter 并没有做缓存优化,我们需要在项目中引入 memoize-one 库实现缓存效果

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
import React, { Component } from 'react';

export default class demo extends Component {
constructor(props) {
super(props);
this.state = {
value: 1
};
}
// 计算属性
get computedValue1 () {
return this.state.value + 10;
}
// 计算属性之间相互依赖
get computedValue2 () {
return this.computedValue1 * 2;
}
// 点击 + 1
add = () => {
this.setState({
value: this.state.value + 1
})
}
render () {
return (
<div>
<div>原state: {this.state.value}</div>
<div>计算属性1 + 10: {this.computedValue1}</div>
<div>计算属性1 = 计算属性1 * 2: {this.computedValue2}</div>
<button onClick={this.add}>点击加一</button>
</div>
);
}
}

React Class Component 实现 Vue 的 watch (数据监听)

对于类组件,使用生命周期 componentDidUpdate 即可实现数据监听:

componentDidUpdate 在首次渲染时不会执行,在组件更新时立即调用,可以在这时候执行 setState 但需要把 setState 放在条件语句中,否则会陷入更新无限循环。通过这个钩子实现对 state/props/getter 的监听 从而触发相应动作。

依赖全局某些需要异步获取的状态时,通常有两种用途:

  1. 直接渲染。当状态改变时会自动触发重新渲染,直接放在 componentDidMount 中即可

  2. 根据状态的值做相应的动作,如发起接口请求,需要监听此状态,当它有值再执行相应操作

案例:在项目初始化时,获取接口数据 “用户信息”,属于异步操作,之后存储在全局状态 redux 中,某页面通过 connect 获取全局状态的 “用户信息”,并根据该 “用户信息” 请求一个专属于该用户的报告列表并渲染。

分析:“用户信息” 是通过接口获取的,属于异步操作,在报告页面需要监听获取到的 “用户信息”,当它有值时再发起 “用户报告” 的请求,所以不能写在 componentDidMount 中,此时,需要在类组件中使用 componentDidUpdate 监听状态数据:

1
2
3
4
5
componentDidUpdate (prevProps, prevState){ 
if(isEmpty(prevProps.xxx) && !isEmpty(this.props.xxx)) {
// 发起请求
}
}

监听上述计算属性 demo 的 state 变化,在 value 超过 10 时在控制台输出 “超过 10 了”

1
2
3
4
5
6
// 监听 state 变化
componentDidUpdate () {
if(this.state.value > 10) {
console.log('超过10了')
}
}

React Function Component 实现 Vue 的 computed (计算属性)

在函数式组件中,使用 useMemo 这个 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
import React, { useMemo, useState } from 'react'

export default function Demo1 () {
const [count, setCount] = useState(0)

const double = useMemo(() => {
console.log('double')
return count * 2
}, [count])

return (
<div>
<button
onClick={() => {
setCount(count + 1)
}}
>
点击+1
</button>
<div>Count is :{count}</div>
<div>Double is :{double}</div>
</div>
)
}

React Function Component 实现 Vue 的 watch (数据监听)

使用 useEffect 和 useRef 自定义一个叫 useWatch 的 hooks,useEffect 实现基本监听功能,useRef 实现 Vue watch 中的 oldValue/immediate/stop:

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
import React, { useMemo, useState, useEffect, useRef } from 'react'

type Callback<T> = (prev: T | undefined) => void;

type Config = {
immediate: boolean;
};

function useWatch<T>(dep: T, callback: Callback<T>, config: Config = { immediate: false }) {
const { immediate } = config;

// 1.useRef在回调函数中拿到旧值
const prev = useRef<T>();
// 2.在组件初始化的时候不要调用这个callback,还是利用useRef来做,利用一个标志位inited来保存组件是否初始化的标记。并且通过第三个参数config来允许用户改变这个默认行为。
const inited = useRef(false);
// 3.stop: 把控制ref标志的逻辑暴露给外部
const stop = useRef(false);

useEffect(() => {
// 1.这样就在每一次更新prev里保存的值为最新的值之前,先调用callback函数把上一次保留的值给到外部。
const execute = () => callback(prev.current);

if (!stop.current) {
if (!inited.current) {
inited.current = true;
if (immediate) {
execute();
}
} else {
execute();
}
prev.current = dep;
}
}, [dep]);

return () => {
stop.current = true;
};
}

const App: React.FC = () => {
const [prev, setPrev] = useState()
const [count, setCount] = useState(0);

const stop = useWatch(count, (prevCount) => {
console.log('prevCount: ', prevCount);
console.log('currentCount: ', count);
setPrev(prevCount)
})

const add = () => setCount(prevCount => prevCount + 1)

return (
<div>
<p> 当前的count是{count}</p>
<p> 前一次的count是{prev}</p>
{count}
<button onClick={add} className="btn">+</button>
<button onClick={stop} className="btn">停止观察旧值</button>
</div>
)
}

export default App;