全面拥抱React-Hooks

丑话说在前面
强烈建议至少刷一遍 《官方文档》,反复研读 《Hooks FAQ》
这里主要以本人关注点聚合,方便理解用于实践

1、React-Hooks要解决什么?

如下是上一代标准写法类组件的缺点,也正是hook要解决的问题css

  • 大型组件很难拆分和重构,也很难测试。
  • 业务逻辑分散在组件的各个方法之中,致使重复逻辑或关联逻辑。
  • 组件类引入了复杂的编程模式,好比 Render props 和高阶组件

设计目的html

  • 增强版函数组件,彻底不使用"类",就能写出一个全功能的组件
  • 组件尽可能写成纯函数,若是须要外部功能和反作用,就用钩子把外部代码"钩"进来

2、如何用好React-Hooks?

明确几点概念

  • 全部的hook,在默认没有依赖项数组每次渲染都会更新
  • 每次 Render 时Props、State、事件处理、Effect等hooks都遵循 Capture Value 的特性
  • Render时会注册各类变量,函数包括hooks,N次Render就会有N个互相隔离的状态做用域
  • 若是你的useEffect依赖数组为[],那么它初始化一次,且使用的state,props等永远是他初始化时那一次Render保存下来的值
  • React 会确保 setState,dispatch,context 函数的标识是稳定的,能够安全地从 hooks 的依赖列表中省略

Function Component中每次Render都会造成一个快照并保留下来,这样就确保了状态可控,hook默认每次都更新,会致使重复请求等一系列问题,若是给[]就会一尘不变,所以用好hooks最重要就是学会控制它的变化react

3、一句话归纳Hook API

  • useState 异步设置更新state
  • useEffect 处理反作用(请求,事件监听,操做DOM等)
  • useContext 接收一个 context 对象并返回该 context 的当前值
  • useReducer 同步处理复杂state,减小了对深层传递回调的依赖
  • useCallback 返回一个 memoized 回调函数,避免非必要渲染
  • useMemo 返回一个 memoized 值,使得控制具体子节点什么时候更新变得更容易,减小了对纯组件的须要,可替代shouldComponentUpdate
  • useRef 返回一个在组件的整个生命周期内保持不变 ref 对象,其 .current 属性是可变的,能够绕过 Capture Value 特性
  • useLayoutEffect 其函数签名与 useEffect 相同,但它会在全部的 DOM 变动以后同步调用 effect
  • useImperativeHandle 应当与 forwardRef 一块儿使用,将 ref 自定义暴露给父组件的实例值
  • useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签

4、关注异同点

useState 与 this.setState

  • 相同点:都是异步的,例如在 onClick 事件中,调用两次 setState,数据只改变一次。
  • 不一样点:类中的 setState 是合并,而useState中的 setState 是替换。

useState 与 useReducer

  • 相同点:都是操做state
  • 不一样点:使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。
  • 推荐:当 state 状态值结构比较复杂时,使用useReducer

useLayoutEffect 与 useEffect

  • 相同点:都是在浏览器完成布局与绘制以后执行反作用操做
  • 不一样点:useEffect 会延迟调用,useLayoutEffect 会同步调用阻塞视觉更新,可使用它来读取 DOM 布局并同步触发重渲染
  • 推荐:一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect

useCallback 与 useMemo

  • 相同点:都是返回memoized,useCallback( fn, deps) 至关于 useMemo( ( ) => fn, deps)
  • 不一样点:useMemo返回缓存的变量,useCallback返回缓存的函数
  • 推荐:不要过早的性能优化,搭配食用口味更佳(详见下文性能优化)

5、性能优化

在大部分状况下咱们只要遵循 React 的默认行为,由于 React 只更新改变了的 DOM 节点,不太重新渲染仍然花费了一些时间,除非它已经慢到让人注意了git

react中性能的优化点在于:

  • 一、调用setState,就会触发组件的从新渲染,不管先后的state是否不一样
  • 二、父组件更新,子组件也会自动的更新

以前的解决方案

基于上面的两点,咱们一般的解决方案是:github

  • 使用immutable进行比较,在不相等的时候调用setState;
  • 在 shouldComponentUpdate 中判断先后的 props和 state,若是没有变化,则返回false来阻止更新。
  • 使用 React.PureComponent

使用hooks function以后的解决方案

传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关, 使用useCallback缓存函数引用,再传递给通过优化的并使用引用相等性去避免非必要渲染的子组件时,它将很是有用编程

  • 一、使用 React.memo等效于 PureComponent,但它只比较 props,且返回值相反,true才会跳过更新
const Button = React.memo((props) => {
  // 你的组件
}, fn);// 也能够自定义比较函数
  • 二、用 useMemo 优化每个具体的子节点(详见实践3)
  • 三、useCallback Hook 容许你在从新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工做(详见实践3)
  • 四、useReducer Hook 减小了对深层传递回调的依赖(详见实践2)

如何惰性建立昂贵的对象?

  • 当建立初始 state 很昂贵时,咱们能够传一个 函数 给 useState 避免从新建立被忽略的初始 state
function Table(props) {
  // ⚠️ createRows() 每次渲染都会被调用
  const [rows, setRows] = useState(createRows(props.count));
  // ...
  // ✅ createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}
  • 避免从新建立 useRef() 的初始值,确保某些命令式的 class 实例只被建立一次:
function Image(props) {
  // ⚠️ IntersectionObserver 在每次渲染都会被建立
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}
function Image(props) {
  const ref = useRef(null);
  // ✅ IntersectionObserver 只会被惰性建立一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }
  // 当你须要时,调用 getObserver()
  // ...
}

6、注意事项

Hook 规则

  • 在最顶层使用 Hook
  • 只在 React 函数中调用 Hook,不要在普通的 JavaScript 函数中调用
  • 将条件判断放置在 hook 内部
  • 全部 Hooks 必须使用 use 开头,这是一种约定,便于使用 ESLint 插件 来强制 Hook 规范 以免 Bug;
useEffect(function persistForm() {
  if (name !== '') {
    localStorage.setItem('formData', name);
  }
});

告诉 React 用到了哪些外部变量,如何对比依赖

useEffect(() => {
  document.title = "Hello, " + name;
}, [name]); // 以useEffect为示例,适用于全部hook

直到 name 改变时的 Rerender,useEffect 才会再次执行,保证了性能且状态可控segmentfault

不要在hook内部set依赖变量,不然你的代码就像旋转的菊花同样停不下来

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);// 以useEffect为示例,适用于全部hook

不要在useMemo内部执行与渲染无关的操做

  • useMemo返回一个 memoized 值,把“建立”函数和依赖项数组做为参数传入 useMemo,它仅会在某个依赖项改变时才从新计算 memoized 值,避免在每次渲染时都进行高开销的计算。
  • 传入 useMemo 的函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的操做。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

7、实践场景示例

实际应用场景每每不是一个hook能搞定的,长篇大论未必说的清楚,直接上例子(来源于官网摘抄,网络收集,自我总结)数组

一、只想执行一次的 Effect 里须要依赖外部变量

【将更新与动做解耦】-【useEffect,useReducer,useState】浏览器

  • 1-一、使用setState的函数式更新解决依赖一个变量

该函数将接收先前的 state,并返回一个更新后的值缓存

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);
  • 1-二、使用useReducer解决依赖多个变量
import React, { useReducer, useEffect } from "react";

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}
export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;
  console.log(count);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

二、大型的组件树中深层传递回调

【经过 context 往下传一个 dispatch 函数】-【createContext,useReducer,useContext】

/**index.js**/
import React, { useReducer } from "react";
import Count from './Count'
export const StoreDispatch = React.createContext(null);
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  switch (action.type) {
    case 'tick':
      return { count: count + step, step };
    case 'step':
      return { count, step: action.step };
    default:
      throw new Error();
  }
}
export default function Counter() {
  // 提示:`dispatch` 不会在从新渲染之间变化
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreDispatch.Provider value={dispatch}>
      <Count state={state} />
    </StoreDispatch.Provider>
  );
}

/**Count.js**/
import React, { useEffect,useContext }  from 'react';
import {StoreDispatch} from '../index'
import styles from './index.css';

export default function(props) {
  const { count, step } = props.state;
  const dispatch = useContext(StoreDispatch);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  return (
    <div className={styles.normal}>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </div>
  );
}

三、代码内聚,更新可控

【层层依赖,各自管理】-【useEffect,useCallback,useContext】

function App() {
  const [count, setCount] = useState(1);
  const countRef = useRef();// 在组件生命周期内保持惟一实例,可穿透闭包传值

  useEffect(() => {
    countRef.current = count; // 将 count 写入到 ref
  });
  // 只有countRef变化时,才会从新建立函数
  const callback = useCallback(() => {
    const currentCount = countRef.current //保持最新的值
    console.log(currentCount);
  }, [countRef]);
  return (
    <Parent callback={callback} count={count}/>
  )
}
function Parent({ count, callback }) {
  // count变化才会从新渲染
  const child1 = useMemo(() => <Child1 count={count} />, [count]);
  // callback变化才会从新渲染,count变化不会 Rerender
  const child2 = useMemo(() => <Child2 callback={callback} />, [callback]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

8、自定义 HOOK

获取上一轮的 props 或 state

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

只在更新时运行 effect

function useUpdate(fn) {
  const mounting = useRef(true);
  useEffect(() => {
    if (mounting.current) {
      mounting.current = false;
    } else {
      fn();
    }
  });
}

组件是否销毁

function useIsMounted(fn) {
  const [isMount, setIsMount] = useState(false);
  useEffect(() => {
    if (!isMount) {
      setIsMount(true);
    }
    return () => setIsMount(false);
  }, []);
  return isMount;
}

惰性初始化useRef

function useInertRef(obj) { // 传入一个实例 new IntersectionObserver(onIntersect)
  const ref = useRef(null);
  if (ref.current === null) {
  // ✅ IntersectionObserver 只会被惰性建立一次
    ref.current = obj;
  }
  return ref.current;
}

参考文章

  1. 精读《useEffect 彻底指南》
  2. 精读《Function VS Class 组件》
  3. 精读《怎么用 React Hooks 造轮子》
  4. 《React Hooks 使用详解》
  5. 《useMemo与useCallback使用指南》
相关文章
相关标签/搜索