【React】React源碼梳理筆記(十一)

前言

  • 結合前面知識,本篇粗略實現下fiber。

虛擬dom處理

  • 先對babel轉譯的createElement進行編寫,完成基礎功能,再給react-dom。

createElement.js

function createElement(type, config, ...children) {
    delete config.__self;
    delete config.__source;
    return {
        type,//類型
        props: {
            ...config,//屬性
            children: children.map(child => {
                return typeof child === 'object' ? child : {//是字符串特殊處理
                    type: ELEMENT_TEXT,
                    props: { text: child, children: [] }
                }
            })
        }
    }
}

製作根節點

  • 然後將虛擬dom傳給reactdom,編寫react-dom,製作根節點:
function render(element, container) {
    let rootFiber = {
        tag: TAG_ROOT,
        stateNode: container,
        props: { children: [element] }
    }
    scheduleRoot(rootFiber);
}
const ReactDOM = {
    render
}
export default ReactDOM;
  • 也就是虛擬dom是在rootFiber的props的children屬性上。掛載點在stateNode上。

初次渲染——虛擬dom轉換爲fiber樹

  • 虛擬dom的孩子是一個數組形式,所以需要轉換爲fiber結構,我們以上一篇例子爲例:
let C1 = { type: 'div', key: 'C1'};
let C2 = { type: 'div', key: 'C2'};
let B1 = { type: 'div', key: 'B1', child:[C1,C2] };
let B2 = { type: 'div', key: 'B2'};
let A1 = { type: 'div', key: 'A1', child:[B1,B2] };
  • 傳統是這樣結構,轉換成fiber結構需要用循環遍歷:
function createFiber(root,child){
    let index=0
    let prevFiber
    if(child){
        while(index<child.length){
            let currentChild=child[index]
            let fiber={
                type:currentChild.type,
                child:currentChild.child,
                key:currentChild.key,
                return:root
            }
            if(index===0){
                root.child=fiber
            }else{
                prevFiber.sibling=fiber
            }
            prevFiber=fiber
            index++
        }
    }
}
function deep(root){
    createFiber(root,root.child)
    if(root.child){
        deep(root.child,root.child.child)//這裏改成return就可以拆開
    }
    while(root){
        if(root.sibling){
            deep(root.sibling,root.sibling.child)
        }
        root=root.return
    }
}
deep(A1)
console.log(A1)
  • 這裏的技巧就是傳2個值,一個是父節點,一個是它的孩子,然後修改父節點,將孩子進行while循環連接。
  • 最後輸出:
    在這裏插入圖片描述
  • 每個節點只有一個孩子。
  • 這樣就完成了轉化fiber結構。而這個轉化過程,是在上一篇console的start處進行的,所以結合一下idleCallback,寫成這樣:
function createDom(currentFiber) {
    if (currentFiber.tag === TAG_TEXT) {
        return document.createTextNode(currentFiber.props.text);
    } else if (currentFiber.tag === TAG_HOST) {
        let stateNode = document.createElement(currentFiber.type);
        updateDOM(stateNode, {}, currentFiber.props);
        return stateNode;
    }
}
updateDOM(stateNode,oldProps,newProps){
    setProps(stateNode,oldProps,newProps)//更新屬性
}
let nextUnitOfWork=null
let workInProgressRoot=null
export function scheduleRoot(rootfiber){
    workInProgressRoot=rootfiber//work是根
    nextUnitOfWork=rootfiber//相當於每個工作單元,workloop裏面會不斷變化
}
function performUnitOfWork(currentFiber){   //這個就是前面deep函數樣例的邏輯
    beginWork(currentFiber)//轉化爲fiber
    if(currentFiber.child){
        return currentFiber.child
    }
    while(currentFiber){
        completeUnitOfWork(currentFiber)
        if(currentFiber.sibling){
            return currentFiber.sibling
        }
        currentFiber=currentFiber.return
    }
}
function completeUnitOfWork(){//收集副作用

}
function beginWork(currentFiber){//如果原生dom,需要創建真實dom元素。current會變
    if(currentFiber.tag===TAG_ROOT){//根不需要建真實dom
        updateHostRoot(currentFiber)
    }else if(currentFiber.tag===TAG_TEXT){
        updateHostText(currentFiber)
    }else if(currentFiber.tag===TAG_HOST){
        updateHost(currentFiber)
    }
}

function updateHost(currentFiber){
    if(!currentFiber.stateNode){
        currentFiber.stateNode=createDom(currentFiber)
    }
    const newChildren = currentFiber.props.children
    reconcilieChildren(currentFiber,newChildren)
}
function updateHostText(currentFiber){
    if(!currentFiber.stateNode){
        currentFiber.stateNode=createDom(currentFiber)
    }
}
function updateHostRoot(currentFiber){
    let newChildren = currentFiber.props.children//取出當前任務的虛擬dom
    reconcilieChildren(currentFiber,newChildren)//爲孩子創建fiber
}
function reconcilieChildren(currentFiber,newChildren){
    let newChildIndex=0
    let prevSibiling
    while(newChildIndex<newChildren.length){//while遍歷孩子構建子節點
        let newChild = newChildren[newChildIndex]
        let tag
        if(newChild.type===ELEMENT_TEXT){
            tag=TAG_TEXT//字符串單獨處理
        }else if(typeof newChild.type==='string'){
            tag=TAG_HOST//原生節點
        }
        let newFiber={
            tag,
            type:newChild.type,
            props:newChild.props,
            stateNode:null,
            return:currentFiber,
            effectTag:PLACEMENT,//增加操作
            nextEffect:null,//鏈表指向
        }
        if(newFiber){
            if(newChildIndex==0){//第一個兒子
                currentFiber.child=newFiber
            }else{
                prevSibiling.sibling=newFiber
            }
            prevSibiling=newFiber
        }
        newChildIndex++
    }
}
function workLoop(deadline){
    let shouldYield =false
    while(nextUnitOfWork&&!shouldYield){
        nextUnitOfWork=performUnitOfWork(nextUnitOfWork)
        shouldYield=deadline.timeRemaining()<=0
    }
    if(nextUnitOfWork){//出while則讓給瀏覽器下次調度
       requestIdleCallback(workLoop,{timeout:1000})
    }else{
        console.log('over')
    }
}
requestIdleCallback(workLoop,{timeout:1000})
  • 其中reconcilieChildren對應上面的createFiber函數,一個邏輯。performUnitOfWork對應上面deep函數。看不懂的看看上面簡寫的就懂了。

初次渲染——收集副作用

  • 上一篇寫到,在while那裏會收集副作用,做成副作用鏈表,這次來編寫下:
function completeUnitOfWork(currentFiber){//收集副作用
    let returnFiber=currentFiber.return
    if(returnFiber){//父節點
        if(!returnFiber.firstEffect){//這2個if的作用是爲了確保根節點能指向鏈表的頭和尾
            returnFiber.firstEffect=currentFiber.firstEffect
        }
        if(currentFiber.lastEffect){//說明他孩子有副作用(沒孩子的進不來)
            if(returnFiber.lastEffect){//如果父有指針,把當前有副作用的孩子掛到鏈表下一個
                returnFiber.lastEffect.nextEffect=currentFiber.firstEffect
            }
            returnFiber.lastEffect=currentFiber.lastEffect
        }
        const effectTag=currentFiber.effectTag//如果這個節點有更新
        if(effectTag){
            if(returnFiber.lastEffect){//這個節點不是第一個,那就用next連接當前
                returnFiber.lastEffect.nextEffect=currentFiber
            }else{
                returnFiber.firstEffect=currentFiber
            }
            returnFiber.lastEffect=currentFiber//父節點的尾指針,始終指向最後個有副作用的孩子
        }
    }
}
  • 這個可能得稍微揣摩下,總之就是最終生成了一個副作用鏈表,沒副作用的就跳過不收集。
  • 這個鏈表很巧的就是都是孩子收集完收集父親纔算完成,這樣可以先生成子節點,然後再掛到父節點上,最後掛根上。

初次渲染——提交階段

  • 提交階段則在上面代碼的console.log over處執行:
function workLoop(deadline){
    let shouldYield =false
    while(nextUnitOfWork&&!shouldYield){
        nextUnitOfWork=performUnitOfWork(nextUnitOfWork)
        shouldYield=deadline.timeRemaining()<=0
    }
    if(nextUnitOfWork){//出while則讓給瀏覽器下次調度
       requestIdleCallback(workLoop,{timeout:1000})
    }else{
        console.log('over')
        commitRoot()
    }
}
function commitRoot(){
    let currentFiber=workInProgressRoot.firstEffect
    while(currentFiber){
        commitWork(currentFiber)
        currentFiber=currentFiber.nextEffect //單鏈表,一個個effect做
    }
    workInProgressRoot=null
}
function commitWork(currentFiber){
    if(!currentFiber)return 
    let returnFiber=currentFiber.return
    let returnDom=returnFiber.stateNode//拿到真實dom
    if(currentFiber.effectTag===PLACEMENT){
        returnDom.appendChild(currentFiber.stateNode)
    }
    currentFiber.effectTag=null
}
  • 這個就是順着鏈表添加就ok了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章