手写 Hooks

hooks 手写仅仅是提供理解和学习hooks的方式,思考hooks的功能原理,而 react 中还提供了一套复杂的hooks管理机制,这部分会后续梳理。React hooks 分为2类:

  • 基础Hooks:useState、useEffect、useContext
  • 衍生Hooks:useReducer、useMemo、useCallback、useRef、useImperativeHandle、useLayoutEffect、useDebugValue

其中高级Hooks,都可以用基础Hooks和高级Hooks实现,而基础Hooks不行。而在 React 中,为确保hooks性能最佳,上述所有hooks都是原生实现,所以该部分实现仅用来加深对hooks的理解,关于原生实现可以直接看下方第二部分 —— hooks状态管理

deps对比功能实现

  1. // 存在不一致返回 true,否则返回 false
  2. const isDepsDiff = (prev, current) => {
  3. return !prev.every((dep, index) => Object.is(dep, current[index]));
  4. }

useRef

  1. const useRef = (initialValue) => {
  2. const [ref] = useState(Object.defineProperties({}, {
  3. _value: {
  4. value: initialValue,
  5. writable: true
  6. },
  7. current: {
  8. get: function() {
  9. return this._value;
  10. },
  11. set: function(newValue) {
  12. this._value = newValue;
  13. },
  14. writable: true,
  15. enumerable: true,
  16. }
  17. }));
  18. return ref;
  19. }

useMemo

  1. const useMemo = (fn, deps) => {
  2. const [value, setValue] = useState(fn);
  3. const mounted = useRef(false);
  4. useEffect(() => {
  5. // 初始化不需要执行, 避免不必要的渲染
  6. if(mounted.current){
  7. const newValue = fn();
  8. setValue(newValue);
  9. } else {
  10. mounted.current = true;
  11. }
  12. }, deps);
  13. return value;
  14. }

useCallback

  1. const useCallback = (fn, deps) => {
  2. return useMemo(() => fn, deps);
  3. }

useReducer

  1. const useReducer = (reducer, initialState, init) => {
  2. const [state, setState] = useState(() => {
  3. return init ? init(initialState) : initialState;
  4. });
  5. // 使用ref,确保 reducer 变化时 dispatch 不变
  6. const reducerRef = useRef<T>(reducer);
  7. reducerRef.current = reducer;
  8. const dispatch = useCallback((action) => {
  9. // 用回调方式,确保dispatch不变
  10. setState(prevState => {
  11. const newState = reducerRef.current(prevState, action);
  12. return {...state, ...newState}
  13. });
  14. }, []);
  15. return [state, dispatch];
  16. }

useImperativeHandle

  1. const useImperativeHandle = (ref, createHandle, deps) => {
  2. useEffect(() => {
  3. ref.current = {
  4. ...ref.current,
  5. ...createHandle()
  6. };
  7. }, deps)
  8. };

useDebugValue

  1. const useDebugValue = (value, formatter) => {
  2. // 该 hook 的实现就是个空函数,惊不惊喜!意不意外!
  3. // https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.old.js#L1608
  4. }

useState

  1. const useState = (function() {
  2. let state = undefined;
  3. let stateInitialized = false;
  4. // setState 定义在闭包内,反复调用 useState 是不会变的
  5. const setState = (newState) => {
  6. state = typeof newState === 'funtion' ? newState(state) : newState;
  7. }
  8. return (initialState) => {
  9. if (stateInitialized) {
  10. state = typeof initialState === 'function' ? initialState() : initialState;
  11. stateInitialized = true;
  12. }
  13. return [state, setState];
  14. }
  15. })();

useEffect/useLayoutEffect

useEffect 和 useLayoutEffect 本身实现是一样的,只是 react 调用两个 hooks 的时间不同。

  1. const useEffect = (function() {
  2. let prevDeps = undefined; // 不要用 useState 管理 deps, 会导致不必要的渲染;
  3. let clearFn = undefined;
  4. return (effect, deps) => {
  5. // 清理前一次的调用状态
  6. clearFn && clearFn();
  7. // useEffect 一定会被调用一次
  8. if(!prevDeps) {
  9. clearFn = effect();
  10. } else if(isDepsDiff(prevDeps, deps)) { // 判断依赖是否改变
  11. clearFn = effect();
  12. }
  13. prevDeps = deps;
  14. };
  15. })();

useContext

  1. const useContext = (ctx) => {
  2. return ctx._currentValue;
  3. }

hooks 状态管理

很明显,上述 useState、useEffect的实现是有问题的。以 useState 为例试想在使用hooks的时候,同一个组件会使用多个hooks,而上述实现会导致所有的state都共用一个变量,甚至跨页面都无法避免。为解决这个问题,React 通过链表来实现状态管理,基本结构如下:
image.png
React 把渲染分为 mount 和 update 两个阶段:

  1. mount 阶段:构建 hooks 链表节点,动态生成 hooks 上述钩子的方法;
  2. update 阶段:按链表流程更新页面中各个 hooks;

也就是说,React 所有 hooks 都会有这样两个状态,即便是 useDebugValue 也存在 mountDebugValue 和 updataDebugValue 两个方法,只是恰好这两个方法都是空函数而已。

useState

不同的hooks节点,数据是不同的,useState如下:

  1. const hooks = {
  2. state: null, // state数据
  3. setState: null, // 修改数据的方法
  4. next: null // 指向下一个节点
  5. }

简化后的useState如下 (试一试):

  1. const Dispatcher = (() => {
  2. let initialRender = true;
  3. let firstWorkInProgressHook = null; // 链表头
  4. let workInProgressHook = null; // 当前节点
  5. const mountWorkInProgressHook = () => {
  6. const hook = {
  7. state: null,
  8. setState: null,
  9. next: null
  10. };
  11. if (workInProgressHook === null) {
  12. firstWorkInProgressHook = workInProgressHook = hook;
  13. } else {
  14. workInProgressHook = workInProgressHook.next = hook;
  15. }
  16. return workInProgressHook;
  17. };
  18. const updateWorkInProgressHook = () => {
  19. let curHook = workInProgressHook;
  20. workInProgressHook = workInProgressHook.next;
  21. return curHook;
  22. };
  23. const useState = (initialState) => {
  24. let curHook;
  25. if (initialRender) {
  26. curHook = mountWorkInProgressHook();
  27. curHook.state = initialState;
  28. } else {
  29. curHook = updateWorkInProgressHook();
  30. }
  31. curHook.setState = function (newState) {
  32. curHook.state = typeof newState === "function" ? newState(curHook.state) : newState;
  33. workInProgressHook = firstWorkInProgressHook;
  34. initialRender = false;
  35. rerender();
  36. };
  37. return [curHook.state, curHook.setState];
  38. };
  39. return {
  40. useState
  41. };
  42. })();

上述实现了一个useState,useEffect实现会更复杂一些,先看一下这个结构:

  1. const effect: Effect = {
  2. tag: number, // 标记 react 类型
  3. create: null, // 副作用功能函数
  4. destroy: null, // 清除副作用的注销函数
  5. deps: null, // 前一次的依赖项
  6. next: null, // 下一个hook
  7. };

内部实现方式:

  1. function pushEffect(tag, create, destroy, deps) {
  2. const effect: Effect = {
  3. tag,
  4. create,
  5. destroy,
  6. deps,
  7. next: null,
  8. };
  9. let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
  10. if (componentUpdateQueue === null) {
  11. componentUpdateQueue = createFunctionComponentUpdateQueue();
  12. currentlyRenderingFiber.updateQueue = componentUpdateQueue;
  13. componentUpdateQueue.lastEffect = effect.next = effect;
  14. } else {
  15. const lastEffect = componentUpdateQueue.lastEffect;
  16. if (lastEffect === null) {
  17. componentUpdateQueue.lastEffect = effect.next = effect;
  18. } else {
  19. const firstEffect = lastEffect.next;
  20. lastEffect.next = effect;
  21. effect.next = firstEffect;
  22. componentUpdateQueue.lastEffect = effect;
  23. }
  24. }
  25. return effect;
  26. }