为何要谈

副作用英文为 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

image.png

医学中,副作用(英语: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]

在中文版中是叫“函数副作用”:

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。

这里要强调一下这里的描述指的是函数副作用,而不是是副作用。因为副作用是相对的,就好像下图中的秋香就是副作用。

image.png

那为什么说“修改全局变量(函数外的变量)或修改参数。”是副作用呢?就好像你说秋香为什么是副作用你要给出个原因吧,不能说她好看她就是副作用。

函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并降低程序的可读性。严格的函数式语言要求函数必须无副作用。这里就有一个存函数的概念:

纯函数:

  1. function f (x) {
  2. return x + 1;
  3. }

非纯函数:

  1. let a = 1;
  2. function f (x) {
  3. a = x;
  4. return x + 1;
  5. }

其实在 React 中,大家也经常会接触到纯函数:

  1. import { Button } from 'antd';
  2. import React from 'react';
  3. const App = (props) => {
  4. const { count } = props;
  5. return (
  6. <div>
  7. <div>{count}</div>
  8. </div>
  9. )
  10. }
  11. 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。
  1. import { Button } from 'antd';
  2. import React, { useState } from 'react';
  3. const App = () => {
  4. const [count, setCount] = useState(0);
  5. return (
  6. <div>
  7. <div>{count}</div>
  8. <Button
  9. onClick={()=>setCount(c=>c+1)}
  10. >
  11. add
  12. </Button>
  13. </div>
  14. )
  15. }
  16. ReactDOM.render(<App />, mountNode);

我们应该怎么做

狭义

  • 尽可能的使用纯函数,消除副作用。将无法消除的副作用推到边界处理或者单独处理,确保关键的核心逻辑是纯函数。
  • 更多更完善的测试。
  • 善用函数式编程的思想,让代码逻辑更加清晰。

广义

  • 做系统架构,大的模块设计的时候把模块设计为“纯模块”让架构更加健壮且灵活。
  • 除了代码的副作用以外,还要尽可能的消除广义上的“副作用”
    • 不必要的注释。
    • 冗余的逻辑。
    • 已经废弃的仓库(只是要说明)。
    • 纯函数一般的文档(得到一个问题去到文档中查阅得到的答案也唯一)。

参考文章