react-fiber原理分析

745 阅读7分钟

React15的调度策略

JavaScript是单线程运行的。在浏览器环境中,他需要负责页面的Js解析和执行、绘制、事件处理、静态资源加载和处理。而且只能一个任务一个任务的执行,如果其中某个任务耗时很长,那后面的任务则执行不了,在浏览器端则会呈现卡死的状态。 image.png React15的渲染和diff会递归比对VirtualDOM树,找出有增删改的节点,然后同步更新他们,整个过程是一气呵成的。那么如果页面节点数量非常庞大,React会一直霸占着浏览器资源,一则会导致用户触发的事件得不到响应,二则会导致掉帧,用户会感知到这些卡顿。

所以针对上述痛点,我们期望将找出有增删改的节点,然后同步更新他们这个过程分解成两个独立的部分,或者通过某种方式能让整个过程可中断可恢复的执行,类似于多任务操作系统的单处理器调度。

浏览器任务调度策略和渲染流程

那么一个帧包含什么呢?

image.png

  • 一帧平均是16.66ms(1s/60),主要分为以下几个部分:脚本执行 - 样式计算 - 布局 - 重绘 - 合成
  • js引擎和页面渲染引擎是在同一个渲染线程,两者是互斥的。当某个任务执行时间过长,浏览器会推迟渲染

requestAnimationFrame

requestAnimationFrame的callback会在浏览器每次绘制前执行,执行的时机:浏览器刷新下一帧渲染周期的起点上

requestIdleCallback

requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响关键事件,如动画和输入响应。当正常帧任务完成不超过16ms,此时就会执行requestIdleCallback中注册的任务

requestAnimationCallback的回调会在每一帧开始时确定执行,属于高优先级任务;requestIdleCallback回调属于低优先级任务,有富余时间时才执行

132014655.jpg

当浏览器很长没有任务执行时,requestIdleCallback的时间会延长,最长是50ms

Fiber

什么是Fiber

Fiber是一个执行单元

Fiber是一个执行单元,每次执行完一个执行单元后,react会检查还剩多少时间,如果没有时间就将控制权交还浏览器;然后继续进行下一帧的渲染。

下图为React结合空闲阶段的调度过程

调度机制.png

这是一种合作式调度,需要程序和浏览器互相信任。浏览器作为领导者,会分配执行时间片(即requestIdleCallback)给程序去选择调用,程序需要按照约定在这个时间内执行完毕,并将控制权交还浏览器。

Fiber也是一种数据结构

React中使用链表将Virtual DOM链接起来,每一个虚拟节点表示一个Fiber。child指向下个节点,sibling指向同级节点return指向父级节点

fiber结构.png

Fiber小结

  • 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应速度
  • 通过Fiber架构,让自己的Reconciliation过程变得可被中断,适时地让出CPU执行权,可以让浏览器及时地响应用户的交互

任务循环

workloop.png 将fiber作为一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去。

function workLoop (deadline) {
  // 这一帧渲染还有空闲时间 || 没超时 && 还存在一个执行单元
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 执行当前执行单元 并返回下一个执行单元
  }
  if (!nextUnitOfWork) { // 不存在执行单元时,此阶段完成
    console.log('render end !')
  } else {
    requestIdleCallback(workLoop, { timeout: 1000 }) // 如果时间不够,在再次等待浏览器的空闲时间执行任务循环
  }
}
requestIdleCallback(workLoop, {timeout: 500});

// 从根节点开始,遍历执行单元。
function performUnitOfWork (fiber) {
  beginWork(fiber) // 开始
  if (fiber.child) {
    return fiber.child
  }
  while (fiber) {
    completeUnitOfWork(fiber) // 结束
    if (fiber.sibling) {
      return fiber.sibling
    }
    fiber = fiber.return
  }
}

Fiber执行阶段

每次渲染有两个阶段:

  • Reconciliation协调/render阶段:可以认为是Diff阶段,这个阶段可以被中断,这个阶段会找出所有节点变更,例如节点增删改等等,这些变更在React中称为Effect(副作用)。
  • Commit提交阶段:将上一个阶段计算出来的需要处理的副作用一次性执行。这个阶段不能中断,必须同步一次性执行完。

Reconciliation阶段

  • 从顶点开始遍历

  • 如果有son,则进行遍历;如果没有,此节点遍历完成

  • 如果sibling,则进行遍历;如果没有,返回父节点标识完成父节点遍历;如果有叔叔,遍历叔叔

  • 没有父节点,遍历结束

  • son--> sibling -->uncle

reconciliation.png

  • beginWork

    1)创建真实dom;没有真实节点时,创建真实dom+添加属性;2)遍历子fiber树,将所有节点fiber化

        function beginWork(currentFiber) {
            console.log('开始' + currentFiber.key)
            if (!currentFiber.stateNode) {
                currentFiber.stateNode = document.createElement(currentFiber.type);//创建真实DOM
                for (let key in currentFiber.props) {//循环属性赋值给真实DOM
                    if (key !== 'children' && key !== 'key')
                        currentFiber.stateNode.setAttribute(key, currentFiber.props[key]);
                }
            }
            let previousFiber;
            currentFiber.props.children.forEach((child, index) => {
                let childFiber = {
                    tag: 'HOST',
                    type: child.type,
                    key: child.key,
                    props: child.props,
                    return: currentFiber,
                    effectTag: 'PLACEMENT',
                    nextEffect: null // 下一个有副作用的节点
                }
                if (index === 0) {
                    currentFiber.child = childFiber; // a. child指向子fiber
                } else {
                    previousFiber.sibling = childFiber; // c. 上一个fiber的sibling指向新的fiber
                }
                previousFiber = childFiber; // b. 变成上一个fiber
            });
        }
        
  • completeUnitOfWork

    当前fiber结束,构建effectList副作用链。firstEffect执行第一个有副作用的子节点,lastEffect指向最后一个有副作用的子节点;中间通过nextEffect链接

effect-list.png

        function completeUnitOfWork(currentFiber) {
            console.log('结束' + currentFiber.key)
            const returnFiber = currentFiber.return;
            if (returnFiber) {
                if (!returnFiber.firstEffect) {
                    returnFiber.firstEffect = currentFiber.firstEffect;
                }
                if (currentFiber.lastEffect) {
                    if (returnFiber.lastEffect) {
                        returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
                    }
                    returnFiber.lastEffect = currentFiber.lastEffect;
                }

                if (currentFiber.effectTag) {
                    if (returnFiber.lastEffect) {
                        returnFiber.lastEffect.nextEffect = currentFiber;
                    } else {
                        returnFiber.firstEffect = currentFiber;
                    }
                    returnFiber.lastEffect = currentFiber;
                }
            }
        }
        

Commit阶段

在performUnitOfWork过程中,通过beginWork将所有节点fiber化。经过completeUnitOfWork,形成effectList链表,即将所有真实DOM、虚拟DOM、Fiber结合,其副作用(增删改)形成一个链表结构。接下来,需要将其渲染到页面。

function workLoop (deadline) {
  // ...
  if (!nextUnitOfWork) {
    commitRoot()
  } else {
    requestIdleCallback(workLoop, { timeout: 1000 })
  }
}

function commitRoot() {
  let fiber = workInProgressRoot.firstEffect
  while (fiber) { // 从firstEffect开始,通过nextEffect找到每个有副作用的fiber节点,根据副作用类型递归进行dom增删改
    commitWork(fiber)
    fiber = fiber.nextEffect
  }
  workInProgressRoot = null
}
function commitWork(currentFiber) { // 根据副作用类型递归进行dom增删改
	...
}

Render阶段

全局定义当前页面对应的fiber,即currentRoot;内存中构建的fiber,即workInProgressRoot;下一个工作单元,即nextUnitOfWork

初次render

render阶段,从根节点开始进行调度渲染。将rootFiber赋值给workInProgressRoot后,进一步赋值给nextUnitOfWork。当浏览器有时间时执行。

let currentRoot = null;  //当前页面对应的根Fiber
let workInProgressRoot = null;  //正在渲染中的根Fiber
let nextUnitOfWork = null;  // 下一个工作单元

function render(element, container) {
    let rootFiber = {
        tag: TAG_ROOT,
        stateNode: container,
        props: { children: [element] }
    }  
    scheduleRoot(rootFiber);
}
function scheduleRoot(rootFiber) {
    if (rootFiber) {
        workInProgressRoot = rootFiber; //把当前树设置为nextUnitOfWork开始进行调度
    } else {
      ...
    }
    nextUnitOfWork = workInProgressRoot;
}

更新render

当通过useReducer更新状态时,会主动调用scheduleRoot方法进行调度渲染。

回顾下useReducer的实现:

function useReducer(reducer, initialValue) {
    let newHook = workInProgressFiber.alternate.hooks[hookIndex];
    if (newHook) {
        newHook.state = newHook.updateQueue.forceUpdate(newHook.state); // 链表结构进行状态更新
    } else {
        newHook = {
            state: initialValue,
            updateQueue: new UpdateQueue()
        };
    }
    const dispatch = action => {
        newHook.updateQueue.enqueueUpdate(
            new Update(reducer ? reducer(newHook.state, action) : action)
        );
        scheduleRoot(); // 调度渲染
    }
    workInProgressFiber.hooks[hookIndex++] = newHook;
    return [newHook.state, dispatch];
}

在更新时,通过双缓存机制实现fiber树的复用

双缓存机制

  • 在进行每次页面绘制前,会先清除上一次绘制的内容。如果当前绘制的计算量比较大,会导致上一次清除到当前内容绘制完成的时间间隙较长,出现白屏闪烁。为了解决这个问题,会在内存中保存当前帧的绘制,等绘制完毕替换上一帧内容,避免画面闪烁。这种在内存中构建并直接替换的技术叫做双缓存。

  • React 根据双缓冲的机制维护了两棵树:

    • 一棵是 Fiber 树用于渲染页面;
    • 一棵是 WorkInProgress Fiber 树,用于在内存中构建,方便在构建完成时直接替换用于渲染页面的 Fiber 树。
let currentRoot = null;         //当前的根Fiber
let workInProgressRoot = null;  //正在渲染中的根Fiber
let nextUnitOfWork = null;      //下一个工作单元

function scheduleRoot(rootFiber) {
    if (rootFiber) {
        workInProgressRoot = rootFiber; // 1. 初次render时,把当前树设置为nextUnitOfWork开始进行调度
    } else {
        if (currentRoot.alternate) { // 3. 再一次数据更新时,为了fiber树的复用,将workInProgressRoot指向currentRoot.alternate,即第一次渲染时的fiber树
            workInProgressRoot = currentRoot.alternate;
            workInProgressRoot.alternate = currentRoot;
        } else { // 2. 第一次数据更新时,没有传rootFiber,此时,将currentRoot复制给workInProgressRoot,并将workInProgressRoot指向currentRoot即上次fiber树
            workInProgressRoot = {
                ...currentRoot,
                alternate: currentRoot
            }
        }
    }
    nextUnitOfWork = workInProgressRoot;
}

function commitRoot() {
   ...
   currentRoot = workInProgressRoot;
   workInProgressRoot = null;
}