近来工作忙了一阵,加上自己精力不足,终于造轮子系列搁置了许久。废话不多说,接着上次的轮子,继续造。
写在前面
React框架具体的实现细节十分庞杂,这里就挑几个最具代表性的特性——JSX
,Function Component
,Hooks
,当然每一个特性的实现细节也十分繁琐,我们的目标是实现核心功能,理解原理就好。另外对于React最新的Fibers树结构和并发模型,也简单探讨下。
总结下来,具体要实现的轮子:
React.createElement
函数——核心功能,解析JSX;ReactDOM.render
函数——核心功能;- Concurrent Mode并发模型——探讨;
- Fibers Tree——探讨;
Function Component
;Hooks
;- 对比源码
正文
前一篇中,我们一起实现了React.createElement
和ReactDOM.render
两个函数,今天我们来探讨一下第三个主题:Concurrent Mode并发模型。
为什么这一节我们要讲并发模型呢?其实是因为上一章中,我们在处理DOM树时使用了递归,而递归会导致一个问题:就是一旦我们开始了就不能停止,这将会浪费不少性能和资源,当DOM树很大时,可能会阻塞主线程,从而导致页面卡顿等不好的用户体验。
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]
})
// 递归整个DOM树
element.props.children.forEach(child =>render(child, dom))
container.appendChild(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
作为队列,可以把它想象成一个setTimeOut
,但是不用设置执行时间,requestIdleCallback
会在主线程空闲时执行。
需要说明的是,React已经不再使用requestIdleCallback
,现在使用的是调度程序包
,但是对于我们现在这个轮子来讲,作用一样,区别并不大。
前面的例子中,requestIdleCallback
也能传递一个deadline参数,通过这个时间,我们可以知道浏览器再次控制之前我们还有多少时间。
写在后面
上面的requestIdleCallback
,精简一下就是这样:
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
}
如果要开始使用队列,我们需要设置第一个任务单元,然后编写一个performUnitOfWork函数,它不仅执行当前任务,还返回下一个任务。