Build your own React(5)-函数组件&useState

150 阅读4分钟

原文链接:Build your own React

  • Step I: The  createElement Function ✅
  • Step II: The  render Function ✅
  • Step III: Concurrent Mode 并发模式 ✅
  • Step IV: Fibers ✅
  • Step V: Render and Commit Phases ✅
  • Step VI: Reconciliation ✅
  • Step VII: Function Components ✅
  • Step VIII: Hooks ✅

** To avoid confusion, I’ll use “element” to refer to React elements and “node” for DOM elements.*

前言

之前的文章中我们完成了普通DOM的渲染,这节我们来看看函数式组件是如何渲染的。

Function Components

首先修改我们的示例:

// main.js
const App = (props) => {
  return createElement("h1", null, "Hi", props.name);
};
const container = document.querySelector("#root");
const element = createElement(App, { name: "luckydog" });
render(element, container);

函数式组件在以下两个方面有些不同:

  • 来自函数组件的fiber没有DOM节点
  • 子元素是运行函数组件的返回结果,不能像以前一样通过props获取

根据第二点差异重构我们的performUnitOfWork函数:

  • 首先需要在此函数中判断接受到的是否是函数组件
  • 针对普通组件和函数组件做不同的处理
  • 普通组件处理逻辑无变化,遵循之前的逻辑
  • 函数组件,需要调用函数获取子元素
// react/render.js
function performUnitOfWork(fiber) {
  // 区分函数式组件
+ const isFunctionComponent = fiber.type instanceof Function;
+ if (isFunctionComponent) {
+   updateFunctionComponent(fiber);
+ } else {
+   updateHostComponent(fiber);
+ }
  ...
}
  
// 处理非函数式组件
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
}
  
// 处理函数式组件
function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

根据第一点差异修改commitWork函数:

  • 由于函数组件没有dom,导致渲染其子元素时找不到父节点(父fiber)的dom
  • 递归向上寻找,直至遇到第一个祖先节点的dom
  • 与删除节点不同的是,删除节点时,我们需要递归向下寻找第一个子孙节点存在的dom

这张图清晰的展现了,当渲染h1标签时,其父节点由于是函数组件,所以不存在dom,但是父节点的父节点存在dom

fn without dom.png

// react/render.js
function commitWork(fiber) {
  if (!fiber) {
    return;
  }
+ let parentDOMFiber = fiber.parent;
+ while (!parentDOMFiber.dom) {
+   parentDOMFiber = parentDOMFiber.parent;
+ }
+ const parentDOM = parentDOMFiber.dom;
  if (fiber.effectTag === "PLACEMENT" && fiber.dom) {
    parentDOM.append(fiber.dom);
  } else if (fiber.effectTag === "DELETION" && fiber.dom) {
+   commitDeletion(fiber, parentDOM);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom) {
    updateDOM(fiber.dom, fiber.alternate.props, fiber.props);
  }
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}
​
+ function commitDeletion(fiber, parentDOM) {
+   if (fiber.dom) {
+     parentDOM.removeChild(fiber.dom);
+   } else {
+     commitDeletion(fiber.child, parentDOM);
+   }
+}

Hooks

既然我们已经实现了函数组件,那肯定离不开hooks,在最后一节,我们也来编写一个useState hook。

首先修改我们的示例:

// main.js
const Counter = () => {
  const [num, setNum] = useState(1);
  return createElement(
    "h1",
    {
      onclick: () => setNum((prev) => prev + 1),
    },
    num
  );
};
const container = document.querySelector("#root");
const element = createElement(Counter);
render(element, container);

现象:点击h1标签,数字1会持续性递增

如果有大家了解过Hooks,那么肯定会知道在React中是通过链表的形式存储hooks的,通过链表告诉程序下一步应该执行哪一个hook,在这里我们将简单使用数组的方式进行存储。

首先初始化一些全局变量:

// react/render.js/**
 * wipFiber相当于currentRoot.child
 * 在这里单独抽出来记录
 */
+ let wipFiber = null;
+ let hookIndex = null;
​
// 处理函数式组件
function updateFunctionComponent(fiber) {
+   wipFiber = fiber;
+   hookIndex = 0;
+   wipFiber.hooks = [];
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

当我们使用 useState 定义state 变量时候,它返回一个有两个值的数组。 第一个值是当前的state,第二个值是更新state 的函数。

编写useState方法:

(1)返回第一个值 当前的state处理逻辑:

  • 函数组件调用useState时,首先检查是否存在旧hook

    • 存在:复制旧hook的state值
    • 不存在:将接收到的初始值赋值给state
  • 添加新hook到fiber中记录,并增加hookIndex索引

  • 返回state

export function useState(init) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  const hook = {
    state: oldHook ? oldHook.state : init,
  }
​
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state]
}

(2)返回第二个值 更新state的函数的处理逻辑:

  • 定义setState函数接受每一个action(在我们的例子中,action即setNum((prev) => prev + 1))
  • 在hook中存储每一个action
  • 借助nextUnitOfWork重新渲染组件
export function useState(init) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  const hook = {
    state: oldHook ? oldHook.state : init,
  }
  
  const setState = action => {
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot
    deletions = []
  }
  
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

(3)在下一次渲染组件的时候遍历执行action,并返回更新后的state

完成版的useState:

export function useState(init) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
​
  const hook = {
    state: oldHook ? oldHook.state : init,
    queue: [],
  };
​
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach((action) => {
    hook.state = typeof action === "function" ? action(hook.state) : action;
  });
​
  const setState = (action) => {
    hook.queue.push(action);
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    // 触发页面重新渲染
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
​
  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

总结

由于最近工作过于繁重、自己精力有限,导致此系列的最后一篇文章迟到了这么久,实属有些惭愧。

至此,如果大家能按部就班亲自动手尝试一遍的话,那么一定会对react的整体宏观架构、包括各个阶段,render、commit、reconcile有所了解,虽然暂时不能理解react源码中的各个细节,但我相信那也只是时间问题~~~~

最后,附上这个项目的github地址:

github.com/luckydog12/…