likes
comments
collection
share

从前端技术的演变了解函数式编程(FP)React函数式组件代替了传统的类组件写法,vue3的组合式也颠覆了选项式的写法。

作者站长头像
站长
· 阅读数 97

2019年,React16.8版本引入React Hooks API,使函数式组件代替了传统的类组件写法;2020年vue3.0的Composition API也颠覆了Option API的写法。两大框架的更新也标志着函数式编程(FP)这一编程范式在前端领域的兴起。本篇文章就一起聊聊函数式编程。

函数式编程是一种编程范式,在了解函数式编程之前,我们先谈谈什么是编程范式。

一、编程范式

编程范式是指编程风格,能体现程序员看待程序世界的角度(世界观)和解决问题的方式(方法论)。前端技术的演变正体现了前辈们不同时间段在前端领域编码方式的最佳实践

1. 命令式与jQuery

命令式编程关注程序执行步骤和过程,即关注怎么做(How)。

2005年,Ajax 的出现让页面局部刷新变成了可能,使 B/S 架构焕发了新的活力。但开发者们在页面业务上需要大量操作 Dom 的代码,来更新页面:

function showModal() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'YOUR_API_URL', true);
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
            var listData = JSON.parse(xhr.responseText);
            var modalList = document.getElementById('modalList');
            listData.forEach(function (item) {
                var listItem = document.createElement('li');
                listItem.textContent = item.text;
                modalList.appendChild(listItem);
            });
            var modal = document.getElementById('myModal');
            modal.style.display = 'block';
        }
    };
    xhr.send();
}

2006年,jQuery 的出现让绑定事件、操作 Dom 和发送 Ajax 请求变得更简洁:

$(function () {
    $('#showModalBtn').click(function () {
        $.ajax({
            url: 'YOUR_API_URL',
            method: 'GET',
            dataType: 'json',
            success: function (listData) {
                var modalList = $('#modalList');
                $.each(listData, function (index, item) {
                    var listItem = $('<li>' + item.text + '</li>');
                    modalList.append(listItem);
                });
                $('#myModal').show();
            }
        });
    });
});

虽然 jQuery 封装了这些常用的功能,使得操作变得简单,但其核心仍然是命令式的,也需要开发者较为明确地告诉浏览器怎么做。

2. 声明式与MVVM架构模式

与命令式编程不同的是,声明式隐藏了具体实现,使编程方式更接近人类思维。如果说命令式编程给指令告诉计算机怎么样执行任务,但是声明式编程告诉计算机需要什么样的结果(what)。

MVVM模式将业务逻辑与 UI 展示分离,Model 负责数据和业务逻辑,View 负责展示,View Model 充当二者之间的桥梁。数据绑定机制让开发者们无需手动操作DOM,只需要变更响应式数据,视图就会同步更新。即开发者只需要描述结果,至于如何实现这个结果交给View Model

以下是使用Vue2实现上述业务:

<template>
  <div>
    <button @click="showModal">Show Modal</button>
    <ul v-if="modalVisible">
      <li v-for="item in listData" :key="item.id">{{ item.text }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      modalVisible: false,
      listData: []
    };
  },
  methods: {
    async showModal() {
      this.listData = await this.fetchData('YOUR_API_URL');
      this.modalVisible = true;
    },
    async fetchData(url) {
      const response = await fetch(url);
      return await response.json();
    }
  }
};
</script>

在data函数的返回中"声明"响应式数据,即可通过{{}}v-ifv-for等来达到数据和视图的同步效果。

3. 面向对象编程(OOP)在vue和react框架设计中的体现

众所周知,面向对象编程有封装继承多态的三大特性,前辈们将这一思想运用在框架设计中,在vue2中我们称为“选项式”,在react中对应的是类式组件(Class)。

封装:

Vue2中,组件通过“选项对象”来定义,数据、方法、和生命周期钩子都被封装在这个对象内;react的类组件的写法,也完全符合ES6类的编写方式。两者的组件内部实现对外部隐藏,符合OOP的封装特性。

继承:

虽然Vue2的组件通常通过组合模式来实现复用,但也可以通过 Vue.extend() 方法来创建基于已有组件的子组件:

const BaseComponent = Vue.extend({
  data() {
    return { baseMessage: 'Base component' };
  }
});

const ChildComponent = BaseComponent.extend({
  data() {
    return { childMessage: 'Child component' };
  }
});

react的类组件的写法,也是可以直接通过extends关键字来继承父组件的状态和方法:

class BaseComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { baseMessage: 'Base component' };
  }
}

class ChildComponent extends BaseComponent {
  render() {
    return <div>{this.state.baseMessage}</div>;
  }
}

多态:

vuereact 中组件接收不同的 propslot 从而产生不同的表现和行为。符合 OOP 的多态特性。

分析问题

面向对象编程关注的是将数据和行为封装在对象中,通过类来定义对象的属性和方法。这种编程范式本身可以促进代码的重用、模块化和维护性,但在前端开发中,如果以选项式或者类组件进行业务开发,一段业务代码的会被分隔不同地方,久而久之变得臃肿不易抽离,最主要的是这不符合常人的思维方式。 另外,在react的类组件编写时,还要考虑到 this 指向问题。

那有没有更小单元的编程方式,让我们可以任意组合代码,同时可以更好的复用?于是 react 和 vue 分别有了React HooksComposition Api。这二者都受到了函数式编程的影响,鼓励开发者们将一段组件拆分为函数的组合。

下面我们来介绍一下当前前端领域最流行也是最合适的编程范式:函数式编程

二、函数式编程

首先我们必须了解的是函数式编程中的几个核心概念:

1. 函数被当作一等公民(函数是第一位)

函数式编程主张函数是“一等公民”,即函数可以像其他基本类型一样被使用、传递和操作。这样可以更加灵活地组织和构建代码:

function add(a, b) {
    return a + b;
}
function operateOnNumbers(operation, x, y) {
    return operation(x, y);
}
const result = operateOnNumbers(add, 5, 3);
console.log(result); // 输出 8

高阶函数

维基百科对高阶函数的定义:

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

  1. 接受一个或多个函数作为输入
  2. 输出一个函数

高阶组件

了解了高阶函数,我们再看看react官方对高阶组件的介绍:

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

具体而言,高阶组件是参数为组件,返回值为新组件的函数。

react 强调组合优于继承,高阶组件通过复用小组件来构建更大的组件使得开发变得简单而又高效。这正是函数式编程思想在现代JS框架中的体现。

2. 纯函数

所谓纯函数,是符合下面两点的函数:

总是为相同的参数产生相同的输出

显然,Math.random()Date()等就不属于纯函数。而这一要求就决定了函数式编程的一个优势:可预测性。可预测性不仅会使调试和维护变得简单,而且方便开发者们独立于其他部分进行单元测试

没有副作用

没有副作用的函数在多线程环境中是安全的,无需担心数据竞争。虽然我们说JS是单线程的,但起码这一点让我们在处理复杂的异步逻辑时,保证数据变化的可追踪性。

值得一提的是,前端响应式的框架设计时为了方便开发者能声明式得使用响应式数据驱动视图,副作用是不可避免的。尽管有副作用,但框架提供了明确的时机和方式来处理它们。比如 vue 中的 watch 函数侦听响应式数据的变化,甚至可以追踪到变化来源(使用onTrack/onTrigger的选项调试侦听器)。但个人认为,过多使用 watch 尤其是 watchEffect,会使数据变化难以追踪。

3. 不可变性

函数式编程同时强调数据的不可变性,即数据一旦创建不可被修改,除非重新创建。这在 vue react 中都有所体现:props 都是单向数据流,不可以在组件内部修改 props 传参。 不可变性具备线程安全的特性,设想一下如果父组件的一个对象作为props传递给多个子组件,而子组件乱修改这个对象,即使框架层面不报错(如果修改基本类型的props值,vue会直接警告),对象的数据变化也会难以追踪。

4. 递归代替循环

函数式编程严格上不能使用 forwhile 循环,而是通过递归来实现。递归函数反复调用自身,以达到目的。

使用递归而不是循环进行斐波那契数列计算:

function fib(n) {
  if (n <= 1) {
      return 1;
  } else { 
      return fib(n - 1) + fib(n - 2);
  }
}

三、思考函数式编程的优势和不足

优势:

  • 因为不可变性、无副作用,纯函数的引用是透明的,因此函数式编程能够更好的进行单元测试的覆盖;
  • 更细粒度的函数封装代替组件,复用性扩展性都有所提高;
  • 函数只关心输入和输出,业务代码只是函数的组合,因此代码的可读性较高(前提是给函数取一个语义化的名称);
  • 具有声明式编程的优势,开发者只需要关注结果而非过程,具体实现交给函数内部,从而编程思路会更加清晰
  • ...

不足:

  • 以一个个函数为基本单元粒度过细,需要再大量的再组合才能构成业务概念;
  • 因为更细粒度的封装,从而需要花更多的时间对函数进行语义化命名;
  • 为了数据不可变性,程序运行可能会产生大量的数据副本,导致时间和空间消耗更大。

四、总结

虽然vue3的Composition Api和react的函数式组件让我们意识到函数式编程在前端领域的兴起。但技术始终服务于业务,我们需要清楚每种编程范式的适用优劣势和适用场景,根据业务在特定的场景下选择甚至融合合适的编程范式,才是更值得思考和修炼的

转载自:https://juejin.cn/post/7419894270162829350
评论
请登录