React函数式组件Hooks开发

2,712 阅读9分钟

WHY

1.React 没有提供组件的可复用性行为

class组件解决此类问题的方案: 高阶组件

  • 高阶组件(HOC)  React 中用于复用组件逻辑的一种高级技巧。   不是 React API 的一部分, 是一种基于 React 的组合特性而形成的设计模式。
  • 具体而言, 高阶组件是参数为组件,返回值为新组件的函数。
  • 组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。 HOC 在 React 的第三方库中很常见,例如 Redux 的 connect 和 Relay 的 createFragmentContainer
  • 需要重新组织 组件的结构。
  • 在React DevTools 中观察React 应用,会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。
  • 尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。React DevTools在线模拟使用

class组件解决此类问题的方案:使用 Hook 从组件中提取状态逻辑\

  • 使得这些逻辑可以单独测试并复用
  • 无需修改组件结构的情况下复用状态逻辑
  • 在组件间或社区内共享 Hook 很便捷

2.组件预编译会带来巨大的潜力 使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案,而且class 也给目前的工具带来了一些问题。e.g:class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,推荐函数式

3.使用 Hook 其中一个目的就是要聚合功能模块,不被生命周期函数分割

  • 比如useState实现值与setState的映射关系
  • 比如effect解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。

函数式组件

函数式组件没有实例的概念,函数通过执行去渲染

//函数组件1.0
const Example = (props) => {
  // 你可以在这使用 Hook
  return <div />;
}

//函数组件2.0
function MyFunctionalComponent() {
 // 你可以在这使用 Hook
  return <input />;
}

Hook API

  • React 16.8 的新增特性
  • Hook API 提供了一系列可嵌入函数式组件的复用逻辑代码,满足开发在函数组件中使用 state 以及其他的 React class组件的特性。hooks名字通常都以 use 开始
  • React Hooks 是函数,在函数组件中使用,当React渲染函数组件时,组件里的每一行代码就会依次执行,Hooks 也就依次调用执行

规定

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。可以在自定义的 Hook 中调用

一、基本的生命周期相关

useState

const [state, setState] = useState(initialState);
如果 initialState 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
  • setState 函数用于更新 state,它接收一个新的 state 值并将组件的一次重新渲染加入队列。
  • React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。

在这里插入图片描述

setState 同步or异步

React 中 setState 什么时候是同步的,什么时候是异步的?

  1. React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事 件,setTimeout/setInterval 等。
  2. 由 React 控制的事件处理程序,以及生命周期函数调用 setState异步更新 state 。
  • 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接同步更新this.state还是放到队列中异步更新
  • isBatchingUpdates默认是false,表示同步更新this.state,但是,有一个函数batchedUpdates会把isBatchingUpdates修改为true。
  • 而当React在调用事件处理函数之前就会调用这个batchedUpdates,所以由React控制的事件处理过程setState不会同步更新this.state。

setState函数中传入函数还是数值

  • 数值:3秒内在同一个异步队列内被优化为一次执行
  • 函数:保有每一次handleClick被触发的执行
function Counter() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setTimeout(() => {
      setCount(count + 1)
    }, 3000);
  }
  function handleClickFn() {
    setTimeout(() => {
      setCount((prevCount) => {
        return prevCount + 1
      })
    }, 3000);
  }
  return (
    <>
      Count: {count}
      <button onClick={handleClick}>+</button>
      <button onClick={handleClickFn}>+</button>
    </>
  );
}

useEffect

  • 在函数式组件的重新渲染是一次函数的执行过程,使用这个 Hook,react会保存传递的函数(即effect),定期调用effect。
  • useEffect Hook 的功能等同于 componentDidMount,componentDidUpdate 和 componentWillUnmount 的组合

与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让应用看起来响应更快。

  • 默认情况下,useEffect 在第一次渲染之后和每次更新之后都会执行(等同于同时使用componentDidMount 和componentDidUpdate ),此外也可以通过第二个参数控制它的触发条件。
  • React 保证了每次运行 effect 的同时,DOM 都已经更新完毕,effect主要负责处理副作用
  • 大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。
  • useEffect 放在组件内部可以在 effect 中直接访问 count state 变量(或其他 props),不需要特殊的 API 来读取。已经保存在函数作用域中。

对比JavaScript的类中方法没有绑定this,Hook 使用了 JavaScript 的闭包机制

副作用

副作用:数据获取,设置订阅以及手动更改 React 组件中的 DOM

  • 例如:在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志
  • 副作用操作的分类:需要清除的和不需要清除的。

无需清除的effect

import React, { useState, useEffect } from 'react';

function Example() {
  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>
  );
}

需要清除的effect

  • effect 可选的清除机制:如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它,如此可以将添加和移除订阅的逻辑放在一起。
  • React 会在执行当前 effect 之前对上一个 effect 进行清除。
import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

why频繁的取消上一次effect的订阅

  • 为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。
  • 此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。 忘记正确地处理 componentDidUpdate 是 React 应用中常见的 bug 来源 例如:假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。
class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

在这里插入图片描述

跳过 Effect

  • 传递数组作为 useEffect 的第二个可选参数
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

二、自定义hooks

render props 和高阶组件 想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式

  • 当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样用这种方式可以将组件逻辑提取到可重用的函数中。
  • 自定义 Hook,自定义 Hook 的名字应该始终以 use 开头。
import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

在这里插入图片描述

三、数据管理相关

共享数据useContext

  1. 全局:ThemeContext = React.createContext(语义化的对象)
  2. 全局:ThemeContext.Provider的 value注入值
  3. 底层使用:useContext(ThemeContext)
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>  );
}

全局数据管理方案:React.useReducer(处理数据方案)+React.useContext(跨层级通信)

处理数据useReducer(useState的代替)

相比useState,更好支持了 设置数据值的 方法隐射,适用于更复杂的数据处理场景,

  • useState:对数据的处理是使用方自己掌控
  • useReducer:useReducer自己定义 对数据的处理(Reducer),使用方通过传参一个type值来调用对应的方法
state:只读(所有修改都要通过action) 
DomainData服务器响应数据、网络请求 UI APP级别
action:一个具有type属性和其他属性的js对象
reducer:接收初始化state和action的函数,响应不同类型的action,修改并返回state 发送给store

Object.assign({},newstate)
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>
    </>
  );
}

四、缓存优化相关 useCallback和useMemo

  • 函数式组件中的state的变化都会导致整个组件被重新执行useCallback、useMemo和useEffect可以设置只有在依赖数据发生变化后,才会重新计算结果,起到缓存的作用
  • 不使用memo和useMemo不应该会导致你的业务逻辑发生变化(memo和useMemo只是用来做性能优化),类似于类组件中的 shouldComponentUpdate
  • 如果该函数或变量作为 props 传给子组件,请一定要用,避免子组件的非必要渲染,类似PureComponent的功能
  • 第二个参数是用于触发-检测上下文中对应值是否变化,如果有变化则会重新声明回调函数,获取静态作用域的值。
  • 如果第二个参数为空数组,则只会在component挂载即componentDidMount运行。如果不存在这个参数,则会在每次渲染时运行。
  • React.memo 和 React.useCallback 需要配对使用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比较也是要消耗那么一点点点的性能。

useMemo 缓存值

useMemo(计算函数,依赖项数组)
  • useMemo和useEffect的执行时机有区别

useMemo缓存计算后的状态。返回值直接参与渲染的,所以useMemo是在渲染期间完成的
useEffect执行的是副作用,所以在渲染之后执行的。

  • 计算函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useCallback 缓存函数(缓存函数作用域,减少函数创建的性能消耗)

inline 函数

  • 因为 onClick 使用了 inline 函数,所以 PureComponent 默认的浅比较也同样失去了意义。
  • 该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。
  • useCallback 缓存了每次渲染时 inline callback 的实例
  <button onClick={() => this.handleClick()}>
        Click me
      </button>
const handleClick = useCallback(
    (value: any) => {
      const targetOption: any = options.find(o => o.value === value);

      if (targetOption) {
        setInstantEditing(targetOption.data);
      }
    },
    [options],
  );

五、其他

useRef

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 对象内容发生变化时,useRef 并不会通知你
  • 给ref赋值一个回调函数,可以实现ref 对象内容发生变化时通知。例如测量DOM的需求

测量DOM

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {    if (node !== null) {      setHeight(node.getBoundingClientRect().height);    }  }, []);
  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}