【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))。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

业火之理

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值