react不是魔法
今天我想来聊一聊函数式编程,和它在react中的体现。
何为函数式编程
我们可以把函数式编程理解成一种范式,一个规范。纯函数是函数式编程的关键概念,函数式编程希望我们尽可能多地使用纯函数。正是依赖于纯函数的特点,函数式编程使得我们对于程序的结果具有更高的掌控性。
至于函数式编程中的什么,函数是第一等公民啊,函数柯里化,高阶函数,闭包,什么之类的,别的文章里面讲烂的东西我也不赘述了,我只想讲讲纯函数。
纯函数
我今天不说编程意义上普遍的“函数”(也被叫做方法),我想谈谈数学上的函数和编程上的纯函数。
数学意义上的函数,近代定义就是定义域到值域的对应关系。通俗地说,我们把函数理解成一个机器(对应关系),给它一个输入,它给你返回一个输出。如果你给它相同的输入,它能确保给你相同的输出,那么它就是一个函数。
纯函数是指,你编写的函数,也能做到和数学意义的函数相同的事情,并且不能有副作用。即以下两点
- 首先,在决定输出的时候,所依赖的东西必须是输入的子集。不能有会变的因素影响到函数的输出。比如我要输出一个数字,我不能根据当时的时间和天气来得到结果,也不能因为某个定义在函数外部的变量得到结果。这些都会导致输入不变时,输出发生变化。
- 其次,函数不能有副作用。什么算副作用呢?所有影响了函数外部的东西的操作,都是副作用(side effect,不是负作用)。比如你修改了外部的一个变量,发送了一个请求,打印了一个数据,这些都是副作用。为啥叫做副作用呢,因为这些操作都是与函数不相关的,它们使得函数的影响不局限于函数本身之内了。这违背了函数式编程的初衷。副作用使得函数的影响范围不可控。
值得注意的是,改变了函数的实参,也是副作用。因为实参是定义在函数外部的变量。一个很好的例子是sort(Array.prototype.sort),它就不是一个纯函数。当你调用a.sort()之后,a变成了排序后的数组。a当然也是sort的参数,因为对于sort来说,a就是this。而sort改变了this。所以说sort不是一个纯函数。
这其实是一个很不好的事,通常在这种情况下,我们应该新建一个对象,将新建的对象返回,而不是在原来的对象上面做更改。当然代价是更多的内存,不过这换来了更高的可调试性。我们试想一下,你写了一个会改变实参的方法,别人把自己定义的变量作为参数传给你,结果变量却被改变了,别人会被你气死,因为他万万想不到,变量的值不符合预期,竟然是因为变量作为参数传给了你。
换句话说,纯函数的要求是相同输入对应相同的输出,并且不能对函数之外的部分造成影响。纯函数有以下优点:
- 可缓存性:因为相同的输入有相同的输出,所以可以把某个输入对应的输出缓存起来,下次拿到相同的输入时,直接返回缓存的结果,不必重新计算。
- 可测试性(重要)或者说,可预期的:纯函数测试起来很简单,你能完全掌握这个函数做的事,只需要通过观察它的输出是否正确。而如果有副作用的非纯函数话,函数对外界做出了影响,这些影响无法通过函数的输出检测到,容易被忽略,但是又可能会导致bug。这就是为什么纯函数比非纯函数好。
- 可并行性:因为纯函数不会对函数外界造成影响,因此多个纯函数就互不影响,可以并行运行,各玩各的。不过js是单线程语言,这点并不能体会到。
- 引用透明:即调用这个函数的语句,可以被函数计算的结果替代。
由于这些优点,纯函数可以大大降低代码的耦合关系,使得结果完全可以预计,你写的变量也不会被别人的代码改变值。这样会使得开发与调试的难度大幅降低。因此,我们在开发的过程中,都应该注意尽量使用纯函数。
React中的函数式编程
react不是魔法
首先解释下“react不是魔法”这句话。是的,react不是魔法,它只是JavaScript代码。
你大可以把他当做一个黑盒子,你学着别人的样子,写着按照jsx语法写着代码,然后你的代码就出现在了屏幕上,就像我之前写vue时一样。我当初用着vue-cli的时候,根本不知道自己写的代码是如何变成dom的,只知道写template,script和style三部分。我不是在黑vue,只是说react的写法看起来更加原始,更能够让人们明白,自己写的每一行代码是如何变成dom的。当然vue也有很多不用脚手架的方法,写起来也不那么“黑盒”,只是当初我完全看不懂,也就无从深入了解罢了。
而实际上,react就是需要你去写一个jsx,然后将这个jsx转成React.createElement。通过这个API去构造虚拟节点。而这个jsx,其实就是类组件中render()的返回值,或者函数组件的返回值。
react的组件其实就是函数调用
我们这里取最简洁的函数组件为例,暂时不考虑使用hooks的情况,函数组件就是一个纯函数,接受一个props,返回一段jsx。react的渲染,其实就是首先用createRoot方法渲染根组件,然后按照组件的层级,一层一层地向下调用函数罢了。我始终忘不掉我之前用Remax写小程序的时候,看到最开始的app.js时的震撼。
1 | import './app.css' |
一开始我不明白这是啥意思啊,后来我知道,原来这就是根组件啊。我没在它里面添加逻辑,它就只需要把它的子组件返回出去,然后让它的子组件去渲染就好了。因为箭头函数的语法,整个组件的代码简洁的令人震惊。看明白了之后我感觉我更加理解react组件是什么东西了。如果只是用函数组件(不带hooks)的话,react就是函数式编程,它的渲染就是一个一个纯函数的调用。
当然了,一个react应用,没有副作用的话很难实现功能,因为发送请求等等都是副作用。所以副作用是几乎不可避免的,函数组件在hooks出现之前功能是不完备的。而函数式编程,也只是一个范式,一个规范,我们只能尽可能去遵守。后面有时间的话,再来说一说带hooks的函数组件,和函数式编程的关系吧。