来自 Web前端 2020-04-29 17:43 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

React-Hooks

时间: 2019-09-07阅读: 133标签: Hooks

时间: 2019-10-09阅读: 84标签: Hooks一、React-Hooks要解决什么?

从 React Hooks 正式发布到现在,我一直在项目使用它。但是,在使用 Hooks 的过程中,我也进入了一些误区,导致写出来的代码隐藏 bug 并且难以维护。这篇文章中,我会具体分析这些问题,并总结一些好的实践,以供大家参考。

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

问题一:我该使用单个 state 变量还是多个 state 变量?

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

useState的出现,让我们可以使用多个 state 变量来保存 state,比如:

设计目的

const [width, setWidth] = useState(100);const [height, setHeight] = useState(100);const [left, setLeft] = useState(0);const [top, setTop] = useState(0);

加强版函数组件,完全不使用"类",就能写出一个全功能的组件组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来二、如何用好React-Hooks?明确几点概念所有的hook,在默认没有依赖项数组每次渲染都会更新每次 Render 时Props、State、事件处理、Effect等hooks都遵循 Capture Value 的特性Render时会注册各种变量,函数包括hooks,N次Render就会有N个互相隔离的状态作用域如果你的useEffect依赖数组为[],那么它初始化一次,且使用的state,props等永远是他初始化时那一次Render保存下来的值React 会确保 setState,dispatch,context 函数的标识是稳定的,可以安全地从 hooks 的依赖列表中省略

但同时,我们也可以像 Class 组件中的this.state一样,将所有的 state 放到一个object中,这样只需一个 state 变量即可:

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

const [state, setState] = useState({ width: 100, height: 100, left: 0, top: 0});

三、一句话概括Hook APIuseState 异步设置更新stateuseEffect 处理副作用(请求,事件监听,操作DOM等)useContext 接收一个 context 对象并返回该 context 的当前值useReducer 同步处理复杂state,减少了对深层传递回调的依赖useCallback 返回一个 memoized 回调函数,避免非必要渲染useMemo 返回一个 memoized 值,使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要,可替代shouldComponentUpdateuseRef 返回一个在组件的整个生命周期内保持不变 ref 对象,其 .current 属性是可变的,可以绕过 Capture Value 特性useLayoutEffect 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effectuseImperativeHandle 应当与 forwardRef 一起使用,将 ref 自定义暴露给父组件的实例值useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签四、关注异同点useState 与 this.setState相同点:都是异步的,例如在 onClick 事件中,调用两次 setState,数据只改变一次。不同点:类中的 setState 是合并,而useState中的 setState 是替换。useState 与 useReducer相同点:都是操作state不同点:使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。推荐:当 state 状态值结构比较复杂时,使用useReduceruseLayoutEffect 与 useEffect相同点:都是在浏览器完成布局与绘制之后执行副作用操作不同点:useEffect 会延迟调用,useLayoutEffect 会同步调用阻塞视觉更新,可以使用它来读取 DOM 布局并同步触发重渲染推荐:一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffectuseCallback 与 useMemo相同点:都是返回memoized,useCallback( fn, deps) 相当于 useMemo( ( ) = fn, deps)不同点:useMemo返回缓存的变量,useCallback返回缓存的函数推荐:不要过早的性能优化,搭配食用口味更佳(详见下文性能优化)五、性能优化

那么问题来了,到底用单个 state 变量还是多个 state 变量呢?

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

如果使用单个 state 变量,每次更新 state 时需要合并之前的 state。它不像 Class 组件的this.setState方法,会把更新的字段合并到 state 对象中。useState返回的setState会替换原来的值:

react中性能的优化点在于:1、调用setState,就会触发组件的重新渲染,无论前后的state是否不同2、父组件更新,子组件也会自动的更新之前的解决方案

const handleMouseMove = (e) = { setState((prevState) = ({ ...prevState, left: e.pageX, top: e.pageY, }))};

基于上面的两点,我们通常的解决方案是:

使用多个 state 变量可以让 state 的粒度更细,更易于逻辑的拆分和组合。比如,我们可以将关联的逻辑提取到自定义 hook 中:

使用immutable进行比较,在不相等的时候调用setState;在 shouldComponentUpdate 中判断前后的 props和 state,如果没有变化,则返回false来阻止更新。使用 React.PureComponent使用hooks function之后的解决方案

function usePosition() { const [left, setLeft] = useState(0); const [top, setTop] = useState(0); useEffect(() = { // ... }, []); return [left, top, setLeft, setTop];}

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

我们发现,每次更新left时top也会随之更新。因此,把top和left拆分为两个 state 变量显得有点多余。在使用 state 之前,我们需要考虑状态拆分的「粒度」问题。如果粒度过细,代码就会变得比较冗余。如果粒度过粗,代码的可复用性就会降低。

1、使用 React.memo等效于 PureComponent,但它只比较 props,且返回值相反,true才会跳过更新

那么,到底哪些 state 应该合并,哪些 state 应该拆分呢?我总结了下面两点:

const Button = React.memo((props) = { // 你的组件}, fn);// 也可以自定义比较函数

将完全不相关的 state 拆分为多组 state。比如size和position。如果某些 state 是相互关联的,或者需要一起发生改变,就可以把它们合并为一组 state。比如left和top。

2、用 useMemo 优化每一个具体的子节点(详见实践3)3、useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工作(详见实践3)4、useReducer Hook 减少了对深层传递回调的依赖(详见实践2)如何惰性创建昂贵的对象?当创建初始 state 很昂贵时,我们可以传一个 函数 给 useState 避免重新创建被忽略的初始 state

function Box() { const [position, setPosition] = usePosition(); const [size, setSize] = useState({width: 100, height: 100}); // ...}function usePosition() { const [position, setPosition] = useState({left: 0, top: 0}); useEffect(() = { // ... }, []); return [position, setPosition];}
function Table(props) { // ⚠️ createRows() 每次渲染都会被调用 const [rows, setRows] = useState(createRows(props.count)); // ... // ✅ createRows() 只会被调用一次 const [rows, setRows] = useState(() = createRows(props.count)); // ...}

问题二:deps 依赖过多,导致 Hooks 难以维护?

避免重新创建 useRef() 的初始值,确保某些命令式的 class 实例只被创建一次:

使用useEffecthook 时,为了避免每次 render 都去执行它的 callback,我们通常会传入第二个参数「dependency array」。这样,只有当「依赖数组」发生变化时,才会执行useEffect的回调函数。

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() // ...}
function Example({id, name}) { useEffect(() = { console.log(id, name); }, [id, name]); }

六、注意事项Hook 规则在最顶层使用 Hook只在 React 函数中调用 Hook,不要在普通的 JavaScript 函数中调用将条件判断放置在 hook 内部所有 Hooks 必须使用 use 开头,这是一种约定,便于使用 ESLint 插件 来强制 Hook 规范 以避免 Bug;

在上面的例子中,只有当id或者name发生变化时,才会打印日志。「dependency array」中必须包含在 callback 内部用到的所有参与 React 数据流的值,比如state、props以及它们的衍生物。如果有遗漏,可能会造成 bug。这其实就是 JS 闭包问题,对闭包不清楚的同学可以自行 google,这里就不展开了。

useEffect(function persistForm() { if (name !== '') { localStorage.setItem('formData', name); }});
function Example({id, name}) { useEffect(() = { // 由于 dependency array 中不包含 name,所以当 name 发生变化时,无法打印日志 console.log(id, name); }, [id]);}

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

在 React 中,除了 useEffect 外,接收「dependency array」的 hook 还有useMemo、useCallback和useImperativeHandle。大部分情况下,使用「dependency array」确实可以节省一些性能的开销。我们刚刚也提到了,「dependency array」千万不要遗漏回调函数内部依赖的值。但如果 「dependency array」依赖了过多东西,可能导致代码难以维护。我在项目中就看到了这样一段代码:

useEffect(() = { document.title = "Hello, " + name;}, [name]); // 以useEffect为示例,适用于所有hook
const refresh = useCallback(() = { // ...}, [name, searchState, address, status, personA, personB, progress, page, size]);

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

不要说内部逻辑了,光是看到这一堆依赖就令人头大!如果项目中到处都是这样的代码,可想而知维护起来多么痛苦。如何才能避免写出这样的代码呢?

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

首先,你需要重新思考一下,这些 deps 是否真的都需要?看下面这个例子:

useEffect(() = { const id = setInterval(() = { setCount(count + 1); }, 1000); return () = clearInterval(id);}, [count]);// 以useEffect为示例,适用于所有hook
function Example({id}) { const requestParams = useRef({}); requestParams.current = {page: 1, size: 20, id}; const refresh = useCallback(() = { doRefresh(requestParams.current); }, []); useEffect(() = { id  refresh(); }, [id, refresh]); // 思考这里的 deps list 是否合理?}

不要在useMemo内部执行与渲染无关的操作useMemo返回一个 memoized 值,把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值,避免在每次渲染时都进行高开销的计算。传入 useMemo 的函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的操作。

虽然useEffect的回调函数依赖了id和refresh方法,但是观察refresh方法可以发现,它在首次 render 被创建之后,永远不会发生改变了。因此,把它作为useEffect的 deps 是多余的。

constmemoizedValue = useMemo(()=computeExpensiveValue(a, b), [a, b]);

其次,如果这些依赖真的都是需要的,那么这些逻辑是否应该放到同一个 hook 中?

七、实践场景示例

function Example({id, name, address, status, personA, personB, progress}) { const [page, setPage] = useState(); const [size, setSize] = useState(); const doSearch = useCallback(() = { // ... }, []); const doRefresh = useCallback(() = { // ... }, []); useEffect(() = { id  doSearch({name, address, status, personA, personB, progress}); page  doRefresh({name, page, size}); }, [id, name, address, status, personA, personB, progress, page, size]);}

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

可以看出,在useEffect中有两段逻辑,这两段逻辑是相互独立的,因此我们可以将这两段逻辑放到不同useEffect中:

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

useEffect(() = { id  doSearch({name, address, status, personA, personB, progress});}, [id, name, address, status, personA, personB, progress]);useEffect(() = { page  doRefresh({name, page, size});}, [name, page, size]);

【将更新与动作解耦】-【useEffect,useReducer,useState】

如果逻辑无法继续拆分,但是「dependency array」还是依赖过多东西,该怎么办呢?就比如我们上面的代码:

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

useEffect(() = { id  doSearch({name, address, status, personA, personB, progress});}, [id, name, address, status, personA, personB, progress]);

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

这段代码中的useEffect依赖了七个值,还是偏多了。仔细观察上面的代码,可以发现这些值都是「过滤条件」的一部分,通过这些条件可以过滤页面上的数据。因此,我们可以将它们看做一个整体,也就是我们前面讲过的合并 state:

useEffect(() = { const id = setInterval(() = { setCount(c = c + 1); }, 1000); return () = clearInterval(id);}, []);
const [filters, setFilters] = useState({ name: "", address: "", status: "", personA: "", personB: "", progress: ""});useEffect(() = { id  doSearch(filters);}, [id, filters]);

1-2、使用useReducer解决依赖多个变量

如果 state 不能合并,在 callback 内部又使用了setState方法,那么可以考虑使用setStatecallback 来减少一些依赖。比如:

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) }); }} / / );}
const useExample = () = { const [values, setValues] = useState({ data: {}, count: 0 }); const [updateData] = useCallback( (nextData) = { setValues({ data: nextData, count: values.count + 1 // 因为 callback 内部依赖了外部的 values 变量,所以必须在 dependency array 中指定它 }); }, [values], ); return [values, updateData];};

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

上面的代码中,我们必须在useCallback的「dependency array」中指定values,否则我们无法在 callback 中获取到最新的values状态。但是,通过setState回调函数,我们不用再依赖外部的values变量,因此也无需在「dependency array」中指定它。就像下面这样:

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

const useExample = () = { const [values, setValues] = useState({}); const [updateData] = useCallback((nextData) = { setValues((prevValues) = ({ data: nextData, count: prevValues.count + 1, // 通过 setState 回调函数获取最新的 values 状态,这时 callback 不再依赖于外部的 values 变量了,因此 dependency array 中不需要指定任何值 })); }, []); // 这个 callback 永远不会重新创建 return [values, updateData];};
/**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 );}

最后,还可以通过ref来保存可变变量,并对它进行读写。举个例子:

3、代码内聚,更新可控

const useExample = () = { const [values, setValues] = useState({}); const latestValues = useRef(values); latestValues.current = values; const [updateData] = useCallback((nextData) = { setValues({ data: nextData, count: latestValues.current.count + 1, }); }, []); return [values, updateData];};

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

在使用ref时要特别小心,因为它可以随意赋值,所以一定要控制好修改它的方法。特别是一些底层模块,在封装的时候千万不要直接暴露ref,而是提供一些修改它的方法。

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

说了这么多,归根到底都是为了写出更加清晰、易于维护的代码。如果发现「dependency array」依赖过多,我们就需要重新审视自己的代码。

八、自定义 HOOK获取上一轮的 props 或 state

「dependency array」依赖的值最好不要超过 3 个,否则会导致代码会难以维护。

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

如果发现 「dependency array」依赖的值过多,我们应该采取一些方法来减少它。

只在更新时运行 effect

去掉不必要的「dependency array」。将 hook 拆分为更小的单元,每个 hook 依赖于各自的「dependency array」。通过合并相关的 state,将多个 dependency 聚合为一个 dependency。通过setState回调函数获取最新的 state,以减少外部依赖。通过ref来读取可变变量的值,不过需要注意控制修改它的途径。问题三:该不该使用useMemo?

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

该不该使用useMemo?对于这个问题,有的人从来没有思考过,有的人甚至不觉得这是个问题。不管什么情况,只要用useMemo或者useCallback「包裹一下」,似乎就能使应用远离性能的问题。但真的是这样吗?有的时候useMemo没有任何作用,甚至还会影响应用的性能。

组件是否销毁

为什么这么说呢?首先,我们需要知道useMemo本身也有开销。useMemo会「记住」一些值,同时在后续 render 时,将「dependency array」中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用useMemo可能会影响程序的性能。

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

要想合理使用useMemo,我们需要搞清楚useMemo适用的场景:

惰性初始化useRef

有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render 都去重新计算。由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。

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

让我们来看个例子:

interface IExampleProps { page: number; type: string;}const Example = ({page, type}: IExampleProps) = { const resolvedValue = useMemo(() = { getResolvedValue(page, type); }, [page, type]); return ExpensiveComponent resolvedValue={resolvedValue}/;};

在上面的例子中,渲染ExpensiveComponent的开销很大。所以,当resolvedValue的引用发生变化时,作者不想重新渲染这个组件。因此,作者使用了useMemo,避免每次 render 重新计算resolvedValue,导致它的引用发生改变,从而使下游组件 re-render。

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:React-Hooks

关键词: