【重构】使用 Hooks 让代码更易于变更

更新日期: 2020-10-16阅读: 1.5k标签: 代码

重构过程中,肯定会遇到新的代码如何做技术选型的问题,要考虑到这套技术的生命力,也就是他是否是更新的技术,还有他的灵活和拓展性,期望能够达到在未来至少 3 年内不需要做大的技术栈升级。我的这次重构经历是把 jquery 的代码变为 react ,你品品,算是最难,劳动最密集的重构任务了吧。看多了之前代码动辄上千行的 Class ,混乱的全局变量使用,越来越觉得,代码一定要写的简单,不要使用过多的黑科技,尤其是各种设计模式,为了复用而迭代出来的海量 if 判断。代码不是给机器看的,是给人看的,他需要让后来人快速的看懂,还要能让别人在你的代码的基础上快速的迭代新的需求。所以我们需要想清楚,用什么技术栈,怎么组织代码。


为什么要用 Function Component

对于 Class Component 和 Function Component 之争由来已久。从我自身的实践来看,我觉得这是两种不同的编程思路。

Class Component 面向对象编程继承生命周期
Function Component 函数式编程组合数据驱动

为什么不用 Class

首先,如果我们使用面向对象这种编程方式,我们要注意,他不只是定义一个 Class 那么简单的事情,我们知道面向对象有三大特性,继承,封装,多态。

首先前端真的适合继承的方式吗?准确的说,UI 真的适合继承的方式吗?在真实世界里,抽象的东西更适合定义成一个类,类本来的意思就是分类和类别,正如我们把老虎,猫,狮子这些生物统称为动物,所以我们就可以定义一个动物的类,但是真实世界并没有动物这种实体,但是页面 UI 都是真实存在可以看到的东西,我们可以把一个页面分成不同的区块,然后区块之间采用的是「组合」的方式。因此我认为 UI 组件不适合继承,更应该组合。如果你写过继承类的组件,你将很难去重构,甚至是重写他。

封装讲究使用封装好的方法对外暴露类中的属性,但是我们的组件基本是通过 props 暴露内部事件和数据,通过 Ref 暴露内部方法,本质上并没有使用封装的特性。

多态就更少用了,多态更多是基于接口,或者抽象类的,但是 JS 这块比较弱,用 TS 或许会好一些。

综上,作为前端 UI 编程,我更倾向于使用函数组合的方式。


为什么要用数据变化驱动

不论是在 React 或者在 vue 里,都讲究数据的变化,数据与视图的绑定关系,数据驱动,数据的变化引起 UI 的重新渲染,但是生命周期在描述这个问题的时候,并不直接,在 Class Component 里,我们如何检测某个数据的变化呢,基本是用 shouldUpdate 的生命周期,为什么我们在编程的时候,正在关注数据和业务的时候,还要关心一个生命周期呢,这部分内容对于业务来说更像是副作用,或者不应该暴露给开发者的。

综上,是我认为 Function Component + Hooks 编程体验更好的地方,但是这也只是一个相对片面的角度,并没有好坏之分,毕竟连 React 的官方都说,两种写法没有好坏之分,性能差距也几乎可以忽略,而且 React 会长期支持这两种写法。


hooks:真正的响应式编程

到底是什么是响应式编程?大家各执一词,模模糊糊,懵懵懂懂。很多人没有把他的本质说明白。从我多年的编程经验来看,响应式编程就是「使用异步数据流编程」。我们来看看前端在处理异步操作的时候通常是怎么做的,常见的异步操作有异步请求和页面的鼠标操作事件,在处理这样的操作的时候,我们通常采取的方法是事件循环,也就是异步事件流的方式。但是事件循环并没有显式的解决事件依赖问题,而是需要我们自己在编码的时候做好调用顺序的管理,比如:

const x = 1;
const a = (x) => new Promise((r, j)=>{
  const y = x + 1;
    r(y);
});
const b = (y) => new Promise((r, j)=>{
  const z = y + 1;
    r(z);
});
const c = (z) =>  new Promise((r, j)=>{
  const w = z + 1;
    r(w);
});
// 上面是三个异步请求,他们之间有依赖关系,我们通常的操作是
a(x).then((y)=>{
    b(y).then((z)=>{
      c(z).then((w)=>{
          // 最终的结果
      console.log(w);
      })
  })
})

上述的基于事件流的回调方式,我们使用 Hooks 来替换的话,就是这样的:

import { useState, useEffect } from 'react';

const useA = (x) => {
    const [y, setY] = useState();
  useEffect(()=>{
    // 假设此处包含异步请求
      setY(x + 1);
  }, [x]);
  return y;
}

const useB = (y) => {
    const [z, setZ] = useState();
  useEffect(()=>{
    // 假设此处包含异步请求
      setZ(y + 1);
  }, [y]);
  return z;
}

const useC = (z) => {
    const [w, setW] = useState();
  useEffect(()=>{
    // 假设此处包含异步请求
      setW(z + 1);
  }, [z]);
  return w;
}

// 上面是三个是自定义 Hooks,他表明了每个变量数据之间的依赖关系,你甚至不需要
// 知道他们每个异步请求的返回顺序,只需要知道数据是否发生了变化。
const x = 1;
const y = useA(x);
const z = useB(y);
const w = useC(z);
// 最终的结果
console.log(w);

我们从上面的例子看到, Hooks 的写法,简直就像是在进行简单的过程式编程一样,步骤化,逻辑清晰,而且每个自定义 Hooks 你可以把他理解为一个函数,他不需要与外界共享状态,他是自封闭的,可以很方便的进行测试。


开始精简代码

我们基于 React Hooks 提供的工具和上面讲的响应式编程的思维,开始我们的精简代码之旅,这次旅程可以概括为:遇到千行代码文件怎么办?拆分最有效!怎么拆分?先按照功能模块来分文件,这里的功能模块是指相同的语法结构,比如副作用函数,事件处理函数等。单个文件内可以按照具体实现写多个自定义 Hooks 和函数。这样做的最终目的就是,让主文件里只保留这个组件要实现的业务逻辑的步骤。



为什么会有上千行的单个代码文件?

如果我们把一个组件的所有代码都写到一个组件里,那么极有可能会出现一个文件里有上千行代码的情况,如果你用的是 Function Component 来写这个组件的话,那么就会出现一个函数里有上千行代码的情况。当然上千行代码的文件对于一个健全的开发者来说都是不可忍受的,对于后来的重构者来说也是一个大灾难。

为什么要把这个代码都放到一个文件里?拆分下不香吗?那下面的问题就变成了如何拆分一个组件,要拆分一个组件,我们要先知道一个典型的组件是什么样子的。


一个典型的组件

Hooks 是个新东西,他像函数一样灵活,甚至不包含我选用了上面的方式来编写新的代码,那我们来看看一个典型的基于 Function Component + Hooks 的组件包含什么?

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
  Row, Select,
} from 'antd';
import Service from '@/services';

let originList = [];
const Demo = ({
  onChange,
  value,
  version,
}) => {
  // 状态管理
  const [list, setList] = useState([]);

  // 副作用函数 
  useEffect(() => {
    const init = async () => {
        const list = await Service.getList(version);
        originList = list;
        setList(list);
    };
    init();
  }, []);

  // 事件 handler
  const onChangeHandler = useCallback((data) => {
    const item = { ...val, value: val.code, label: val.name };
    onChange(item);
  }, [onChange]);
  
  const onSearchHandler = useCallback((val) => {
    if (val) {
      const listFilter = originList.filter(item => item.name.indexOf(val) > -1);
      setList(listFilter);
    } else {
      setList(originList);
    }
  }, []);
  
  // UI 组件渲染
  return (
    <Row>
        <Select
        labelInValue
        showSearch
        filterOption={false}
        value={value}
        onSearch={onSearchHandler}
        onChange={onChangeHandler}
        >
         {list.map(option => (<Option value={option.id} key={option.id}>{option.name}</Option>))}
        </Select>
    </Row>
  );
};

export default Demo;

从上面的例子我们可以看出,一个基本的 Function Component 包含哪些功能模块:

  • useState 为主的状态管理
  • useEffect 为主的副作用管理
  • useCallback 为主的事件 handler
  • UI 部分
  • 转换函数,用于请求返回数据的转换,或者一些不具有通用性的工具函数

拆分功能模块

首先,我们把上面讲到的功能模块拆分成多个文件:

|— container
        |— hooks.js // 各种自定义的 hooks
      |— handler.js // 转换函数,以及不需要 hooks 的事件处理函数
        |— index.js // 主文件,只保留实现步骤
        |— index.css // css 文件

什么样的代码一看就懂?

我重构过太多别人的代码,但凡遇到那种看着逻辑代码一大堆放在一起的,我就头大,后来发现,这些代码都犯了一个相同的错误。没有分清楚什么是步骤,什么是实现细节。当你把步骤和细节写在一起的时候,灾难也就发生了,尤其是那种长年累月迭代出来的代码,if 遍地。
Hooks 是一个做代码拆分的高效工具,但是他也非常的灵活,业界一直没有比较通用行的编码规范,但是我有点不同的观点,我觉得他不需要像 Redux 一样的模式化的编码规范,因为他就是函数式编程,他遵循函数式编程的一般原则,函数式编程最重要的是拆分好步骤和实现细节,这样的代码就好读,好读的代码才是负责任的代码。

到底怎么区分步骤和细节?有一个很简单的方法,在你梳理需求的时候,你用一个流程图把你的需求表示出来,这时候的每个节点基本就是步骤,因为他不牵扯到具体的实现。解释太多,有点啰嗦了,相信你肯定懂,对吧。
步骤和细节分清楚以后,对重构也有很大的好处,因为每个步骤都是一个函数,不会有像 class 中 this 这种全局变量,当你需要删除一个步骤或者重写这个步骤的时候,不用影响到其他步骤函数。
同样,函数化以后,无疑单元测试就变得非常简单了。


按照步骤拆分主文件

目的是主文件里只保留业务步骤。

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
  Row, Select,
} from 'antd';
import { onChangeHandler } from './handler';
import { useList } from './hooks';
import Service from '@/services';

const Demo = ({
  onChange,
  value,
  version,
}) => {
  // list 状态的操作,其中有搜索改变 list 
  const [originList, list, onSearchHandler] = useList(version);
  
  // UI 组件渲染
  return (
    <Row>
      <Select
      labelInValue
      showSearch
      filterOption={false}
      value={value}
      onSearch={onSearchHandler}
      onChange={() => onChangeHandler(originList, data, onChange)}
      >
        {list.map(option => (<Option value={option.id} key={option.id}>{option.name}</Option>))}
      </Select>
    </Row>
  );
};

export default Demo;

看到上面是基于步骤和细节分离的思路,将上面的组件做了一次重构,只包含两步:

  • 对 list 数据的操作
  • UI 渲染

通过拆分以后主文件代码里就只包含一些步骤了,全部使用自定义的 hooks 替换了,自定义的 hooks 可以写到 hooks.js 文件中。
hooks.js 里文件内容如下:

import { useState, useEffect, useCallback } from 'react';

let originList = [];
export const useList = (version) => {
  // 状态管理
  const [list, setList] = useState([]);
   // 副作用函数 
  useEffect(() => {
    const init = async () => {
        const list = await Service.getList(version);
        originList = list;
        setList(list);
    };
    init();
  }, []);
  
  // 处理 select 搜索
  const onSearchHandler = useCallback((val) => {
    if (val) {
      const listFilter = originList.filter(item => item.name.indexOf(val) > -1);
      setList(listFilter);
    } else {
      setList(originList);
    }
  }, []);
  
  return [originList, list, onSearchHandler];
}

可以看到 hooks.js 文件里包含的就是数据和改变数据的方法,所有的副作用函数都包含在里面。同时建议所有的异步请求都是用 await 来处理。啥好处可以自行 Google。

handler.js 文件内容如下:

// 事件 handler
export const onChangeHandler = (originList, data, onChange) => {
  const val = originList.find(option => (option.id === data.value));
  const item = { ...val, value: val.code, label: val.name };
  onChange(item);
};

上面的例子非常简单,你可能觉得根本不需要这样重构,因为本来代码量就不大,这样拆分增加了太多文件。很好!这样抬杠说明你有了思考,我同意你的观点,一些简单的组件根本不需要如此拆分,但是我将这种重构方法不是一种规范,不是一种强制要求,相反他是一种价值观,一种对于什么是好的代码的价值观。这种价值观归根结底就是一句话:让你的代码易于变更。 Easier To Change! 简称 ETC。


编码价值观 ETC

ETC 这种编码的价值观是很多好的编码原则的本质,比如单一职责原则,解耦原则等,他们都体现了 ETC 这种价值观念。能适应使用者的就是好的设计,对于代码而言,就是要拥抱变化,适应变化。因此我们需要信奉 ETC 。价值观念是帮助你在写代码的时候做决定的,他告诉你应该做这个?还是做那个?他帮助你在不同编码方式之间做选择,他甚至应该成为你编码时的一种潜意识,如果你接受这种价值观,那么在编码的时候,请时刻提醒自己,遵循这种价值观。


参考

来自:https://segmentfault.com/a/1190000037490145


链接: https://www.fly63.com/article/detial/9715

不要浪费时间写完美代码

一个系统可以维持5年,10年,甚至20年以上,但是代码和设计模式的生命周期非常短,当对一个解决方案使用不同的方法进行迭代的时候,通常只能维持数月,数日,甚至几分钟的时间

Google内部在代码质量上的实践

良好的编程习惯涉及到很多方面,但在软件行业内,大多数的公司或组织都不会把良好的编程习惯列为主要关注点。 例如,具有可读性和可维护性的代码比编写好的测试代码或使用正确的工具更有意义,前者的意义在于可以让代码更易于理解和修改。

减少嵌套,降低代码复杂度

减少嵌套会让代码可读性更好,同时也能更容易的找出bug,开发人员可以更快的迭代,程序也会越来越稳定。简化代码,让编程更轻松!

关于 Google 发布的 JS 代码规范

Google为了那些还不熟悉代码规范的人发布了一个JS代码规范。其中列出了编写简洁易懂的代码所应该做的最佳实践。代码规范并不是一种编写正确JavaScript代码的规则,而是为了保持源代码编写模式一致的一种选择。

你解决的问题比你编写的代码更重要!

程序员似乎忘记了软件的真正目的,那就是解决现实问题。您编写的代码的目的是为了创造价值并使现有世界变得更美好,而不是满足您对自我世界应该是什么的以自我为中心的观点。有人说:如果你拥有的只是一把锤子,那么一切看起来都像钉子一样

tinymce与prism代码高亮实现及汉化的配置

TinyMCE是一个轻量级的基于浏览器的所见即所得编辑器,由JavaScript写成。它对IE6+和Firefox1.5+都有着非常良好的支持。功能方强大,并且功能配置灵活简单。另一特点是加载速度非常快的。

js函数式编程与代码执行效率

函数式编程对应的是命令式编程, 函数式编程的核心当然是对函数的运用. 而高阶函数(Higher-order)是实现函数式编程的基本要素。高阶函数可以将其他函数作为参数或者返回结果。所以JS天生就支持函数式编程

接手代码太烂,要不要辞职?

朋友发表了一条说说:入职新公司,从重构代码到放弃”,我就问他怎么了?他说,刚进一家新公司,接手代码太烂,领导让我先熟悉业务逻辑,然后去修复之前项目中遗留的bug,实在不行就重构

js高亮显示关键词_页面、搜索关键词高亮显示

页面实现关键词高亮显示:在项目期间遇到一个需求,就是搜索关键词时需要高亮显示,主要通过正则匹配来实现页面关键词高亮显示。在搜索结果中高亮显示关键词:有一组关键词数组,在数组中筛选出符合关键字的内容并将关键字高亮

写优雅的代码,做优雅的程序员

软件工程学什么? 学计算机,写程序,做软件,当程序员。听说学计算机很辛苦? 是的,IT行业加班现象严重。在计算机世界里,技术日新月异,自学能力是程序员最重要的能力之一。选了这个专业,就要时刻保持好奇心和技术嗅觉,不能只满足于完成课内作业。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!