抛开并发,要实现一个react
其实是非常简单的。一个简单的rect
通常由如下几部分构成:
- render
- createElement
通过createElement
,来构建虚拟DOM
树,例如:我们有如下代码:
function a(){
return <div classname='' onClick={()=>{}}> <div></div><A/></div>
}
function A(){
return <div>3333</div>
}
通过babel编译后实际上会变成:
function a() {
return /*#__PURE__*/React.createElement("div", {
classname: "",
onClick: () => {}
}, " ", /*#__PURE__*/React.createElement("div", null), /*#__PURE__*/React.createElement(A, null), " ");
}
function A() {
return /*#__PURE__*/React.createElement("div", null, "3333");
}
运行上述代码最后上会形成类似这样的虚拟DOM
结构:
我们暂时只关注:
- type
- props
1. 实现一个createElement
关于type
type的类型,在我们实现的简单的版本中只有三种
- 普通节点类型,例如:
div
,span
等等 - 文本节点类型
- 函数组件类型
所以我们先写一个createElement
:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
};
}
我们改造一下刚才babel
编译的产物:
function a() {
return createElement("div", {
classname: "",
onClick: () => {}
}, " ",createElement("div", null), createElement(A, null), " ");
}
function A() {
return createElement("div", null, "3333");
}
直接调用a()
,可以看到生成了如下结构
嗷,可以看到生成了类似的。
2. render函数
接下来写render函数,来遍历这个结构:
function updateComponent(elements, container){}
function updateFunctionComponent(elements, container) {}
function render(elements, container) {
const { type } = element;
switch (typeof type) {
case 'string':
updateComponent(element, container);
break;
case 'function':
updateFunctionComponent(element, container);
break;
}
}
updateComponent
是最终来挂载dom
的,而updateFunctionComponent
是用来处理type为函数类型的虚拟节点。我们先实现updateComponent
,很简单,根据type
直接生成dom
,然后插入到container
就行,之后就是遍历虚拟dom
的children
属性,一致递归下去:
function updateComponent(element, container) {
const { type, props } = element;
let dom = document.createElement(type);
container.append(dom);
props.children?.forEach(child => {
// 挂载子节点
render(child, dom);
});
}
至于type
是函数组件
的类型,我们就需要调用type
,并且返回调用结果:
function updateFunctionComponent(element, container) {
const { type, props } = element;
const children = type(props);
render(children, container);
}
到此可以得出我们的代码:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
};
}
function updateFunctionComponent(element, container) {
const { type, props } = element;
const children = type(props);
render(children, container);
}
function updateComponent(element, container) {
const { type, props } = element;
let dom = document.createElement(type);
container.append(dom);
props.children?.forEach(child => {
// 挂载子节点
render(child, dom);
});
}
function render(element, container) {
const { type } = element;
switch (typeof type) {
case 'string':
updateComponent(element, container);
break;
case 'function':
updateFunctionComponent(element, container);
break;
}
}
接着我们测试一下:
render(createElement(a,null), document.body)
可以发现插入成功了:
但是很奇怪,没有"3333":
这是因为我们没有对文本节点做处理
,所以我们需要处理文本节点,只需要构造一个虚拟dom就行
,我们改造一下createElement
:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child => {
if (typeof child === 'string') {
return createTextElement(child);
} else {
return child;
}
}),
},
};
}
/**
* 创建文本节点
* @param {*} text
* @returns
*/
function createTextElement(text) {
return {
type: ELEMENT_TYPE.TEXT_ELEMENT,
props: {
nodeValue: text,
children: [],
},
};
}
再次运行,可以发现插入成功:
给出当前完整代码
const ELEMENT_TYPE = {
TEXT_ELEMENT: 'TEXT_ELEMENT',
};
const isEvent = eventName => eventName.startsWith('on');
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child => {
if (typeof child === 'string') {
return createTextElement(child);
} else {
return child;
}
}),
},
};
}
/**
* 创建文本节点
* @param {*} text
* @returns
*/
function createTextElement(text) {
return {
type: ELEMENT_TYPE.TEXT_ELEMENT,
props: {
nodeValue: text,
children: [],
},
};
}
function updateComponent(element, container) {
const { type, props } = element;
let dom = null;
if (type === ELEMENT_TYPE.TEXT_ELEMENT) {
dom = document.createTextNode(element.props.nodeValue);
} else {
dom = document.createElement(type);
}
Object.keys(props)
.filter(prop => prop !== 'children')
.forEach(prop => {
if (isEvent(prop)) {
dom.addEventListener(prop.slice(2).toLowerCase(), props[prop]);
}
dom[prop] = props[prop];
});
container.append(dom);
props.children?.forEach(child => {
render(child, dom);
});
}
function updateFunctionComponent(element, container) {
const { type, props } = element;
const children = type(props);
render(children, container);
}
/**
* 渲染器
* @param {*} element
* @param {*} container
*/
function render(element, container) {
const { type } = element;
switch (typeof type) {
case 'string':
updateComponent(element, container);
break;
case 'function':
updateFunctionComponent(element, container);
break;
}
}
export { render, createElement };
3.实现并发
接下来我们实现react
的并发模式,也就是引入scheduler
和FiberNode
,scheduler
我们使用requestIdleCallback
来代替。
3.1 问题分析
在之前的实现中,我们的渲染过程是同步的,一旦开始渲染,就会一直执行到结束。这会导致两个问题:
- 如果渲染树很大,会阻塞主线程,导致页面卡顿
- 如果在渲染过程中有高优先级的任务(如用户输入),无法中断渲染来优先处理
React的并发模式就是为了解决这些问题,它的核心思想是:
- 将渲染工作分解成小单元
- 可以暂停和恢复渲染
- 可以为不同的更新分配优先级
3.2 Fiber架构
Fiber是React并发模式的核心,它是一种数据结构,也是一种工作单元。我们先定义Fiber节点:
/**
* Fiber节点结构
*/
function createFiber(type, props, dom) {
return {
type,
props,
dom,
parent: null,
child: null,
sibling: null,
alternate: null,
effectTag: 'PLACEMENT',
};
}
Fiber节点包含以下重要属性:
type
: 与虚拟DOM的type相同props
: 与虚拟DOM的props相同dom
: 对应的真实DOM节点parent
: 父Fiber节点child
: 第一个子Fiber节点sibling
: 下一个兄弟Fiber节点alternate
: 上一次渲染的Fiber节点effectTag
: 标记对DOM的操作类型(PLACEMENT, UPDATE, DELETION)
3.3 工作循环
接下来,我们需要实现工作循环,它负责调度和执行工作单元:
// 下一个工作单元
let nextUnitOfWork = null;
// 正在构建的Fiber树(workInProgress树)
let wipRoot = null;
// 上一次提交到DOM的Fiber树
let currentRoot = null;
// 需要删除的节点
let deletions = [];
/**
* 调度工作循环
*/
function workLoop(deadline) {
// 是否应该让出控制权
let shouldYield = false;
// 如果有下一个工作单元且不需要让出控制权
while (nextUnitOfWork && !shouldYield) {
// 执行当前工作单元并返回下一个工作单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// 检查是否还有剩余时间
shouldYield = deadline.timeRemaining() < 1;
}
// 如果没有下一个工作单元且有根Fiber,提交整个Fiber树
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
// 继续请求下一次空闲回调
requestIdleCallback(workLoop);
}
// 开始第一次空闲回调
requestIdleCallback(workLoop);
3.4 执行工作单元
每个工作单元的执行过程如下:
/**
* 执行工作单元
* @param {*} fiber 当前Fiber节点
* @returns 下一个工作单元
*/
function performUnitOfWork(fiber) {
// 处理当前Fiber节点
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 创建子Fiber节点
const elements = fiber.type === 'function'
? fiber.type(fiber.props)
: fiber.props.children;
reconcileChildren(fiber, elements);
// 返回下一个工作单元
// 优先处理子节点
if (fiber.child) {
return fiber.child;
}
// 没有子节点则处理兄弟节点
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
// 没有兄弟节点则回到父节点
nextFiber = nextFiber.parent;
}
}
/**
* 创建DOM节点
*/
function createDom(fiber) {
const dom =
fiber.type === ELEMENT_TYPE.TEXT_ELEMENT
? document.createTextNode("")
: document.createElement(fiber.type);
// 设置属性
updateDom(dom, {}, fiber.props);
return dom;
}
/**
* 协调子节点
*/
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
// 遍历子元素,创建子Fiber
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
// 比较新旧Fiber
const sameType = oldFiber && element && element.type === oldFiber.type;
// 类型相同,执行更新
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
// 有新元素,执行创建
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
// 有旧Fiber但没有新元素,执行删除
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
// 移动旧Fiber指针
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// 将新Fiber添加到Fiber树中
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
3.5 提交阶段
在工作循环中,我们将DOM操作分离到提交阶段,这样可以避免用户看到不完整的UI:
/**
* 提交整个Fiber树到DOM
*/
function commitRoot() {
// 先处理需要删除的节点
deletions.forEach(commitWork);
// 提交wipRoot的子树
commitWork(wipRoot.child);
// 保存当前Fiber树,用于下次比较
currentRoot = wipRoot;
// 清空wipRoot
wipRoot = null;
}
/**
* 提交单个Fiber节点到DOM
*/
function commitWork(fiber) {
if (!fiber) {
return;
}
// 找到最近的有DOM的父Fiber
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
// 根据effectTag执行相应的DOM操作
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
// 递归提交子节点和兄弟节点
commitWork(fiber.child);
commitWork(fiber.sibling);
}
/**
* 处理节点删除
*/
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
// 如果当前Fiber没有DOM节点(如函数组件),递归查找子节点
commitDeletion(fiber.child, domParent);
}
}
/**
* 更新DOM属性
*/
function updateDom(dom, prevProps, nextProps) {
// 移除旧属性
Object.keys(prevProps)
.filter(key => key !== "children")
.filter(key => !(key in nextProps))
.forEach(key => {
// 移除事件监听器
if (key.startsWith("on")) {
const eventType = key.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[key]);
} else {
dom[key] = "";
}
});
// 设置新属性
Object.keys(nextProps)
.filter(key => key !== "children")
.forEach(key => {
// 添加事件监听器
if (key.startsWith("on")) {
const eventType = key.toLowerCase().substring(2);
// 移除旧的事件监听器
if (prevProps[key]) {
dom.removeEventListener(eventType, prevProps[key]);
}
dom.addEventListener(eventType, nextProps[key]);
} else {
dom[key] = nextProps[key];
}
});
}
3.6 重新设计render函数
现在我们需要重新设计render函数,使其适配Fiber架构:
/**
* 渲染函数
*/
function render(element, container) {
// 设置wipRoot为新的Fiber根节点
wipRoot = {
type: "div",
props: {
children: [element],
},
dom: container,
alternate: currentRoot,
effectTag: "UPDATE",
};
deletions = [];
nextUnitOfWork = wipRoot;
}
4. 实现Hooks
React的另一个重要特性是Hooks,它允许在函数组件中使用状态和其他React特性。我们来实现最基本的useState
:
4.1 状态管理
首先,我们需要一些全局变量来跟踪hooks:
// 当前正在处理的函数组件对应的Fiber
let wipFiber = null;
// 当前hook的索引
let hookIndex = null;
4.2 处理函数组件
我们需要修改处理函数组件的逻辑:
/**
* 处理函数组件
*/
function updateFunctionComponent(fiber) {
// 设置wipFiber和重置hookIndex
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
// 执行函数组件,获取子元素
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
4.3 实现useState
最后,实现useState钩子:
/**
* useState钩子
*/
function useState(initial) {
// 获取旧的hook
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
// 创建新的hook
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
};
// 执行上一次渲染后入队的所有action
const actions = oldHook ? oldHook.queue : [];
actions.forEach(action => {
hook.state = action(hook.state);
});
// 设置状态更新函数
const setState = action => {
hook.queue.push(typeof action === "function" ? action : () => action);
// 触发重新渲染
wipRoot = {
type: currentRoot.type,
props: currentRoot.props,
dom: currentRoot.dom,
alternate: currentRoot,
effectTag: "UPDATE",
};
nextUnitOfWork = wipRoot;
deletions = [];
};
// 保存hook
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
5. 完整代码
将所有代码整合在一起,我们得到了一个简单但功能完整的React实现:
const ELEMENT_TYPE = {
TEXT_ELEMENT: 'TEXT_ELEMENT',
};
// 全局变量
let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;
let deletions = [];
let wipFiber = null;
let hookIndex = null;
/**
* 创建元素
*/
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child => {
if (typeof child === 'string' || typeof child === 'number') {
return createTextElement(child);
} else {
return child;
}
}).filter(Boolean),
},
};
}
/**
* 创建文本元素
*/
function createTextElement(text) {
return {
type: ELEMENT_TYPE.TEXT_ELEMENT,
props: {
nodeValue: text,
children: [],
},
};
}
/**
* 渲染函数
*/
function render(element, container) {
wipRoot = {
type: "div",
props: {
children: [element],
},
dom: container,
alternate: currentRoot,
effectTag: "UPDATE",
};
deletions = [];
nextUnitOfWork = wipRoot;
}
/**
* 工作循环
*/
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
/**
* 执行工作单元
*/
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
// 返回下一个工作单元
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
/**
* 处理函数组件
*/
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
/**
* 处理宿主组件
*/
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren(fiber, fiber.props.children);
}
/**
* useState钩子
*/
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
};
const actions = oldHook ? oldHook.queue : [];
actions.forEach(action => {
hook.state = action(hook.state);
});
const setState = action => {
hook.queue.push(typeof action === "function" ? action : () => action);
wipRoot = {
type: currentRoot.type,
props: currentRoot.props,
dom: currentRoot.dom,
alternate: currentRoot,
effectTag: "UPDATE",
};
nextUnitOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
// 导出API
export { createElement, render, useState };
6. 总结
通过这个简单的实现,我们了解了React的核心工作原理:
- 虚拟DOM:使用JavaScript对象表示UI结构
- 协调算法:比较新旧虚拟DOM树,找出需要更新的部分
- Fiber架构:将渲染工作分解成小单元,实现可中断的渲染
- 并发模式:利用浏览器空闲时间执行渲染工作,提高用户体验
- Hooks:在函数组件中使用状态和其他React特性
这个实现虽然简单,但包含了React的核心思想。实际的React代码要复杂得多,包含了更多的优化和特性,如:
- 更高效的协调算法
- 更完善的事件系统
- 更多的Hooks(useEffect, useContext等)
- 错误边界
- 服务端渲染
- Suspense和并发特性
通过理解这个简化版的实现,我们可以更好地理解React的工作原理,从而更有效地使用React进行开发。