实现mini-react(一)
项目地址
jsx在babel编译后的结果是React.createElement,由createElement生成虚拟DOM
React.createElement有三个参数
- type
- props
- children
实现createElement函数
递归判断children
是普通元素还是文本元素,如果是文本元素就用createTextNode
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child => {
return typeof child === "string" ? createTextNode(child) : child
}),
},
}
}
function createTextNode(text, ...children) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children,
},
}
}
实现render函数
根据传入的虚拟DOM的type属性来创建对应的DOM,再把props挨个设置到DOM,再递归遍历children调用render,最终把DOM添加到container
function render(el, container) {
const dom = el.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(el.type)
Object.keys(el.props).forEach(key => {
if (key !== "children") {
dom[key] = el.props[key]
}
})
const children = el.props.children
children.forEach(child => {
render(child, dom)
})
container.append(dom)
}
实现ReactDOM.createRoot函数
实际是对React.render做一层封装
import React from './React';
const ReactDOM = {
createRoot(container) {
return {
render(el) {
React.render(el, container);
},
};
},
};
export default ReactDOM;
实现效果
使用vite创建一个Vanilla项目
创建App.jsx文件,使用React.createElement
在main.js文件中使用ReactDOM.createRoot传入虚拟DOM
渲染成功
实现fiber
在上面的render中使用了递归,这样会导致无法中断,页面会卡顿。可以使用requestIdleCallback
这个函数,在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
function workLoop(deadline) {
let shouldRun = false
while (!shouldRun) {
// 当剩余时间小于1的时候,就执行下个任务
shouldRun = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
虚拟DOM怎么转成fiber结构? 每个fiber做了三件事:
- 根据type创建dom并添加
- 遍历子元素创建fiber
- 选择下一个工作单元
从根节点开始,先修改render,将创建dom代码抽离
let nextWork = null
function render(el, container) {
nextWork = {
dom: container,
props: {
children: [el],
},
}
}
function createDom(fiber) {
const dom =
fiber.type == 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(fiber.type);
const isProperty = (key) => key !== 'children';
Object.keys(fiber.props)
.filter(isProperty)
.forEach((name) => {
dom[name] = fiber.props[name];
});
return dom;
}
Fiber每一个节点都是一个fiber,一个 fiber 包括了 child(第一个子节点)、sibling(兄弟节点)、parent(父节点)属性。创建完fiber节点再依次返回子节点>兄弟节点>父节点进行下个工作单元
function performWorkOfUnit(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
const children = fiber.props.children;
// 上一个兄弟节点
let prevChild = null;
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
child: null,
parent: fiber,
sibling: null,
dom: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevChild.sibling = newFiber;
}
prevChild = newFiber;
});
// 寻找下一个子节点,如果有返回
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
// 如果有兄弟节点,返回兄弟节点
if (nextFiber.sibling) {
return nextFiber.sibling;
}
// 否则返回父节点
nextFiber = nextFiber.parent;
}
}
在工作循环中从根节点开始创建fiber
function workLoop(deadline) {
let shouldYield = false
while (!shouldYield && nextWork) {
+ nextWork = performWorkOfUnit(nextWork)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
实现效果
打印每个工作单元的nextWork依次为app节点>div1节点>111文本节点>div2节点>222文本节点
实现统一提交
现在处理一个元素,都要向DOM添加一个新的节点,再加上shouldRun可中断渲染,中途有可能没空余时间,会看到渲染一半的DOM
nextWork会代替掉,新增root保存一开始的根节点。再判断nextWork为空代表节点遍历完了再递归渲染元素
let nextWork = null;
+let root = null;
function render(el, container) {
nextWork = {
dom: container,
props: {
children: [el],
},
};
+ root = nextWork;
}
function performWorkOfUnit(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
- if (fiber.parent) {
- fiber.parent.dom.appendChild(fiber.dom);
- }
}
function workLoop(deadline) {
let shouldRun = false;
while (nextWork && !shouldRun) {
nextWork = performWorkOfUnit(nextWork);
// 当剩余时间小于1的时候,就执行下个任务
shouldRun = deadline.timeRemaining() < 1;
}
+ if (!nextWork && root) {
+ commitRoot();
+ }
requestIdleCallback(workLoop);
}
function commitRoot() {
commitWork(root.child);
root = null;
}
function commitWork(fiber) {
if (!fiber) return;
fiber.parent.dom.append(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
实现函数组件
函数组件fiber的type属性是函数,加个判断是否是函数组件,如果是函数组件就不创建DOM
function performWorkOfUnit(fiber) {
+ const isFunctionComponent = typeof fiber.type === 'function';
+ if (isFunctionComponent) {
+ // 更新函数组件
+ updateFunctionComponent(fiber)
+ } else {
+ // 更新普通节点
+ updateHostComponent(fiber)
+ }
- if (!fiber.dom) {
- fiber.dom = createDom(fiber);
- }
- const children = fiber.props.children;
+ reconcileChildren(fiber, children);
}
// 遍历子节点创建fiber
function reconcileChildren(fiber, children) {
let prevChild = null;
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
child: null,
parent: fiber,
sibling: null,
dom: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevChild.sibling = newFiber;
}
prevChild = newFiber;
});
}
因为函数组件的type是函数,所以要把将fiber中的参数传入并执行得到DOM作为数组传入reconcileChildren,不是函数组件就走之前逻辑,创建DOM传入reconcileChildren
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren(fiber, fiber.props.children);
}
此时报错是因为函数组件的fiber没有创建DOM
fiber.type(fiber.props)
得到是函数组件真正的节点存在fiber的child属性,需要把得到的DOM挂载到父节点的DOM
在同一渲染时判断fiber对应的父节点没有DOM属性时,一直向上取到有DOM属性的父节点把fiber的DOM挂载到父节点的DOM
function commitWork(fiber) {
if (!fiber) return;
+ let fiberParent = fiber.parent;
+ while (!fiberParent.dom) {
+ fiberParent = fiberParent.parent;
+ }
+ if (fiber.dom) {
+ fiberParent.dom.append(fiber.dom);
+ }
- fiber.parent.dom.append(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
实现传递数字
之前在createElement函数中判断child为string才调用createTextNode,应该再加个判断判断child为number也调用createTextNode
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
+ const testNode = typeof child === 'string' || typeof child === 'number';
+ return testNode ? createTextNode(child) : child;
- return typeof child === 'string' ? createTextNode(child) : child;
}),
},
};
}
实现效果
未完待续...
转载自:https://juejin.cn/post/7376582861662011433