Skip to content

深入理解React hooks原理和应用 #113

@bojue

Description

@bojue

深入理解React hooks原理和应用

React Hook

hook解决了什么问题:

  1. 逻辑复用的问题,hook能够更好的代码组织,UI和逻辑层分离
  2. class难以理解,复杂组件难以维护,hooks不需要class也能够提供props,state的能力
  3. hook作为特殊的函数,必须以use作为前缀,使用eslint插件eslint-plugin-react-hooks在检测阶段取保识别到hook代码模块是否符合规则

问题思考

  1. hook为什么只能在组价最外层调用,不能动态创建或者判断条件创建
  2. 如何只渲染一次hook
  3. useRef为什么需要current

Hook的实现原理

我们学习原理主要学习负责状态操作的useState,处理副作用的useEffect,保存比对结果的useMemo,保存数据对象的useRef。

useState

特点

  1. 设置初始值
  2. 返回一个数组,包含一个最新值和set方法
  3. 调用set方法,触发重新渲染

原理

let nextState
const useState = initState => {
    nextState = nextState || initState 
    function setState(newState) {
        nextState = newState
        render()
    }
    return [nextState, setState]
};

useEffect

特点

  1. 处理副作用的hooks,不能理解成监听的概念

原理

// 声明依赖deps的类型
type depsType = ReadonlyArray<any> | undefined

const lastDepsCatch = []
let currIndex = 0
const useEffect = (callBack: Function, deps: depsType) => {
    const lastDeps = lastDepsCatch[currIndex] 

    // 判断依赖deps是否更新
    const depsChangBool = 
    !lastDeps // 首次渲染判断
    || !deps  // 没有依赖判断,如果没有deps每次都会执行
    || deps.some((dep, index) => dep !== lastDepsCatch[index]) 

    if (depsChangBool) {
      lastDepsCatch[currIndex] = deps
      callback()
    }
    currIndex++;
};

useMemo

特点

  1. 第一参数参数是回调函数 () => value,第二个参数是依赖deps:any[]
  2. 当deps发生更改,胡重新计算新的newValue
  3. 当deps没有发生更改,返回旧的oldValue

原理

// 声明依赖deps的类型
type depsType = ReadonlyArray<any> | undefined

const memoizedState: any[] = [];
let hookIndex = 0;
const useMemo = (callBack: Function, deps: depsType) => {
    //判断是不是第一次渲染
    if(memoizedState[hookIndex]) {
        const [oldVal, oldDeps] = memoizedState[hookIndex];
        // 判断是否相等
        const isEqual = deps?.every(
            (item: any, index: number) => item === oldDeps[index]
        );
        if (isEqual) {
            hookIndex++
            return oldVal
        } 
    }
    const newValue = callBack()
    memoizedState[hookIndex++] = [newValue, deps]
    return newValue
};

useRef

特点

  1. 跨渲染周期保存数据

原理:

useRe方法的调用涉及到两个阶段,分别是mount(第一次渲染)阶段和update(非第一次渲染/更新)阶段,两个不同的阶段涉及的方法的调用逻辑是不相同的

// 参数类型
interface MutableRefObject<T> {
  current: T;
}

// mount 阶段
const mountRef<T>(initialValue: T): MutableRefObject<T> => {

  const hook = mountWorkInProgressHook()
  const ref = {current: initialValue }
  hook.memoizedState = ref 
  return ref
}

const HooksDispatcherOnMount: Dispatcher = {
  useRef: mountRef
}

// update 阶段
const updateRef<T>(initialValue: T): MutableRefObject<T> => {
    const hook = updateWorkInProgressHook()
    return hook.memoizedState
}

const HooksDispatcherOnMount: Dispatcher = {
    useRef: updateRef
}

其他Hook:

  1. useReducer: useReducer和useState的功能和实现原理类似,区别是useReducer使用函数管理状态。
  2. useCallback:和useMemo的实现原理比较类似,是对useMemo函数的一个封装

多个Hook如何管理

如何确保声明的useState, useEffect的初始化和执行更新顺序:

  1. 在初始化的时候将所声明的hook保存到memoizedState数组中
  2. 更新的时候按照数组的执行顺序,将数组的下标依次获取缓存的值进行比对,更新

在React的内部是通过单链表的方式维护memoizedState的

type Hooks = {
  memoizedState: any, // 指向当前渲染节点Fiber
  baseState: any, //初始化state,每次dispath完成更新
  baseUpdate: Update<any> | null,// 需要更新的Update,每次更新完毕之后更新到最新值
  queue: UpdateQueue<any> | null,// UpdateQueue 通过
  next: Hook | null // 单链表指向的下一个更新的hook
}

Hooks应用

(1)基于hook的性能优化场景之一:子组件只触发一次render

  1. useMemo:采用useMemo包裹子组件,deps不变的情况下子组件只需要绘制一次
// 需要缓存的组件
const CatchComponent = ...

useMemo(()=> CatchComponent, [deps])
  1. forwarRef和useImperativeHandle

forwarRef和useImperativeHandle是将子组件的内部状态和方法挂在到ref上,类似下面的伪代码:

const Children = forwardRef((_, ref) => {
    useImperativeHandle(ref, ...);
});
const Parent = forwardRef(({ children }, ref) => {
  const onClick = () => { ref.update() }
  return <>
    {children}
  </>
});
const ForwardRefExample = () => {
  const ref = useRef(null);
  return 
    <Parent ref={ref}>
      <Children  ref={ref} />
    </Parent>
}

(2)封装自己的生命周期hook

  1. useMountHook:组件渲染时触发执行

    使用场景:初始化数据,网络资源请求,开屏动画,性能埋点等操作

const useMountHook = (fun: () => void) => {
  useEffect(() => {
    fun()
  }, [])
}
  1. useUpdateHook:强制函数组渲染,但是不需要通过修改不必要的state完成

    使用场景:刷新页面

const useUpdateHook = (fun: () => void) => {
  const [, setState] = useState({})
  return useCallback(() => setState({}), [])
}

用例:

const UpdateHookExample = () => {
  const updateHook = useUpdateHook()
  return (
    <button type="button" onClick={update}>
      currnetTime: {Date.now()}
    </button>
  );
}
  1. useUnMountHook:组件卸载时触发执行

    使用场景:组件卸载时需要清除定时器,canvas,webgl内容等内容,释放未完成的网络请求,防止内存泄漏,或者性能埋点上报

如果不封装自己的useUnMountHook组价,我们目前使用的方案是通过useEffec开发完成:

const unMoundeHandle = () => {
  console.info('组件卸载,清除定时器!') // 卸载逻辑伪代码
}

useEffect(() => {
  ...// 业务代码
  return unMoundeHandle()
}, [])

我们只需要通过抽象回调函数作为参数,进行简单的组件封装,就可以完成类似unMount的功能实现。

const useUnMountHook = (fun: () => void) => {
  useEffect(() => () => {
    fun()
  }, []);
};

// 使用
useUnMountHook(unMoundeHandle)

(3)逻辑复用:功能hook封装

用例:引用来自社区张立理的代码片段,使用独立的hook实现了用户列表的维护功能,包含了获取用户列表,删除,添加以及网络请求loading状态维护,能够完成耦合功能的模块封装和复用。

const useUserList = () => {
    const [pending, setPending] = useState(false);
    const [users, setUsers] = useState([]);
    const load = async params => {
        setPending(true);
        setUsers([]);
        const users = await request('/users', params);
        setUsers(users);
        setPending(false);
    };
    const deleteUser = useCallback(
        user => setUsers(users => without(users, user)),
        []
    );
    const addUser = useCallback(
        user => setUsers(users => users.concat(user)),
        []
    );
    return [users, {pending, load, addUser, deleteUser}];
};

性能优化

react性能优化的原则

  1. 避免订阅不相关的数据,再采用useMemo和useCallback优化,而是不订阅无相关的数据

hooks的最佳实践

  1. useMemo:缓存计算结果,有条件的发生变化

    useMemo 在线Demo

interface UserInfoType = {
  id:string0
  name:string
  score:string
}

const  UserInfo = ({name, address}) => {
return (
    <>
      <div>产品名称{name}</div>
      <div>产地{address}<>
    </>
    )
}

const Hook =() => {

  const [info, setInfo] = useState<UserInfoType>({})
  const updateScore = () => setInfo(s => {...s,score:score+1})

  return(
      <div>
      {
        useMemo(()=> UserInfo, [info.name, info.address])
      }
        <div>用户得分:{info?.score} </div>
        <button onClick={updateScore}>加分数<button>
      </div>
    )
}
  1. useRef:缓存数据对象,保持不变

  2. useCallback:缓存计算结果,有条件的发生变化,一般和useMemo配合使用

  3. useReducer:解决复杂结构的state和state处理逻辑比较复杂的情况

    (a) useReducer最常见的使用场景,state是一个对象或者数组,state的变化比较复杂,当我们操作state对象涉及更改多个状态的更新

    (b) useReducer 实现UI和业务逻辑分开维护,减少维护成本

    useReducer 在线Demo

  4. 类useUnMount生命周期函数的应用,及时清除定时器,canvas,销毁页面释放请求中的网络请求

const TestComponent = () => {
  const timer = setInterval(()=> {
  }, 1000)

  useEffect(()=> {
    return ()=> clearInterval(timer)
  }, [])
}
/**
 * 涉及三个逻辑的处理
 * 1. 分数score的计算
 * 2. 提示信息message的维护
 * 3. 出局状态gameover状态的维护,状态和ui交互联动
 * 
  */
const reducer = (state, action) => {
  if (state.gameover) {
    console.log("比赛已经结束");
    return state;
  }
  if (action === "add") {
    console.info("恭喜");
    return {
      ...state,
      score: state.score + 1
    };
  } else if (action === "reduce") {
    const score = Math.max(state.score - 1, 0);
    const message = !score
      ? "非常遗憾,你已经淘汰出局"
      : "出现失误,扣一分作为处罚";
    console.info(message);
    return {
      ...state,
      score,
      gameover: !score
    };
  }
  return state;
};


function UseReducerExample() {
  const initData = {
    name: "MrLi",
    score: 0,
    gameover: false
  };
  const [player, updatePlayer] = useReducer(reducer initData);
  return (
    <div>
      <p className="title">useReducer-example</p>
      <div>
        分数:{player.score}
        <span className={player.gameover ? "over" : "hidden"}>已经出局!</span>
      </div>
      <button onClick={() => updatePlayer("add")}>Player 得分</button>
      <button onClick={() => updatePlayer("reduce")}>Player 犯规</button>
    </div>
  );
}

注意事项

hooks的闭包陷阱

function EffectExample() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count);
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  } , []);

  const addCount = () => {
    setCount((s) => ++s);
  };

  return (
    <>
      <p className="title">useEffect闭包陷阱</p>
      <button onClick={addCount}> 加1 </button>
    </>
  );
}

问题分析:当我们无论触发多少次addCount方法,currCount方法都打印的1,因为在第一次渲染的过程中useEffect的依赖为空数组只触发一次,currCount方法接收参数count:0形成闭包,之后不会在触发currCount函数的执行,count不会更新闭包内的值,所以打印的是1;

解决方案:

  1. count作为useEffect的依赖,当corrent发生更改,重新触发副作用useEffect
useEffect(currCount , [count])
  1. useRef
const ref = useRef(null);
ref.current = count;
const fn = () => {
  const timer = setInterval(() => {
    console.log(ref.current);
  }, 1000);
};

避免使用场景

  1. 过度使用useCallback

    (a) 需要和useMemo,reactMemo结合使用,要不然反向优化

  2. 过度使用useMemo场景

    (a) deps为空:空间复杂度没收益

    1. 可以考虑使用ref方案

    (b) deps非空且更新频繁

    1. 时间复杂度:比对deps依赖时额外的时间复杂度
    2. 空间复杂度没收益

    (c) 太过复杂的deps:需要设计合理的deps结构

    (d) 引用相同和开销不大的场景

const useMemoTest = ({id, name}) {
  const getUserInfo = useMemo(() => {
    queryUserInfo(id,name)
  }, [id, name])
  
  return <UserInfo info={getUserInfo}/>
}

极致的性能优化粒度

  1. 解构的性能消耗

    [newCount,setConut]= useCount() 是解构的写法,结构结构复杂和解构数据量比较大也需要考虑优化, 相关资讯Chrome浏览器未来针对于解构的性能优化

参考资料:

  1. react Hook 官网
  2. ahooks
  3. react-use
  4. React Hooks 里的 useCallback 更新幽灵
  5. 知乎问题:如何去合理使用 React hook?
  6. V8 将为 React hooks 改进数组解构的性能
  7. 「react进阶」一文吃透react-hooks原理

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions