react hooks

hook规则

在这里插入图片描述

为什么不可以?

下面这样一段代码。执行 useState 重新渲染,和初始化渲染 顺序不一样就会出现如下问题
如果了解了上面 useState 模拟写法的存储方式,那么这个问题的原因就迎刃而解了
在这里插入图片描述

常用 hook

1. useState

 const [state, setState] = useState(initialState)
  • useState 有一个参数,该参数可以为任意数据类型,一般用作默认值
  • useState 返回值为一个数组,数组的第一个参数为我们需要使用的 state,第二个参数为一个 setFn。
  • 完整例子
function Love() {
    const [like, setLike] = useState(false)
    const likeFn = () => (newLike) => setLike(newLike)
    return (
      <>
        你喜欢我吗: {like ? 'yes' : 'no'}
        <button onClick={likeFn(true)}>喜欢</button>
        <button onClick={likeFn(false)}>不喜欢</button>
      </>
    )
  }

使用规则

  1. 只在 React 函数中调用 Hook;
  2. 不要在循环、条件或嵌套函数中调用 Hook。
function mountWorkInProgressHook() {
 // 注意,单个 hook 是以对象的形式存在的
 var hook = {
  memoizedState: null,
  baseState: null,
  baseQueue: null,
  queue: null,
  next: null
 };
 if (workInProgressHook === null) {
        firstWorkInProgressHook = workInProgressHook = hook;
        /* 等价
            let workInProgressHook = hooks
            firstWorkInProgressHook = workInProgressHook
        */
 } else {
  workInProgressHook = workInProgressHook.next = hook;
 }
 // 返回当前的 hook
 return workInProgressHook;
}

每个 hook 都会有一个 next 指针,hook 对象之间以单向链表的形式相互串联, 同时也能发现 useState 底层依然是 useReducer 再看看更新阶段发生了什么

// ReactFiberHooks.js
const HooksDispatcherOnUpdate: Dispatcher = {
      // ...
     useState: updateState,
  }
  function updateState(initialState) {
    return updateReducer(basicStateReducer, initialState);
  }

function updateReducer(reducer, initialArg, init) {
    const hook = updateWorkInProgressHook();
    const queue = hook.queue;
    if (numberOfReRenders > 0) {
        const dispatch = queue.dispatch;
        if (renderPhaseUpdates !== null) {
            // 获取Hook对象上的 queue,内部存有本次更新的一系列数据
            const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
            if (firstRenderPhaseUpdate !== undefined) {
                renderPhaseUpdates.delete(queue);
                let newState = hook.memoizedState;
                let update = firstRenderPhaseUpdate;
                // 获取更新后的state
                do {
                    // useState 第一个参数会被转成 useReducer
                    const action = update.action;
                    newState = reducer(newState, action);
                    //按照当前链表位置更新数据
                    update = update.next;
                } while (update !== null);
                hook.memoizedState = newState;
                // 返回新的 state 以及 dispatch
                return [newState, dispatch];
            }
        }
    }
    // ...
}

结合实际让我们看下面一组 hooks

 let isMounted = false
    if(!isMounted) {
        [name, setName] = useState("张三");
        [age] = useState("25");
        isMounted = true
    }
    [sex, setSex] = useState("男");
    return (
        <button
            onClick={() => {
            setName(李四");
            }}
        >
            修改姓名
        </button>
  );

首次渲染时 hook 顺序为
name => age => sex
二次渲染的时根据上面的例子,调用的 hook 的只有一个
setSex

所以总结一下初始化阶段构建链表,更新阶段按照顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染当两次顺序不一样的时候就会造成渲染上的差异。

2.useEffect

effect 每次完成渲染之后触发, 配合 array 去模拟类的生命周期
如果不传,则每次 componentDidUpdate 时都会先触发 returnFunction(如果存在),再触发 effect
[] 模拟 componentDidMount
[id] 仅在 id 的值发生变化以后触发
清除 effect
useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  };
});

3. useLayoutEffect

  • 跟 useEffect 使用差不多,通过同步执行状态更新可解决一些特性场景下的页面闪烁问题
  • useLayoutEffect 会阻塞渲染,请谨慎使用

4 .useContext

  • useContext 从名字上就可以看出,它是以 Hook 的方式使用 React Context, 先简单介绍 Context 的概念和使用方式
import React, { useContext, useState, useEffect } from "react";
const ThemeContext = React.createContext(null);
const Button = () => {
  const { color, setColor } = React.useContext(ThemeContext);
  useEffect(() => {
    console.info("Context changed:", color);
  }, [color]);
  const handleClick = () => {
    console.info("handleClick");
    setColor(color === "blue" ? "red" : "blue");
  };
  return (
    <button
      type="button"
      onClick={handleClick}
      style={{ backgroundColor: color, color: "white" }}
    >
      toggle color in Child
    </button>
  );
};
// app.js
const App = () => {
  const [color, setColor] = useState("blue");

  return (
    <ThemeContext.Provider value={{ color, setColor }}>
      <h3>
        Color in Parent: <span style={{ color: color }}>{color}</span>
      </h3>
      <Button />
    </ThemeContext.Provider>
  );
};

5. useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init)

function init(initialCount) {
    return {count: initialCount};
}
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        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: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}

6. useRef

const refContainer = useRef(initialValue);

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

  • 解决引用问题–useRef 会在每次渲染时返回同一个 ref 对象
  • 解决一些 this 指向问题
  • 对比 createRef – 在初始化阶段两个是没区别的,但是在更新阶段两者是有区别的。
  • 我们知道,在一个局部函数中,函数每一次 update,都会在把函数的变量重新生成一次。所以我们每更新一次组件, 就重新创建一次 ref, 这个时候继续使用 createRef 显然不合适,所以官方推出 useRef。useRef 创建的 ref 仿佛就像在函数外部定义的一个全局变量,不会随着组件的更新而重新创建。但组件销毁,它也会消失,不用手动进行销毁

总结下就是 ceateRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用

7 .useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

onst MemoDemo = ({ count, color }) => {
   useEffect(() => {
       console.log('count effect')
   }, [count])
   const newCount = useMemo(() => {
       console.log('count 触发了')
       return Math.round(count)
   }, [count])
   const newColor = useMemo(() => {
       console.log('color 触发了')
       return color
   }, [color])
   return <div>
       <p>{count}</p>
       <p>{newCount}</p>
   {newColor}</div>
}


我们这个时候将传入的 count 值改变 的,log 执行循序
count 触发了
count effect

可以看出有点类似 effect, 监听 a、b 的值根据值是否变化来决定是否更新 UI

  • memo 是在 DOM 更新前触发的,就像官方所说的,类比生命周期就是 - shouldComponentUpdate
  • 对比 React.Memo 默认是是基于 props 的浅对比,也可以开启第二个参数进行深对比。在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。使用 useMemo 可以精细化控制,进行局部 Pure

8. useCallback

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

useCallback 的用法和上面 useMemo 差不多,是专门用来缓存函数的 hooks

// 下面的情况可以保证组件重新渲染得到的方法都是同一个对象,避免在传给onClick的时候每次都传不同的函数引用
import React, { useState, useCallback } from 'react'

function MemoCount() {
   const [value, setValue] = useState(0)

   memoSetCount = useCallback(()=>{
       setValue(value + 1)
   },[])

   return (
       <div>
           <button
               onClick={memoSetCount}
               >
               Update Count
           </button>
           <div>{value}</div>
       </div>
   )
}
export default MemoCount

useMeno useCallback useEffect

通过源代码的参数可以发现使用上 useCallback 和 useMemo 的参数跟 useEffect 一致,他们之间最大的区别有是 useEffect 会用于处理副作用,而前两个 hooks 不能。
useMemo 和 useCallback 都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个 hooks 都返回缓存的值,useMemo 返回缓存的变量,useCallback 返回缓存的函数。

重要一点

useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种 hook s的执行,反映的也都是当前的状态,我们无法使用它们来捕获上一次的状态。如果需要获取之前的状态,我们应该使用ref来访问。

自定义 hooks

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

import { useEffect, useState } from 'react';
export default function useWindowSize(el) {
   const [windowSize, setWindowSize] = useState({
       width: undefined,
       height: undefined,
   });
   useEffect(
       () => {

           function handleResize() {
               setWindowSize({
                   width: window.innerWidth,
                   height: window.innerHeight,
               });
           }

           window.addEventListener('resize', handleResize);
           handleResize();
           return () => window.removeEventListener('resize', handleResize);
       },
       [el],
   );
   return windowSize;
}

API

像之前的我们有一个公用的城市列表接口,在用 redux 的时候可以放在全局公用,不用的话我们就可能需要复制粘贴了。有了 hooks 以后我们只需要 use 一下就可以在其他地方复用了
import { useState, useEffect } from 'react';
import { getCityList } from '@/services/static';
const useCityList = (params) => {
   const [cityList, setList] = useState([]);
   const [loading, setLoading] = useState(true)
   const getList = async () => {
       const { success, data } = await getCityList(params);
       if (success) setList(data);
       setLoading(false)
   };
   useEffect(
       () => {getList();},
       [],
   );
   return {
       cityList,
       loading
   };
};
export default useCityList;
// bjs
function App() {
   // ...
   const { cityList, loading } = useCityList()
   // ...
}

logic

逻辑类,比如我们有一个点击用户头像关注用户或者取消关注的逻辑,可能在评论列表、用户列表都会用到,我们可以这样做
import { useState, useEffect } from 'react';
import { followUser } from '@/services/user';
const useFollow = ({ accountId, isFollowing }) => {
    const [isFollow, setFollow] = useState(false);
    const [operationLoading, setLoading] = useState(false)
    const toggleSection = async () => {
        setLoading(true)
        const { success } = await followUser({ accountId });
        if (success) {
            setFollow(!isFollow);
        }
        setLoading(false)
    };
    useEffect(
        () => {
            setFollow(isFollowing);
        },
        [isFollowing],
    );
    return {
        isFollow,
        toggleSection,
        operationLoading
    };
};
export default useFollow;

只需暴露三个参数就能满足大部分场景
UI

还有一些和 UI 一起绑定的 hook, 但是这里有点争议要不要和 ui 一起混用。就我个人而言一起用确实帮我解决了部分复用问题,我还是分享出来。
import React, { useState } from 'react';
import { Modal } from 'antd';
// TODO 为了兼容一个页面有多个 modal, 目前想法通过唯一 key 区分,后续优化
export default function useModal(key = 'open') {
    const [opens, setOpen] = useState({
        [key]: false,
    });
    const onCancel = () => {
        setOpen({ [key]: false });
    };
    const showModal = (type = key) => {
        setOpen({ [type]: true });
    };
    const MyModal = (props) => {
        return <Modal key={key} visible={opens[key]} onCancel={onCancel} {...props} />;
    };
    return {
        showModal,
        MyModal,
    };
}
// 使用
function App() {
    const { showModal, MyModal } = useModal();
    return <>
          <button onClick={showModal}>展开</button>
          <MyModal onOk={console.log} />
       </>
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值