【React】React源碼梳理筆記(五)

前言

  • 繼續上次React

無移動domdiff

  • 上一篇把屬性更新搞定了,但是點擊按鈕,雖然屬性變了,但是數字沒變,所以要實現domdiff來改變數字。
  • react的domdiff跟vue不太一樣,我感覺react的domdiff纔是正常人思維,vue那個感覺像是專門搞算法的人寫的。
  • 首先,需要一個map,來獲得老元素的映射。
function getChildrenElementsMap(oldChildrenElements){
    let oldChildrenElementsMap={}
    if(!oldChildrenElements.length){//如果沒有length說明是單個虛擬dom
        oldChildrenElements=[oldChildrenElements]
    }
    for(let i=0 ;i<oldChildrenElements.length;i++){
        let oldkey = oldChildrenElements[i].key ||i.toString()
        oldChildrenElementsMap[oldkey]=oldChildrenElements[i]
    }
    return oldChildrenElementsMap
}
  • 這玩意長的樣子的就是{key:{xxxxx虛擬domxxx}}這樣,如果沒有key,就是索引。
  • 然後,需要通過遍歷新節點,獲得新map,其中如果有可以複用的vdom,拿新節點的屬性更新老節點然後。
function diff(dom,oldChildrenElements,newChildrenElements){
    let oldChildrenElementsMap=getChildrenElementsMap(oldChildrenElements)
    getNewChildrenElementsMap(newChildrenElements,oldChildrenElementsMap)
}
function getNewChildrenElementsMap(newChildrenElements,oldChildrenElementsMap){
    let newChildrenElementsMap={}
    
    if(!newChildrenElements.length){
        newChildrenElements=[newChildrenElements]
    }
    for (let i=0;i<newChildrenElements.length;i++){
        let newChildElement = newChildrenElements[i]
        if(newChildElement){//如果有虛擬dom
            let newKey =newChildElement.key||i.toString()
            let oldChildElement = oldChildrenElementsMap[newKey]//取到相同的虛擬Dom
            if(canDeepCompare(oldChildElement,newChildElement)){//類型一樣複用
                updateElement(oldChildElement,newChildElement)//遞歸複用老的,新屬性更新
                newChildrenElements[i]=oldChildElement //直接把老的拿來套用新的
            }
            newChildrenElementsMap[newKey]=newChildrenElements[i]//新map所對應的最新虛擬dom
        }
    }
    return newChildrenElementsMap
}
function canDeepCompare(oldChildElement,newChildElement){
    if(!!oldChildElement&&!!newChildElement){
        return oldChildElement.type===newChildElement.type
    }
    return false
}
  • 同時注意下有個bug需要改一下,就是前面字符串賦值的地方,不能使用type,因爲這裏還要對比type,所以包裝字符串節點需要新增屬性,在createDom中也需要進行判斷,還有dom對比字符串節點,一共3處地方需要稍微修改下。對於字符串節點,如果裏面字相同就不動它:
function updateElement(oldelement,newelement ){
    let currentDom =newelement.dom =oldelement.dom //在這裏的就可以複用dom了。
    if(oldelement.$$typeof===REACT_TEXT_TYPE&&newelement.$$typeof===REACT_TEXT_TYPE){//文本比較
        if(currentDom.textContent!==newelement.content)currentDom.textContent=newelement.content//修改文本
    }else if(oldelement.$$typeof===REACT_ELEMENT_TYPE){//元素類型
        updateDomProperties(currentDom,oldelement.props,newelement.props)
        updateChildrenElements(currentDom,oldelement.props.children,newelement.props.children)//比對子節點
    }else if(oldelement.$$typeof===FUNCTION_COMPONENT){//類型都是函數組件
        updateFunctionComponent(oldelement,newelement)
    }else if(oldelement.$$typeof===CLASS_COMPONENT){//類型都是函數組件
        updateClassComponent(oldelement,newelement)
    }
}
  • 這樣就已經可以渲染了。點擊加號,數字和屬性都能變了。而button就直接複用,沒有換新的。
  • 所以這個過程是個深度優先遞歸。

有移動domdiff

  • 前面是無移動的情況,還可能要進行dom移動。
  • 建個例子:
class Counter extends React.Component{
  constructor(props){
    super(props)
    this.state={show:true}
  } 
  handleClick=()=>{
    this.setState((state)=>({show:!state.show}))
  
  }
  render(){
    if(this.state.show){
      return (
        <ul onClick={this.handleClick}>
          <li key="A">A</li>
          <li key="B">B</li>
          <li key="C">C</li>
          <li key="D">D</li>
        </ul>
      )
    }else{
      return(
        <ul onClick={this.handleClick}>
        <li key="A">A1</li>
        <li key="C">C1</li>
        <li key="B">B</li>
        <li key="E">E1</li>
        <li key="F">F</li>
      </ul>
      )
    }
  }
}
ReactDOM.render(
  <Counter></Counter>,
  document.getElementById('root')
);

  • 我們可以通過一個全局變量記錄遞歸深度有點類似:
dep=0
function godeep(){
    dep++
    godeep()
    dep--
    if(dep===0)...
}
  • 還需要修改下創建孩子虛擬dom時的邏輯,要加個屬性,標識這個節點是它父親的第幾個孩子。
function createNativeDOMChildren(parentNode,children){
    if(children){
        if(Array.isArray(children)){
            flatChildren(children).forEach((child,index) => {
                child._mountIndex=index//增加屬性
                let childDom = createDOM(child)
                parentNode.appendChild(childDom)
            });
        }else{//chilren是單個
            children._mountIndex=0//增加屬性
            let childDom = createDOM(children)
            parentNode.appendChild(childDom)
        }
    }
}
  • 前面已經獲取了2個map,分別是老節點的map和新節點的map,新節點的map的元素有部分是直接從老map里拉來的已經更新過屬性的虛擬dom。所以,得到2個map後,需要做個補丁包,遞歸結束後得到總共需要打補丁的操作,再進行執行。
let updateDepth=0
let diffQueue =[]
export const MOVE='MOVE'
export const REMOVE='REMOVE'
export const INSERT='INSERT'
function updateChildrenElements(dom,oldChildrenElements,newChildrenElements){
    updateDepth++
    diff(dom,oldChildrenElements,newChildrenElements,diffQueue)
    updateDepth--
    if(updateDepth===0){
        patch(diffQueue)
        diffQueue.length=0//清空
    }
}
function patch(){
    console.log(JSON.stringify(diffQueue,null,2))
    console.log(diffQueue)
}

function diff(parentNode,oldChildrenElements,newChildrenElements,diffQueue){
    let oldChildrenElementsMap=getChildrenElementsMap(oldChildrenElements)
    let newChildrenElementsMap=getNewChildrenElementsMap(newChildrenElements,oldChildrenElementsMap)
    let lastIndex=0
    for(let i=0;i<newChildrenElements.length;i++){
        let newChildElement=newChildrenElements[i]
        if(newChildElement){
            let newKey=newChildElement.key||i.toString()
            let oldChildElement=oldChildrenElementsMap[newKey]
            if(newChildElement===oldChildElement){//說明同一個節點,因爲前面獲取newmap操作有個賦值,直接把老節點拿過來複用
                if(oldChildElement._mountIndex<lastIndex){//掛載點小於說明要移動,等於大於不動,後面會把lastindex調到最大
                    diffQueue.push({//掛載點小於的情況是在lastindex之間的元素移動到後面
                        parentNode,//也就是Lastindex代表不用動的最後一個節點,之間的都得移動或者刪除
                        type:MOVE,
                        fromIndex:oldChildElement._mountIndex,//原掛載點
                        toIndex:i
                    })
                }
                lastIndex=Math.max(oldChildElement._mountIndex,lastIndex)//老的掛載點和最後一個不要移動的之間最大值
            }else{//新老元素不相等,直接插入
                diffQueue.push({
                    parentNode,
                    type:INSERT,
                    toIndex:i,
                    dom:createDOM(newChildElement)
                })
            }
            newChildElement._mountIndex=i //更新新的掛載點
        }
    }
    for(let oldkey in oldChildrenElementsMap){//在老map裏遍歷如果新Map裏沒有,就刪除
        if(!newChildrenElementsMap.hasOwnProperty(oldkey)){
            let oldChildElement=oldChildrenElementsMap[oldkey]
            diffQueue.push({
                parentNode,
                type:REMOVE,
                fromIndex:oldChildElement._mountIndex,
            })
        }else{//key相同,type不同,也刪除
            let oldChildElement = oldChildrenElementsMap[oldkey];
            let newChildElement = newChildrenElementsMap[oldkey];
            if (oldChildElement !== newChildElement) {
                diffQueue.push({
                    parentNode,
                    type: REMOVE,
                    fromIndex: oldChildElement._mountIndex
                });
            }
        }
    }
}
  • 其中diffqueue就是個用來收集補丁包的玩意,通過前面深度判斷,當深度回到0則進行打補丁操作。
  • 而在每層的diff中,遍歷新map,在舊節點中尋找相同的虛擬dom,如果全等(因爲上一段在新map遍歷中會去找老map中對應相同key或者對應相同索引的節點進行比較type,如果相同,更新完屬性後直接複用。)那麼就代表可以複用,並且得確認位置,製作補丁包。最後進行打補丁。
  • 這個補丁包設計上掛個父節點就是到時候好用父節點來操作子節點dom。
  • 這樣console出來的補丁包就是這樣:
[
  {
    "parentNode": {
      "eventStore": {}
    },
    "type": "MOVE",
    "fromIndex": 1,
    "toIndex": 2
  },
  {
    "parentNode": {
      "eventStore": {}
    },
    "type": "INSERT",
    "toIndex": 3,
    "dom": {}
  },
  {
    "parentNode": {
      "eventStore": {}
    },
    "type": "INSERT",
    "toIndex": 4,
    "dom": {}
  },
  {
    "parentNode": {
      "eventStore": {}
    },
    "type": "REMOVE",
    "fromIndex": 3
  }
]
  • 再改一下patch代碼:
function patch(diffQueue){
    let deleteMap={}
    let deleteChildren =[]
    for(let i =0;i<diffQueue.length;i++){
        let difference =diffQueue[i]
        if(difference.type===MOVE||difference.type===REMOVE){//提取移動和刪除操作
            let fromIndex=difference.fromIndex
            let oldChildDom = difference.parentNode.children[fromIndex]
            deleteMap[fromIndex]=oldChildDom//移動操作用來複用的節點
            deleteChildren.push(oldChildDom)
        }
    }
    deleteChildren.forEach(child=>{//先刪除後插入,因爲前面已經把掛載點位置改了,所以刪除才能使掛載點匹配位置
        child.parentNode.removeChild(child)
    })
    for (let i = 0; i < diffQueue.length; i++) {
        let { type, fromIndex, toIndex, parentNode, dom } = diffQueue[i];
        switch (type) {
            case INSERT:
                insertChildAt(parentNode, dom, toIndex);
                break;
            case MOVE://移動就可以從Map中拿出剛纔刪掉的節點
                insertChildAt(parentNode, deleteMap[fromIndex], toIndex);
                break;
            default:
                break;
        }
    }
}
function insertChildAt(parentNode, newChildDOM, index) {
    let oldChild = parentNode.children[index];//先取出這個索引位置的老的DOM節點
    oldChild ? parentNode.insertBefore(newChildDOM, oldChild) : parentNode.appendChild(newChildDOM);
}
  • 移動實際上就是先刪再插,判斷當前位置有沒有節點,有節點用inserBefore,無節點用append。
  • 還有最後複用的地方需要把新的虛擬dom拿來賦給currentdom別忘了。
function compareTwoElement(oldelement,newelement){//這個函數內部要操作dom,返回虛擬dom
    let currentDom = oldelement.dom 
    let currentElement = oldelement
    if(newelement===null){//如果爲空,直接刪除
        currentDom.parentNode.removeChild(currentDom)
        currentDom= null
        currentElement=null
    }else if(oldelement.type!== newelement.type ){//新舊類型不一樣
        let newDom = createDOM(newelement)
        currentDom.parentNode.replaceChild(newDom,currentDom)
        currentElement=newelement //把當前虛擬dom換成新的
    }else{ //類型一樣,那就可以複用,所以深度比較
        updateElement(oldelement,newelement)
        currentElement=newelement
    }
    return currentElement
}
  • 這樣就實現了。感覺循環有點多,不看深度的話,每一層比較需要循環m取得老map,循環n取得新map,遍歷新map獲取移動插入補丁包,遍歷老map獲取刪除元素補丁包,遍歷補丁包獲取移動刪除元素,遍歷刪除移動和刪除元素,遍歷補丁包插入移動和插入的元素。如果老map是m,新map是n,補丁包大小不定,一般不會超過m+n,就當k,還有個需要移動或者刪除的元素,不會超過k,就當w,那麼總計就是O(2m+2n+2k+w)。如果變態點把k和w換成m+n那就是O(5(m+n))。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章