重构过程当中,确定会遇到新的代码如何作技术选型的问题,要考虑到这套技术的生命力,也就是他是不是更新的技术,还有他的灵活和拓展性,指望可以达到在将来至少 3 年内不须要作大的技术栈升级。个人此次重构经历是把 jQuery 的代码变为 React ,你品品,算是最难,劳动最密集的重构任务了吧。看多了以前代码动辄上千行的 Class ,混乱的全局变量使用,愈来愈以为,代码必定要写的简单,不要使用过多的黑科技,尤为是各类设计模式,为了复用而迭代出来的海量 if 判断。代码不是给机器看的,是给人看的,他须要让后来人快速的看懂,还要能让别人在你的代码的基础上快速的迭代新的需求。因此咱们须要想清楚,用什么技术栈,怎么组织代码。javascript
对于 Class Component 和 Function Component 之争由来已久。从我自身的实践来看,我以为这是两种不一样的编程思路。css
Class Component | 面向对象编程 | 继承 | 生命周期 |
---|---|---|---|
Function Component | 函数式编程 | 组合 | 数据驱动 |
首先,若是咱们使用面向对象这种编程方式,咱们要注意,他不仅是定义一个 Class 那么简单的事情,咱们知道面向对象有三大特性,继承,封装,多态。前端
首先前端真的适合继承的方式吗?准确的说,UI 真的适合继承的方式吗?在真实世界里,抽象的东西更适合定义成一个类,类原本的意思就是分类和类别,正如咱们把老虎,猫,狮子这些生物统称为动物,因此咱们就能够定义一个动物的类,可是真实世界并无动物这种实体,可是页面 UI 都是真实存在能够看到的东西,咱们能够把一个页面分红不一样的区块,而后区块之间采用的是「组合」的方式。所以我认为 UI 组件不适合继承,更应该组合。若是你写过继承类的组件,你将很难去重构,甚至是重写他。java
封装讲究使用封装好的方法对外暴露类中的属性,可是咱们的组件基本是经过 props 暴露内部事件和数据,经过 Ref 暴露内部方法,本质上并无使用封装的特性。react
多态就更少用了,多态更可能是基于接口,或者抽象类的,可是 JS 这块比较弱,用 TS 或许会好一些。程序员
综上,做为前端 UI 编程,我更倾向于使用函数组合的方式。编程
不管是在 React 或者在 Vue 里,都讲究数据的变化,数据与视图的绑定关系,数据驱动,数据的变化引发 UI 的从新渲染,可是生命周期在描述这个问题的时候,并不直接,在 Class Component 里,咱们如何检测某个数据的变化呢,基本是用 shouldUpdate 的生命周期,为何咱们在编程的时候,正在关注数据和业务的时候,还要关心一个生命周期呢,这部份内容对于业务来讲更像是反作用,或者不该该暴露给开发者的。设计模式
综上,是我认为 Function Component + Hooks 编程体验更好的地方,可是这也只是一个相对片面的角度,并无好坏之分,毕竟连 React 的官方都说,两种写法没有好坏之分,性能差距也几乎能够忽略,并且 React 会长期支持这两种写法。数组
究竟是什么是响应式编程?你们各执一词,模模糊糊,懵懵懂懂。不少人没有把他的本质说明白。从我多年的编程经验来看,响应式编程就是「使用异步数据流编程」。咱们来看看前端在处理异步操做的时候一般是怎么作的,常见的异步操做有异步请求和页面的鼠标操做事件,在处理这样的操做的时候,咱们一般采起的方法是事件循环,也就是异步事件流的方式。可是事件循环并无显式的解决事件依赖问题,而是须要咱们本身在编码的时候作好调用顺序的管理,好比:antd
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 包含哪些功能模块:
首先,咱们把上面讲到的功能模块拆分红多个文件:
|— 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;
看到上面是基于步骤和细节分离的思路,将上面的组件作了一次重构,只包含两步:
经过拆分之后主文件代码里就只包含一些步骤了,所有使用自定义的 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 。价值观念是帮助你在写代码的时候作决定的,他告诉你应该作这个?仍是作那个?他帮助你在不一样编码方式之间作选择,他甚至应该成为你编码时的一种潜意识,若是你接受这种价值观,那么在编码的时候,请时刻提醒本身,遵循这种价值观。
文章可随意转载,但请保留此原文连接。
很是欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com 。