重构过程中,必定会遇到新的代码如何做技术选型的问题,要思考到这套技术的生命力,也就是他是否是更新的技术,还有他的灵便和拓展性,冀望可能达到在将来至多 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://zhuanlan.zhihu.com/p/27678951
- 《程序员修炼之道》Andrew Hunt,David Thomas
文章可随便转载,但请保留此原文链接。
十分欢送有激情的你退出 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com。