原文链接: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
// 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地址: