react中hooks分享

news2024/11/19 21:28:04

一. 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. 创建新的文件用来重构,保证之前功能可用,一步一步替换其中代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/830667.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【iOS】锁

线程安全 当一个线程访问数据的时候&#xff0c;其他的线程不能对其进行访问&#xff0c;直到该线程访问完毕。简单来讲就是在同一时刻&#xff0c;对同一个数据操作的线程只有一个。而线程不安全&#xff0c;则是在同一时刻可以有多个线程对该数据进行访问&#xff0c;从而得…

LeetCode--剑指Offer75(1)

目录 题目描述&#xff1a;剑指 Offer 05. 替换空格&#xff08;简单&#xff09;题目接口解题思路1代码解题思路2代码 PS: 题目描述&#xff1a;剑指 Offer 05. 替换空格&#xff08;简单&#xff09; 请实现一个函数&#xff0c;把字符串 s 中的每个空格替换成"%20&quo…

Qt Creator中designer使用QWebEngine异常排查

Qt Creator中designer使用QWebEngine异常排查 1、前提背景 最近由于版权的原因&#xff0c;我们采取了自编译的Qt Creator。编译完成之后启动Qt Creator刚开始一切都是很顺利。 但是在Creator中打开designer&#xff0c;使用QWebEngine控件就发生了异常&#xff0c;Qt Creat…

新一代图像合成模型:Stable Diffusion XL(SDXL)上线!

几个使用Stable Diffusion XL 1.0生成的图像示例。 新的SDXL 1.0发布允许在本地计算机上运行的高分辨率人工智能图像合成。 周三&#xff0c;Stability AI发布了其下一代开源权重人工智能图像合成模型Stable Diffusion XL 1.0&#xff08;SDXL&#xff09;。它可以根据文本描述…

有多卷?智慧金融可视化大屏可以这样子

科学技术不断发展&#xff0c;数字化转型不断加快&#xff0c;智慧金融正成为金融业的新引擎。数字孪生、大数据、物联网等新一代信息技术在推动智慧金融更加强调效率、优化精准营销。数据可视化大屏如何为金融单位提供低代码、定制化的服务&#xff0c;让金融单位的数据可视、…

Python编程从入门到实践练习第三章:列表简介

目录 一、字符串1.1 在字符串中使用变量 二、列表2.1 遍历列表练习题代码 2.2 列表元素的插入和删除涉及方法练习题代码 2.3 组织列表涉及方法练习题代码 2.4 索引 参考书&#xff1a;Python从入门到实践&#xff08;第二版&#xff09; 一、字符串 1.1 在字符串中使用变量 f…

【力扣】92. 反转链表 II <链表指针>

【力扣】92. 反转链表 II 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回反转后的链表。 示例 1 输入&#xff1a;head [1,2,3,4,5], left 2, right 4 输出&#xff…

JVM面试突击1

JVM面试突击 JDK&#xff0c;JRE以及JVM的关系 我们的编译器到底干了什么事&#xff1f; 仅仅是将我们的 .java 文件转换成了 .class 文件&#xff0c;实际上就是文件格式的转换&#xff0c;对等信息转换。 类加载机制是什么&#xff1f; 所谓类加载机制就是 虚拟机把Class文…

Prometheus实现系统监控报警邮件

Prometheus实现系统监控报警邮件 简介 Prometheus将数据采集和报警分成了两个模块。报警规则配置在Prometheus Servers上&#xff0c; 然后发送报警信息到AlertManger&#xff0c;然后我们的AlertManager就来管理这些报警信息&#xff0c;聚合报警信息过后通过email、PagerDu…

怎么迅速做出高端、还会动的数据图表?来看看这五个大数据可视化神器!

什么叫大数据可视化&#xff1f; 其实很简单。大数据可视化就是指通过图表、图形、地图等视觉化方式&#xff0c;将庞大、复杂的大数据集合转化为直观、易于理解和分析的图像展示。 它的目的是帮助人们更好地理解和解释大数据&#xff0c;发现数据中的模式、趋势和关联&#…

CLion中avcodec_receive_frame()问题

1. 介绍 在提取音视频文件中音频的PCM数据时&#xff0c;使用avcodec_receive_frame()函数进行解码时&#xff0c;遇到了一些问题&#xff0c;代码在Visual Studio 2022中运行结果符合预期&#xff0c;但是在CLion中运行时&#xff0c;获取的AVFrame有错误&#xff0c;和VS中获…

谈「效」风生 |“效能指标”,该由谁来定义?

#第5期&#xff1a;效能指标&#xff0c;该由谁来定义&#xff1f;# 回顾上期《「自动化」聊起来简单&#xff0c;做起来难》我们聊了聊如何打造「自动化」的事&#xff0c;这也是真正实现研发效能提升的必要条件。从单点自动化提升效率&#xff0c;到全工具链自动化&#xff…

【Java环境不会搭建?一文带你读懂Windows下安装Java!】

JKD下载网址 —— https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html 1、如果你是32位系统下载 jdk-8u241-windows-i586.exe&#xff08;32位&#xff09;&#xff1b; 2、如果你是64位系统下载 jdk-8u241-windows-x64.exe&#xff08;64位&…

【freespace】HybridNets: End-to-End Perception Network

目录 摘要 1. 介绍 1.1. 背景 1.2. 相关工作 2. 方法 2.1. 网络体系结构 2.2. 编码器 2.3. 译码器 2.4. 损失函数和训练 3. 实验与评估 3.1. 实验设置 3.2. 评价指标 3.3. 成本计算性能 3.4. 多任务性能 4. 结论与展望 摘要 端到端网络在多任务处理中变得越来越重要…

Godot 4 源码分析 - 增加格式化字符串功能

Godot 4的主要字符串类型为String&#xff0c;已经设计得比较完善了&#xff0c;但有一个问题&#xff0c;格式化这块没怎么考虑。 String中有一个format函数&#xff0c;但这个函数只有两个参数&#xff0c;这咋用&#xff1f; String String::format(const Variant &va…

Rocketmq 定时消息源码分析

定时消息定义 生产者将消息投放到broker后&#xff0c;不会马上被消费者消费。需要等待到特定时间才会被消费。 调用链路 producer 将定时消息写入commitLog线程ReputThead 休息1毫秒&#xff0c;读取一次commitlog数据&#xff0c;写入ConsumeQueue和IndexFile线程Scheduled…

所学即所用:方飞将AI技术运用于反偷猎领域

原创 | 文 BFT机器人 方飞&#xff0c;高中毕业于江苏省常州高级中学&#xff0c;于2007年进入清华大学电子工程系攻读学士学位&#xff0c;2011年本科毕业后赴美国南加州大学计算机系攻读博士&#xff0c;主要从事安全博弈研究&#xff0c;师从安全博弈领域的权威专家 Milind…

vxworks文件系统分析

参考https://www.freebuf.com/articles/endpoint/335030.html 测试固件 https://service.tp-link.com.cn/detail_download_7989.html 固件提取 binwalk解压固件&#xff0c;在第一部分即为要分析的二进制文件&#xff0c;可以拖进ida分析 设置为arm小端字节序&#xff0c;点…

爆火的“为i做e”梗,小红书如何成为年轻人的社交货币?

话题浏览超13亿&#xff0c;“新社交密码”抢占用户心智 2023-08-03 草稿临时预览&#xff0c;有效期剩余59分59秒 请勿包含诱导分享&#xff0c;虚假中奖&#xff0c;违法违纪等信息。 爆火的“为i做e”梗、将MBTI写进个人简介、花样百出的MBI梗图 ...... 从去年5月到现在&…

手把手教你安装Eclipse最新版本的详细教程 (非常详细,非常实用)

简介 首先声明此篇文章主要是针对测试菜鸟或者刚刚入门的小伙们或者童鞋们&#xff0c;大佬就没有必要往下看了。 写这篇文章的由来是因为后边要用这个工具&#xff0c;但是由于某些原因有部分小伙伴和童鞋们可能不会安装此工具&#xff0c;为了方便小伙伴们和童鞋们的后续学习…