react虚拟dom实现和diff算法实现

235 阅读3分钟

react中虚拟算法有两种方式

1,jsx语法

var virtualDom1=<div>这是jsx语法实现的虚拟dom</div>

2,React.createElement()

var virtualDom2=React.createElement('ul',{className:"ul-list"},[
    React.createElement('li',{},["a"]),
    React.createElement('li',{},["b"]),
React.createElement('li',{},["c"])]
);
ReactDOM.render(virtualDom2,root)

react内部会自动将jsx转化为createElement方式的虚拟dom

实现react的虚拟dom

下面是用原生js模拟react中的createElement以及render实现虚拟dom。

myCreateElemnt是模拟react中的createElement:

var virtualDom=myCreateElemnt('ul',{className:"ul-list"},[
    myCreateElemnt('li',{},["a"]),
    myCreateElemnt('li',{},["b"]),
myCreateElemnt('li',{},["c"])]
);

让我们看看myCreateElemnt()的实现吧:

把每一个节点封装为一个Element对象:

function  Element(type,props,children) {
    this.type=type;
    this.props=props;
    this.children=children;
}
//创建虚拟dom的实现
function  myCreateElemnt(type,props,children){
        return  new Element(type,props,children);
}

这样一个简单的虚拟dom就实现了,是不是很简单,其实虚拟dom就是js对象。

ReactDOM.render()方法会将虚拟dom渲染为真实dom,然后会将真实的dom添加到页面上进行展示,这里可以分为两步走:

第一步:将真实dom转换为真实dom

//虚拟dom渲染为真实dom
var actualDom=createActualDom(virtualDom);
//创建真实dom的实现
function createActualDom(virtualDom){
    var actualDom=document.createElement(virtualDom.type);
        for(var prop in virtualDom.props){
            setAttr(actualDom,prop,virtualDom.props[prop]);
        }
        virtualDom.children.forEach(child=>{
        if(child instanceof Element){
            var el=createActualDom(child);
            actualDom.appendChild(el);
        }else{
            var el=document.createTextNode(child);
            actualDom.appendChild(el);
        }
    })
    return actualDom;
}

第二步: 将真实dom节点渲染到页面上:

var targetDom=document.getElementById("root");
//将真实dom渲染到页面上
renderDom(actualDom,targetDom);
function renderDom(actualDom,targetDom){
    targetDom.appendChild(actualDom);
}

好了,这样从虚拟dom到页面真实dom的实现,我们已经实现了~~

diff算法实现

diff算法,主要是比较两个虚拟dom的差异,然后将差异更新到真实dom上面。

diff函数传入两个虚拟dom.然后返回比较的差异对象。

var patches=diff(virtualDom,virtualDom2);
function diff(virtualDom,virtualDom2){
    var patches={};
    //比较
    walk(virtualDom,virtualDom2,index,patches);
   return patches;
}

walk是比较两个虚拟dom,然后层层递归实现层层之间的dom比较。

//只是两个节点的属性比较而已
function  walk(virtualDom,virtualDom2,index,pathches){
    var currentPatch=[];
    var attrs;
    if(!virtualDom2){
        currentPatch.push({type:'REMOVE',index})
        }else if(Object.prototype.toString.call(virtualDom)==='[object String]' && Object.prototype.toString.call(virtualDom2)==='[object String]'){
            currentPatch.push({type:'TEXT',text:virtualDom2})
        }else if(virtualDom.type===virtualDom2.type){
            attrs=diffAttr(virtualDom.props,virtualDom2.props);
            if(Object.keys(attrs).length>0){
                currentPatch.push({type:'ATTR',attrs});
            //比较子节点
        }
        diffChildren(virtualDom.children,virtualDom2.children,pathches);
    }else{
        //节点类型不同,直接替换
        currentPatch.push({type:'REPLACE',newNode:virtualDom2});
    }
    if(currentPatch.length>0){
        pathches[index]=currentPatch;
    }
    return pathches;
}

//比较节点的children
function diffChildren(oldChildren,newChildren,pathches){
    oldChildren.forEach((child,inde)=>{
        walk(oldChildren[inde],newChildren[inde],++index,pathches);
    })
}

//两个节点的属性比较
function diffAttr(oldProps,newProps,index,patches){
    var attrsPatch={};
    //遍历老节点的属性进行修改
    for(let key in oldProps){
        if(oldProps[key]!==newProps[key]){
            attrsPatch[key]=newProps[key];
        }
    }

    //对新增节点但是没有老节点的属性进行修改
    for(let key in newProps){
        if(!oldProps.hasOwnProperty(key)){
            attrsPatch[key]=newProps[key];
        }
    }
    return attrsPatch;
}

现在,我们已经获取到两个虚拟dom时间不同的地方,接下来,我们需要将这些变化也就是patches对象更新到真实dom上。

//将补丁渲染到真实dom上面
doPatch(actualDom);


function doPatch(node){
    var currentNodePatches=patches[index2++];
    var children=node.childNodes;
    children.forEach((child=>{
                    doPatch(child);
        }))
    if(currentNodePatches){
        currentNodePatches.forEach(patch=>{
           switch (patch.type) {
               case 'REPLACE':
                   var newNodeActualDom= patch.newNode instanceof Element?createActualDom(patchnewNode):document.createTextNode(patch.newNode);
                   node.parentNode.replaceChild(newNodeActualDom,node);
                   break;
               case 'REMOVE':
                   node.parentNode.removeChild(node);
                   break;
               case 'TEXT':
                   node.textContent=patch.text;
                   break;
               case 'ATTR':
                       for(let key in  patch.attrs){
                           var value=patch.attrs[key];
                           if(value){
                               setAttr(node,key,value)
                           }else{
                               node.removeAttribute(key)
                           }
                       }
                   break;
               default:
                   break;


           }

        })
    }
}

这样虚拟dom的diff算法就完成了,虽然代码很简短,但是关键是逻辑要能够理解清楚,建议初学者跟着代码多敲几遍哈哈·~~~~