「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。
实现
我们将分为8步,一步一步的实现一个小型的React
- 实现createElement函数
- 实现render函数
- Currnet Mode模式
- fibers
- render和commit阶段
- 协调器
- function组件
- hooks
Currnet Mode模式
接下来我们之前的代码进行重构,上次也说到了,问题是递归地渲染dom节点,一旦开始渲染,就必须得渲染完整个dom树,如果dom树巨大,可能会占用主线程太多时间。
此时如果浏览器需要执行优先级比较高的任务比如用户输入,必须等到渲染完成,对用户来说,实在不够友好。
所以我们需要将整个渲染过程分成一个一个小任务,当我们完成一个小任务后,如果浏览器需要完成其他内容,我们将让浏览器中断渲染。
let nextUnitOfWork = null // 记录下一个任务
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) { // 存在下一个任务并且存在空闲时间
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
这里我们使用requestIdleCallback来循环执行任务,而React使用的时自己实现的调度器,但原理都是一样的,在浏览器有空闲时间的时候执行我们的任务
requestIdleCallback方法会执行我们传入的回调函数并传入一个deadline参数,我们可以使用deadline来判断是否还有剩余的空闲时间
完整代码:
const Didect = {
createElement,
render,
}
let nextUnitOfWork = null // 记录下一个任务
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) { // 存在下一个任务并且存在空闲时间
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
container.appendChild(dom)
}
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
Didect.render(element, container)
总结
- 为了实现可中断渲染,我们完整的渲染分成一个一个小的渲染任务
- 将递归渲染改为循环渲染
- 使用requestIdleCallback方法,当浏览器有空闲时间时才继续执行下一个任务
要开始循环执行任务,我们需要设置首个任务和实现performUnitOfWork方法,performUnitOfWork将返回下一个需要执行的任务。