函数式编程理念📝

函数式编程

写React几个月了,接触到的,听到的较多的一个词“functional programming”函数式编程。在工作中也遇到了不少之前的大佬写的函数式代码,总结记录一些有关函数式的东西。

函数式?函数式编程?

1、函数式是一个数学概念:

f(x) = y;

函数式本身是很纯粹的一个概念,表达的就是函数定义域到值域之间一种特定的映射关系。(定义域中的一个值经过计算有且仅有一个值与之对应,也即只能得到一个确定的结果)。

f(x) = y;

这个数学函数式表达了以下几层意思:

  • 函数总是接受一个参数—x
  • 函数总是返回一个值—y
  • 函数应该根据接受的参数而不是外部环境运行
  • 对于一个给定的x只会输出一个唯一的y

2、函数式编程技术主要就是基于数学函数和它的思想。

比如有一个简单的计税函数如下:

let percentValue = 5;
let calculateTax = (value) => {
    return value/100 * (100 + percentValue)
}            

这个函数准确的实现了我们的想法,但是从数学的定义上分析,就会发现calculateTax依赖了全局变量percentValue,因此在数学上它不能被称之为一个真正的函数。修复方法很简单,我们只需要移动percentValue,把它作为函数的参数。

let calculateTax = (value, percentValue) => {
return value/100 * (100 + percentValue)
}        

现在calculateTax函数可以被称为一个真正的函数了。

移除一个函数内部对全局变量的访问会使得该函数的测试更加容易

函数式编程的特性

1、引用透明性

定义:所有的函数对于相同的输入都将返回相同的值,函数的这一特性被称为引用透明性

🌰例:

let identity = (i) = {return i}

定义一个简单的函数identity,无论传入什么作为输入,该函数都会把它返回,在函数内部没有全局引用,我们的函数只根据传入的参数i进行操作,该函数满足引用透明性条件。

sum(4,5) + identity(1)

根据引用透明性的定义,可以把上面的语句转换为:

sum(4,5) + 1; // 直接用结果替换函数的计算过程

该过程称为替换模型,因为我们可以直接替换函数的结果(因为函数的逻辑不依赖其他全局变量)而正是这一特性使得代码和缓存成为可能!

2、 并发?

引用透明性使得我们可以轻松地采用多线程来运行代码,根本不需要同步。为什么呢?因为同步的问题在于线程在并发运行的的时候不应该依赖全局变量。而遵循引用透明性的函数只依赖来自参数的输入,不会对全局产生任何依赖,这里没有任何“锁”🔒的概念,所以线程可以自由的运行(虽然js引擎是单线程…)

3、 缓存?

由于函数会为给定饿输入返回相同的值,实际上我们就可以缓存它。假设函数factorial用来计算给定数组的阶乘,其接受输入作为参数以计算其阶乘,5的阶乘是120,如果用户第二次用5来调用factorial,如果factorial遵循引用透明性,我们就可以直接返回缓存的值120而不用再重新计算一遍。

函数式编程对比命令式编程主张移除“如何”做的部分,使用一个函数来处理“如何”做的部分,将关注点放在手头的工作上即”做什么“。

为什么函数式好?

slice和splice两个函数在做数据处理的时候很类似,但是实现的方式却大不相同。

let 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];
.
.
. // 无论调用多少次它都能保证相同的输出。

而splice却会嚼烂调用它的那个数组,然后再吐出来🤮,这就产生了副作用,即这个数组永久的改变了🤷‍♂️

函数式编程讨厌这种会改变数据的函数,追求的是那种更加”可靠“的每次都能返回同样结果的函数。

就像上面的那个计税函数中,非函数式(即纯函数)的版本中,其计算结果依赖了全局变量percentValue,这是令人沮丧的🤦‍♂️,因为引入外部变量,增加了我们对函数的认知负荷和心理负担。程序中到处充斥着的这些不纯的函数,往往是产生BUG的罪魁祸首!

另一方面,使用纯函数的形式,函数自己就能做到自给自足,并且随时都可以放心大胆将其“打包带走”📦

函数式编程让我想到了之前在网上看到的两种关于线缆艺术的图片:

非函数式编程就行下图:

每个函数从哪里来到哪里去,因为有依赖全局变量的可能因素存在,同时函数本身也可能会改变其他函数所依赖的外部变量,所以让人看上去就比较乱,不敢轻举妄动。

函数式编程就好很多:

函数式编程因为做到了“字给自足”我需要的东西都在我自己内部,不依赖外部变量,同时你放心我很单纯我也不会去改变外面的什么,所以给人的感觉就是看上去比较清爽,数据清晰的感觉。

本来下面接着的内容该是柯里化、compose函数组合、管道啥的,但是对于柯里化看了一段时间感觉还是没有一个让自己“豁然开朗”的点,先留着,转而说说React

函数式编程与React…

关于react有一个很形象的公式 UI = F(state) 可以说是总结的非常到位了!

React强调数据的不可变性,把每一确定时刻的UI状态当作是电影🎬中的某一个确定的帧,长什么样子是确定的是由确定的state状态确定的,要让我渲染UI的另外一种状态,麻烦用另一个state来驱动,这给我一种“死板且靠谱”的感受~

Vue信奉数据可变哲学🙆,React则选择数据不可变哲学🙅‍♂️

作用? 副作用?

作用:可以理解为一切除了计算结果之外发生的事情。

副作用:是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的任何可观察的交互。

具体到前端开发中,副作用包括但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

函数式编程的哲学里认为副作用是造成问题的主要原因。这一假定不无道理,在只有我和你的世界里,我不会出问题,然而问题产生了,那么肯定是你出了问题…
React主张的即是函数式的编程,但是我们做业务开发不是写静态页面,我们是用页面来承载我们的业务。从这个角度看,我有一种“业务都是通过副作用完成的”感觉~

那React是怎么平衡纯函数与副作用的呢?

effect? hooks?

React16.8引入了hooks API.对其中的useEffect感触比较多.在类组件中render函数就是个纯函数,负责实现UI = F(state)而业务操作多放在componentDidMount、updated、willUnmounted…等生命周期钩子中,其实单单就从组件生命周期的心智模型来理解,这样就挺好的。

1、但是生命周期是咋来的?

2、how do you think about componentDidMount?

3、useEffect(() => {doSomething}, []) === componentDidMount really???

在看React的hooks介绍文档的时候,在介绍到effect的执行时机的时候看到有这么一句话:

与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。

what??😺 就是说componentDidMount会阻塞浏览器更新屏幕咯?
答案: 是的。componentDidMount指的是组件在内存上中已经完成“挂载”一切已成定数,只等浏览器的GUI线程对页面进行渲染了,只有浏览器的GUI线程对页面进行了绘制操作,我们的肉眼才能看到屏幕上的内容。所以如果一个组件定义如下:

class Demo extends Compponent({
    componentDidMount() {
        console.log('now you see me!')
    }
    render() {
        return (
            <div>You can see me with your naked eye</div>
        )
    }
})

我们会先在控制台看到log的打印输出,然后才看到屏幕上渲染的内容。所以说componentDidMount会阻塞浏览器更新屏幕,指的就是componentDidMount中的内容属于当前Tick任务周期内的工作,浏览器会等到V8将这一工作做完,才让GUI线程执行绘制操作。

useEffect与componentDidMount的不同点在与useEffect中的effect操作会等到浏览器render完成之后(是真正意义上的render,肉眼看见的那种,不是内存中的挂载。)才会执行副作用操作,所以其不会阻塞浏览器的渲染。真正跟componentDidMount一致的是useLayoutEffect。它会排在浏览器GUI线程对页面进行渲染之前执行。
React建议一开始先使用useEffect,只有当它出问题时候再尝试使用useLayoutEffect。

另外:关于“用了hooks的函数式组件每次渲染都有属于每次自己的、单独的effect,每次的effect都是全新的,而不是上次的。。。
”此类的说法。理解: effect是传给useEffect()这个API的回调函数,而不是useEffect这个api(听起来好像是废话~)

const effect = () => {
    console.log('i am effect')
}

useEffect(effect, [])

useEffect是一个API,react内部会“hold住它~” 而传递进去的函数才是每次都不一样的effect.

生命周期函数也叫生命周期钩子函数 life cycle hooks function,其中也有一个单词’hook’,回过头来看,似乎又有一种“大一统”的感觉。

其实都是业务模型的抽象,我们在写componentDidMount或者其他任何生命周期钩子函数的时候并不是在写componentDidMount或者什么,而是在写“我希望在这个时候做点什么事情。。。“的业务需求模型

hooks用函数式的方式优化更新了这种业务模型,使之看起来似乎更加合理,编写起来更加清爽方便。