react中hooks分享

一. 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 。价值观念是帮助你在写代码的时候做决定的,他告诉你应该做这个?还是做那个?他帮助你在不同编码方式之间做选择,他甚至应该成为你编码时的一种潜意识,如果你接受这种价值观,那么在编码的时候,请时刻提醒自己,遵循这种价值观。

总结:

  1. 使每个函数处理的事情尽量单一化,尽可能写成纯函数,便于维护及测试。也方便理解。
  2. 重构未动,测试先行
    重构之前一定要要有充分的测试用例,保证不漏掉一个功能,及改错功能。
  3. 梳理好功能点,先找到痛点
    • 例如很多重复,但又不得不写的代码,可以提取成方法。
    • 例如投放系统中很多场景下用到了form表单提交,可以考虑如何简化写法(使用数组进行渲染各个formItem? 数组的结构该怎么定义?)
    • 如何能提高代码的复用性,和可扩展性
    • 需要高质量的技术方案,确定要如何重构
    • 避免出现重构过程中发现其他问题,影响重构进度
  4. 小心求证,为每行代码负责
  5. 创建新的文件用来重构,保证之前功能可用,一步一步替换其中代码。

猜你喜欢

转载自blog.csdn.net/qq_28992047/article/details/132081532