阅读 207

浅析React Hooks

React 的有两套 API:类(class)API 和基于函数的钩子(hooks) API,官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较"轻",而类比较"重"。而且,钩子是函数,更符合 React 函数式的本质。

组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。

一、类和函数的差异

  • 类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。

  • 函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。

二、副作用是什么

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
复制代码

这个函数只做一件事,就是根据输入的参数,返回组件的 HTML 代码。这种只进行单纯的数据计算(换算)的函数,在函数式编程里面称为 "纯函数"(pure function),那么问题来了:那些不涉及计算的操作(比如生成日志、储存数据、改变应用状态等等)应该写在哪里呢?

函数式编程将那些跟数据计算无关的操作,都称为 "副作用" (side effect)

三、钩子(hook)的作用

钩子(hook)就是 React 函数组件的副作用解决方案,用来为函数组件引入副作用。

函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副作用)都必须通过钩子引入。

React 为许多常见的操作(副作用),都提供了专用的钩子:

  • useState():保存状态
  • useContext():保存上下文
  • useRef():保存引用
  • ......

上面这些钩子,都是引入某种特定的副作用,而 useEffect()是通用的副作用钩子 。找不到对应的钩子时,就可以用它。

四、useEffect

import React, { useEffect } from 'react';

function Welcome(props) {
  useEffect(() => {
    document.title = '加载完成';
  });
  return <h1>Hello, {props.name}</h1>;
}
复制代码

上面例子中,useEffect()的参数是一个函数,它就是所要完成的副作用(改变网页标题)。组件加载以后,React 就会执行这个函数。

1. useEffect() 的第二个参数

function Welcome(props) {
  useEffect(() => {
    document.title = `Hello, ${props.name}`;
  }, [props.name]);
  return <h1>Hello, {props.name}</h1>;
}
复制代码

作用相当于**componentDidUpdate()**方法。

如果第二个参数是一个空数组,就表明副作用参数没有任何依赖项。因此,副作用函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。此时作用相当于**componentDidMount()**方法。

2. useEffect() 的返回值

副作用是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副作用。

useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副作用。

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    subscription.unsubscribe();
  };
}, [props.source]);
复制代码

上例中,useEffect()在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。作用相当于**componentWillUnmount()**方法。

3. 注意点

如果有多个副作用,应该调用多个useEffect(),而不应该合并写在一起。

五、useState():状态钩子

const [state, setState] = React.useState(initialState);
复制代码

返回一个 state,以及更新 state 的函数。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

1. 函数式更新

setState(i => i+1)
复制代码

state多次操作优先使用此形式

2. useState 不会自动合并更新对象

可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果:

const [state, setState] = useState({});
setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});
复制代码

3. 惰性初始 state

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});
复制代码

六、useReducer():action 钩子

React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux。

Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState

useReducers()钩子用来引入 Reducer 功能:

const [state, dispatch] = useReducer(reducer, initialState);
复制代码

它相当于useState 的复杂版本。它接收一个reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

下面是一个计数器的例子。用于计算状态的 Reducer 函数如下:

const myReducer = (state, action) => {
  switch(action.type)  {
    case('countUp'):
      return  {
        ...state,
        count: state.count + 1
      }
    default:
      return  state;
  }
}
复制代码

组件代码如下:

function App() {
  const [state, dispatch] = useReducer(myReducer, { count:   0 });
  return  (
    <div className="App">
      <button onClick={() => dispatch({ type: 'countUp' })}>
        +1
      </button>
      <p>Count: {state.count}</p>
    </div>
  );
}
复制代码

由于 Hooks 可以提供共享状态和 Reducer 函数,所以它在这些方面可以取代 Redux,步骤:

  1. 将数据集中在一个store对象
  2. 将所有操作集中在reducer
  3. 创建一个Context
  4. 创建对数据的读写API
  5. 将第4步的内容放在第3步的Context
  6. Context.ProviderContext提供给所有组件
  7. 各个组件用useContext获取读写API

但是,useReducer无法提供中间件(middleware)和时间旅行(time travel),如果你需要这两个功能,还是要用 Redux。

七、useContext():共享状态钩子

const value = useContext(MyContext);
复制代码

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

如果需要在组件之间共享状态,可以使用useContext()

现在有两个组件 Navbar 和 Messages,我们希望它们之间共享状态:

<div className="App">
  <Navbar/>
  <Messages/>
</div>
复制代码

第一步就是使用 React Context API,在组件外部建立一个 Context:

const AppContext = React.createContext({});
复制代码

组件封装代码如下:

<AppContext.Provider value={{
  username: 'superawesome'
}}>
  <div className="App">
    <Navbar/>
    <Messages/>
  </div>
</AppContext.Provider>
复制代码

上面代码中,AppContext.Provider提供了一个 Context 对象,这个对象可以被子组件共享。

Navbar 组件的代码如下:

const Navbar = () => {
  const { username } = useContext(AppContext);
  return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
  );
}
复制代码

八、useRef

const refContainer = useRef(initialValue);
复制代码

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

一个常见的用例便是命令式地访问子组件:

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>
    </>
  );
}
复制代码

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

九、useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码

返回一个 memoized 值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值,相当于Vue 2中的computed。这种优化有助于避免在每次渲染时都进行高开销的计算。

传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

**你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。**将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

十、自定义Hook

自定义 Hook 是一个函数,其名称以 “**use**” 开头,函数内部可以调用其他的 Hook。

与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 Hook 的规则。

更多内容

文章分类
前端
文章标签