上一节中,我们通过 requestIdleCallback
利用了空余时间去完成每个task。这种方式存在一个问题,当中途没有空余时间时,用户可能会看到渲染一半的dom。
我们可以采用统一提交的方式去解决这个问题,先处理链表,最后再统一添加到屏幕中。
实现统一提交
实现统一提交我们需要搞清楚两个关键点:
- 何时处理完链表
- 根结点位置
根据我们performWorkOfUnit
的逻辑:
js
return fiber.child
? fiber.child
: fiber.sibling
? fiber.sibling
: fiber.parent.sibling
? fiber.parent.sibling
: null;
当其return null
时,我们的链表就结束了,即 nextWorkOfUnit===null
而根结点其实就是我们在render
函数中初始化的 nextWorkOfUnit
知道了这两个关键点我们就可以轻松地实现统一提交了。
首先先注释掉 performWorkOfUnit
中添加dom的逻辑
diff
- fiber.parent.dom.append(dom);
+ //fiber.parent.dom.append(dom);
定义一个变量 root 代表根结点,并在render
函数中赋值
js
let root = null;
function render(el, container) {
nextWorkOfUnit = {
dom: container,
props: {
children: [el],
},
};
root = nextWorkOfUnit;
}
然后实现添加dom的函数commitWork
,为了更易理解,我们再创建一个函数commitRoot
去调用一下
js
function commitRoot() {
commitWork(root.child);
}
function commitWork(fiber) {
if (!fiber) return;
fiber.parent.dom.append(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
最终在链表结束时调用我们的commitRoot
,为了保证只调用一次,我们再把root
设置为null
diff
function workLoop(deadline) {
let shouldYield = false;
while (!shouldYield && nextWorkOfUnit) {
shouldYield = deadline.timeRemaining() < 1;
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
}
+ if (!nextWorkOfUnit && root) {
+ commitRoot();
+ root = null;
+ }
requestIdleCallback(workLoop);
}
这样我们就实现了统一提交,解决了无空余时间的问题。
实现Function Component
目前我们的mini-react还不支持Function Component,组件是对象的形式:
js
const App = (
<div className="app">
hello-mini-react
<CounterContainer />
</div>
);
ReactDOM.createRoot(document.querySelector("#root")).render(App);
如果我们改成Function Component的形式,浏览器就会报错:
js
function App() {
return (
<div className="app">
hello-mini-react
</div>
);
}
ReactDOM.createRoot(document.querySelector("#root")).render(<App />);
我们发现传入的是一个函数,所以我们可以进行一个判断,如果传入的是函数就调用一下
js
const isFunctionComponent = typeof fiber.type === "function";
if (!isFunctionComponent) {
updateHostComponent(fiber);
} else {
updateFunctionComponent(fiber);
}
因为传入的是函数,我们便不需要根据其type去创建dom了,之后我们需要对children进行处理,调用后返回的是一个对象而不是数组,我们需要将其包裹一下:
js
function updateHostComponent(fiber) {
if (!fiber.dom) {
const dom = (fiber.dom = createDom(fiber.type));
updateProps(dom, fiber.props);
}
const children = fiber.props.children;
initChildren(fiber, children);
}
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
initChildren(fiber, children);
}
Function Component并无dom结构,所以我们需要递归去寻找其父节点,将dom添加到父节点上
js
function commitWork(fiber) {
if (!fiber) return;
let fiberParent = fiber.parent;
while (!fiberParent.dom) {
fiberParent = fiberParent.parent;
}
if (fiber.dom) {
fiberParent.dom.append(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
要支持props我们只需将其作为参数传入到函数中即可,但我们之前的判断逻辑只支持了string
类型,如果传入number
类型就会报错
js
function Counter({ num }) {
return <div className="counter">Count:{num}</div>;
}
修改一下判断逻辑,支持number
类型:
js
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
const isTextNode =
typeof child === "string" || typeof child === "number";
return isTextNode ? createTextNode(child) : child;
}),
},
};
}
当我们嵌套使用Function Component时, 会出现只能渲染一个的情况
js
function Counter({ num }) {
return <div className="counter">Count:{num}</div>;
}
function CounterContainer() {
return (
<div>
<Counter num={10} />
<Counter num={20} />
</div>
);
}
function App() {
return (
<div className="app">
hello-mini-react
<CounterContainer />
</div>
);
}
原因是<Counter num={10} />
的parent
是div
,找不到其parent.sibling
所以我们要进行处理,找到真正的parent.sibling
也就是<Counter num={20} />
js
if (fiber.child) {
return fiber.child;
}
if (fiber.sibling) {
return fiber.sibling;
}
let nextFiber = fiber.parent;
while (nextFiber) {
if (nextFiber.sibling) return nextFiber.sibling;
nextFiber = nextFiber.parent;
}
这样我们就实现了对Function Component的支持
参考代码见github