为何要谈
副作用英文为 Side Effect。effect 是大家在前端开发中经常看到的一个关键词,比如 dva model 中的 effects,react 的 useEffect hook。但是大家可能都还停留在知道和会用的阶段,知道怎么通过 dva 的 effects 或者 useEffect 来异步加载数据,但是 effect 到底是什么,它的本质是什么,项目中应该如何处理和它相关的内容。希望能够通过这篇分享能够让大家对一些涉及到 effect 的技术点做到不仅仅是会写,而是做到“懂得”,从而才能真真写好。
副作用是什么
先看看维基百科上面的解释。
医学
https://zh.wikipedia.org/wiki/%E5%89%AF%E4%BD%9C%E7%94%A8
在医学中,副作用(英语:side effect)是指药品往往有多种作用,作用于不同身体部位受体,治疗时利用其一种或一部分受体作用,其他作用或是受体产生作用即变成为副作用。虽然副作用一词常被用来形容 不良反应 (医学)),但事实上副作用也可以指那些“有益处、意料之外”的效果。
那么一个最典型的例子就是化疗,化疗本质是想要通过化学治疗药物杀灭癌细胞达到治疗目的。但是化疗在杀死癌细胞的过程也会把普通的细胞杀死,所以化疗的病人通常会出现食欲不振,脱发等现象。由这个例子来看我们通常意义上是不期望副作用出现的。
计算机
那在计算机领域副作用是指什么呢?
https://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E5%89%AF%E4%BD%9C%E7%94%A8
在英文版本中是 Side effect (computer science):
In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation. State data updated “outside” of the operation may be maintained “inside” a stateful object or a wider stateful system within which the operation is performed. Example side effects include modifying a non-local variable, modifying a static local variable, modifying a mutable argument passed by reference, performing I/O or calling other side-effect functions.[1] In the presence of side effects, a program’s behaviour may depend on history; that is, the order of evaluation matters. Understanding and debugging a function with side effects requires knowledge about the context and its possible histories.[2][3]
在中文版中是叫“函数副作用”:
在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。
这里要强调一下这里的描述指的是函数副作用,而不是是副作用。因为副作用是相对的,就好像下图中的秋香就是副作用。
那为什么说“修改全局变量(函数外的变量)或修改参数。”是副作用呢?就好像你说秋香为什么是副作用你要给出个原因吧,不能说她好看她就是副作用。
函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并降低程序的可读性。严格的函数式语言要求函数必须无副作用。这里就有一个存函数的概念:
纯函数:
function f (x) {
return x + 1;
}
非纯函数:
let a = 1;
function f (x) {
a = x;
return x + 1;
}
其实在 React 中,大家也经常会接触到纯函数:
import { Button } from 'antd';
import React from 'react';
const App = (props) => {
const { count } = props;
return (
<div>
<div>{count}</div>
</div>
)
}
ReactDOM.render(<App />, mountNode);
那相比那种有 state,有网络请求的组件自然这种组件更让人喜欢,更不容易出错。
前端
在前端副作用来自,但不限于:
- 进行一个 HTTP 请求
- Mutating data
- 输出数据到屏幕或者控制台
- DOM 查询/操作
- Math.random()
- 获取的当前时间
如何看待副作用
更加准确的说应该是如何看待函数副作用,正如上面所说,副作用是相对的,所以通常你无法消除副作用,因为有时候有些副作用本身它就是必然会存在的。比如网络请求,相对于 Stateless Component 来说它就是副作用,但是它也是无法消除的。所以我们只能尽可能的规避副作用带来的负面影响,也就是尽可能的保证纯函数的优势:
一个函数只有当它没有副作用的情况下它才能称做是纯函数。纯函数是函数式编程里面的一个概念,它的定义是:
- 如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。
- 没有副作用。
类似面向对象,函数式编程也是一种编程范式。纯函数就是函数式编程中的一个概念,对于严格的函数式编程来说要求它的函数必须是纯函数。
这里函数式编程也不展开细讲了,就具体讲讲纯函数吧。即使不是严格的函数式编程,我们也应该去学会函数式编程中的一些思想。尤其是存函数,其实在前端大家已经在广泛的使用这一概念了。比如 react 的 render 函数就“应该”是一个纯函数,只要 props 和 states 确定,那么最终得到的 dom 就确定了。
纯函数有什么好处呢?从我的角度来看:
- 纯函数具有引用透明(如果程序中任意两处具有相同输入值的函数调用能够互相置换,而不影响程序的动作,那么该程序就具有引用透明性)的特性。而且纯函数组成的函数还是纯函数,这样使得函数能够更加方便的被重构。
- 纯函数的输入就决定了输出,便于调试。
- 大部分错误不是由于代码逻辑编写错误导致的,而是由于状态太复杂而没有考虑到一些逻辑分支而导致的。如果程序都是由纯函数构成,那么程序本身的逻辑其实已经很清晰了。这会大大提升程序的质量,所以现在有人非常的推崇函数式编程。
- 纯函数更方便被测试。
所以按道理来说我们应该确保所有的函数都是纯函数,但是这是很难的,至少对于现在的前端来说是比较困难的。主要体现在两个方面:
- 有些副作用是无法消除的,比如网络请求。
- 一味地最求存函数有可能会导致更高的编写成本(至少在现有的技术框架下,hooks 就是为了在一定程度上解决这一问题的)。
所以对于副作用我们应该怎么处理,有两个方案:
- 将副作用推到边界处理,保证核心是 pure function,比如:React,redux-thunk,另外 hooks 也是一个体现,副作用会被限制在 hook 函数内。
- 副作用单独处理,保证核心是 pure function,比如:redux-saga。
import { Button } from 'antd';
import React, { useState } from 'react';
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<Button
onClick={()=>setCount(c=>c+1)}
>
add
</Button>
</div>
)
}
ReactDOM.render(<App />, mountNode);
我们应该怎么做
狭义
- 尽可能的使用纯函数,消除副作用。将无法消除的副作用推到边界处理或者单独处理,确保关键的核心逻辑是纯函数。
- 更多更完善的测试。
- 善用函数式编程的思想,让代码逻辑更加清晰。
广义
- 做系统架构,大的模块设计的时候把模块设计为“纯模块”让架构更加健壮且灵活。
- 除了代码的副作用以外,还要尽可能的消除广义上的“副作用”
- 不必要的注释。
- 冗余的逻辑。
- 已经废弃的仓库(只是要说明)。
- 纯函数一般的文档(得到一个问题去到文档中查阅得到的答案也唯一)。
参考文章
- 维基百科
- CSDN 什么是副作用 https://blog.csdn.net/racaljk/article/details/76020371
- 语雀 JS 函数式编程,副作用处理 https://www.yuque.com/naifu/fe/yywkei
- redux saga https://redux-saga-in-chinese.js.org/
- 什么是纯函数以及为什么要用纯函数 http://www.fly63.com/article/detial/1274
- 什么是纯函数 https://zcfy.cc/article/master-the-javascript-interview-what-is-a-pure-function-2186.html