关于React16 Fiber的 二三事

293 阅读5分钟

前言:最新在学习React16 内部原理的事项,为了更好的记录学习,梳理学习内容,把内容更新如下啦~

React16 Fiber的前生React15使用的递归有何弊端?

思路:在React15 的版本中采用的是Virtual Dom对比的方案,通过对比VirtualDom找出差异的部分,从而只将差异的部分更新到页面中去,避免更新整体Dom以提高性能。

弊端:但是在Virtual Dom 对比的过程中,React15 使用了递归,而递归的调用过程是不能终止的。所以当Virtual Dom 的层级比较深,递归的过程会长期占据主线程,而JavaScript 又是单线程的,不能同时执行多个任务。其他任务就需要等待递归的执行结束,才能开始。加上javaScript 的执行和UI的渲染是互斥的,此时的用户要么看到空白的界面,要么就是有界面但不能响应用户的操作。所以用户体验非常差。

简而言之,react15在查找对比的过程中,会大量长时间占据主线程,而且不能中断,容易造成页面的卡顿!

上代码:

//  通过balbel 转换jsx 代码,其实是一个就是js语法糖,
const jsx = (
    <div id = "a1">
        <div id = "b1">
            <div id = "c1"></div>
            <div id = "c2"></div>
        </div>
        <div id = "b2"></div>
    </div>
)
// 转化为一个js 对象
{
    "tyep":"div",
    "prop":{
        "id":"a1",
        "children":[
            {
                "type":"div",
                "props":{
                    "id":"b1",
                    "children":[
                        {
                            "type":"div",
                            "props":"c1"
                        },
                        {
                            "type":"div",
                            "props":"c2"
                        }
                    ]
                }
            },
            {
                "type":"div",
                "props":{
                    "id":"b2"
                }
            }
        ]
    }
}
render(jsx对象,页面根元素 ) 
//!!重点!!使用render 函数添加到页面
function render (vdom,container){
    // 创建元素
    const  newElement =  document.createElement(vdom.type)
    // 为元素添加属性
    Object.keys(vdom.props)
    .filter(propsName => propsName !== "children")
    .forEach(propsName => newElement[propsName] =  vdom.props[propsName])
    // 关键代码!!!递归添加子元素 =》 在此处会消耗大量的性能
    if(Array.isArray(vdom.props.children)){
        vdom.props.children.forEach(child => render( child, container))
    }
    // 将元素添加到页面中
    container.appendChild(element)
}

Fiber为啥能解决React15递归产生的弊端?

  1. 遍历方式:Fiber 放弃了原先递归的方式,而是采取循环,而循环是可以随时终止,后面再继续执行。这个特点为执行更加优先级的任务提供了可能

  2. 数据处理:React15 是将整个树作为一个大对象进行处理,故处理的比较耗时。而React16将分割成多个不同的小单元,然后通过链式表的结构去告知每个单元的父级,子级,兄弟级的元素。这个特点减少的耗时的时间,小而美~

    通俗来说就是把一个大的树对象变成多个小对象,里面包含更多的属性去指向自己的父级,子级,和兄弟级。主要的属性如下

image.png

image.png

  1. 执行方式:Fiber 采用利用浏览器的空余时间去执行的,在浏览器有空余的时间的时候才会去进行dom 的对比更新,一旦发现浏览器没有更多的空余时间,或者有更高级的任务,就会退出来执行更加优先级的任务。 这个特点会避免dom 对比的过程中 大量占据主线程,而是把主线程的控制权交给浏览器。

以下是执行的流程示意图

image.png

Fiber的实现方式,利用循环模拟递归

Fiber的工作分为两个阶段:render 阶段和 commit 阶段。 render 阶段:构建render对象,构建链表,在链表中标记要执行的dom操作,可中断。 commit 阶段:根据构建好的链表进行DOM操作,不可中断。

循环的循序:从上往下走,构建节点对应的Fiber对象。从A1找到子级B1,B2;再找到B1的子级C1,C2;再找C1的子集,如果C1没有子集,那么开始找兄弟C2,再找兄弟C2是否有子集,没有子集,再找是否有兄弟集,如果都没有,则开始网上寻找B1 ,寻找B1是否有兄弟集-B2,找到B2,再次查看是否有子集和兄弟集。

image.png 似乎有点绕~~简单来说,从根节点出发,自上而下先找子集,有子集,子集优先构建,如果没有子集,查找兄弟节点。有兄弟节点再次判断该兄弟节点是否是有子集和兄弟集,若两样都没有,则开始循环往上寻找。最后,由下往上回到根节点。

实现思路代码

// 利用Fiber代码实现

const container = document.getElementById("root")

// 主动构建根节点的Fiber对象
const workInProgressRoot = {
    stateNode : container,
    props:{
        children:[jsx]
    }
}

// 下一个要执行的任务,是否还有其他的对象
let nextUnitOfWork = workInProgressRoot
function workLoop(deadline){
    // 1.是否有空余时间
    // 2.是否有执行的任务
    while(nextUnitOfWork && deadline.timeRemaining() > 0){
        nextUnitOfWork =  performUnitOfWork(nextUnitOfWork)
    }
    // 如果没有执行的任务,代表任务已经执行完成
    if( !nextUnitOfWork){
        //进行更新Dom的操作
        commitRoot()
    }
}

function performUnitOfWork(workInProgressFiber){
    // 向下开始构建每个fiber对象
    beginWork(workInProgressFiber)
    //如果当前有子集,继续返回对象,接着去构建子集的子集
    if(workInProgressFiber.child){
        return workInProgressFiber.child
    }
    while(workInProgressFiber){
        // 开始向上构建链表
        completeUnitOfWork(workInProgressFiber)
        // 如果有同级
        if(workInProgressFiber.sibling){
            // 返回 同级,构建同级的子集
            return workInProgressFiber.sibling
        }
        workInProgressFiber = workInProgressFiber.return
    }

}
function completeUnitOfWork(){
    // 获取当前Fiber 的父级
    const returnFiber = workInProgressFiber.return
    // 判断父级是否存在
    if(returnFiber){
        //要执行的Dom操作
        if(workInProgressFiber.effectTag){
            // 由下往上开始构建链表,比较复杂
            if(!returnFiber.lastEffect){
                returnFiber.lastEffect = workInProgressFiber.lastEffect
            }
            if(returnFiber.firstEffect){
                returnFiber.lastEffect.nextEffect = workInProgressFiber
            }
            if(returnFiber.lastEffect){
                returnFiber.lastEffect.nextEffect = workInProgressFiber
            }else{
                returnFiber.lastEffect = workInProgressFiber
            }
            returnFiber.lastEffect = workInProgressFiber
        }
    }
}

function commitRoot(){
    let currentFiber = workInProgressFiber.firstEffect
    while(currentFiber){
        currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
        currentFiber = currentFiber.nextEffect
    }
}
function beginWork(workInProgressFiber){
     // 1.创建Dom元素,并添加到stateNOde 属性
    if( !workInProgressFiber.stateNode){
        workInProgressFiber.stateNode = document.createElement(workInProgressFiber.type)
    }
    // 为Dom添加属性
    for(let attr in workInProgressFiber.props){
        if(attr !== "children"){
            workInProgressFiber.stateNode[attr] = workInProgressFiber.props[attr]
        }
    }
    
    // 2;构建当前Fiber的子集Fiber
    if(Array.isArray(workInProgressFiber.props.child)){
        let preChildFiber = null
        workInProgressFiber.props.child.forEach((child,index)=>{
            let childFiber = {
                type: child.type,
                props: child.props,
                effectTag:'PLACEMENT',
                return: workInProgressFiber
            }
            if(index === 0){
                workInProgressFiber.child = childFiber   
            }else{
                preChildFiber.sibling = childFiber

            }
            preChildFiber = childFiber 

        })
    }
}

// 在浏览器的空余时间去执行构建剩余Fiber对象的代码
requesetIdleCallback (workLoop)

完毕~