普通视图

发现新文章,点击刷新页面。
昨天以前首页

深入理解react——3. 函数组件与useState

作者 time_rg
2026年1月19日 15:51

前两篇文章已经将fiber的基本工作原理介绍完成,接下来我们添加函数式组件与useState,将miniReact正式完结。

一,函数组件

1.1 performUnitOfWork(工作单元计算)

从前两篇文章我们知道,每个组件经过编译后其实都是一个对象,那么函数组件自然也就是执行函数 return 出来的jsx对象,所以我们对原来的performUnitOfWork做一点修改。

export function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;

  if (isFunctionComponent) {
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }

  // 3. 返回下一个工作单元
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

基本逻辑不变,只是做一个if的判断,将函数式组件与非函数式组件分开处理。这里值得注意的地方是,函数式组件的fiber.type就是函数本身。也就是React.createElement(MyComponent, { propKey: propValue })]

function updateFunctionComponent(fiber) {
  console.log("updateFunctionComponent执行");
  wipFiber = fiber;
  wipFiber.hooks = [];
  hookIndex = 0;
  // TODO 更新函数组件
  const children = [fiber.type(fiber.props)]; // 执行函数组件,直到此时,函数中的setState才会被调用
  reconcileChildren(fiber, children);
}

function updateHostComponent(fiber) {
  // 添加节点元素到dom
  // 如果没有dom属性,根据fiber新构建
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  // 遍历节点的children属性创建Fiber对象
  const elements = fiber.props.children;
  // 调和fiber对象,设置状态:添加、更新和删除
  reconcileChildren(fiber, elements);
}

1.2 commitWork

另外在提交节点commitWork函数也需要做一些修改,因为函数式组件并没有对应的dom节点,所以新增和删除实际上需要向上或者向下递归寻找到真实的dom节点。

/**
 * commitWork 函数
 * 递归提交Fiber节点的DOM更新
 * @param {Object} fiber - 当前的Fiber节点
 */
export function commitWork(fiber) {
  if (!fiber) {
    return;
  }

  // 函数组件本身没有dom属性,需要向上寻找
  let domParentFiber = fiber.parent;
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }

  const domParent = domParentFiber.dom; // 获取父DOM节点
  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  }

  if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

  if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent);
    return;
  }

  commitWork(fiber.child); // 递归提交子节点
  commitWork(fiber.sibling); // 递归提交兄弟节点
}

export function commitDeletion(fiber, domParent) {
  // 找不到dom,继续向下寻找
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

至此,函数式组件就添加完成了,我们来测试一下。

function MyComponent(props) {
  return React.createElement(
    "div",
    null,
    `hellow,${props.name}`
  );
}

const element =  React.createElement(MyComponent, { name: "World" })
render(element, rootDOM);

二,useState

上面我们已经添加了函数组件了,那么hook自然需要提上日程,不然函数组件就没法用。我们先添加最常用的useState。

export function useState(initial) {
  console.log("useState执行");
  const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];

  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],
  };

  // 执行所有setState的回调函数
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach((action) => {
    hook.state = action(hook.state);
  });

  const setState = (action) => {
    // 推入队列
    hook.queue.push(action);
    // 将下一次任务设为当前根fiber
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    deletions = [];
    nextUnitOfWork = wipRoot;
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

这里有几个非常值得注意的点,我们一个一个来说:

  1. 首先是执行时机,useState在函数组件中执行的时机其实是函数组件本身被执行它才被执行
  2. 那么我们可以回去看看处理函数组件组件fiber的时候做了什么,我们有一个全局的wipFiber指向的正是当前正在计算的fiber,所以上面的wipFiber.hooks.push(hook);实际上可以简单的看出每一个函数组件的hook都是存在当前组件的fiber中
  3. 另外由于useState函数本身并没有带什么特殊标识,在寻找oldHook时是凭借全局的hookIndex最后再做返回return [hook.state, setState];。这也引出一个常问的面试问题,为什么hook不能放在if条件中使用。
  4. 还有一个问题,useState到底是同步还是异步的,从这里可以看出,函数肯定是同步执行,但是相对于渲染是异步的,因为实际state值的更改实际上是下一次fiber计算和构建时才会触发。如果将useState放在定时器中,那么他又将是在下一次事件循环中的宏任务中去执行。(所以看博客不能知其所以然的地方,亲自写过源码后才会更加的清晰。ps:听说更加高版本的react批处理已经把定时器的情况也考虑进去了,我这里没有了解过,有知道的大佬可以评论区分享一下)

到这里为止,我们的useState就算是完成了。老规矩,搞点测试。

function MyComponent(props) {
  const [count, setCount] = useState(0);

  const handleClick_1 = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount((count) => count + 1);
    console.log("handleClick_1 clicked:", count);
  };

  return React.createElement(
    "div",
    null,
    React.createElement("button", { onClick: handleClick_1, className: "button1" }, `Count, ${count}`),
    `hellow,${props.name}`
  );
}

const element =  React.createElement(MyComponent, {})
render(element, rootDOM);

大功告成,完成并理解useState后再添加别的hook就会变得简单很多。

三,useEffect

既然正在兴头上,那我们就再写一个useEffect

export function useEffect(callback, deps) {
  console.log("useEffect执行");
  const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
  const hasChanged = !oldHook || !deps || deps.some((dep, i) => dep !== oldHook.deps[i]);

  const hook = {
    deps,
  };

  if (hasChanged) {
    if (oldHook && oldHook.cleanup) {
      oldHook.cleanup(); // 清理上一次的副作用
    }
    hook.cleanup = callback(); // 执行副作用,并保存清理函数
  }

  wipFiber.hooks.push(hook);
  hookIndex++;
}

很简单就完成了,逻辑都是一样的,我们再来测试一下

function MyComponent(props) {
  const [count, setCount] = useState(0);

  const handleClick_1 = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount((count) => count + 1);
    console.log("handleClick_1 clicked:", count);
  };
  
  useEffect(()=>{
      console.log("我监听到count的变化了",count)
      return ()=>{
          console.log("我正在执行清理")
      }
  },[count])

  return React.createElement(
    "div",
    null,
    React.createElement("button", { onClick: handleClick_1, className: "button1" }, `Count, ${count}`),
    `hellow,${props.name}`
  );
}

const element =  React.createElement(MyComponent, {})
render(element, rootDOM);

至此,深入理解react这个小专栏到这里就算是结束了,源码请查看:github.com/time-is-cod…

❌
❌