如何理解「副作用」
React 在函数组件中,推出了 hooks,其中非常重要的、平时大家用的也非常多的一个就是 useEffect。很多同学把这个 hooks 用作"数据监听器",在依赖发生改变的时候做一些事情。这是正确的用法嘛?为什么这个 hooks 叫 effect 呢,而不叫 listener 呢?effect 表示什么?这篇文章将给你解答。
副作用
现实意义的「副作用」
生活中我们听到「副作用」最多的场景就是吃药,通常情况下药物都存在「副作用」(也叫做不良反应)。举个例子,头孢用于治疗细菌感染,但是存在腹泻、反胃、皮疹等不良反应。
所以可以理解成,为了达到某个目的,却带来了其他的影响,这个叫做「副作用」
编程中的「副作用」
「副作用」这个词通常出现在函数式编程中,在讲「副作用」前,我们先简单聊聊「函数式编程」。
函数式编程中有一种函数叫做:纯函数:
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
举个例子:
function add(a: number, b: number) {
return a + b
}
这就是一个纯函数,对于任意一个相同的参数 a 和 b,通过这个函数,不管在何时何地输出的结果都是一样的。再举个例子,JS 中经常分不清的两个函数:slice
和 splice
,没错,这两个函数都能实现从一个数组中截取一部分,但是他们的实现却不同。我们可以认为 slice
是一个纯函数,而 splice
不是,因为 splice
改变了原数组,造成了可以观察到的副作用:
var xs = [1,2,3,4,5];
// 纯的
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
// 不纯的
xs.splice(0,3);
//=> [1,2,3]
xs.splice(0,3);
//=> [4,5]
xs.splice(0,3);
//=> []
如果一个函数在执行的过程中,对外部环境造成了影响,称之为产生了「副作用」。通常会产生「副作用」的场景包括但不限于:
- 修改任何外部变量或对象属性(例如,全局变量、静态变量等)
- 修改函数的参数
- 抛出异常
- 打印或记录日志
- 读写文件(IO 操作)
- 数据库操作
- 调用网络请求
概括来说,只要是跟函数外部环境发生的交互就都是副作用。那副作用是坏的嘛?肯定不是!实际的编程场景中,必然会存在以上场景。函数式编程的哲学是假定副作用是造成不正当行为的主要原因,并不代表要禁用一切副作用,而是要让他们在可控的范围内发生。具体如何可控,感兴趣可以阅读「题外话」。
React
useEffect
我们再回过头来看一下
React
的 useEffect,首先,这个问题只存在 Function Component 下(毕竟 Function Component 才有 hooks 嘛),那这不就是函数吗。是函数那就会存在副作用,如何处理副作用呢?useEffect 就是
React
提供给你处理副作用的时机。
我们来看一下 useEffect 的 API 文档:
useEffect is a
React Hook that lets you synchronize a component with an external system.
翻译一下就是我们上面的意思,useEffect 能够让你和外部系统同步,简称处理副作用(所以 useEffect 的作用肯定不是让你当成监听器来使用的)。同时注意点里面也写了:
If you’re not trying to synchronize with some external system, you probably don’t need an Effect. 如果你不是和外部系统同步,你可能不需要 Effect
建议大家仔细阅读「拓展阅读」中的
React
Effect 五连,后续将分享
题外话:函数式编程中如何处理副作用
在函数式编程中,提倡把副作用分离出来,让没有副作用的纯逻辑待在一,这时候我们需要 IO Monad。
首先我们来了解一下什么是 Monad(单子),Monad 用来解决「函数组合」和「异常处理」这两个问题。
假如我们有两个函数 f 和 g,他们的输入和输出都是一个数字(不考虑函数的具体实现,以下只是函数签名):
f::Number -> Number
g::Number -> Number
现在我们希望把他们组合起来,得到一个新函数 h,也是一个输入和输出都是数字的函数:
h::Number -> Number
这个问题很简单,我们只需要构造一个组合函数的工具函数:
let compose = (func1, func2) => x => fun1(func2(x))
let h = compose(g, f)
上面简单解决了函数组合的问题,接下来我们来看看异常处理的问题。在函数式编程中,通常认为异常处理不应该打断逻辑的执行,所以 try-catch 来捕获异常是不可行的。为了能够做到处理异常的同时不打断执行,需要一种「容器」讲函数的结果包装起来。这种容器我们称之为 Maybe
interface Maybe<T> {
error?: boolean
data?: T
}
此时,我们来改造 f 和 g 两个函数,让他们返回 Maybe,同时希望组合后的 mh 函数也是如此
mf::Number -> Maybe Number
mg::Number -> Maybe Number
mh::Number -> Maybe Number
但是我们先前的 compose 方法已经不适用了,因为 mf 的输出为 Maybe Number 而 mg 的输入是 Number。此时我们需要一个新的 mcompose 方法。此时 Maybe 就成为了一个 Monad。
转化成链式调用的形式可能更好理解一点
let mf = x => Maybe.of(x).map(f)
let mg = x => Maybe.of(x).map(g)
let mh = x => Maybe.of(x).chain(mf).chain(mg)
of 把输入一个数字,包装成一个 Maybe 结构
chain 方法通过输入的函数,对自身持有的值进行处理,输出一个持有新的结果的 Maybe 结构
map 用来做具体的值转换
ES 原生的 Monad:Array
let f = x => x + 1
let g = x => x ** 2
let mf = x => Array.of(x).map(f)
let mg = x => Array.of(x).map(g)
let mh = x => Array.of(x).flatMap(mf).flatMap(mg)
同时 Monad 也是流的雏形,各种流式框架的核心都是 Monad,比如 rx 中的 Observable
说完了 Monad 我们再来看下什么是 IO Monad。我们提到副作用是和外部系统的交互,交互无非就是读和写,所以 IO Monad 中的 IO 就是 Input/Output,是包裹对外部世界读写行为的 Monad,他的核心思想就是:
无论遇到什么副作用都不用害怕,把它包在一个函数里晚点处理
举个例子:
function Test() {
const dataStr = localStorage.getItem('key')
const data = JSON.parse(dataStr)
const reviewDate = data.review
const reviewOutput = reviewData.map((text, index) => `${index}: ${text}`)
console.log(reviewOutput)
}
这段代码中,第二行和第六行是具有副作用的,那我们要做的就是把这两段代码用函数包装起来:
const readFronStorage = () => localStorage.getItem('key')
const writeToConsole = console.log
然后把 readFronStorage 用一个 IO Monad 包裹起来
const storageIO = new IO(readFromStorage)
storageIO 包裹的 value 并不是一个从 storage 取出来的具体的值,而是 readFromStorage 这个函数,这个函数作为一个值本身是可控且确定的。接下来把其他的逻辑也包成函数,然后用 map 链式调用组织起来:
const parseJSON = string => JSON.parse(string);
const getReviewProp = data => data.review;
const mapReview = reviewData => reviewData.map((text, index) => `${index}: ${text}`)
const task = storageIO
.map(parseJSON)
.map(getReviewProp)
.map(mapReview)
最后我们执行,IO Monad 特有的 fork 方法订阅了 writeToStorage 函数
task.fork(writeToConsole)
拓展阅读
React
useEffect
React
Effect 五连
https://react.dev/learn/synchronizing-with-effects
https://react.dev/learn/you-might-not-need-an-effect
https://react.dev/learn/lifecycle-of-reactive-effects
https://react.dev/learn/separating-events-from-effects
https://react.dev/learn/removing-effect-dependencies
参考资料
https://medium.com/nerd-for-tech/what-are-side-effects-in-programming-51f7ef340f98
https://medium.com/@remoteupskill/what-is-a-react-side-effect-a5525129d251