带你了解react的diff算法

258 阅读4分钟

概述

react中diff算法的出现极大地减少了了浏览器修改Dom的性能损耗,diff通过算法算出需要修改的最小节点,并把该节点进行替换,而不是连着父节点一起修改,操作数据结构是永远比操作dom来的快的,所以就可以减少很多不及要的dom操作,进而提高性能

虚拟dom

真实dom在网页上的属性有三个 1.tagName(标签名字)2.attrs(属性:classname,title等等)3.child(标签里面嵌套的孩子节点) 好了既然真实dom的属性我们掌握了,那么我们能不能用js表示出来呢?当然能,毕竟一切皆对象。我们来实现一个虚拟dom类

export class Element{
    constructor(tagName,attrs = {},child = []){ //attrs是一串键值对 child是对象数组
        this.tagName = tagName
        this.attrs = attrs
        this.child = child
    }}

接着实现一个构造函数

export function newElement(tag,attr,child){
    return new Element(tag,attr,child)
}

接着把一个dom结构通过我们的构造函数构造成dom

const VdObj1 = newElement('ul',{id: 'list'},[    newElement('li',{class: 'list',style:'color:red' }, ['clhhh']),
    newElement('li',{class: 'list',style:'color:red' }, ['js']),
    newElement('li',{class: 'list' }, ['onclick']),  
    newElement('li',{class: 'list' }, ['onclick']) 
])

好了我们来console.log(VdObj1);一下吧

控制台输出
Element {  //自己创的节点类
  tagName: 'ul', //tagName属性
  attrs: { id: 'list' },
  child: [
    Element { tagName: 'li', attrs: [Object], child: [Array] },
    Element { tagName: 'li', attrs: [Object], child: [Array] },
    Element { tagName: 'li', attrs: [Object], child: [Array] },
    Element { tagName: 'li', attrs: [Object], child: [Array] }
  ]//思考一下孩子节点中的attrs和child属性为什么打印不出来呢
}

现在我们虚拟dom创造出来了 就要开始把它挂载到真实Dom上了

挂载到真实dom上

首先 把atrrs的对象属性变成标签属性 (我们先示范转换style属性)

 const function =SetVdtoDom(node,key,value){
     switch(key)
         case 'style':
         node.style.cssText=value
 }
 

接着在类中实现render函数 进行渲染

export class Element{
    constructor(tagName,attrs = {},child = []){ //attrs是一串键值对 child是对象数组
        this.tagName = tagName
        this.attrs = attrs
        this.child = child
    }
    render(){
        let ele =document.createElement(this.tagName)//创建一个document对象
        let attrs=this.attrs
        for(let key in attrs){
            SetVdtoDom(ele,key,attrs[key]) 设置attrs属性
        }
        let childNodes=this.child
        childNodes.foreach(function(child){
            let childEle = child instanceof Element ? render 
            : documen.createTextNode(child) 
            //如果孩子节点是Element就递归,直到发现文本节点 创造文本节点 结束递归
            ele.append(childEle)//给父节点加上孩子节点
        })
        return ele
    }
}

这时候js表示的虚拟dom已经挂载在真实dom上了,尤其是递归那里看不懂的同学要多看几遍噢。 虽然js表示的Dom是虚拟的,但是我们也要把握得住!

diff 算法

const diff=(oldNode,newNode)=>{
    let difference={} //来一个对象来表示差异 key为index value为[index:index,type:type,value:{newNode}]
    getdiff(oldNode,newNode,index,difference)
    return difference
}
const REMOVE = 'remove'
const MODIFY_TEXT =  'modify_text'
const CHANGE_ATTRS = 'change_attrs'
const TAKEPLACE = 'replace'
let initIndex = 0

const getdiff=(oldNode,newNode,index,difference)=>{
    let diffResult=[]
//如果新节点不存在 
    if(!newNode){
        diffResult.push(
            {
            index,
            type: REMOVE
            }
        )
    }
    //如果是文本节点 直接替换
    else if(typeof newNode==='string'&&typeof oldNode==='string'){
        diffResult.push({
            index,
            type:MODIFY_TEXT,
            newNode
        })      
    }
    //已经是节点了判断属性相同
    else if (oldNode.tagName===newNode.tagName){        
        let storeAttrs={}
        for(let key in oldNode.attrs){
           if(oldNode.attrs[key]!=newNode.attrs[key]){
            storeAttrs[key]=newNode.attrs[key]
           }
        }   
        for(let key in newNode.attrs){
            if(!oldNode.attrs.hasOwnProperty(key))
            storeAttrs[key]=newNode.attrs[key]
            console.log(storeAttrs);
        }
        if(Object.keys(storeAttrs).length>0){
            diffResult.push({
                index,
                type:CHANGE_ATTRS,
                value:storeAttrs
            })
        }
        oldNode.child.forEach((child,index)=>{
            getdiff(oldNode,newNode,initIndex++,difference)
        })
        //如果标签都不相同则直接替换
    }else if(newNode.tagName!==oldNode.tagName){
        diffResult.push({
            index,
            type:TAKEPLACE,
            value:newNode
        })
    }
     if(!oldNode){
        diffResult.push({
            index,
            type:TAKEPLACE,
            value:newNode
        })
    }
    if(diffResult.length){
        for(let key in diffResult){
        difference[key]=diffResult
    }
   
}

diff算法就写好了 完整代码在下面

export class Element{
    constructor(tagName,attrs = {},child = []){
        this.tagName = tagName
        this.attrs = attrs
        this.child = child
    }
    render(){
        let ele = document.createElement(this.tagName)

        let attrs = this.attrs
        for(let key in attrs){
            SetVdToDom(ele,key,attrs[key])
        }
        let childNodes = this.child
        childNodes.forEach(function(child){
           let childEle  = child instanceof Element ? 
           child.render() : document.createTextNode(child)
             ele.appendChild(childEle)
        })
     return ele  
    }
}
export function newElement(tag,attr,child){
    return new Element(tag,attr,child)
}
export const SetVdToDom = function(node,key,value){
    switch(key){
        case 'style':
        node.style.cssText = value
        break
        // case 'value':
        //     let tagName = node.tagName || ''
        //     tagName = tagName.toLowerCase()
        //     if(tagName === 'input' || tagName === 'textarea'){//注意input类型的标签
        //         node.value = value
        //     }else{
        //         node.setAttribute(key,value)
        //     }
        //     break
        //     default:
        //         node.setAttribute(key,value)
        //         break
    }
}

const diff = (oldNode,newNode)=>{
    let difference = {} //用来保存两个节点之间的差异
    getDiff(oldNode,newNode,0,difference)
    return difference
}
const REMOVE = 'remove'
const MODIFY_TEXT =  'modify_text'
const CHANGE_ATTRS = 'change_attrs'
const TAKEPLACE = 'replace'
let initIndex = 0
const getDiff = (oldNode,newNode,index,difference)=>{
    let diffResult = []
    //新节点不存在的话说明节点已经被删除
    if(!newNode){
        console.log("节点已经被删除");
        diffResult.push({
            index,
            type: REMOVE
        })
        
        //如果是文本节点直接替换就行
    }else if(typeof newNode === 'string' && typeof oldNode === 'string'){
        if(oldNode !== newNode){
            diffResult.push({
                index,
                value: newNode,
                type: MODIFY_TEXT
            })
        } //如果节点类型相同则则继续比较属性是否相同
    }else if(oldNode.tagName === newNode.tagName){
        let storeAttrs = {}
        for(let  key in oldNode.attrs){ 
            if(oldNode.attrs[key] !== newNode.attrs[key]){
              
                storeAttrs[key] = newNode.attrs[key] 
          
            }
        }
    //    console.log(storeAttrs);
        for (let key in newNode.attrs){
            if(!oldNode.attrs.hasOwnProperty(key)){
                storeAttrs[key] = newNode[key]
            }
        }   
        
        //判断是否有不同
        if(Object.keys(storeAttrs).length>0){
            diffResult.push({
                index,
                value: storeAttrs,
                type: CHANGE_ATTRS
            })
        } //遍历子节点
        oldNode.child.forEach((child,index)=>{
            //深度遍历所以要保留index
             getDiff(child,newNode.child[index],++initIndex,difference)
        }) 
        //如果类型不相同,那么无需对比直接替换掉就行
        
        
    }else if(oldNode.tagName !== newNode.tagName){
        diffResult.push({
            type: TAKEPLACE,
            index,
            newNode
        })
    } //最后将结果返回
    if(!oldNode){
        diffResult.push({
            type: TAKEPLACE,
            newNode
        })
    }
    //最后将结果返回
    if(diffResult.length){
        console.log(index);
        difference[index] = diffResult
    }
    
}


const fixPlace = (node,difference)=>{
    let pacer = { index: 0 }
    pace(node,pacer,difference)
}
/*
接收一个真实DOM(需要更新节点),接收diff过后的最小差异集合
*/

const pace = (node,pacer,difference) =>{
    
    let currentDifference = difference[pacer.index]
    let childNodes = node.childNodes
    // console.log(difference)
    childNodes.forEach((child)=>{
        pacer.index ++
        pace(child,pacer,difference)
    })
    if(currentDifference){
        doFix(node,currentDifference)
    }
}

const doFix = (node,difference) =>{
     difference.forEach(item=>{
         switch (item.type){
             case 'change_attrs':
                 const attrs = item.value
                 for( let key in attrs ){
                     if(node.nodeType !== 1) 
                     return 
                     const value = attrs[key]
                     if(value){
                         SetVdToDom(node,key,value)
                         
                     }else{
                         node.removeAttribute(key)
                     }
                 }
                 break
                 case 'modify_text':
                     node.textContent = item.value
                     break
                case 'replace': 
                   let newNode = (item.newNode instanceof Element) ? item.newNode.render(item.newNode) : 
                   document.createTextNode(item.newNode)
                    node.parentNode.replaceChild(newNode,node)
                    break
                case 'remove' :
                    node.parentNode.removeChild(node)
                    break
                default: 
                    break
         }
     })
}

const VdObj1 = newElement('ul',{id: 'list'},[
    newElement('li',{class: 'list',style:'color:red' }, ['clhhh']),
    newElement('li',{class: 'list',style:'color:red' }, ['js']),
    newElement('li',{class: 'list' }, ['onclick']),  
    newElement('li',{class: 'list' }, ['onclick']) 
])
const VdObj = newElement('ul',{id: 'list'},[
    newElement('li',{class: 'list',style:'color:blue' }, ['clh']),
    newElement('li',{class: 'list' ,style:'color:grey'}, ['ts']),
    newElement('li',{class: 'list' }, ['onclick']),  
    // newElement('li',{class: 'list-1' }, ['Vue']),
    // newElement('input',{value:'id' }, ['Vue'])

])
const RealDom = VdObj1.render()
const renderDom = function(element,target){
    target.appendChild(element)
}
export default function start(){
   renderDom(RealDom,document.body)
   const diffs = diff(VdObj1,VdObj)
   console.log(diffs)
   fixPlace(RealDom,diffs)
}