一. HOOKS是什么
在计算机程序设计中,钩子一词涵盖了一系列技术,这些技术用来通过拦截函数调用、消息或在软件组件之间传递的事件来改变或增加操作系统、应用程序或其他软件组件的行为。处理这些被截获的函数调用、事件或消息的代码称为“hook”。
在react中,有两种组件:类(class)组件 和 函数(function)组件。
类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。
函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。这种只进行单纯的数据计算(换算)的函数,在函数式编程里面称为 “纯函数”(pure function)。
**函数式编程将那些跟数据计算无关的操作,都称为 “副效应” 。**如果函数内部直接包含产生副效应的操作,就不再是纯函数了,我们称之为不纯的函数。纯函数内部只有通过间接的手段(即通过其他函数调用),才能包含副效应。
钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。
Hooks使得react可在不编写类组件的情况下使用 state(状态) 和其他 React 功能。
二. 为什么要有hooks
- 在组件之间重用有状态逻辑很困难
React 没有为复用状态逻辑提供原生途径。通常类组件的逻辑复用会使用 HOC (高阶组件)或 render props 的方案,但是此类方案通常需要你重新组织组件结构,且过多的嵌套抽象层组件很容易形成“嵌套地狱”。
使用 Hook ,可以从组件中提取有状态逻辑,以便可以独立测试并重用。Hooks 允许在不更改组件层次结构的情况下重用有状态逻辑。
// 例如在对于接口请求的情况,每个页面都需要在componentDidMount中调用接口,调用接口时需要将state中的loading置为true,结束后,再置为false。
class Test extends PureComponent {
state = {
loading: false,
data: null
}
componentDidMount() {
this.setState({
loading: true,
})
fakeGet("xxx.com/xxx").then(res => {
this.setState({
data: res,
loading: false
})
})
}
render() {
const { loading, data } = this.state;
return (
<div>
{
loading ? <Loading /> : (
data.map(item => (<Item data={item} />))
)
}
</div>
)
}
}
// 因为类组件的state是自身特有的,所以不能直接复用,因此每个类组件都需要写一遍这个逻辑
// 如果使用hooks呢
const Test = ({}) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
setLoading(true);
fakeGet("xxx.com/xxx").then(res => {
setData(res);
setLoading(false)
})
})
return (
<div>
{
loading ? <Loading /> : (
data.map(item => (<Item data={item} />))
)
}
</div>
)
}
// 这时可以把状态提取至公共状态
const useRequest = (option) => {
const { url, ...opt } = option;
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
setLoading(true);
fakeGet(url, opt).then(res => {
setData(res);
setLoading(false)
})
})
return { loading, data };
}
const Test = ({}) => {
const { loading, data } = useRequest({
url: "xxxx.com/xxx",
method: "GET",
})
return (
<div>
{
loading ? <Loading /> : (
data.map(item => (<Item data={item} />))
)
}
</div>
)
}
// 之后需要做接口请求的地方都可以使用useRequest这个hooks,不用重复定义loading等状态。
- 复杂的组件变得难以理解
我们常常不得不维护一些组件,这些组件一开始很简单,但后来却变成了一堆难以管理的有状态逻辑和副作用。每个生命周期方法通常包含一组不相关的逻辑。例如,
组件可能在componentDidMount 和 componentDidUpdate中执行一些数据获取。
相同的 componentDidMount 方法可能还包含一些不相关的逻辑,它们设置事件监听器,并在 componentWillUnmount 中执行清理。
一起更改的相互关联的代码会被分离,但是完全不相关的代码最终会组合在一个方法中。这很容易引入错误和不一致。
Hooks可以根据相关内容(例如设置订阅或获取数据)将一个组件拆分为较小的函数,而不是基于生命周期方法强制拆分。还可以选择使用 useReducer 管理组件的本地state(状态),以使其更具可预测性。
虽然hooks可以模拟出大部分生命周期,但是像 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 等生命周期 API,使用 Hooks 不能完全替代。
三. hooks、HOC、render Props对于封装的差异
1. HOC - 高阶组件
如下是一个常见 HOC 的用法。使用 connect 连接 store, 使用 withRouter 获取路由参数,这种嵌套的写法可读性和可维护性非常差(才两层嵌套就很难受了),虽然可以使用 compose 组合高阶组件,或者装饰器简化写法,但本质还是 HOC 的嵌套。
const App = withRouter(connect(commentSelector)(WrappedComponent));
// 优化 可以使用一个 compose 函数组合HOC
const enhance = compose(withRouter, connect(commentSelector));
const App = enhance(WrappedComponent);
// 优化 使用装饰器
@connect
class App extends React.Component {}
每一次 HOC 调用都会产生一个组件实例,多层嵌套会增加React虚拟Dom的深度并且影响性能,此外包裹太多层级之后,可能会带来props属性的覆盖问题。此外,HOC 对于使用者更像是一个黑盒,通查需要看具体的实现来使用。
2. Render Props
如下是复用监听 window size 变化的逻辑
<WindowSize>
(size) => <OurComponent size={size} />
</WindowSize>
然后,如果再想复用监听鼠标位置的逻辑
<WindowSize>
(size) => (
<Mouse>
(position) => <OurComponent size={size} mouse={position} />
</Mouse> )
</WindowSize>
到这里可能不会再想复用其他逻辑了,虽然 render props 解决了 hoc 存在的一些问题,比如对使用者黑盒,属性名覆盖等,但是使用 render props 时,如果复用逻辑过多会仍然会导致嵌套过深,形成回调地狱。
3. Hooks - 为复用状态逻辑提供原生途径
// 复用监听 window size 变化的逻辑 const size = useSize()
// 复用监听鼠标位置的逻辑 const position = useMouse()
用自定义 Hooks 改写之后,难道不“香”吗,谁还想回头写 HOC 和 render props。自定义 Hooks 复用状态逻辑的方式得到 React 原生的支持,与React组件不同的是,自定义 Hooks 就是一个以 use 开头的函数,因此也更易于测试和复用。除此之外,在“真香”的自定义 Hooks 中也可以使用其他 Hooks。
四. 基础hooks
useState(状态钩子)
initialValue可以传一个函数,然后将初始值return出来。
setState不会帮你自动merge数据,如
const [data, setState] = useState({a:1, b:2})
setState({ c: 1 });
// state会被改成{c:1},而不是{a:1, b:2, c:1}
setState会使用Object.is来判断前后状态是否相同,相同时不会触发渲染
多次setState或者不同useState的setState方法,如果在React“可控”流程中(比如同步的事件回调、useEffect同步函数中等),会进行优化,只会触发一次渲染
const Demo4 = () => {
const [number, setNumber] = useState(0);
// 第一次为0
// effect后为 1
// click后为 4,说明多个setState进行了合并,而回调函数的setState将正常改变
// 再增加一个setTimeout会怎么样?
console.log(0, number);
useEffect(() => {
setNumber(number + 1);
console.log('1', number); // 0
}, [])
function handleAdd() {
setNumber(number + 1);
console.log(2, number); // 1
setNumber(number + 1);
console.log(3, number); // 1
setNumber(number + 1);
console.log(4, number); // 1
setNumber((prev) => {
console.log('prev', prev); // 2
return prev + 1;
})
setNumber((prev) => {
console.log('prev1', prev); // 3
return prev + 1;
})
// 如果增加下面这个,会发生什么呢?
// setTimeout(() => {
// setNumber(number + 1);
// console.log(6, number); // ??
// }, 0)
console.log(5, number); // 1
}
return (
<div>
<p>number: {number}</p>
<button onClick={handleAdd}>+++</button>
</div>
)
}
粒度问题
根据逻辑模块划分,如果多个state相关联,建议封装在一起 例如:pagination state中的current、total、pageSize等状态
考虑性能优化进行划分,尽量避免无意义渲染 例如:request state中的loading、dataSource、error等状态
同时也要兼顾代码可维护性,不要和类组件一样,把所有state都塞在一起 例如:table state中的pagiantion、query、selection等状态
如果状态实在过多而且又想封装在一个State中,考虑使用useReducer 采用redux中的store、dispatch方式去更好地管理状态
与类组件中state的区别
// 类组件下
addHandleTimeout2 = () => {
const { count } = this.state;
console.log(`----timeout count ---- ${count}`) // 0
this.setState({count:count + 1});
setTimeout(() => {
console.log(`----this.state.count---- ${this.state.count}`); // 1
console.log(`----count---- ${count}`); // 0
}, 2000);
}
// hook function component
const addHandleTimeout2 = () => {
console.log(`----timeout count ---- ${count}`) // 0
setCount(count + 1);
setTimeout(() => {
console.log(`----count---- ${count}`); // 0
}, 2000);
}
// 会输出什么?count初始值为0。
首先是对 class component 的解释:
state 是 Immutable 的,setState 后一定会生成一个全新的 state 引用。
但 Class Component 通过 this.state 方式读取 state,这导致了每次代码执行都会拿到最新的 state 引用,所以快速点击4次的结果是 4 4 4 4。
然后是对 function component useState 的解释:
useState 产生的数据也是 Immutable 的,通过数组第二个参数 Set 一个新值后,原来的值在下次渲染时会形成一个新的引用。
但由于对 state 的读取没有通过 this. 的方式,使得 每次 setTimeout 都读取了当时渲染闭包环境的数据,虽然最新的值跟着最新的渲染变了,但旧的渲染里,状态依然是旧值。
2. useReducer(action 钩子)
React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux。
Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState。
useState的替代方案。同样接受类型为 (state, action) => newState 的reducer,并返回与 dispatch 方法配对的当前状态。
官方推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等,并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化(因为useState对于值的更新是直接替换,而不做合并处理,如果遇到深层级更新的操作,会比较麻烦,没有useReducer给力)。
除此之外还有一个好处,Reducer其实一个与UI无关的纯函数,useReducer的方案使得我们更容易构建自动化测试用例。
// 使用方式如下
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
3. useEffect(副作用钩子)
使用useEffect可以模拟很多class组件中的生命周期,如componentDidMount,componentDidUpdate, componentWillUnmount等
与 componentDidMount、componentDidUpdate 不同的是,传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。
使用方式
第一个参数为副作用函数 副作用函数可以选择返回一个函数,会在下一次执行该副作用或组件注销时调用
第二个参数为依赖数组,选填参数,在依赖变化时会触发副作用函数重新执行
如果依赖数组不传,则组件每次render时都会执行 ,而传递一个空数组时,则只会在组件创建时被执/行一次。
副作用函数在任何情况下一定会调用至少一次
const Demo5 = () => {
const [name, setName] = useState('');
useEffect(() => {
console.log('name:', name)
}, [name])
return (
<div>
<p>name: {name}</p>
<button onClick={() => setName("aaa")}>change</button>
</div>
)
}
闭包问题:每一次渲染执行的effect拿到的都是当次渲染的最新变量,而clean up拿到的是上次渲染时的旧变量
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
实际操作中,可以使用useEffect来对状态进行监听,当监听的状态发生改变后,便会执行方法。
与useEffect类似的还有一个useLayoutEffect,它会在所有的 DOM 变更之后同步调用 effect,可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新。这也将阻塞了浏览器的绘制。
当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题(比如根据状态去计算宽高,或者位置的时候,需要使用useLayoutEffect,其余90%以上的场景都只需要使用useEffect)
4. useCallback/useMemo
保证变量稳定,性能优化避免无意义渲染
deps数组必填,如果不填则无使用意义
绝大多数情况下,只要使用到的state和props及衍生变量,必须包含在deps数组里,否则拿到的永远是初始状态的值
如果出满足以下情况,不需要memo:
值未被其他hooks依赖
值未传入其他组件作为props
值为简单类型且计算基本无消耗
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},[a, b]);
// 只要a b不发生改变,这个值也不会发生改变,computeExpensiveValue就只会执行一次
// 主要是用来缓存计算量比较大的函数结果,可以避免不必要的重复计算
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
return (
<div onClick={memoizedCallback}>
{memoizedValue}
</div>
)
5. useRef
保存变量,区别于state,值改变不会触发渲染
值修改时不会触发渲染,所以用来保存不希望触发渲染的变量
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
// 也可以直接给ref.current赋值,如下方的例子usePreviousValue,通过ref来保存之前的值
}
// ref的穿透操作,父级使用子组件中的方法
const Parent = () => {
const childRef = useRef(null);
const handleClick = () => {
if (childRef) {
childRef.current.fff();
}
};
return (
<div>
<Child ref={childRef} />
<button onClick={handleClick}>click</button>
</div>
);
};
// 子组件中需要使用forwardRef包裹一下,在props中是获取不到ref的值,refs 不会透传下去。
// 这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理
// 否则你就需要改变一下ref的名字,如aref等,避开关键字,就可以在props中拿到了
const Child = forwardRef((props, ref) => {
const currentRef = useRef(null);
const [number, setNumber] = useState(0);
useImperativeHandle(ref, () => ({
fff() {
currentRef.current.focus();
setNumber(number + 1);
}
}));
return (
<>
<p>number: {number}</p>
<input ref={currentRef} />
</>
);
});
6. useContext(共享状态钩子)
传入一个context,可以直接获取到其value
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button
style={{
background: theme.background,
color: theme.foreground
}}
>
I am styled by theme context!
</button>
);
}
7. memo(使组件可以记忆化)
类似于class组件中的shouldComponentUpdate,用于根据prevProps与nextProps进行对比,来判断是否需要更新内部组件。
与shouldComponentUpdate不同的是,shouldComponentUpdate返回true时才会更新,而memo返回true表示不更新。
尽可能在所有的组件外部加上memo方法
const Test = () => <div>test</div>
export default memo(Test, (prevProps, nextProps) => {
if (prevProps.xxx === nextProps.xxx) {
return true;
}
return false;
})
五. Hooks的规范
1. 只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。后面简易版的实现原理中会讲到。
2. 只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:
- 在 React 的函数组件中调用 Hook
- 在自定义 Hook 中调用其他 Hook
3. 自定义 Hook 必须以 “use” 开头
自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
实现一个hooks,理解其原理
// 实例代码
function App() {
// index = 0;
const [count, setCount] = useState(0);
// const [count2, setCount2] = useState(0);
return (
<div>
<div>{count}</div>
<Button
onClick={() => {
setCount(count + 1);
}}
>
点击
</Button>
// <div>{count2}</div>
// <Button
// onClick={() => {
// setCount2(count2 + 1);
// }}
// >
// 点击
// </Button>
</div>
);
}
// 先来一个useState,但是setState后数据并没有更新,原因是每次都被初始化了,所以要将值记录在外部,优先读取外部数据,没有的话,在使用初始化数据,这样就保证了数据的持久性。
let value;
function useState(initialValue) {
var state = initialValue;
value = value || initialValue;
function setState(newState) {
value = newState;
render();
}
return [value, setState];
}
// 第二步,这个useState只能写一个,写第二个useState的时候,就会覆盖掉前面的,所以再改一下
let memoizedState = [];
let cursor = 0;
function useState(initialValue) {
const currentIndex = cursor;
cursor++;
memoizedState[currentIndex] = memoizedState[currentIndex] || initialValue;
function setState(newState) {
memoizedState[currentIndex] = newState;
render(); // 模拟 reRender,这一行不需要关心
}
return [memoizedState[currentIndex], setState];
}
下面来实现一个useEffect,我们知道 useEffect 有几个特点:
有两个参数 callback 和 dependencies 数组
如果 dependencies 不存在,那么 callback 每次 render 都会执行
如果 dependencies 存在,只有当它发生了变化, callback 才会执行,初始化时都会执行一下。
function useEffect(callback, depArray) {
const currentIndex = cursor;
cursor ++;
// 从数组中取出上次保存的值,用于此次判断
const { callback: unmountCallback , depArray: oldDepArray } = memoizedState[currentIndex] || {};
unmountCallback && unmountCallback();
// 没有依赖项,或者依赖项中有一个发生改变,都需要触发callback
const noDep = !depArray;
const dspHaveChange = oldDepArray ? !!depArray && depArray.some((item, index) => item !== oldDepArray[index]) : true;
const newEffect = {};
newEffect.depArray = depArray;
if(noDep || dspHaveChange) {
newEffect.callback = callback();
}
memoizedState[currentIndex] = newEffect;
}
此时我们应该可以解答一个问题:
Q:为什么第二个参数是空数组,相当于 componentDidMount ?
A:因为依赖一直不变化,callback 不会二次执行。
React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。
六. 自定义hooks
1. useDidMount
// 利用useEffect的特性
function useDidMount(fn) {
useEffect(() => {
fn()
}, [])
}
2. useWillUnmount
// 利用useEffect的第一个参数的返回值会在每次渲染前执行的特性,模拟卸载组件。
// useRef则可以持久保存数据,且不触发render
function useWillUnmount(fn) {
const fnRef = useRef(null);
fnRef.current = fn;
useEffect(() => {
return () => {
fnRef.current();
}
}, [])
}
3. useForceUpdate
// 用于刷新本组件
function useForceUpdate() {
const [, setState] = useState(false);
const forceUpdate = useCallback(() => {
setState((v) => !v);
})
return forceUpdate;
}
// antd 版
export default function useForceUpdate() {
const [, forceUpdate] = useReducer(x => x + 1, 0);
return forceUpdate;
}
4. usePreviousValue
// 获取上一次render时某个变量的值
function usePreviousValue(value) {
const currentRef = useRef(null);
const prevRef = useRef(null);
// 1、直接改变
prevRef.current = currentRef.current;
currentRef.current = value;
// 2、获取与之前不一样的值
const shouldUpdate = !Object.is(current.value, value);
if (shouldUpdate) {
prevRef.current = currentRef.current;
currentRef.current = value;
}
return prevRef.current;
}
5. useBoolean
// 可以用于切换modal的visible属性
function useBoolean(initValue) {
const [state, setState] = useState(initValue || false);
const actions = useMemo(() => {
return {
setTrue() {
setState(true);
},
setFalse() {
setState(false);
},
toggle() {
setState((v) => !v);
},
setValue(value) {
setState(value);
},
}
}, [])
return [state, actions]
}
// 这个方法也可以使用useReducer改造
const reducer = (state, action) => {
switch(action.type) {
case 'true':
return true;
case 'false':
return false;
case 'toggle':
return !state;
case 'set':
return action.type;
}
}
const [state, dispatch] = useReducer(reducer, false);
6. useRequest
function useRequest(id) {
const [loading, setLoading] = useState(false);
const [body, setBody] = useState(null);
const count = useRef(0);
useEffect(() => {
const currentCount = count.current;
setLoading(true);
getData(id).then(res => {
if (currentCount !== count.current) return;
setLoading(false);
setBody(res);
})
return () => {
count.current ++;
}
}, [id])
return [loading, body];
}
const [loading, body] = useRequest(id);
7. useUpdateEffect
// 监听依赖完成渲染后的操作,return的操作是在下次执行该副作用之前调用
const useUpdateEffect = (effect, deps) => {
const isMounted = useRef(false);
useEffect(() => {
// 第一次执行是在mount阶段,故不返回effect,第二次执行之后才会设置effect,在第三次执行前,会执行effect方法
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
七. 推荐使用的hooks的库
- ahooks,有很多hooks可用,而且有一些和antd相关联的hooks,如useAntdTable,基于 useRequest 实现,加载态,分页都可支持
- hox:hooks中的状态管理器,代码简单,有兴趣的可以去看一看。
// 定义modal
import { createModel } from 'hox';
/* 任意一个 custom Hook */
function useCounter() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return {
count,
decrement,
increment
};
}
export default createModel(useCounter)
// 使用modal
import useCounterModel from "../models/useCounterModel";
function App(props) {
const counter = useCounterModel();
return (
<div>
<p>{counter.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}
八. 如何重构代码
1. 重构的目标
重构的主要目的在于改善既有代码的设计,而不是修改缺陷、新增功能等。
重构可以是修改变量名、重新安排目录这样简单的物理重构,也可以是抽取子函数、精简冗余设计这样稍许复杂的逻辑重构。但均不改变现有代码的功能。
重构可以将意大利面条式的杂乱代码整理为千层饼式的整洁代码。整洁的代码更加健壮,因此便于建立完善的测试防护网。同时,新手老人均可放心地修改。
期望重构之后,代码逻辑一目了然,扩展和修改非常方便,出现故障时能迅速定位和修复。前人摔跤过的地方后人不再栽倒,前人思考出的成果后人可直接借用。总之,高度人性化,极大解放人力和脑力。
2. 什么样的代码一看就懂?
但凡遇到那种看着逻辑代码一大堆放在一起的,就头大,后来发现,这些代码都犯了一个相同的错误。没有分清楚什么是步骤,什么是实现细节。当你把步骤和细节写在一起的时候,灾难也就发生了,尤其是那种长年累月迭代出来的代码,if 遍地。Hooks 是一个做代码拆分的高效工具,但是他也非常的灵活,业界一直没有比较通用行的编码规范,但是我有点不同的观点,我觉得他不需要像 Redux 一样的模式化的编码规范,因为他就是函数式编程,他遵循函数式编程的一般原则,函数式编程最重要的是拆分好步骤和实现细节,这样的代码就好读,好读的代码才是负责任的代码。
到底怎么区分步骤和细节?有一个很简单的方法,在你梳理需求的时候,你用一个流程图把你的需求表示出来,这时候的每个节点基本就是步骤,因为他不牵扯到具体的实现。解释太多,有点啰嗦了,相信你肯定懂,对吧。步骤和细节分清楚以后,对重构也有很大的好处,因为每个步骤都是一个函数,不会有像 class 中 this 这种全局变量,当你需要删除一个步骤或者重写这个步骤的时候,不用影响到其他步骤函数。同样,函数化以后,无疑单元测试就变得非常简单了。
3. 编码价值观 ETC
ETC 这种编码的价值观是很多好的编码原则的本质,比如单一职责原则,解耦原则等,他们都体现了 ETC 这种价值观念。能适应使用者的就是好的设计,对于代码而言,就是要拥抱变化,适应变化。因此我们需要信奉 ETC 。价值观念是帮助你在写代码的时候做决定的,他告诉你应该做这个?还是做那个?他帮助你在不同编码方式之间做选择,他甚至应该成为你编码时的一种潜意识,如果你接受这种价值观,那么在编码的时候,请时刻提醒自己,遵循这种价值观。
总结:
- 使每个函数处理的事情尽量单一化,尽可能写成纯函数,便于维护及测试。也方便理解。
- 重构未动,测试先行
重构之前一定要要有充分的测试用例,保证不漏掉一个功能,及改错功能。 - 梳理好功能点,先找到痛点
- 例如很多重复,但又不得不写的代码,可以提取成方法。
- 例如投放系统中很多场景下用到了form表单提交,可以考虑如何简化写法(使用数组进行渲染各个formItem? 数组的结构该怎么定义?)
- 如何能提高代码的复用性,和可扩展性
- 需要高质量的技术方案,确定要如何重构
- 避免出现重构过程中发现其他问题,影响重构进度
- 小心求证,为每行代码负责
- 创建新的文件用来重构,保证之前功能可用,一步一步替换其中代码。