)
}
}
}
```
- **反向继承** (Inheritance Inversion): 返回出一个组件,**继承于被包裹组件**,常用于以下操作:
```js
function IIHoc(Comp) {
return class extends Comp {
render() {
return super.render();
}
};
}
```
- **渲染劫持** (Render Highjacking)
- **条件渲染**: 根据条件,渲染不同的组件
```js
function withLoading(Comp) {
return class extends Comp {
render() {
if(this.props.isLoading) {
return
} else {
return super.render()
}
}
};
}
```
- 可以直接修改被包裹组件渲染出的 React 元素树
- **操作状态** (Operate State): 可以直接通过 `this.state` 获取到被包裹组件的状态,并进行操作。但这样的操作容易使 state 变得难以追踪,不易维护,谨慎使用。
- **应用场景**:
- **权限控制**,通过抽象逻辑,统一对页面进行权限判断,按不同的条件进行页面渲染:
```js
function withAdminAuth(WrappedComponent) {
return class extends React.Component {
constructor(props){
super(props)
this.state = {
isAdmin: false,
}
}
async componentWillMount() {
const currentRole = await getCurrentUserRole();
this.setState({
isAdmin: currentRole === 'Admin',
});
}
render() {
if (this.state.isAdmin) {
return
;
} else {
return (
您没有权限查看该页面,请联系管理员!
);
}
}
};
}
```
- **性能监控**,包裹组件的生命周期,进行统一埋点:
```js
function withTiming(Comp) {
return class extends Comp {
constructor(props) {
super(props);
this.start = Date.now();
this.end = 0;
}
componentDidMount() {
super.componentDidMount && super.componentDidMount();
this.end = Date.now();
console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);
}
render() {
return super.render();
}
};
}
```
- **代码复用**,可以将重复的逻辑进行抽象。
- 使用注意:
- 1. **纯函数**: 增强函数应为纯函数,避免侵入修改元组件;
- 2. **避免用法污染**: 理想状态下,应透传元组件的无关参数与事件,尽量保证用法不变;
- 3. **命名空间**: 为 HOC 增加特异性的组件名称,这样能便于开发调试和查找问题;
- 4. **引用传递**: 如果需要传递元组件的 refs 引用,可以使用`React.forwardRef`;
- 5. **静态方法**: 元组件上的静态方法并无法被自动传出,会导致业务层无法调用;解决:
- 函数导出
- 静态方法赋值
- 6. **重新渲染**: 由于增强函数每次调用是返回一个新组件,因此如果在 Render 中使用增强函数,就会导致每次都重新渲染整个HOC,而且之前的状态会丢失;
5. Redux
Redux 是一个 **数据管理中心**,可以把它理解为一个全局的 data store 实例。它通过一定的使用规则和限制,保证着数据的健壮性、可追溯和可预测性。它与 React 无关,可以独立运行于任何 JavaScript 环境中,从而也为同构应用提供了更好的数据同步通道。
- **核心理念**:
- **单一数据源**: 整个应用只有唯一的状态树,也就是所有 state 最终维护在一个根级 Store 中;
- **状态只读**: 为了保证状态的可控性,最好的方式就是监控状态的变化。那这里就两个必要条件:
- Redux Store 中的数据无法被直接修改;
- 严格控制修改的执行;
- **纯函数**: 规定只能通过一个纯函数 (Reducer) 来描述修改;
- 大致的数据结构如下所示:

- **理念实现**:
- **Store**: 全局 Store 单例, 每个 Redux 应用下只有一个 store, 它具有以下方法供使用:
- `getState`: 获取 state;
- `dispatch`: 触发 action, 更新 state;
- `subscribe`: 订阅数据变更,注册监听器;
```js
// 创建
const store = createStore(Reducer, initStore)
```
- **Action**: 它作为一个行为载体,用于映射相应的 Reducer,并且它可以成为数据的载体,将数据从应用传递至 store 中,是 store **唯一的数据源**;
```js
// 一个普通的 Action
const action = {
type: 'ADD_LIST',
item: 'list-item-1',
}
// 使用:
store.dispatch(action)
// 通常为了便于调用,会有一个 Action 创建函数 (action creater)
funtion addList(item) {
return const action = {
type: 'ADD_LIST',
item,
}
}
// 调用就会变成:
dispatch(addList('list-item-1'))
```
- **Reducer**: 用于描述如何修改数据的纯函数,Action 属于行为名称,而 Reducer 便是修改行为的实质;
```js
// 一个常规的 Reducer
// @param {state}: 旧数据
// @param {action}: Action 对象
// @returns {any}: 新数据
const initList = []
function ListReducer(state = initList, action) {
switch (action.type) {
case 'ADD_LIST':
return state.concat([action.item])
break
defalut:
return state
}
}
```
> **注意**:
>
> 1. 遵守数据不可变,不要去直接修改 state,而是返回出一个 **新对象**,可以使用 `assign / copy / extend / 解构` 等方式创建新对象;
> 2. 默认情况下需要 **返回原数据**,避免数据被清空;
> 3. 最好设置 **初始值**,便于应用的初始化及数据稳定;
- **进阶**:
- **React-Redux**: 结合 React 使用;
- `
`: 将 store 通过 context 传入组件中;
- `connect`: 一个高阶组件,可以方便在 React 组件中使用 Redux;
- 1. 将`store`通过`mapStateToProps`进行筛选后使用`props`注入组件
- 2. 根据`mapDispatchToProps`创建方法,当组件调用时使用`dispatch`触发对应的`action`
- **Reducer 的拆分与重构**:
- 随着项目越大,如果将所有状态的 reducer 全部写在一个函数中,将会 **难以维护**;
- 可以将 reducer 进行拆分,也就是 **函数分解**,最终再使用`combineReducers()`进行重构合并;
- **异步 Action**: 由于 Reducer 是一个严格的纯函数,因此无法在 Reducer 中进行数据的请求,需要先获取数据,再`dispatch(Action)`即可,下面是三种不同的异步实现:
- [redex-thunk](https://github.com/reduxjs/redux-thunk)
- [redux-saga](https://github.com/redux-saga/redux-saga)
- [redux-observable](https://github.com/redux-observable/redux-observable)
6. React Hooks
React 中通常使用 **类定义** 或者 **函数定义** 创建组件:
在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。
- **好处**:
- 1、**跨组件复用**: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
- 2、**类定义更为复杂**:
- 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
- 时刻需要关注`this`的指向问题;
- 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
- 3、**状态与UI隔离**: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
- **注意**:
- 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
- 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
- 不能在`useEffect`中使用`useState`,React 会报错提示;
- 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;
- **重要钩子***:
- **状态钩子** (`useState`): 用于定义组件的 State,其到类定义中`this.state`的功能;
```js
// useState 只接受一个参数: 初始状态
// 返回的是组件名和更改该组件对应的函数
const [flag, setFlag] = useState(true);
// 修改状态
setFlag(false)
// 上面的代码映射到类定义中:
this.state = {
flag: true
}
const flag = this.state.flag
const setFlag = (bool) => {
this.setState({
flag: bool,
})
}
```
- **生命周期钩子** (`useEffect`):
类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (`useEffect`),这里可以看做`componentDidMount`、`componentDidUpdate`和`componentWillUnmount`的结合。
- `useEffect(callback, [source])`接受两个参数
- `callback`: 钩子回调函数;
- `source`: 设置触发条件,仅当 source 发生改变时才会触发;
- `useEffect`钩子在没有传入`[source]`参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;
```js
useEffect(() => {
// 组件挂载后执行事件绑定
console.log('on')
addEventListener()
// 组件 update 时会执行事件解绑
return () => {
console.log('off')
removeEventListener()
}
}, [source]);
// 每次 source 发生改变时,执行结果(以类定义的生命周期,便于大家理解):
// --- DidMount ---
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- WillUnmount ---
// 'off'
```
- 通过第二个参数,我们便可模拟出几个常用的生命周期:
- `componentDidMount`: 传入`[]`时,就只会在初始化时调用一次;
```js
const useMount = (fn) => useEffect(fn, [])
```
- `componentWillUnmount`: 传入`[]`,回调中的返回的函数也只会被最终执行一次;
```js
const useUnmount = (fn) => useEffect(() => fn, [])
```
- `mounted `: 可以使用 useState 封装成一个高度可复用的 mounted 状态;
```js
const useMounted = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
!mounted && setMounted(true);
return () => setMounted(false);
}, []);
return mounted;
}
```
- `componentDidUpdate`: `useEffect`每次均会执行,其实就是排除了 DidMount 后即可;
```js
const mounted = useMounted()
useEffect(() => {
mounted && fn()
})
```
- **其它内置钩子**:
- `useContext`: 获取 context 对象
- `useReducer`: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:
- 并不是持久化存储,会随着组件被销毁而销毁;
- 属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;
- 配合`useContext`的全局性,可以完成一个轻量级的 Redux;([easy-peasy](https://github.com/ctrlplusb/easy-peasy))
- `useCallback`: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;
- `useMemo`: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;
- `useRef`: 获取组件的真实节点;
- `useLayoutEffect`:
- DOM更新同步钩子。用法与`useEffect`类似,只是区别于执行时间点的不同。
- `useEffect`属于异步执行,并不会等待 DOM 真正渲染后执行,而`useLayoutEffect`则会真正渲染后才触发;
- 可以获取更新后的 state;
- **自定义钩子**(`useXxxxx`): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的`useMounted`。又例如,我们需要每个页面自定义标题:
```js
function useTitle(title) {
useEffect(
() => {
document.title = title;
});
}
// 使用:
function Home() {
const title = '我是首页'
useTitle(title)
return (
{title}
)
}
```
7. SSR
SSR,俗称 **服务端渲染** (Server Side Render),讲人话就是: 直接在服务端层获取数据,渲染出完成的 HTML 文件,直接返回给用户浏览器访问。
- **前后端分离**: 前端与服务端隔离,前端动态获取数据,渲染页面。
- **痛点**:
- **首屏渲染性能瓶颈**:
- 空白延迟: HTML下载时间 + JS下载/执行时间 + 请求时间 + 渲染时间。在这段时间内,页面处于空白的状态。
- **SEO 问题**: 由于页面初始状态为空,因此爬虫无法获取页面中任何有效数据,因此对搜索引擎不友好。
- 虽然一直有在提动态渲染爬虫的技术,不过据我了解,大部分国内搜索引擎仍然是没有实现。
最初的服务端渲染,便没有这些问题。但我们不能返璞归真,既要保证现有的前端独立的开发模式,又要由服务端渲染,因此我们使用 React SSR。
- **原理**:
- Node 服务: 让前后端运行同一套代码成为可能。
- Virtual Dom: 让前端代码脱离浏览器运行。
- **条件**: Node 中间层、 React / Vue 等框架。 结构大概如下:
- **开发流程**: (此处以 React + Router + Redux + Koa 为例)
- 1、在同个项目中,**搭建** 前后端部分,常规结构:
- build
- public
- src
- client
- server
- 2、server 中使用 Koa **路由监听** 页面访问:
```js
import * as Router from 'koa-router'
const router = new Router()
// 如果中间也提供 Api 层
router.use('/api/home', async () => {
// 返回数据
})
router.get('*', async (ctx) => {
// 返回 HTML
})
```
- 3、通过访问 url **匹配** 前端页面路由:
```js
// 前端页面路由
import { pages } from '../../client/app'
import { matchPath } from 'react-router-dom'
// 使用 react-router 库提供的一个匹配方法
const matchPage = matchPath(ctx.req.url, page)
```
- 4、通过页面路由的配置进行 **数据获取**。通常可以在页面路由中增加 SSR 相关的静态配置,用于抽象逻辑,可以保证服务端逻辑的通用性,如:
```js
class HomePage extends React.Component{
public static ssrConfig = {
cache: true,
fetch() {
// 请求获取数据
}
}
}
```
获取数据通常有两种情况:
- 中间层也使用 **http** 获取数据,则此时 fetch 方法可前后端共享;
```js
const data = await matchPage.ssrConfig.fetch()
```
- 中间层并不使用 http,是通过一些 **内部调用**,例如 Rpc 或 直接读数据库 等,此时也可以直接由服务端调用对应的方法获取数据。通常,这里需要在 ssrConfig 中配置特异性的信息,用于匹配对应的数据获取方法。
```js
// 页面路由
class HomePage extends React.Component{
public static ssrConfig = {
fetch: {
url: '/api/home',
}
}
}
// 根据规则匹配出对应的数据获取方法
// 这里的规则可以自由,只要能匹配出正确的方法即可
const controller = matchController(ssrConfig.fetch.url)
// 获取数据
const data = await controller(ctx)
```
- 5、创建 Redux store,并将数据`dispatch`到里面:
```js
import { createStore } from 'redux'
// 获取 Clinet层 reducer
// 必须复用前端层的逻辑,才能保证一致性;
import { reducers } from '../../client/store'
// 创建 store
const store = createStore(reducers)
// 获取配置好的 Action
const action = ssrConfig.action
// 存储数据
store.dispatch(createAction(action)(data))
```
- 6、注入 Store, 调用`renderToString`将 React Virtual Dom 渲染成 **字符串**:
```js
import * as ReactDOMServer from 'react-dom/server'
import { Provider } from 'react-redux'
// 获取 Clinet 层根组件
import { App } from '../../client/app'
const AppString = ReactDOMServer.renderToString(
)
```
- 7、将 AppString 包装成完整的 html 文件格式;
- 8、此时,已经能生成完整的 HTML 文件。但只是个纯静态的页面,没有样式没有交互。接下来我们就是要插入 JS 与 CSS。我们可以通过访问前端打包后生成的`asset-manifest.json`文件来获取相应的文件路径,并同样注入到 Html 中引用。
```js
const html = `
${AppString}
`
```
- 9、进行 **数据脱水**: 为了把服务端获取的数据同步到前端。主要是将数据序列化后,插入到 html 中,返回给前端。
```js
import serialize from 'serialize-javascript'
// 获取数据
const initState = store.getState()
const html = `
`
ctx.status = 200
ctx.body = html
```
> **Tips**:
>
> 这里比较特别的有两点:
>
> 1. 使用了`serialize-javascript`序列化 store, 替代了`JSON.stringify`,保证数据的安全性,避免代码注入和 XSS 攻击;
>
> 2. 使用 json 进行传输,可以获得更快的加载速度;
- 10、Client 层 **数据吸水**: 初始化 store 时,以脱水后的数据为初始化数据,同步创建 store。
```js
const hydratedEl = document.getElementById('SSR_HYDRATED_DATA')
const hydrateData = JSON.parse(hydratedEl.textContent)
// 使用初始 state 创建 Redux store
const store = createStore(reducer, hydrateData)
```
8. 函数式编程
函数式编程是一种 **编程范式**,你可以理解为一种软件架构的思维模式。它有着独立一套理论基础与边界法则,追求的是 **更简洁、可预测、高复用、易测试**。其实在现有的众多知名库中,都蕴含着丰富的函数式编程思想,如 React / Redux 等。
- **常见的编程范式**:
- 命令式编程(过程化编程): 更关心解决问题的步骤,一步步以语言的形式告诉计算机做什么;
- 事件驱动编程: 事件订阅与触发,被广泛用于 GUI 的编程设计中;
- 面向对象编程: 基于类、对象与方法的设计模式,拥有三个基础概念: 封装性、继承性、多态性;
- 函数式编程
- 换成一种更高端的说法,面向数学编程。怕不怕~🥴
- **函数式编程的理念**:
- **纯函数**(确定性函数): 是函数式编程的基础,可以使程序变得灵活,高度可拓展,可维护;
- **优势**:
- 完全独立,与外部解耦;
- 高度可复用,在任意上下文,任意时间线上,都可执行并且保证结果稳定;
- 可测试性极强;
- **条件**:
- 不修改参数;
- 不依赖、不修改任何函数外部的数据;
- 完全可控,参数一样,返回值一定一样: 例如函数不能包含`new Date()`或者`Math.randon()`等这种不可控因素;
- 引用透明;
- 我们常用到的许多 API 或者工具函数,它们都具有着纯函数的特点, 如`split / join / map`;
- **函数复合**: 将多个函数进行组合后调用,可以实现将一个个函数单元进行组合,达成最后的目标;
- **扁平化嵌套**: 首先,我们一定能想到组合函数最简单的操作就是 包裹,因为在 JS 中,函数也可以当做参数:
- `f(g(k(x)))`: 嵌套地狱,可读性低,当函数复杂后,容易让人一脸懵逼;
- 理想的做法: `xxx(f, g, k)(x)`
- **结果传递**: 如果想实现上面的方式,那也就是`xxx`函数要实现的便是: 执行结果在各个函数之间的执行传递;
- 这时我们就能想到一个原生提供的数组方法: `reduce`,它可以按数组的顺序依次执行,传递执行结果;
- 所以我们就能够实现一个方法`pipe`,用于函数组合:
```js
// ...fs: 将函数组合成数组;
// Array.prototype.reduce 进行组合;
// p: 初始参数;
const pipe = (...fs) => p => fs.reduce((v, f) => f(v), p)
```
- **使用**: 实现一个 驼峰命名 转 中划线命名 的功能:
```js
// 'Guo DongDong' --> 'guo-dongdong'
// 函数组合式写法
const toLowerCase = str => str.toLowerCase()
const join = curry((str, arr) => arr.join(str))
const split = curry((splitOn, str) => str.split(splitOn));
const toSlug = pipe(
toLowerCase,
split(' '),
join('_'),
encodeURIComponent,
);
console.log(toSlug('Guo DongDong'))
```
- **好处**:
- 隐藏中间参数,不需要临时变量,避免了这个环节的出错几率;
- 只需关注每个纯函数单元的稳定,不再需要关注命名,传递,调用等;
- 可复用性强,任何一个函数单元都可被任意复用和组合;
- 可拓展性强,成本低,例如现在加个需求,要查看每个环节的输出:
```js
const log = curry((label, x) => {
console.log(`${ label }: ${ x }`);
return x;
});
const toSlug = pipe(
toLowerCase,
log('toLowerCase output'),
split(' '),
log('split output'),
join('_'),
log('join output'),
encodeURIComponent,
);
```
> Tips:
>
> 一些工具纯函数可直接引用`lodash/fp`,例如`curry/map/split`等,并不需要像我们上面这样自己实现;
- **数据不可变性**(immutable): 这是一种数据理念,也是函数式编程中的核心理念之一:
- **倡导**: 一个对象再被创建后便不会再被修改。当需要改变值时,是返回一个全新的对象,而不是直接在原对象上修改;
- **目的**: 保证数据的稳定性。避免依赖的数据被未知地修改,导致了自身的执行异常,能有效提高可控性与稳定性;
- 并不等同于`const`。使用`const`创建一个对象后,它的属性仍然可以被修改;
- 更类似于`Object.freeze`: 冻结对象,但`freeze`仍无法保证深层的属性不被串改;
- `immutable.js`: js 中的数据不可变库,它保证了数据不可变,在 React 生态中被广泛应用,大大提升了性能与稳定性;
- `trie`数据结构:
- 一种数据结构,能有效地深度冻结对象,保证其不可变;
- **结构共享**: 可以共用不可变对象的内存引用地址,减少内存占用,提高数据操作性能;
- 避免不同函数之间的 **状态共享**,数据的传递使用复制或全新对象,遵守数据不可变原则;
- 避免从函数内部 **改变外部状态**,例如改变了全局作用域或父级作用域上的变量值,可能会导致其它单位错误;
- 避免在单元函数内部执行一些 **副作用**,应该将这些操作抽离成更独立的工具单元;
- 日志输出
- 读写文件
- 网络请求
- 调用外部进程
- 调用有副作用的函数
- **高阶函数**: 是指 以函数为参数,返回一个新的增强函数 的一类函数,它通常用于:
- 将逻辑行为进行 **隔离抽象**,便于快速复用,如处理数据,兼容性等;
- **函数组合**,将一系列单元函数列表组合成功能更强大的函数;
- **函数增强**,快速地拓展函数功能,
- **函数式编程的好处**:
- 函数副作用小,所有函数独立存在,没有任何耦合,复用性极高;
- 不关注执行时间,执行顺序,参数,命名等,能专注于数据的流动与处理,能有效提高稳定性与健壮性;
- 追求单元化,粒度化,使其重构和改造成本降低,可维护、可拓展性较好;
- 更易于做单元测试。
- **总结**:
- 函数式编程其实是一种编程思想,它追求更细的粒度,将应用拆分成一组组极小的单元函数,组合调用操作数据流;
- 它提倡着 纯函数 / 函数复合 / 数据不可变, 谨慎对待函数内的 状态共享 / 依赖外部 / 副作用;
> Tips:
>
> 其实我们很难也不需要在面试过程中去完美地阐述出整套思想,这里也只是浅尝辄止,一些个人理解而已。博主也是初级小菜鸟,停留在表面而已,只求对大家能有所帮助,轻喷🤣;
>
> 我个人觉得: 这些编程范式之间,其实并不矛盾,各有各的 **优劣势**。
>
> 理解和学习它们的理念与优势,合理地 **设计融合**,将优秀的软件编程思想用于提升我们应用;
>
> 所有设计思想,最终的目标一定是使我们的应用更加 **解耦颗粒化、易拓展、易测试、高复用,开发更为高效和安全**;
>
> 有一些库能让大家很快地接触和运用函数思想: `Underscore.js` / `Lodash/fp` / `Rxjs` 等。
## 结语
到此,想必大家会发现已经开始深入一些理论和原理层面了,并不像上篇那么的浅显易懂了。但这也是个必经之路,不可能永远停留在 **5分钟掌握的技术** 上。不再停留在语言的表面,而是理解更深入的原理,模式,架构,因果,你就会突然发现你成为高级软件工程师了。😁。
希望各位小伙伴能沉下心来,一些理论、概念虽然枯燥,但反复琢磨后再自己实践尝试下,就能有自己的理解。
当你开始面试高级工程师时,面试官便不再重点关注你会不会写`stopPropagation`或者会不会水平居中了,而是更在乎你自己的思考和研究能力了。表现出自己深入理解研究的成果,定会让面试官刮目相看。
- [面试上篇](https://github.com/xd-tayde/blog/blob/master/interview-1.md)。
- [面试下篇](https://github.com/xd-tayde/blog/blob/master/interview-3.md);
> Tips:
>
> 联系我请发邮件 159042708@qq.com 或关注下面公众号加我微信详聊哈。
>
> 博主真的写得很辛苦,再不 star 下,真的要哭了。~ [github](https://github.com/xd-tayde/blog/blob/master/interview-2.md)。🤑
