React Hook【基础篇】

152 阅读7分钟

前言

为什么出现hooks

组件之间复用状态逻辑难

React没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。 rReact提供的解决此类问题的方案,比如 render props和高阶组件需要重新组织组件结构,使代码难以理解。 在React DevTools中观察会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。

使用Hook从组件中提取状态逻辑,使这些逻辑可以单独测试并复用。可以在无需修改组件结构的情况下复用状态逻辑,在组件间或社区内共享Hook更便捷。

复杂组件难以理解

组件维护过程中逐渐被状态逻辑副作用充斥。 例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
Hook将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

API

  • useLayoutEffect 作用基本与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染 推荐先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect

  • useReducer
    管理包含多个子值得state对象 使用场景:

  1. state逻辑复杂 且包含多个子值
  2. 下一个state依赖其他的state
  • useCallback 函数只有在依赖项发生变化时才会更新 使用引用相等性避免渲染 useCallback(fn,[deps]) == useMemo(()=>fn ,[deps])

  • useMemo 「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果

  • useImperativeHandle 让你在使用 ref 时自定义暴露给父组件的实例值

useContext

返回一个memoized值。 接收context对象(React.createContext 的返回值)并返回该 context 的当前值

useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>

const themes = {
  light: { background: "#eeeeee"},
  dark: { background: "#222222"}
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  const theme = useContext(ThemeContext);
  return (
    <div>
      <button style={{ background: theme.background }}>      
         I am styled by theme context!
      </button> 
    </div>
  );
}

调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,可以使用 memoization 来优化

useState

在函数调用时保存变量的方式,与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

 1:  import React, { useState } from 'react';
 2:
 3:  function Example() {
 4:    const [count, setCount] = useState(0);
 5:
 6:    return (
 7:      <div>
 8:        <p>You clicked {count} times</p>
 9:        <button onClick={() => setCount(count + 1)}>
10:         Click me
11:        </button>
12:      </div>
13:    );
14:  }

如果更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过

函数式更新

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prev => prev + 1)}>+</button>
    </>
  );
}

惰性初始化

//传入函数 返回初始state,此函数只会在初始渲染时被调用
const [state, setState] = useState(() => {
      ...
      ...
    return initialState;
  });

useState 不会自动合并更新对象。 如何合并更新对象?

  1. 可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。
setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});
  1. useReducer管理包含多个子值得state对象

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
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>
    </>
  );
}

惰性初始化

function init(initialCount) {  return {count: initialCount};}
function reducer(state, action) {
  switch (action.type) {
    case 'reset':      
      return init(action.payload);    
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);  
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useEffect

在函数组件中执行副作用,与class中的生命周期函数极为类似。 数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。

useEffect可看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

const [count,setcount]=useState(0)

useEffect(() => {
  //
  //
  
  const subscription = props.source.subscribe();
  return () => {
    // 消除副作用(比如取消订阅)  
    // 清除函数执行时机 组件卸载前执行
    subscription.unsubscribe();
  };
},[依赖]);

函数主体

  • 告诉组件在渲染后执行哪些操作,Dom更新后执行
  • 改变 DOM、添加订阅、设置定时器、记录日志
  • 在组件渲染到屏幕/某些值改变后执行

执行时机

  • 默认每次渲染/执行更新之后都会执行,不考虑“挂载”还是“更新”,effect发生在渲染之后。React 保证了每次运行effect的同时,DOM都已经更新完毕。
  • 浏览器完成布局与绘制之后 新的渲染执行之前
  • 如何控制执行时机 只有依赖数组内的值发生变化才会执行

effect中可获取最新的count的值,每次重新渲染都会生成新的effect,替换掉之前的。某种意义上讲,effect更像是渲染结果的一部分 —— 每个effect“属于”一次特定的渲染。

为什么每次更新都需要更新Effect 为什么effect的清除阶段在每次重渲染都会执行

清除函数

目的:消除副作用

  • 上一次的effect会在重新渲染后被清除
  • 在函数卸载前执行(防止内存泄漏)
  • 组件多次渲染,则在执行下一个effect之前,上一个effect就已被清除

无需清除的副作用: 网络请求,手动变更DOM,记录日志

useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。
大多数情况下,effect 不需要同步地执行。
需要同步执行的情况下(例如测量布局),可使用useLayoutEffect

规范:

  • 使用多个Effect实现关注点分离 将不同逻辑按用途分离
  • 正确使用依赖 通过跳过effect实现性能优化--依赖项发生改变 effect才会执行
    • 在effect内部声明所需要的函数(容易看出effect依赖了组件作用域中哪些值)

为什么

useLayoutEffect

useCallback

把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useMemo

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。

  • 传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作

useRef

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

  • useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
  • 当 ref 对象内容发生变化时,useRef 并不会通知你。
  • 变更.current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,需要使用回调 ref来实现。

获取 DOM 节点的位置或是大小的基本方式是使用callback ref,每当 ref 被附加到一个另一个节点,React 就会调用 callback

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>
    </>
  );
}

没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),依然能够在父组件接收到相关的信息,以便更新测量结果。

useRef 与ref的区别

useImperativeHandle

让你在使用 ref 时自定义暴露给父组件的实例值

useImperativeHandle(ref, createHandle, [deps])