调度器和协调器是如何协作生成fiber树——mini-react代码篇

128 阅读4分钟

看过我的《jsx如何转换成react-fiber》文章的朋友都知道,jsx先是通过babel编译和转化,变成vNode结构,这个结构很重要,react的渲染是离不开current Fiber树和workInprogress Fiber树,这两棵树都是由以下结构为模板生成的。

let vNode = {
  "type": "div",
  "props": {
    "className": "container",
    "children": [{
      "type": "h1",
      "props": {
        "children": [{
          "type": "TEXT_ELEMENT",
          "props": {
            "nodeValue": "Hello, World!",
            "children": []
          }
        }]
      }
    }, {
      "type": "p",
      "props": {
        "children": [{
          "type": "TEXT_ELEMENT",
          "props": {
            "nodeValue": "This is an example component.",
            "children": []
          }
        }]
      }
    }]
  }
}

生成一棵fiber树是不可以用递归去生成的。原因是,递归如果太深会造成render函数长时间占用主线程,造成页面卡顿。所以,在这里必须使用这两者配合去生成fiber树:调度器、协调器。

调度器

requestIdleCallback在浏览器空闲时期被调用并执行workLoop函数,在workLoop中会检查nextUnitOfWork是否存在,存在的话,就继续执行performUnitOfWork

let nextUnitOfWork = null 
function workLoop(deadline) { 
    let shouldYield = false 
    while (nextUnitOfWork && !shouldYield) { 
        nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) 
        shouldYield = deadline.timeRemaining() < 1 
    } 
    requestIdleCallback(workLoop) 
} 

requestIdleCallback(workLoop)

协调器

那么nextUnitOfWork是什么呢?performUnitOfWork又做了什么呢?

let nextUnitOfWork = null 
let currentRoot = null // 保存着当前页面对应的fiber tree,初始化时为null

function render(element, container){ 
    nextUnitOfWork = { 
        dom: container, 
        props: { children: [element]}, 
        alternate: currentRoot,
    } 
}

const container = document.getElementById("root") 
const element = vNode;
render(element, container)

在这里,vNode是由最新的JSX重新编译出来的vNode树,结构在最上面可见。(不太了解可以看《JSX是如何变成react-fiber的》)

所以,当初始化或者更新的时候,就会调用render函数,给nextUnitOfWork进行赋值,当满足(nextUnitOfWork && !shouldYield)时,调用performUnitOfWork函数。

function performUnitOfWork(fiber) { 
    // 第一步 根据fiber节点创建真实的dom节点,并保存在fiber.dom属性中 
    if(!fiber.dom){ 
        fiber.dom = createDom(fiber)  // 不核心,所以就不展示代码了
    } 
    // 第二步 给子元素创建对应的fiber节点 
    const children = fiber.props.children 
    reconcileChildren(fiber, children) 
    // 第三步,查找下一个工作单元 
    if(fiber.child){ 
        return fiber.child 
    } 
    let nextFiber = fiber 
    while(nextFiber){ 
        if(nextFiber.sibling){ 
            return nextFiber.sibling 
        } 
        nextFiber = nextFiber.parent 
    }     
}

function reconcileChildren(wipFiber, elements) { 
    let index = 0 
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child 
    let prevSibling = null 
    while (index < elements.length || oldFiber != null) { 
        const element = elements[index] 
        let newFiber = null 
        const sameType = oldFiber && element && element.type == oldFiber.type 
        if (sameType) { 
            newFiber = { 
                type: oldFiber.type, 
                props: element.props, 
                dom: oldFiber.dom, 
                parent: wipFiber, 
                alternate: oldFiber, 
                effectTag: "UPDATE", 
             } 
         } 
       if (element && !sameType) { 
           newFiber = { 
               type: element.type, 
               props: element.props, 
               dom: null, 
               parent: wipFiber, 
               alternate: null, 
               effectTag: "PLACEMENT", 
           } 
       } 
       if (oldFiber && !sameType) { 
           oldFiber.effectTag = "DELETION" 
           deletions.push(oldFiber) 
       } 
       if (oldFiber) { 
           oldFiber = oldFiber.sibling 
       } 
       if (index === 0) { 
           wipFiber.child = newFiber 
       } else if (element) { 
           prevSibling.sibling = newFiber 
       } 
       prevSibling = newFiber index++ 
    } 
}

以上代码做了什么事情呢?首先children是什么呢?它就是[vNode,vNode]结构。

如果您嫌代码太长,理不通顺,请跟进我的脚步!让我们一起拆开来看看。

第二步

首先,当前Fiber节点有孩子的时候,也就是以下的vNodechildren的时候,就将该children作为参数传入reconcileChildren函数中,同时也传入当前的Fiber节点

let vNode = {
  "type": "div",
  "props": {
    "className": "container",
    "children": [{
      "type": "h1",
      "props": {
        "children": [{
          "type": "TEXT_ELEMENT",
          "props": {
            "nodeValue": "Hello,",
            "children": []
          }
        },{
          "type": "TEXT_ELEMENT",
          "props": {
            "nodeValue": "World!",
            "children": []
          }
        }]
      }
    }
}

进入reconcileChildren函数中,我们可以发现,它循环了children,创建第一个子节点时,父节点和它使用child进行关联,其余的子节点通过sibling属性进行关联第一个Child节点

// 参数:父Fiber节点,父Fiber节点的children(vNode结构)

function reconcileChildren(wipFiber, elements) { 
    let index = 0 
    let prevSibling = null 
    while (index < elements.length || oldFiber != null) { 
        const element = elements[index] 
        let newFiber = null 
        // 这里的new Fiber是我写的伪代码,指的是根据不同状态去创建Fiber节点
        newFiber = new Fiber()
        
       if (index === 0) { 
           wipFiber.child = newFiber 
       } else if (element) { 
           prevSibling.sibling = newFiber 
       } 
       prevSibling = newFiber index++ 
    } 
}
第三步

在第二步中,我们发现父Fiber节点和子Fiber节点关联,那么,完成第二步后,到第三步时,下一个nextUnitOfWork就变成了Fiber的子节点,如果Fiber并没有子节点呢?没关系,它会去寻找Fiber的兄弟节点作为下一个nextUnitOfWork,并返回。

if(fiber.child){ 
    return fiber.child 
} 
let nextFiber = fiber 
while(nextFiber){ 
    if(nextFiber.sibling){ 
        return nextFiber.sibling 
    } 
    nextFiber = nextFiber.parent 
}

在这里我们可以看出,创建Fiber树的过程并不是连续的,它一边一层一层地深度遍历Vnode模板,一边创建Fiber节点,并且在完成一次Fiber节点的创建后,再将下一个“毛坯”状态的Fiber给到公共变量nextUnitOfWork,而下一个Fiber的选择,首先是深度遍历,给到child,如果没有child再寻找sibling节点;当都没有的时候,就递归回到上一层,找上一层的sibling。

然后,等待下一次的浏览器空余时间片,再重新调起

function workLoop(deadline) { 
    let shouldYield = false 
    while (nextUnitOfWork && !shouldYield) { 
        nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) 
        shouldYield = deadline.timeRemaining() < 1 
    } 
    requestIdleCallback(workLoop) 
} 

requestIdleCallback(workLoop)

完美闭环!

创作不易,谢谢mini-react的创作者,让我在短时间内看懂react原理,再也不怕了!

参考代码:github.com/lizuncong/m…