2024年前端最全前端架构之 React 领域驱动设计,面试前端岗

后话

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

对于面试,说几句个人观点。

面试,说到底是一种考试。正如我们一直批判应试教育脱离教育的本质,为了面试学习技术也脱离了技术的初心。但考试对于人才选拔的有效性是毋庸置疑的,几千年来一直如此。除非你有实力向公司证明你足够优秀,否则,还是得乖乖准备面试。这也并不妨碍你在通过面试之后按自己的方式学习。
其实在面试准备阶段,个人的收获是很大的,我也认为这是一种不错的学习方式。首先,面试问题大部分基础而且深入,这些是平时工作的基础。就好像我们之前一直不明白学习语文的意义,但它的意义就在每天的谈话间。

所谓面试造火箭,工作拧螺丝。面试往往有更高的要求,也迫使我们更专心更深入地去学习一些知识,也何尝不是一种好事。

// …

};

}

最小权限 ???


人为保证代码结构种,各个组成之间的最小权限,是一个好习惯

  • 所有大写字母开头的 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 {

name: string;

password: string;

constructor() {}

get nameStr() {}

changePassword() {}

}

function OOStyleFactory() {

return new OOStyle(/* … */);

}

这是面向对象风格的写法(注意,只是风格,不是指只有这个是面向对象)

function funcStyle(name, password) {

return {

name,

password,

getName() {},

changePassword() {},

};

}

这个是函数风格的写法(注意,这只是风格,这同时也是面向对象)

这两种风格的逻辑是一样的,唯一的区别,只在于可读性

不要理解错,这里的可读性,还包括对于程序而言的可读性,即:自动生成文档,自动生成代码结构或者由产品设计直接导出代码框架等功能

但是函数风格牺牲了可读性,得到了灵活性这一点,也是值得考虑的

编程其实是个权衡过程,对于我来说,我愿意

  • 在处理复杂结构时使用 面向对象 风格

  • 在处理复杂逻辑时,使用 函数 风格 各取所长,才是最佳方案!

Redux

// redux reducer

function todoApp(state, action) {

if (typeof state === “undefined”) {

return initialState;

}

// 这里暂不处理任何 action,

// 仅返回传入的 state。

return state;

}

这其实就是用函数风格实现的 面向对象 封装,没有这一步,你无法进行顶层设计!

用类的写法来转换一下 redux 的写法:

class MonistRedux {

// initial,想要不变性可以将 name,password 组合为 state

name = “”;

password = “”;

// 惰性初始化(配合工厂)

constructor() {

this.name = “”;

this.password = “”;

}

// action

changeName() {}

}

只有 函数 的封装性才受副作用限制

注意这一点,React 程序员非常容易犯的错误,就是到了 class 里面还在想纯度的问题,恨不得将每个成员函数都变成纯函数

没必要以词害意,需要融汇贯通

同样,以上例子也说明,如果你的技术栈提供直接生成对象的方案 —— 你可以只用函数直接完成面向对象和函数式的设计

function ImAClass() {  return {    // …  };}

我就说这个是类!为什么不行?

他要成员变量有成员变量,要成员函数有成员函数,封装,多态,哪个特性没有?

什么?继承?这年头还有搞面向对象的提继承?组合优于继承是常识!

抛弃了继承,你需要 this 么?你不需要,本来你就不需要 this(除了装饰器等附加逻辑,但是函数本身就能够实现附加逻辑 —— 高阶函数)

同样,你也可以综合面向对象和函数式的特点,各取所长,对你的项目进行顶层构建和底层实现

这也是很方便的

Hooks,Composition,ngModule


我们来看看上面的那个函数风格的类

像不像什么东西?

function useThisClass() {

const [val1, setVal1] = useState(0);

const [val2, setVal2] = useState(0);

useEffect(() => {}, []);

const otherObject = useOtherClass();

return { val1, setVal1, val2, setVal2, otherObject };

}

Hooks 恭喜各位,用得开心!

以 React 为例,老一代的 React 在 组件结构上是管道,也就是单向数据流,但是对于我们这些使用者来说,我们写的逻辑,基本上是放养状态,根本没有接入 React 的体系,完全游离在函数式风格以外:

老一代的 React

换句话来说,只有 React 写的代码才叫函数式风格,你写的顶多叫函数!

你没有办法把逻辑写在 React 的组件之外,注意,是完全没有办法!

好办,我逻辑全部写在顶层组件,那不就行了?

新一代的 React

(其中 s/a 指的是 stateaction

为什么要有 stateaction

为什么要在每个组件里用 s/a ?

action 其实是用命令模式,将逻辑复写为状态,以便 Context 传递,为何?

因为生命周期在组件里,setState 在组件的 this 上

换句话说,框架没有提供给你,将用户代码附加于框架之上的能力!

这个能力,叫做 IOC 控制反转,即 框架将功能控制权移交到你的手上

不要把这个类 Redux 开发模式作为最自然的开发方式,否则你会非常痛苦!

只有集成度不高的系统,才需要中介模式,才需要 MVC


之前的 React/Vue 集成度不高,没有 Redux 作为中介者 Controller,你无法将用户态代码在架构层级和 React/Vue 产生联系,并且这个层级天然应该用领域模块的思想方法来处理问题

因为框架没有这个能力,所以你才需要这些工具

所谓的状态管理,所谓的单一 Store ,都是没有 IOC 的妥协之举,并且是在完全抛弃面向对象思想的基础上强行用函数式语言解释的后果,是一种畸形的技术产物,是框架未达到最终形态之前的临时方案,不要作为核心技术去学习,这些东西不是核心技术!

回头看看 React 那些暧昧的话语,有些值得玩味:

  • Hook 使你在无需修改组件结构的情况下复用状态逻辑 (注意!是状态逻辑,不是状态,是状态逻辑一起复用,不是状态复用)

  • 我们推荐用 自定义 hooks 探索更多可能

  • 提供渐进式策略,提供 useReducer 实现大对象操作(好的领域封装哪来的操作大对象?)

他决口不提 面向对象,领域驱动,和之前的设计失误,是因为他需要顾及影响和社区生态,但是使用者不要被这些欺骗!

当你有了 hooksComposition ,Service/Module 的时候,你应该主动抛弃所有类似

  • 状态管理

  • 一定要写纯函数

  • 副作用要一起处理

  • 一定要保证不变形

这之类的所有言论,因为在你手上,不仅仅只有函数式一件武器

你还有面向对象和领域驱动

用领域驱动解决高层级问题,用函数式解决低层级问题,才是最佳开发范式

也就是说,函数式和面向对象,没有好坏,他们只是两个关注点不同的思想方法而已

总结

三套“算法宝典”

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

28天读完349页,这份阿里面试通关手册,助我闯进字节跳动

算法刷题LeetCode中文版(为例)

人与人存在很大的不同,我们都拥有各自的目标,在一线城市漂泊的我偶尔也会羡慕在老家踏踏实实开开心心养老的人,但是我深刻知道自己想要的是一年比一年有进步。

最后,我想说的是,无论你现在什么年龄,位于什么城市,拥有什么背景或学历,跟你比较的人永远都是你自己,所以明年的你看看与今年的你是否有差距,不想做咸鱼的人,只能用尽全力去跳跃。祝愿,明年的你会更好!

由于篇幅有限,下篇的面试技术攻克篇只能够展示出部分的面试题,详细完整版以及答案解析,有需要的可以关注

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值