1. 爱猫说设计师网首页
  2. 前端
  3. JS技术

React Hooks 工程实践总结

 

12 - React Hooks工程实践总结

最近在项目中基本上全部使用了 React Hooks, 历史项目也用 React Hooks 重写了一遍,相比于 Class 组件,React Hooks 的优点可以一句话来概括:就是简单,在 React hooks 中没有复杂的生命周期,没有类组件中复杂的 this 指向,没有类似于 HOC,render props 等复杂的组件复用模式等。本篇文章主要总结一下在 React hooks 工程实践中的经验。

  • React hooks 中的渲染行为
  • React hooks 中的性能优化
  • React hooks 中的状态管理和通信

一、React hooks 中的渲染行为

1.React hooks 组件是如何渲染的

理解 React hooks 的关键,就是要明白,hooks 组件的每一次渲染都是独立,每一次的 render 都是一个独立的作用域,拥有自己的 props 和 states、事件处理函数等。概括来讲:

每一次的 render 都是一个互不相关的函数,拥有完全独立的函数作用域,执行该渲染函数,返回相应的渲染结果

而类组件则不同,类组件中的 props 和 states 在整个生命周期中都是指向最新的那次渲染.

React hooks 组件和类组件的在渲染行为中的区别,看起来很绕,我们可以用图来区别,

12 - React Hooks工程实践总结

上图表示在 React hooks 组件的渲染过程,从图中可以看出,react hooks 组件的每一次渲染都是一个独立的函数,会生成渲染区专属的 props 和 state. 接着来看类组件中的渲染行为:

12 - React Hooks工程实践总结

类组件中在渲染开始的时候会在类组件的构造函数中生成一个 props 和 state, 所有的渲染过程都是在一个渲染函数中进行的并且,每一次的渲染中都不会去生成新的 state 和 props,而是将值赋值给最开始被初始化的 this.props 和 this.state。

2. 工程中注意 React hooks 的渲染行为

理解了 React hooks 的渲染行为,就指示了我们如何在工程中使用。首先因为 React hooks 组件在每一次渲染的过程中都会生成独立的所用域,因此,在组件内部的子函数和变量等在每次生命的时候都会重新生成,因此我们应该减少在 React hooks 组件内部声明函数。

写法一:

function App() {
  const [counter, setCounter] = useState(0);
  function formatCounter(counterVal) {
    return `The counter value is ${counterVal}`;
  }
  return (
    <div className="App">
      <div>{formatCounter(counter)}</div>
      <button onClick={() => setCounter(prevState => ++prevState)}>
        Increment
      </button>
    </div>
  );
}

写法二:

function formatCounter(counterVal) {
  return `The counter value is ${counterVal}`;
}
function App() {
 const [counter, setCounter] = useState(0);
 return (
   <div className="App">
     <div>{formatCounter(counter)}</div>
     <button onClick={()=>onClick(setCounter)}>
       Increment
     </button>
   </div>
 );
}

App 组件是一个 hooks 组件,我们知道了 React hooks 的渲染行为,那么写法 1 在每次 render 的时候都会去重新声明函数 formatCounter,因此是不可取的。我们推荐写法二,如果函数与组件内的 state 和 props 无相关性,那么可以声明在组件的外部。如果函数与组件内的 state 和 props 强相关性,那么我们下节会介绍 useCallback 和 useMemo 的方法。

React hooks 中的 state 和 props, 在每次渲染的过程中都是重新生成和独立的,那么我们如果需要一个对象,从开始到一次次的 render1 , render2, … 中都是不变的应该怎么做呢。(这里的不变是不会重新生成,是引用的地址不变的意思,其值可以改变)

我们可以使用 useRef,创建一个 “常量”,该常量在组件的渲染期内始终指向同一个引用地址。

通过 useRef,可以实现很多功能,比如在某次渲染的时候,拿到前一次渲染中的 state。

function App(){
   const [count,setCount] = useState(0)
   const prevCount = usePrevious(count);
   return (
    <div>
      <h1>Now: {count}, before: {prevCount}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
   );
}
function usePrevious(value) {
   const ref = useRef();
   useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

上述的例子中,我们通过 useRef() 创建的 ref 对象,在整个 usePrevious 组件的周期内都是同一个对象,我们可以通过更新 ref.current 的值,来在 App 组件的渲染过程中,记录 App 组件渲染中前一次渲染的 state.

这里其实还有一个不容易理解的地方,我们来看 usePrevious:

function usePrevious(value) {
   const ref = useRef();
   useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

这里的疑问是:为什么当 value 改变的时候,返回的 ref.current 指向的是 value 改变之前的值?

也就是说:

为什么 useEffect 在 return ref.current 之后才执行?

为了解释这个问题,我们来聊聊神奇的 useEffect.

3. 神奇的 useEffect

hooks 组件的每一次渲染都可以看成一个个独立的函数 render1,render2 … rendern, 那么这些 render 函数之间是怎么关联的呢,还有上小节的问题,为什么在 usePrevious 中,useEffect 在 return ref.current 之后才执行。带着这两个疑问我们来看看在 hooks 组件中,最为神奇的 useEffect。

用一句话概括就是:

每一渲染都会生成不同的 render 函数,并且每一次渲染通过 useEffect 会生成一个不同的 Effects,Effects 在每次渲染后声效。

每次渲染除了生成不同的作用域外,如果该 hooks 组件中使用了 useEffect,通过 useEffect 还会生成一个独有的 effects,该 effects 在渲染完成后生效。

举例来说:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

上述的例子中,完成的逻辑是:

  • 渲染初始的内容:<p>You clicked 0 times</p>
  • 渲染完成之后调用这个 effect:{ document.title = ‘You clicked 0 times’ }。
  • 点击 Click me
  • 渲染新的内容渲染的内容: <p>You clicked 1 times</p>
  • 渲染完成之后调用这个 effect:() => { document.title = ‘You clicked 1 times’ }。

也就是说每次渲染 render 中,effect 位于同步执行队列的最后面,在 dom 更新或者函数返回后在执行。

我们在来看 usePrevious 的例子:

function usePrevious(value) {
   const ref = useRef();
   useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

因为 useEffect 的机制,在新的渲染过程中,先返回 ref.current 再执行 deps 依赖更新 ref.current, 因此 usePrevios 总是返回上一次的值。

现在我们知道,在一次渲染 render 中,有自己独立的 state,props, 还有独立的函数作用域,函数定义,effects 等,实际上,在每次 render 渲染中,几乎所有都是独立的。我们最后来看两个例子:

(1)

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

(2)

function Counter() {
  const [count, setCount] = useState(0);
  
  setTimeout(() => {
      console.log(`You clicked ${count} times`);
  }, 3000);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

这两个例子中,我们在 3 内点击 5 次 Click me 按钮,那么输出的结果都是一样的。

You clicked 0 times
You clicked 1 times
You clicked 2 times
You clicked 3 times
You clicked 4 times
You clicked 5 times

总而言之,每一次渲染的 render, 几乎都是独立和独有的,除了 useRef 创建的对象外,其他对象和函数都没有相关性.

二、React hooks 中的性能优化

前面我们讲了 React hooks 中的渲染行为,也初步
提到了说将与 state 和 props 无关的函数,声明在 hooks 组件外面可以提高组件的性能,减少每次在渲染中重新声明该无关函数. 除此之外,React hooks 还提供了 useMemo 和 useCallback 来优化组件的性能.

(1).useCallback

有些时候我们必须要在 hooks 组件内定义函数或者方法,那么推荐用 useCallback 缓存这个方法,当 useCallback 的依赖项不发生变化的时候,该函数在每次渲染的过程中不需要重新声明

useCallback 接受两个参数,第一个参数是要缓存的函数,第二个参数是一个数组,表示依赖项,当依赖项改变的时候会去重新声明一个新的函数,否则就返回这个被缓存的函数.

function formatCounter(counterVal) {
  return `The counter value is ${counterVal}`;
}
function App(props) {
 const [counter, setCounter] = useState(0);
 const onClick = useCallback(()=>{
   setCounter(props.count)
 },[props.count]);
 return (
   <div className="App">
     <div>{formatCounter(counter)}</div>
     <button onClick={onClick}>
       Increment
     </button>
   </div>
 );
}

上述例子我们在第一章的例子基础上增加了 onClick 方法,并缓存了这个方法,只有 props 中的 count 改变的时候才需要重新生成这个方法。

(2).useMemo

useMemo 与 useCallback 大同小异,区别就是 useMemo 缓存的不是函数,缓存的是对象(可以是 jsx 虚拟 dom 对象),同样的当依赖项不变的时候就返回这个被缓存的对象,否则就重新生成一个新的对象。

为了实现组件的性能优化,我们推荐:

在 react hooks 组件中声明的任何方法,或者任何对象都必须要包裹在 useCallback 或者 useMemo 中。

(3)useCallback,useMemo 依赖项的比较方法

我们来看看 useCallback,useMemo 的依赖项,在更新前后是怎么比较的

import is from 'shared/objectIs';
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (prevDeps === null) {
    return false;
  }

  
 if (nextDeps.length !== prevDeps.length) {
   return false
 }
 
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++)   {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

其中 is 方法的定义为:

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) 
  );
}

export default (typeof Object.is === 'function' ? Object.is : is);

这个 is 方法就是 es6 的 Object.is 的兼容性写法,也就是说在 useCallback 和 useMemo 中的依赖项前后是通过 Object.is 来比较是否相同的,因此是浅比较。

三、React hooks 中的状态管理和通信

react hooks 中的局部状态管理相比于类组件而言更加简介,那么如果我们组件采用 react hooks,那么如何解决组件间的通信问题。

(1) UseContext

最基础的想法可能就是通过 useContext 来解决组件间的通信问题。

比如:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContext(null)

function CounterDisplay() {
  let counter = useContext(Counter)
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  let counter = useCounter()
  return (
    <Counter.Provider value={counter}>
      <CounterDisplay />
      <CounterDisplay />
    </Counter.Provider>
  )
}

在这个例子中通过 createContext 和 useContext,可以在 App 的子组件 CounterDisplay 中使用 context, 从而实现一定意义上的组件通信。

此外,在 useContext 的基础上,为了其整体性,业界也有几个比较简单的封装:

https://github.com/jamiebuilds/unstated-next
https://github.com/diegohaz/constate

但是其本质都没有解决一个问题:

如果 context 太多,那么如何维护这些 context

也就是说在大量组件通信的场景下,用 context 进行组件通信代码的可读性很差。这个类组件的场景一致,context 不是一个新的东西,虽然用了 useContext 减少了 context 的使用复杂度。

(2) Redux 结合 hooks 来实现组件间的通信

hooks 组件间的通信,同样可以使用 redux 来实现。也就是说:

在 React hooks 中,redux 也有其存在的意义

在 hooks 中存在一个问题,因为不存在类似于 react-redux 中 connect 这个高阶组件,来传递 mapState 和 mapDispatch, 解决的方式是通过 redux-react-hook 或者 react-redux 的 7.1 hooks 版本来使用。

  • redux-react-hook

在 redux-react-hook 中提供了 StoreContext、useDispatch 和 useMappedState 来操作 redux 中的 store,比如定义 mapState 和 mapDispatch 的方式为:

import {StoreContext} from 'redux-react-hook';

ReactDOM.render(
  <StoreContext.Provider value={store}>
    <App />
  </StoreContext.Provider>,
  document.getElementById('root'),
);

import {useDispatch, useMappedState} from 'redux-react-hook';

export function DeleteButton({index}) {
  // Declare your memoized mapState function
  const mapState = useCallback(
    state => ({
      canDelete: state.todos[index].canDelete,
      name: state.todos[index].name,
    }),
    [index],
  );

  // Get data from and subscribe to the store
  const {canDelete, name} = useMappedState(mapState);

  // Create actions
  const dispatch = useDispatch();
  const deleteTodo = useCallback(
    () =>
      dispatch({
        type: 'delete todo',
        index,
      }),
    [index],
  );

  return (
    <button disabled={!canDelete} onClick={deleteTodo}>
      Delete {name}
    </button>
  );
}
  • react-redux 7.1 的 hooks 版

这也是官方较为推荐的,react-redux 的 hooks 版本提供了 useSelector()、useDispatch()、useStore() 这 3 个主要方法,分别对应与 mapState、mapDispatch 以及直接拿到 redux 中 store 的实例.

简单介绍一下 useSelector, 在 useSelector 中除了能从 store 中拿到 state 以外,还支持深度比较的功能,如果相应的 state 前后没有改变,就不会去重新的计算.

举例来说,最基础的用法:

import React from 'react'
import { useSelector } from 'react-redux'

export const TodoListItem = props => {
  const todo = useSelector(state => state.todos[props.id])
  return <div>{todo.text}</div>
}

实现缓存功能的用法:

import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const selectNumOfDoneTodos = createSelector(
  state => state.todos,
  todos => todos.filter(todo => todo.isDone).length
)

export const DoneTodosCounter = () => {
  const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
  return <div>{NumOfDoneTodos}</div>
}

export const App = () => {
  return (
    <>
      <span>Number of done todos:</span>
      <DoneTodosCounter />
    </>
  )
}

在上述的缓存用法中,只要 todos.filter(todo => todo.isDone).length 不改变,就不会去重新计算.

爱猫说是一个完全独立的媒体,运营只靠网络广告支撑,如果您支持爱猫说设计狮网的话,请对此网站关闭广告拦截功能(如:Adblock)爱猫说设计狮网衷心的感谢您。

本文来自投稿,不代表爱猫说设计师网立场,如若转载,请注明出处:https://www.imaoshuo.com/react-hooks-gong-cheng-shi-jian-zong-jie/

发表评论

登录后才能评论