最后
其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)
《前端开发四大模块核心知识笔记》
最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。
function SomeComponent() {
return (
<GlobalService.Provider value={useGlobalService()}></GlobalService.Provider>
);
}
function useSomeService() {
const globalService = useContext(GlobalService);
return
}
上下文注入节点,本身就是按照试图来的
函数 DDD
只用函数实现 DDD,它有多优美
我们先比较一下这两种写法,对于一个类:
class SomeClass {
name:string,
password:string,
constructor(name,password){
this.name = name
this.password = password
}
}
const initValue = { name: “”, password: “” };
function useClass() {
const [state, setState] = useState(initValue);
return { state, setState };
}
下面的自带响应式,getter,setter 也自动给出了,同时使用了工厂模式,不需要了解函数内部的逻辑。
生命周期复用
每个 useFunc 都是 拆掉 的管道函数,框架帮你组装,简直就是一步到位!
效率
–
function useSomeService() {
const [form] = useForm();
const request = useRequest();
const model = useModel();
useEffect(() => {
form.onFieldsChange = () => {
request.run(form.getFieldsValue);
};
}, [form]);
return {
model,
form,
};
}
<Form.Item name=“xxx” label=“xxx”>
</Form.Item>
这个表单服务你想要在哪控制?
想要多个组件同时控制?
加个 token,也就是 createContext,把依赖提上去!
他特么自然了!
React Hooks 版本架构
================
执行 LIFT 原则
-
顶层文件夹最多包含:assets,pages,layouts,app 四个(其中 pages,layouts 是为了照顾某些 ssr 开发栈),名字可以变更,但是不可以有多余文件夹,激进的话可以只有一个 app 文件夹
-
按功能划分文件夹,每个功能只能包含以下四种文件:Xxx.less, Xxx.tsx, useXxx.ts,useXxx.spec.ts , 采用嵌套结构组织
-
一个文件夹包含该领域内所有逻辑(视图,样式,测试,状态,接口),禁止将逻辑放置于文件夹以外
-
如果需要由其他功能调用,利用 SOA 反转
为何如此?
-
功能结构即文件结构,开发人员可以快速定位代码,扫一眼就能知道每个文件代表什么,目录尽可能保持扁平,既没有重复也没有多余的名字
-
当有很多文件时(例如 10 个以上),在专用目录型结构中定位它们会比在扁平结构中更容易
-
惰性加载可路由的功能变得更容易
-
隔离、测试和复用特性更容易
-
管理上,相关领域文件夹可以分配给专人,开发效率高,可追责和计量工作量,很明显应该禁止多人同时操作同一层级文件
-
只需要对 useXxx 进行测试,测试复杂度,工作量都很小,视图测试交给 e2e
利用 SOA 实现跨组件逻辑复用
利用 注入令牌
+ 服务函数
+ 注入点
,实现灵活的 SOA
命名格式为
XxxService = useToken(useXxxService)
XxxService
为注入令牌 和文件名
useXxxService
为服务函数
<XxxService.Provider value={useXxxService()} />
XxxService.Provider
为注入点
注入令牌与服务函数紧挨
与注入节点处于同一文件结构层级
禁止除 SOA 以外的所有数据源
为何如此?
-
符合单一数据,单以职责,接口隔离原则
-
通过泛型约束,可以有更加自然的 Typescript 体验,不需要手动声明注入数据类型,所有类型将自动获得
-
层次化注入,可以实现 DDD,将逻辑全部约束与一处,方便团队协作
-
当你在根注入器上提供该服务时,该服务实例在每个需要该服务的组件和服务中都是共享的。当服务要共享方法或状态时,这是数学意义上的最理想的选择。
-
配合组件和功能划分,可以方便处理嵌套结构,防止对象复制被滥用,类似深复制之类的操作应该禁止
实现一个 IOC 注入令牌的方法为
import { createContext } from ‘react’;
/**
* 泛型约束获取注入令牌
* @export
* @template T
* @param {(…args: any[]) => T} func
* @param {(T | undefined)} [initialValue=undefined]
* @returns
*/
export default function useToken(
func: (…args: any[]) => T,
initialValue: T | undefined = undefined,
) {
return createContext(initialValue as T);
}
一个典型可注册服务为:
import { useState } from “react”;
import useToken from “./useToken”;
export const AppService = useToken(useAppService);
export default function useAppService() {
// 可注入其他服务,以嵌套
// eq:
// const someOtherService = useSomeOtherService(SomeOtherService)
const [appName, setAppName] = useState(“appName”);
return {
appName,
setAppName,
// …
};
}
最小权限 ???
人为保证代码结构种,各个组成之间的最小权限,是一个好习惯
-
所有大写字母开头的 tsx 文件都是组件
-
所有 use 开头的文件,都是服务,其中,useXxxService 是可注入服务,默认将所有组件配套的服务设置为可注入服务,可以方便进行依赖管理
-
禁止在组件函数种出现任何非服务注入代码,禁止在组件中写入与视图不想关的
-
为复杂结构数据定义 class
-
如果可以的话,将单例服务由全局 service 组织,嵌套结构,共享实例,页面初始化 除外
-
❌ 禁止深复制
为何如此?
-
当逻辑被放置到服务里,并以函数的形式暴露时,可以被多个组件重复使用
-
在单元测试时,服务里的逻辑更容易被隔离。当组件中调用逻辑时,也很容易被模拟
-
从组件移除依赖并隐藏实现细节
-
保持组件苗条、精简和聚焦
-
利用 class 可以减少初始化复杂度,以及因此产生的类型问题
-
局管理单例服务,可以一步消灭循环依赖问题(道理同 Redux 替代 Flux)
-
深复制有非常严重的性能问题,且容易产生意外变更,尤其是 useEffect 语境下
JUST USE REACT HOOKS
抛弃 class 这样的,this 挂载变更的历史方案,不可复用组件会污染整个项目,导致逻辑无法集中于一处,甚至出现耦合, LIFT,SOA,DDD 等架构无从谈起
项目只存在
-
大写并与文件同名的组件,且其中除了注入服务操作外,return 之前,无任何代码
-
use 开头并与文件夹同名的服务
-
use 开头,Service 结尾,并与文件夹同名的可注入服务
-
服务中只存在 基础 hooks,自定义 hooks,第三方 hooks,静态数据,工具函数,工具类
以下为细化阐述为何如此设计的出发点
-
快速定位
Locate
-
一眼识别
Identify
-
尽量保持扁平结构 (Flattest)
-
尝试
Try
遵循DRY
(Do Not Repeat Yourself
, 不重复自己)
此为 LIFT 原则
-
优先将组件视为元素,而并非功能逻辑单位(视图的归视图,业务的归业务)
-
隔离原则(属于一个成员的工作,必定属于该成员负责的文件夹,也只能属于该成员负责的文件夹)
-
最小依赖(禁止不必要的工具使用,比如当前需求下,引入 Redux/Flux/Dva/Mobx 等工具,并没有解决什么问题,却导致功能更加受限,影响隔离原则比如当两个组件需要服务的不同实例的情况,以上工具属于上个版本或某种特殊需求,比如前后端同构,不能影响这个版本当前需求的架构)
-
优先响应式(普及管道风格的函数式方案,大胆使用 useEffect 等 api,不提倡松散的函数组合,只要是视图所用的数据,必须全部都为响应式数据,并响应变更)
-
测试友好(边界清晰,风格简洁,隔离完整,即为测试友好)
-
设计友好(支持模块化设计)
建议的技术栈搭配
-
create-react-app + react-router-dom + antd + ahooks + styled-components (大多数场景下,强烈推荐!可以上 ProComponent,但是要注意提取功能逻辑,不可将逻辑写于组件)
-
umi + ahooks (请删除 models,services,components,utils 等非必要顶层文件夹,禁止使用 dva)
-
umi (ssr) + dva + ahooks(同上,但可仅基于 dva 沟通前后端和首屏数据,非 ssr 同样禁用 dva)
-
next.js + react suite/material ui + swr(利用不到 useAntdTable 之类的功能,ahooks 就鸡肋了)
Hook 使你在无需修改组件结构的情况下复用状态逻辑
==========================
当你思维聚焦于组件时,在这种情况下,你是必须逼迫自己,在组件里写业务逻辑,或者重新组织业务逻辑!???
并且,因为 state 是不反应业务逻辑的,它也天然不可以对业务逻辑进行组合
function useSomeTable() {
// 这个是个表单,抽象的
const [form] = Form.useForm();
// 这个是个表单联动的表格
const { tableProps, loading } = useAntdTable(
// 自动处理分页相关问题
({ curren, pageSize }, formData) => fetch(“http://sdfdsfsdf”), // 抽象的状态请求
{
form, // 表单在这里与表格组合,实现联动
defaultParams: {
// …
},
// 很多功能都能集成,只需要一个配置项
debounceInterval: 300, // 节流
}
);
return {
form,
loading,
tableProps,
};
}
你能将视图和逻辑完全组织为一个结构,交给一个特定的人,完全不用关心他到底是怎么开发的
这便是 —— 逻辑视图分离???
React SOA
=========
基本的服务
function useSimpleService() {
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
useEffect(() => {
setVal2(val1);
}, [val1]);
return {
val1,
setVal1,
val2,
};
}
-
叫它 service,是 SOA 模型下的管用叫法,意思是 —— 我只会在这样的结构种写逻辑,组件中的逻辑全部消失(优先将组件视为元素)
-
只暴露你需要暴露的状态逻辑(状态逻辑必须一起说,只做状态复用很扯淡,毕竟 2021 年了)
-
useRef,同样也可以封装在 Service 中,而且建议如此做,ref 的获取不是视图,是逻辑
组合服务
有另外一个服务,useAnotherService
function userAnotherService() {
const [val, setVal] = useLocalstorage(0);
return { vale, setVal };
}
然后与基本服务进行组合
function useSimpleService() {
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
const { setVal } = userAnotherService();
useEffect(() => {
setVal2(val1);
}, [val1]);
useEffect(() => {
setVal(val2);
}, [val2]);
return {
val1,
setVal1,
val2,
};
}
就能为基本服务动态添加功能
-
为什么不直接
import
?因为需要框架内的响应式能力,这个叫控制反转,框架将响应式的控制权转交给了开发者 -
如果有另外一个服务,单单只要
AnotherService
的功能,你只需要调用useAnthor Service
就好了 -
最好是调用者修改被调用者,可以对比
ahooks
对代useRef
的改动,就是本着这个次序,因为被调用者可能被多次调用,保证复用性 -
useEffect
是一种管道模型,如同rxjs
一般,只是框架帮你按顺序组装而已(你以为为啥非要你按顺序来?),是极限的函数式方案,不存在纯度问题,函数式得不要不要的。但是有个要求,依赖必须写清楚,这个依赖是管道操作中的参数,React
将你的hook
重新组合成了管道,但是参数必须提供,在它能自动分析依赖之前 -
使用了
useAnotherService
的细节被隐藏,形成了一个树形调用结构,这种结构被称作 “依赖树” 或者 “注入树”,别盯着我,名字不是我定的
注入单例服务
当前服务如果需要被多个组件使用,服务会被初始化很多次,如何让它只注入一次?
利用 createContext
export const SimpleService = createContext(null);
export default function useSimpleService() {
// …
}
但是,单例需要注入到唯一节点,因此,你需要在所有需要用到这个服务的组件的最顶层:
<SimpleService.Provider value={useSimpleService()}>
{props.children}
</SimpleService.Provider>
这样,这个服务的单例就对所有子孙组件敞开了怀抱,同时,所有子孙组件对其的修改都将生效
function SomeComponent(){
const {val1,setVal1} = useContext(SomeService)
return <div onClick={()=>{setVal1(‘fuck’)}>val1
}
-
直接在 jsx 的 provider 种 value = {useSomeService ()} 在本组件没有任何其它响应式变量的情况下是可行的,因为不会重新初始化,在良好的架构下 —— 组件除注入,无任何逻辑,return 之前没有东西,同时,上下文单独封装组件,可以作为 “模块标识”
-
这个有共同单例 Service 的一系列组件,被称为模块,它们有自己的 “限界上下文”,并且,视图,逻辑,样式都在其中,如果这个模块是按照功能划分的,那么这种 SOA 实现被称为 领域驱动设计 (DDD) ,某些架构强推的所谓’微前端’,目的就是得到这个东西
-
一定要注意,这个模块的上层数据变更,模块的限界上下文会刷新,这个是默认操作,这也是为何 jsx 直接赋值 的原因,如果你不需要这个东西,可以采用 const value = useService () 包裹,或者直接 memo 这个模块标识组件
单例服务,解决深层嵌套对象问题
深层嵌套对象怎么处理?useReducer?immutable? 还是直接深复制?
你首先明白你要实现什么逻辑,深层嵌套对象之所以难处理,是因为你想在子组件实现 对深层目标的部分变更逻辑
之前你之所以有这些奇奇怪怪工具甚至深复制的需求,是因为你没有办法将逻辑也拆分给子组件,明白为什么如此
现在,逻辑可以拆分复用:
function useSomeService() {
const [value, setValue] = useState({
username: “”,
password: “”,
info: {
nickname: “”,
others: [],
},
});
return { value, setValue };
}
// 注入部分省略…
修改 info:
setValue((res) => {
res.info.nickname === “fuck”;
return res;
});
配合 map 修改数组:
// 分形部分:
new Array(5).map((_, key) => );
// 组件
function SomeComponent(props) {
const { setValue } = useContext(SomeService);
return (
onClick={() => {
setValue((res) => {
res.info.others[props.index] = “fuck”;
return res;
});
}}
);
}
如果需要划分模块,通过 getter,setter 传递这个嵌套结构:
function subInjectedService() {
const { value, setValue } = useContext(SomeService);
const info = useMemo(() => value.info.others, [value]);
const setInfo = useCallback((val) => {
setValue((res) => {
res.info.others[props.index] = val;
return res;
});
}, []);
return {
info,
setInfo,
};
}
// 忽略注入部分…
这样的话,这个重新划分的模块内部,想要修改上层的数据,只需要通过 info,setInfo 即可
-
不用担心纯度和不变性的问题,因为 hooks 都是纯的,没有不纯的情况
-
全局副作用是状态 + 函数全局逻辑封装(分层)考虑的问题,将函数和组件,视图功能逻辑样式全部作为模块,副作用是以模块为单位的,而 info 和 setInfo 的 getter,setter 封装,叫做 —— 模块间通讯
-
useReducer 只涉及调试,也就是有个 action 名字方便你定位问题,模块划分如果足够细,你根本不需要这个 action 来记录你的变更,采用 useReducer 与 DDD 原则背离,但是也不会禁止。不过,全局 useReducer 必须明令禁止,这种方式是个灾难,useReducer 必须是以模块为单位,不能更小,也不能更大
-
组件和服务一起,处理一部分数据,保证了单例修改,不变性也不用担心,hooks 来保证这个
-
在这里,你会发现 props 的功能好像只有’分形’,也就是 map 种将数据的标识传递给子组件,是的 —— 优先使用服务共享状态逻辑
-
getter,setter 叫做响应式,如果你不需要响应式修改,setter 可以删除,但是 getter 同时还有防止重新渲染的作用,保留即可,除非纯组件
服务获取时的类型问题
如果你使用的是 Typescript ,那么,用泛型约束获得自动类型推断,会让你如虎添翼
import { createContext } from ‘react’;
/**
* 泛型约束获取注入令牌
* @export
* @template T
* @param {(…args: any[]) => T} func
* @param {(T | undefined)} [initialValue=undefined]
* @returns
*/
export default function useToken(
func: (…args: any[]) => T,
initialValue: T | undefined = undefined,
) {
return createContext(initialValue as T);
}
然后将 createContext()
改为 useToken(SomeService)
即可,这样你就拥有了指哪打哪的类型支持,无需单独的类型声明,代码更加清爽
-
如果是
Javascript
环境,建议老老实实写createContext
的defaultValue
,虽然注入之后,子孙组件都不会出现defaultValue
,但是javascript
语境下有代码提示 -
不建议
typescript
下声明defaultValue
,因为模块外的服务调用,应该被禁止,这是DDD
架构的基础,如果你想要在外部使用单例服务 —— 请将其提升至外部
顶层注入服务
平凡提升模块服务层级,可能会产生循环依赖,而且会影响模块的封装度,因此:
⚠️优先思考清楚自己应用的模块关系!
循环依赖产生根源是功能领域,功能模块划分有问题,优先解决根本问题,而不是转移矛盾。如果你实在思考不清楚,又想要立刻开始开发,那么可以尝试顶层注入服务:
function useAppService(){
return {
someService:useSomeService()
anotherService:useAnotherService()
}
}
-
模块间进行嵌套组合将变得无比困难,不再是一个 getter,setter 能够搞定的,如果不是绝对的必要,尽量不要采用此种方式!它有悖于 DDD 原则 —— 分治
-
多组件共享不同实例将彻底失败,这不是你愿意看到的
可选服务
模块服务划分的另一个巨大优势,就是将逻辑变为可选项,这在重型应用中,几乎就是采用 DDD 的关键
function useServiceByOneLogic() {
return {
activated,
// …
};
}
function useServiceByAnotherLogic() {
return {
activated,
// …
};
}
function useSomeService() {
const […servicList] = [useServiceByOneLogic(), useServiceByAnotherLogic()];
// 选择激活的服务
const usedService = useMemo(() => {
for (let service of serviceList) {
if (service.activated === true) {
return service;
}
}
}, [serviceList]);
return service;
}
// 注入过程省略…
-
你也可以通过各种条件筛选服务,这种方式是在前端实现的高可用
-
⚠ 注意,服务最好只是内部实现不同,接口应该尽可能相同,否者会出现可选类型
-
最典型的应用,就是多家云服务厂商的短信验证(验证码,人机校验等),通过可选服务根据用户网络情况进行筛选,用最适合当前用户的那一个
-
还有一个非常有意思的方案,通过服务来做数据 mock,因为服务直接对接视图,你只需要模拟视图数据即可,提供两个服务,一个真实服务,一个 mock 服务,这样是用真实数据还是 mock 数据,都是服务自动判断的,对你来说没有流程差别
样式封装
注意,模块是包含了样式的,上文在讲述逻辑和视图的封装,接下来说说样式
-
典型的 cssModule, styled-components 之类的方案
-
shadowDom,仿真样式(Angular 原生支持,React 可以用 cssModule 之类工具间接实现),可以实现跨技术栈样式封装(没错,所谓 ‘微前端’ 的样式封装)
-
样式最好只包含排版,企业 vis 统一性是标准,没有必要违背这个
继续分析 SOA
========
从上一篇文章的例子可以看出什么呢?
首先,按照功能领域划分文件,可以很快分析出应用的逻辑结构
也就是逻辑可读性更强,这个可读性不只是针对用户的,还有针对软件的
比如,TodoService
和 TableHandlerService
有什么关系?
useTableHandlerService
useTableHandlerService
useTodoService
这些逻辑关系,仅仅依靠相关工具就能定位,并生成图形,辅助你分析领域间的关系
谁依赖谁,一目了然 —— 比如 有个 useState
的值 依赖 useLocalStorageState
,肉眼看起来比较困难,但是在图中一目了然
只是,不具名这一点有点神烦!
还有,React
内部因为没有管理好这个部分传递,没办法像 Angular
一样,瞬间生成一大堆密密麻麻的依赖树,这就给 React
在大项目工程化上带来了阻碍
不过一般项目做不到那么大,领域驱动可以帮助你做到 Angular
项目极限的 95%,剩下那 5%,也只是稍稍痛苦些而已,并且,没有办法给管理者看到完整蓝图
不过就国内目前前端技术管理者和产品经理的水品,你给他们看 uml 蓝图,我担心他们也看不懂,所以这部分不用太在意,感觉有地方依赖拿不准,只显示这个领域的蓝图就好
其次,测试边界清晰,且易于模拟
视图你不用测试,因为没有视图逻辑,什么时候需要视图测试?比如 Form
和 FormItem
等出现嵌套注入的地方,需要进行视图测试,这部分耦合出现的概率非常小,大部分都是第三方框架的工作
你只需要测试这些 useFunction
就好,并且提供两个个框,比如空组件直接 use
,嵌套组件先 provide
再 useContext
,然后直接只模拟 useFunction
边界,并提供测试,大家可以尝试一下,以前觉得测试神烦,现在可以试试在清晰领域边界下,测试可以有多愉悦
最后
谁再提状态管理我和谁急!
你看看这个应用,哪里有状态管理插手的地方?任何状态管理库都不行,它是上个时代的遮羞布
服务间通讯结构
=======
全局单一服务(类 Redux
方案)
但是,单一服务是不得已而为之,老版本没有逻辑复用导致的
在这种方式下,你的调试将变得无比复杂,任何一处变更将牵扯所有本该封装为模块的组件
所以必须配合相应的调试工具
所有多人协作项目,采用此种方式,最后的结果只有项目不可维护一条路!
中台 + 其他服务(双层结构)
由一个,appService
提供基础服务,并管理服务间的调度,此种方式比第一种要好很多,但是还是有个问题,顶层处理服务关系,永远比服务间处理服务关系来的复杂,具体问题详见上文 “顶层注入”
树形结构模块
这是理论最优的结构,它的优势不再赘述,上文有提到
劣势有一个:
跨模块层级的变更,容易形成循环依赖(也不叫劣势,因为此种变更对于其他方式来说,是灾难)
理清自己的业务逻辑,有必要划出功能结构图,再开始开发,是个好习惯,同时,功能层级发生改变,应该敏锐意识到,及时提升服务的模块层级即可
编程范式
====
首先,编程范式除了实现方式不同以外,其区别的根源在于 – 关注点不同
-
函数的关注点在于 —— 变化
-
面向对象的关注点在于 —— 结构
对于函数,因为结构方便于处理变化,即输入输出是天然关注点,所以 ——
管理状态
和副作用
很重要
js
var a = 1;
function test© {
var b = 2;
a = 2;
var a = 3;
c = 1;
return { a, b, c };
}
这里故意用 var 来声明变量,让大家又更深的体会
在函数中变更函数外的变量 —— 破坏了函数的封装性???
这种破坏极其危险,比如上例,如果其他函数修改了 a,在 重新 赋值之前,你知道 a 是多少么?如果函数很长,你如何确定本地变量 a 是否覆盖外部变量?
无封装的函数,不可能有可装配性
和可调试性
所以,使用函数封装逻辑,不能引入任何副作用!注意,这个是强制的,在任何多人协作,多模块多资源的项目中 ——
封装是第一要务,更是基本要求❗
所以,你必须将数据(或者说状态)全部包裹在函数内部,不可以在函数内修改任何函数以外的数据!
所以,函数天然存在一个缺点 —— 封装性需要人为保证(即你需要自己要求自己,写出无副作用函数)
当然,还存在很多优点 —— 只需要针对输入输出测试,更加符合物体实际运行情况(变化是哲学基础)
这部分没有加重点符号,是因为它不重要 —— 对一个思想方法提优缺点,只有指导意义,因为思想方法可以综合运用,不受限制
再来看看面相对象,来看看类结构:
class Test {
a = 1;
b = 2;
c = 3;
constructor() {
this.changeA();
}
changeA() {
this.a = 2;
}
}
这个结构一眼看去就具有 —— 自解释性,自封装性
还有一个涉及应用领域的优势 —— 对观念系统的模拟 —— 这个词不打着重符,不需要太关心,翻译过来就是,可以直译人脑中的观念(动物,人,车等等)
但它也有非常严重的问题 —— 初始化,自解耦麻烦,组合麻烦
需要运用到大量的’构建’,’运行’设计模式!
对的,设计模式的那些名字就是怎么来的
其实,你仔细一想,就能明白为什么会这样 ——
如果你关注变化,想要对真实世界直接模拟,你就需要处理静态数据,需要自己对一个领域进行人为解释 如果你关注结构,想要对人的观念进行模拟,你就需要处理运行时问题,需要自己处理一个运行时对象的生成问题
鱼与熊掌,不可兼得,按住了这头,那头就会翘起来,你按住了那头,这头就会翘起来
想要只通过一个编程范式解决所有问题,就像用手去抓沙子,最后你什么都得不到
极限的函数式,面向对象
通过函数和对象(注意是对象,类是抽象的,观念中的对象)的分析,很容易发现他们的优势
函数 —— 测试简单,模拟真实(效率高)
对象 —— 自封装性,模拟观念(继承多态)
将两者发扬光大,更加极限地使用,你会得到以下衍生范式:
管道 / 流
既然函数只需要对输入输出进行测试,那么,我将无数函数用函数串联起来,就形成了只有统一输入输出的结构
听不懂?换个说法 ——
只需要 e2e 测试,不需要单元测试!
如果我加上类型校验,就可以构造出 —— 理想无 bug 系统
这样的话,你就只剩调试,没有测试(如果顶层加个校验取代 e2e 的话)
而且,还有模式识别,异步亲和性等很多好处,甚至可以自建设计语言(比如麻省老教材《如何设计计算机语言》就是以 lisp 作为标准)
在 js 中, Cycle.js 和 Rxjs 就是极限的管道风格函数式,还有大家熟悉并且讨厌的 Node.js 的 Stream 也是如此,即便他是用 类 实现的,骨子里也是浓浓的函数式
分析一下这样的系统,你会发现 ——
它首先关注底层逻辑 —— 也就是 or/c , is-a,and/c,not/c 这样的函数,最后再组装
按照范畴学的语言(就是函数式的数学解释,不想看这个可以不看,只是补充说明):
范畴学
u of i2,i2 of g 的讲法,与它的真实运行方向,是相反的!
函数的组合方式,与开发目标的构建方式,也是相反的!
它的构建方法叫做 —— 自底向上
这也是为啥你在很多 JS 的库中发现了好多零零碎碎的东西,还有为何会有 lodash,ramda 等粒度非常小的库了
在极限函数式编程下 ——
我先做出来,再看能干什么,比先确定干什么,再做,更重要!
因为这部分,可以第三方甚至官方自己提供!
所以,函数式是库的第一优先级构建范式!因为作为库的提供者,你根本不可能预测用户会用这个库来干什么
领域模块
函数式可以将其优势通过管道发挥到极致,面向对象一样可以将其优势发挥到极致,这便是领域模块
领域,就是一系列相同目的,相同功能的资源的集合
比如,学校,公司,这两个类,如果分别封装了大量的其他类以及相关资源,共同构成一个整体,自行管理,自行测试,甚至自行构建发布,对外提供统一的接口,那这就是领域
这么说,如果实现了一个类和其相关资源的自行管理,自行测试,这就是 —— DDD
如果实现了对其的自行构建发布,这就是 —— 微服务
这种模型给了应用规模化的能力 —— 横向,纵向扩展能力
还有高可用,即类的组合间的松散耦合范式
对于这样的范式,你首先思考的是 —— 你要做什么!
这就是 ——
** 这种模型给了应用规模化的能力 —— 横向,纵向扩展能力
还有高可用,即类的组合间的松散耦合范式
对于这样的范式,你首先思考的是 —— 你要做什么!
这就是 —— 自顶向下
-
我要做什么应用?
-
这个应用有哪些功能?
-
我该怎么组织我的资源和代码?
-
该怎么和其他职能合作?
-
工期需要多久?
现实告诉你,单用任何一种都不行 开发过程中,不止有自底向上封装的工具,还有自顶向下设计的结构
产品经理不会把要用多少个 isObject 判断告诉你,他只会告诉你应用有哪些功能
同理,再丰富细致的功能划分,没有底层一个个工具函数,也完成不了任何工作
这个世界的确处在变化之中,世界的本质就是变化,就是函数,但是软件是交给人用,交给人开发的
观念系统和实际运行,缺一不可!
凡是动不动就跟你说某某框架函数式,某某应用要用函数式开发的人,大多都学艺不精,根本没有理解这些概念的本质
人类编程历史如此久远,真正的面向用户的纯粹函数式无 bug 系统,还没有出现过……
当然,其在人工智能,科研等领域有无可替代的作用。不过,是人,就有组织,有公司,进而有职能划分,大家只会通过观念系统进行交流 —— 你所说的每一个词汇,都是观念,都是类!
React 提倡函数式
class OOStyle {
总结
根据路线图上的重点去进行有针对性的学习,在学习过程中,学会写笔记,做总结。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
这里分享一些前端学习笔记:
-
html5 / css3 学习笔记
-
JavaScript 学习笔记
-
Vue 学习笔记