(三)mini-react 任务调度器和fiber架构
任务调度器
我们之前实现的render函数,执行过程是一个递归的过程,当遇到dom树特别庞大的时候就会出现页面的卡顿(因为js一直在执行render函数),因此特别希望将render任务拆解成一系列的子任务,可以中断和恢复,并在合适的时机执行这些任务。
先说第一个问题,找到合适的时机执行任务
requestIdleCallback
requestIdleCallback是一个Web API,允许开发者安排在主线程空闲时执行的低优先级回调函数。这个函数的主要目的是使得开发者能够在不影响关键事件如动画和输入响应的情况下,执行后台或低优先级的任务。
关于requestIdleCallback的关键概念:
-
回调函数: 回调函数是在主线程空闲时被调用的函数。每次调用时,都会传入一个IdleDeadline对象,该对象提供一个timeRemaining()方法,用来检测当前帧中剩余的空闲时间。
-
空闲时间和截止时间(deadline) : IdleDeadline对象的timeRemaining()方法返回一个DOMHighResTimeStamp,表示在执行回调函数时,在当前帧中剩余多少空闲时间(毫秒)。开发者可以使用这个时间来执行任务,并在时间耗尽前选择适当的时机终止任务,从而避免影响关键渲染或事件处理。
-
调度和取消回调: requestIdleCallback函数安排一个回调函数在主线程下一次空闲时被执行,并返回一个ID,可以用这个ID通过cancelIdleCallback函数取消回调。
-
超时: 你还可以给requestIdleCallback传递一个对象,其中一个属性是timeout,用来指定最长时间(毫秒)。如果任务在指定的时间内尚未执行,即使主线程不空闲,浏览器也会尽量执行回调
我们可以来试验一下
let taskId=0
function workLoop(deadline){
let shouldYield=false
taskId++
while(!shouldYield){
console.log(taskId,deadline.timeRemaining())
shouldYield=(deadline.timeRemaining()<10)
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
可以明显的看到当剩余空闲时间大于10的时候 taskId会被不停的打印,直到剩余时间不够时会进入下一次workLoop,taskId会自增。
fiber架构
递归过程很难中断和恢复,因此需要一种新的数据结构来表示dom树,react中采用的链表来记录每个vdom的对应关系
上图中的每一个节点都有如下的属性
const newWork={
type:,
props,
child,
parent,
dom
}
如果我们在渲染过程也变成了如果干个workloop
let nextWorkOfUnit=null
function workLoop(deadline){
let shouldYield=false
while(!shouldYield && nextWorkOfUnit){
//每一次处理完成后返回下一个工作节点
nextWorkOfUnit=performWorkofUnit(nextWorkOfUnit)
shouldYield=(deadline.timeRemaining()<=2)
}
requestIdleCallback(workLoop)
}
我们的目标是在每一次循环中处理一个工作节点,每次在performWorkofUnit这个函数中处理当前节点的dom,这样当所有的节点都处理完成时,dom也都能渲染完成。而由于每一个节点中都保存着子节点或者兄弟节点的信息。因此无论上述循环在何时退出,都可以顺利的恢复
function performWorkofUnit(fiber){
//1.dom 创建 可能已经有dom render入口处 产生的 nextWorkOfUnit dom是container
if(!fiber.dom){
const dom = (fiber.dom= fiber.type==="TEXT_ELEMENT"?document.createTextNode(""):document.createElement(fiber.type))
//dom添加到 父级容器中
fiber.parent.dom.append(dom)
//2.处理props
Object.keys(fiber.props).forEach((key)=>{
if(key!=='children'){
dom[key]=fiber.props[key]
}
})
}
//3.转换链表 设置好指针
const children=fiber.props.children
let prevChild=null
children.forEach((child,index)=>{
const newFiber={
type:child.type,
props:child.props,
child:null,
parent:fiber,
dom:null
}
//parent的child只指向第一个子节点
if(index==0){
fiber.child=newFiber
}else{
//子节点指向其兄弟节点
prevChild.sibling=newFiber
}
prevChild=newFiber
})
//4.返回下一个执行的任务
//先返回子节点
if(fiber.child){
return fiber.child
}
//如果没有就返回兄弟节点
if(fiber.sibling){
return fiber.sibling
}
//还没有就返回父节点的兄弟节点
return fiber.parent?.sibling
}
requestIdleCallback(workLoop)
接下来我们来改造一下我们的render方法,其实很简单就是设置处始的节点
const render=(el,container)=>{
nextWorkOfUnit={
dom:container,
props:{
children:[el]
}
}
//整个dom渲染过程都在workloop中完成
//createEl
//const dom=el.type==="TEXT_ELEMENT"?document.createTextNode(""):document.createElement(el.type)
//设置pros
// 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)
}
代码优化
最后我们对整个代码做个优化,将对子节点的处理封装成 initChildren,对dom的处理写入createDom,对属性的处理写入updateProps
React.js
const render=(el,container)=>{
nextWorkOfUnit={
dom:container,
props:{
children:[el]
}
}
}
function createDom(type){
return type==="TEXT_ELEMENT"?
document.createTextNode(""):
document.createElement(type)
}
function updateProps(dom,props){
Object.keys(props).forEach((key)=>{
if(key!=='children'){
dom[key]=props[key]
}
})
}
function initChildren(fiber){
const children=fiber.props.children
let prevChild=null
children.forEach((child,index)=>{
const newfiber={
type:child.type,
props:child.props,
child:null,
parent:fiber,
dom:null
}
if(index==0){
fiber.child=newfiber
}else{
prevChild.sibling=newfiber
}
prevChild=newfiber
})
}
//任务调度
let nextWorkOfUnit=null
function workLoop(deadline){
let shouldYield=false
while(!shouldYield && nextWorkOfUnit){
nextWorkOfUnit=performWorkofUnit(nextWorkOfUnit)
shouldYield=(deadline.timeRemaining()<=2)
}
requestIdleCallback(workLoop)
}
function performWorkofUnit(fiber){
//1.dom 创建 可能已经有dom render入口处 产生的 nextWorkOfUnit dom是container
if(!fiber.dom){
const dom =fiber.dom=createDom(fiber.type)
//dom添加到 父级容器中
fiber.parent.dom.append(dom)
//2.处理props
updateProps(dom,fiber.props)
}
//3.转换链表 设置好指针
initChildren(fiber)
//4.返回下一个执行的任务
if(fiber.child){
return fiber.child
}
if(fiber.sibling){
return fiber.sibling
}
return fiber.parent?.sibling
}
requestIdleCallback(workLoop)
function createElement(type,props,...children){
console.log('createElement')
return {
type,
props:{
...props,
children:children.map(child=>{
return typeof child==='string'?createTextNode(child):child
})
}
}
}
function createTextNode(text){
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
const React={
render,
createElement
}
export default React
一次性提交
仔细观察上述过程会发现一个问题,就是dom的渲染是发生在每一个wookloop中,而这个过程在浏览器空闲时间不足的时候会被中断,这样在视图就会产生只渲染了一部分dom的情况。为此我们做一点小的修改,让dom的渲染发生在所有节点处理完成
const render=(el,container)=>{
nextWorkOfUnit={
dom:container,
props:{
children:[el]
}
}
//记录根节点
root=nextWorkOfUnit
}
let root=null
//....
function workLoop(deadline){
let shouldYield=false
while(!shouldYield && nextWorkOfUnit){
nextWorkOfUnit=performWorkofUnit(nextWorkOfUnit)
shouldYield=(deadline.timeRemaining()<=2)
}
//确定任务结束点
if(!nextWorkOfUnit && root){
commitRoot()
}
requestIdleCallback(workLoop)
}
//一次性提交
function commitRoot(){
commitWork(root.child)
root=null//将root置空
}
function commitWork(fiber){
if(!fiber)return
//递归添加dom元素到根元素
fiber.parent.dom.append(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
那么相应的在performWokofUnit中就不用处理dom添加了
function performWorkofUnit(fiber){
//1.dom 创建 可能已经有dom render入口处 产生的 nextWorkOfUnit dom是container
if(!fiber.dom){
const dom =fiber.dom=createDom(fiber.type)
//此处代码删除
//fiber.parent.dom.append(dom)
//2.处理props
updateProps(dom,fiber.props)
}
//....
}
转载自:https://juejin.cn/post/7360312278920626216