2021-03-06 React原理解析之useState Hooks

173 阅读5分钟

Hook简介

Hook是React16.8的新增特性,它可以在你不编写class的情况下使用state以及其他的React特性

  1. Hooks是什么?为了拥抱正能量函数式
  2. Hooks带来的变革,让函数组件有了状态和其他的React特性,可以替代class

没有破坏性改动

  • 完全可选。无需重写任何已有代码就可以在一些组件中尝试Hook
  • 100%向后兼容。Hook不包含任何破坏性改动
  • 现在可用。Hook发布于v16.8

没有计划从React中移除class

Hook不会影响你对React的理解,恰恰相反,Hook为已知的React概念提供了更直接的API:props,state,context,refs以及生命周期

Hook解决了什么问题

  1. 组件之间复用状态逻辑很难

React没有提供将可复用行为‘附加’到组件的途径(例如,把组件连接到store),比如render props高阶组件,但这类方案需要重新组织你的组件结构,这可能会很麻烦。这说明了一个更深层次的问题:React需要为共享状态逻辑提供更好的原生途径

你可以使用Hook从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook使你在无需修改组件结构的情况下复用状态逻辑,这使得在组件间共享Hook变得更便捷

  1. 复杂组件变得难以理解

我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期尝尝包含一些不相关的逻辑。例如,组件尝尝在componentDidMountcomponentDidUpdate中获取数据,但是同一个componentDidMount中也可能包含很多其它的逻辑,如设置事件监听,而后需要在componentWillUnmount中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起,如此很容易产生bug,并且导致逻辑不一致

这时候很多人将React与状态管理库结合使用,但是,这往往会引入很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难

为了解决这个问题,Hook将组件中相互关联的部分拆分为更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分,你还可以使用reducer来管理组件的内部状态,使其更加可预测

  1. 难以理解的class

除了代码复用和代码管理会遇到困难,还需要去理解Javascript中this的工作方式和class类,对于class组件和函数组件的差异也需要去区分两者的使用场景。另外class不能很好的压缩,并且会使热重载出现不稳定的情况

为了解决这些问题,Hook使你在非class的情况下使用更多的React特性。从原则上讲,React组件一直更像函数,而Hook则拥抱了函数,同时也没有牺牲React的精神原则,Hook提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术

Hook API

基础的Hook

  • useState
  • useEffect
  • useContext

注意:useState返回一个数组[state,setState],是因为使用数组解构方面灵活命名,如果返回对象则属性名是写死的,不利于开发者命名

额外的Hook

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect

自定义Hook

  • 以use开头的函数

详情请查阅官方文档Hook简介

useState简单实现

实现一个useState简单版本

测试文件index.js

function FunctionComponent(props) {
  const [count, setCount] = useState(0);
  console.log("count", count, typeof count);
  return (
    <div className="border">
      <p>{props.name}</p>
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </div>
  );
}

....省略啦

hook之所以要按顺序放在函数的最顶层,是因为hook是用链表结构来存储的,存在函数fiber节点的memoizedState指针上

// hook的链表结构
hook = {
  memoizedState: //状态值
  queue: // 数组,处理批量在空闲的时候
  next: // 指向下一个hook
};

全部代码文件react-dom.js

/**
 * vnode 虚拟DOM
 * node 真实DOM
 */

import { Placement, Update } from "./const";

// 根节点 fiber
let wipRoot = null;
let currentRoot = null;

function render(vnode, container) {
  // console.log("vnode", vnode);
  /*  // vnode->node
  const node = createNode(vnode);
  // node 插入到container中
  container.appendChild(node); */

  wipRoot = {
    type: "div",
    props: { children: { ...vnode } },
    stateNode: container
  };

  nextUnitWork = wipRoot;
  // console.log("netUnitWork", nextUnitWork);
}

function isStringOrNumber(str) {
  return typeof str === "string" || typeof str === "number";
}

// 根据vnode,生成node
function createNode(workInProgress) {
  let node = document.createElement(workInProgress.type);
  updateNode(node, {}, workInProgress.props);

  return node;
}

// 更新原生标签的属性,如className,href,id,(style,事件等)
function updateNode(node, prevVal, nextVal) {
  Object.keys(prevVal).forEach(k => {
    if (k === "children") {
      // 有可能是文本
      if (isStringOrNumber(prevVal[k])) {
        node.textContent = "";
      }
    } else if (k.slice(0, 2) === "on") {
      const eventName = k.slice(2).toLocaleLowerCase();
      node.removeEventListener(eventName, prevVal[k]);
    } else {
      if (!(k in nextVal)) {
        node[k] = "";
      }
    }
  });

  Object.keys(nextVal)
    // .filter(k => k !== "children")
    .forEach(k => {
      if (k === "children") {
        // 有可能是文本
        if (isStringOrNumber(nextVal[k])) {
          node.textContent = nextVal[k] + "";
        }
      } else if (k.slice(0, 2) === "on") {
        const eventName = k.slice(2).toLocaleLowerCase();
        node.addEventListener(eventName, nextVal[k]);
      } else {
        node[k] = nextVal[k];
      }
    });
}

// 原生标签
function updateHostComponent(workInProgress) {
  // 修身
  // 构建真实dom节点
  if (!workInProgress.stateNode) {
    workInProgress.stateNode = createNode(workInProgress);
  }

  // 齐家
  // 协调子节点
  reconcileChildren(workInProgress, workInProgress.props.children);
  // console.log("workInProgress", workInProgress);
}

// 文本
function updateTextComponent(workInProgress) {
  if (!workInProgress.stateNode) {
    workInProgress.stateNode = document.createTextNode(workInProgress.props);
  }
}

// 函数组件
function updateFunctionComponent(workInProgress) {
  // 当前正在工作的fiber以及hook的初始化
  currentlyRenderingFiber = workInProgress;
  currentlyRenderingFiber.memoizedState = null; // 存取Hook
  workInProgressHook = null;

  const { type, props } = workInProgress;
  const children = type(props);
  reconcileChildren(workInProgress, children);
}

// 类组件
function updateClassComponent(workInProgress) {
  const { type, props } = workInProgress;
  // 先实例化
  const instance = new type(props);
  const children = instance.render();
  reconcileChildren(workInProgress, children);
}

// <>Fragment节点
function updateFragmentComponent(workInProgress) {
  reconcileChildren(workInProgress, workInProgress.props.children);
}

// 最假的吧,最简单的也是协调
function reconcileChildren(workInProgress, children) {
  if (isStringOrNumber(children)) {
    return;
  }
  let newChildren = Array.isArray(children) ? children : [children];

  let previousNewFiber = null;
  let oldFiber = workInProgress.alternate && workInProgress.alternate.child;
  for (let i = 0; i < newChildren.length; i++) {
    let child = newChildren[i];

    let same =
      child &&
      oldFiber &&
      // child.key === oldFiber.key &&
      child.type === oldFiber.type;

    let newFiber;
    if (same) {
      // 节点复用
      newFiber = {
        type: child.type, // 类型
        props: { ...child.props }, // 属性
        stateNode: oldFiber.stateNode, // 如果是原生标签代表dom节点,如果是类组件就代表实例
        child: null, // 第一个子节点fiber
        sibling: null, // 下一个兄弟节点 fiber
        return: workInProgress, // 父节点
        alternate: oldFiber, // 上一次fiber
        flags: Update
      };
    }

    if (!same && child) {
      // 构建fiber节点
      newFiber = {
        type: child.type, // 类型
        props: { ...child.props }, // 属性
        stateNode: null, // 如果是原生标签代表dom节点,如果是类组件就代表实例
        child: null, // 第一个子节点fiber
        sibling: null, // 下一个兄弟节点 fiber
        return: workInProgress, // 父节点
        alternate: null, // 上一次fiber
        flags: Placement
      };
    }

    if (!same && oldFiber) {
      // 删除
    }

    if (isStringOrNumber(child)) {
      newFiber.props = child;
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    if (i === 0) {
      // 第一个子fiber
      workInProgress.child = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }

    previousNewFiber = newFiber;
  }
}

/**
 * fiber数据结构
 * type
 * props
 * key
 * stateNode 如果是原生标签代表dom节点,如果是类组件代表实例
 * child 第一个子节点
 * sibling 下一个兄弟节点
 * return 指向父节点
 * alternate 上一次fiber
 */

// workInProgress当前正在进行的中的fiber
function performNextUnitWork(workInProgress) {
  // step1 执行当前任务
  const { type } = workInProgress;
  if (isStringOrNumber(type)) {
    // 原生标签
    updateHostComponent(workInProgress);
  } else if (typeof type === "function") {
    if (type.prototype.isReactComponent) {
      // 类组件
      updateClassComponent(workInProgress);
    } else {
      // 函数组件
      updateFunctionComponent(workInProgress);
    }
  } else if (typeof type === "undefined") {
    // 文本
    updateTextComponent(workInProgress);
  } else {
    // Fragment
    updateFragmentComponent(workInProgress);
  }

  // step2 并且返回下一个任务(深度优先遍历)
  if (workInProgress.child) {
    // 有孩子返回孩子
    return workInProgress.child;
  }

  let nextFiber = workInProgress;
  while (nextFiber) {
    // 有兄弟返回兄弟,没有返回叔叔
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    // 叔叔就是爸爸的兄弟
    nextFiber = nextFiber.return;
  }
}

// 下一个要执行的任务
let nextUnitWork = null; // fiber
function workLoop(IdleDeadline) {
  while (nextUnitWork && IdleDeadline.timeRemaining() > 1) {
    // 执行当前任务,并且返回下一个任务
    nextUnitWork = performNextUnitWork(nextUnitWork);
  }

  // 没有任务就提交
  if (!nextUnitWork && wipRoot) {
    commitRoot();
  }

  requestIdleCallback(workLoop);
}

// 在浏览器的空闲时间段内调用的函数排队
requestIdleCallback(workLoop);

function commitRoot() {
  commitWorker(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

function commitWorker(workInProgress) {
  if (!workInProgress) {
    return;
  }
  // 更新自己
  let parentNodeFiber = workInProgress.return;

  while (!parentNodeFiber.stateNode) {
    parentNodeFiber = parentNodeFiber.return;
  }

  let parentNode = parentNodeFiber.stateNode;
  if (workInProgress.flags & Placement && workInProgress.stateNode) {
    parentNode.appendChild(workInProgress.stateNode);
  } else if (workInProgress.flags & Update && workInProgress.stateNode) {
    updateNode(
      workInProgress.stateNode,
      workInProgress.alternate.props,
      workInProgress.props
    );
  }

  // 更新子节点
  commitWorker(workInProgress.child);
  // 更新下一个兄弟节点
  commitWorker(workInProgress.sibling);
}

// 当前正在工作的fiber
let currentlyRenderingFiber = null;

// 当前正在工作的hook
let workInProgressHook = null;

// 一个hook函数
export function useState(initialState) {
  let hook;
  // 判断是否是组件初次渲染
  if (currentlyRenderingFiber.alternate) {
    // 不是初次渲染,是更新阶段
    currentlyRenderingFiber.memoizedState =
      currentlyRenderingFiber.alternate.memoizedState;

    if (workInProgressHook) {
    } else {
      // 是第一个hook
      workInProgressHook = currentlyRenderingFiber.memoizedState;
    }

    hook = workInProgressHook;
  } else {
    // 初次渲染
    hook = {
      memoizedState: initialState, // 状态值
      queue: [], // 批量更新数组存取
      next: null // 下一个hook
    };

    if (workInProgressHook) {
      // 不是第一个hook
      hook = workInProgressHook;
    } else {
      // 是第一个hook,挂到fiber节点上
      currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
    }
  }

  // 模拟批量处理
  hook.queue.forEach(action => (hook.memoizedState = action));

  const dispatch = action => {
    hook.queue.push(action);

    wipRoot = {
      type: currentRoot.type,
      stateNode: currentRoot.stateNode,
      props: currentRoot.props,
      alternate: currentRoot
    };

    nextUnitWork = wipRoot;

    // console.log("action", action);
  };

  // 指向下一个hook
  workInProgressHook = workInProgressHook.next;

  return [hook.memoizedState, dispatch];
}

// eslint-disable-next-line
export default { render };