前言
文章摘抄自罗德里戈·彭博在2019年11月更新的Build your own React,将按照以下顺序解读 React 源码。
创建 React 应用最基础的代码,如下所示:
const element = <h1 title="foo">Hello</h1>;
const container = document.getElementById("root");
ReactDOM.render(element, container);
通过 babel 将 JSX 代码转成 JS 代码。
// 替换前(JSX 代码)
const element = <h1 title="foo">Hello</h1>
// 替换后(JS 代码)
const element = React.createElement(
"h1", // tagname
{ title: "foo" }, // props
"Hello" // children
)
React.createElement 函数创建并返回一个 element 元素,即虚拟 DOM。
// type是tagename,props是元素所有的属性键值对,children通常是一个包含更多元素的数组
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
利用 ReactDom.render 将虚拟 DOM 渲染为真实 DOM 元素,并展示到页面上。
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
};
const container = document.getElementById("root");
const node = document.createElement(element.type);
node["title"] = element.props.title;
const text = document.createTextNode("");
text["nodeValue"] = element.props.children;
node.appendChild(text);
container.appendChild(node);
createElement & render
根据以上可知 React.createElement 函数负责生成虚拟 DOM, ReactDOM.render 负责将虚拟 DOM 转为真实 DOM,渲染到屏幕上。
第一步: 构建自己的 createElement 方法
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
// 处理 string、number 等类型的 child
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
第二步: 构建自己的 render 方法
function render(element, container) {
if (!element) return;
// 处理 text 文本节点
if (!element.type) {
container.innerHTML= element;
return;
}
// 创建 dom
let dom;
if (element.type !== "TEXT_ELEMENT") {
dom = document.createElement(element.type);
} else {
document.createTextNode("");
}
// 处理 props
const isProperty = (key) => key !== "children";
if(element.props) {
Object.keys(element.props)
.filter(isProperty)
.forEach((name) => {dom[name] = element.props[name];});
}
// 处理 children
if (element.props?.children) {
const childs =
typeof element.props.children === "object"
? element.props.children
: [element.props.children];
// 递归循环调用 render
childs.forEach((child) => render(child, dom));
}
// 挂载dom
container.appendChild(dom);
}
第三步: export 导出
const Didact = {
createElement,
render,
};
export default Didact;
第四步:index.jsx 文件中导入
import Didact from './utils/Didact';
/** @jsx Didact.createElement **/
const element = (
<div style="background: red;width: 500px; height: 500px">
<h1>Hello World</h1>
<h2 style="text-align:right;width: 100px; height: 100px;background: pink">from Didact</h2>
</div>
);
const container = document.getElementById("root");
Didact.render(element, container);
Concurrent 模式
React 为了解决 15 版本存在的问题:组件的更新是递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。
React 引入了 Fiber 的架构,同时配合 Schedduler 的任务调度器,在 Concurrent(并发) 模式下可以将 React 的组件更新任务变成可中断、恢复的执行,就减少了组件更新所造成的页面卡顿。
// 初始化下一个工作单元变量
let nextUnitOfWork = null
// workLoop 负责在浏览器空闲的时候执行 performUnitOfWork 函数
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
// performUnitOfWork 函数负责执行当前工作单元并返回下一个工作单元
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// requestIdleCallback 提供一个截止日期参数
// 使用参数检查还有多久需要归还浏览器的主动权
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
// 初始调用一次循环
requestIdleCallback(workLoop)
Fibers
React 中每个 element 都有一个 fiber,每个 fiber 都可以看做一个工作单元。
每个 fiber 需要做三件事情:
-
- 将 element 添加到 DOM 上
-
- 为 element 的 children 创建 fiber
-
- 选择下一个工作单元
每个 fiber 与其 children,下一个sibling(兄弟姐妹),parent 都有关联,所以 fiber 数据结构的目的就是为了更容易得查找下一个工作单元。
fibler 查找下一个工作单元遵循以下原则:
- 如果 fiber 有 child,下一个工作单元就是第一个 child
- 如果没有 child,下一个工作单元是 sibling
- 既没有 child,也没有 sibling,则去找 parent 的 sibling,也就是 uncle
- 如果 parent 没有 sibling,则继续向上寻找 parent 的 sibling,直到 root
- 如果到达了 root,表示我们完成了所有的 render 工作
首先将创建 DOM 的逻辑从 render 函数中提取出来:
function creatDom (fiber) {
// 创建dom
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
// 创建props
const isProperty = key => key !== "children";
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name];
});
return dom
}
当浏览器准备就绪,将调用 workLoop,从 root 节点开始工作:
function render(element, container) {
nextUnitOfWork = {
dom: container, // document.getElementByid('root')
props: {
children: [element],
}
}
}
完善 performUnitOfWork 函数, performUnitOfWork 函数主要负责创建 dom,为所有 child 创建 fiber, 建立 fiber 关联,根据 fiber 机制返回下一个 fiber:
function performUnitOfWork(fiber) {
if (!fiber.dom) fiber.dom = creatDom(fiber);
if (fiber.parent) fiber.parent.dom.appendChildren(fiber.dom);
const elements = fiber.props.children
let index = 0
let prevSibling = null
// 遍历所有children,将fible的下一级的第一个元素设置为child
// 并依次将child中的子fible的sibling设置为下一个元素
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
// 上一个fiber的sibling设置为当前fible
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// 如果存在child,直接return
if(fiber.child){
return fible.child
}
let nextFiber = fiber
while (nextFiber) {
// 如果存在同级fible,返回同级
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 即不存在child,又不存在sibling,则回到parent节点,返回parent的sibling
nextFiber = nextFiber.parent
}
}
Render & Commit 阶段
当所有的 fiber 都执行完毕后,就会触发 commitRoot 函数,进行 dom 的渲染。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
// 如果不存在下一个节点(所有fible创建并执行完毕),提交fible树
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
// 提交 fiber 节点
function commitRoot() {
commitWork(wipRoot.child)
// 跟踪 fiber 树的变量
wipRoot = null
}
// 递归处理 DOM 的挂载
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Reconciliation 调和
将上一个 commit 阶段提交给 DOM 的 fiber 作为属性记录到当前 fiber 中,在 render 函数中收到 element 后,与提交给 DOM 的最后一个 fiber 对比,这个过程即为调和,使虚拟 DOM 和真实 DOM 一致。
// 增加一个currentRoot
let currentRoot = null
function commitRoot() {
commitWork(wipRoot.child)
// 备份当前提交给 DOM 的 fible
currentRoot = wipRoot
wipRoot = null
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
// 增加指针
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
实现 Hook
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
根据以上代码实现一个 useState,首先分析 useState 特点:
- 存储私有变量,私有变量可以在函数外部改变,并且不会相互污染
- return 的 setState 可以操作 hook 对象,hook 对象需要存储 state 值,和一个 queue 队列数组
useState 与 fiber 的关联
每次执行 useState 时,将 hook 赋值给 wipFiber 对象,而 wipFiber 始终是当前正在处理的 fiber,所以就将 hook 状态巧妙的存储到了 fiber 中,达到了持久化的效果。
由于 fiber 的关联属性,在处理新的 fiber 的过程中,我们也能找到 oldFiber 中的 hook 对象;相当于通过将 hook 赋值给 fiber,利用 fiber 的关联属性,持久化并传递 hook。
setState 执行逻辑
当触发 setState 的时候,会直接往 hook.queue 中添加一个 action,相当于操作了当前组件的 fiber 中的 hook 对象(同一个引用地址)。
之后,setState 重新设置 nextUnitOfWork 之后,woorkLoop 满足执行条件,再次触发新 fiber 的创建流程,也就是 rerender 的过程。
当再次执行到该组件中的 useState 的时候,会从当前 fiber 的关联 oldFiber 中取出 hook 对象,里面有上一次渲染的 state,以及 setState 触发添加的 action,执行这个 action,得到新的 state,并初始化一个新的 hook 对象, push 给当前 fiber 的 hooks 对象(存在多个useState的时候,需要通过全局的 hookIndex 来找到对应的 hook)。
由于 wipFiber 的 hooks 数组顺序是按照执行先后顺序来的。下一次组件再次执行的时候,通过全局的 hookIndex 找到 oldFiber 中对应的 oldHook,这也是为什么组件中的 hooks 使用不能写在条件语句或者循环体中,是为了保证 hooks 的顺序不会乱。
// 当前正在进行中的fiber
let wipFiber = null;
// 全局变量,在同一个fiber(组件)中用来在hooks找到对应hook
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
// 每执行一次hooks,往这个数组里保存一份最新的state,hook:{state,queue:[]}
wipFiber.hooks = [];
// useState在此时执行
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: []
};
// 执行action,更新state
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
});
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];
}