分析diff算法与虚拟dom(理解现代前端框架思想)

React 和 Vue 作为目前国内主力的前端开发框架,想必大家在日常的开发当中也是非常熟悉了。不可否认的它们的存在大大地提高了我们的开发效率以及使得我们的代码可维护性得到提高,但是使用它们的 “巧妙” 的之后,对技术有着追求的你,是不是应该了解一下这些框架背后的一些思想呢?如果还没有,没关系,我们一起来!

本文全部代码小的已经上传 github🐶

虚拟 DOM

直观来说,虚拟 DOM 其实就是用数据结构表示真实的 DOM 结构。使用它的原因是,频繁的操作 DOM 会使得网站的性能下降,为了保证性能,我们需要使得 DOM 的操作尽量精简,我们可以通过操作虚拟 DOM 的方法,去比较新旧节点的差异然后精确的获取最小的,最为必要的 DOM 集合,最终挂载到真实的 DOM 上。因为操作数据结构,远比我们直接修改 DOM 节点来的快,我们真实的 DOM 操作在最好的情况下,其实只需要在最后来那么一下,不是吗

如何表示 DOM 结构

6fa56e5cb73a45869cc218753ff2a40f.png5fa8d16924394f907f2cf55788639b2d.png

这是一段列表的 DOM 结构,我们分析一下,其中需要包含的信息有

1. 标签类型 ul,li...

2. 标签属性 class,style...

3. 孩子节点 ul->li li->text ...

无论再复杂的结构,也都是类似的,那么我们在找到 DOM 结构的共性之后,我们应该怎么表示呢

1a38b628549981c5ee67bcb356f7a065.png

通过这张图我们可以发现,我们可以用对象 JS 对象轻易地就将它表示出来,几个属性也是非常好理解

  • tagName 对应真实的标签类型

  • attrs 表示节点上的所有属性

  • child 表示该节点的孩子节点

那这样我们是不是可以给这个虚拟 DOM 设定一个类 like this

7e9cd768fc474b680e63bae0d41c1186.png
function newElement(tag,attr,child){ //创建对象函数
    return new Element(tag,attr,child)
}
复制代码

测试一下

1cefe1eed94f69e2e0c48e647655655b.png

ok 没问题是不是,那现在虚拟 DOM 其实就已经被创建出来了,那么有了虚拟 DOM 之后怎么挂载到真实 DOM 上呢

生成真实 DOM 节点

首先我们会需要一个根据对象属性来设置标签属性的方法

ab839b7e8d5bbb29a23c8cb633f62b18.png

然后我们在类的内部添加创建节点的 render 方法

371c589b6fcc807c2bc00d84b72f2704.png

到这里我们就可以通过使用 render 方法创建真实的 DOM 节点了,在方法内部,我们通过调用 SetVdToDom 方法对属性进行设置,然后对子节点进行类型判断,递归到最后剩下的文本节点。

最后我们通过一个 renderDom 方法将 dom 渲染到浏览器看看

//vdmock.js 部分
const VdObj1 = newElement('ul',{id: 'list'},[
    newElement('li',{class: 'list-1',style:'color:red' }, ['lavie']),
    newElement('li',{class: 'list-2' }, ['virtual dom']),
    newElement('li',{class: 'list-3' }, ['React']),  
    newElement('li',{class: 'list-4' }, ['Vue']) 
])
const RealDom = VdObj1.render()
const renderDom = function(element,target){
    target.appendChild(element)
}
export default function start(){
   renderDom(RealDom,document.body)
}

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta >
    <script type="module" src="./vdmock.js"  ></script>
    
    <title>Document</title>
</head>
<body >
    <script type="module" >
        import start from './vdmock.js'
        start()
    </script>
</body>
</html>
复制代码

结果如下:

1395fc8e44cb7194361cafa0c78aff81.png

虚拟 DOM diff

通过上面方法,我们可以很简单的生成虚拟 DOM 并且将它渲染到浏览器上面,那么我们在用户进行操作之后,如何计算出前后虚拟 DOM 之间的差异呢?下面就来介绍一下 diff 算法

766607d9e43fe20c6c2a58c94f138533.png

我们通过给 diff 传入新旧的两个节点通过内部的 getDiff 递归对比节点并存储变化然后返回,下面我们来实现一下 getDiff

获取最小差异数组

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'){
        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]
            }
        }
        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){
        difference[index] = diffResult
    }
}


复制代码

测试结果如下:

d2edd239938e25a4ada11b961cc4cf9d.png e66caf92e341a26279ddf2549e9a0b6f.png

更新 dom

现在我们已经生成了两个虚拟 DOM, 并且将两个 DOM 之间的差异用对象的方式保存了下来,接下来,我们就要通过这些来将差异更新到真实的 DOM 上面去!!!

9fd96d118226340835128dcd6be0351d.png 6759bfa4eba52b49e03b2008f8abf0d4.png

pace 函数会自身进行递归,对当前节点的差异用 dofix 进行更新

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-1',style:'color:red' }, ['lavie']),
    newElement('li',{class: 'list-2' }, ['virtual dom']),
    newElement('li',{class: 'list-3' }, ['React']),  
    newElement('li',{class: 'list-4' }, ['Vue']) ,
])
const VdObj = newElement('ol',{id: 'list'},[
    newElement('h2',{class: 'list-1',style:'color:green' }, ['lavieee']),
    newElement('li',{class: 'list-2' }, ['virtual dom']),
    newElement('li',{class: 'list-3' }, ['React']), 
    newElement('li',{class: 'list-4' }, ['Vue']) ,
    newElement('li',{class: 'list-5' }, ['Dva']) ,
    newElement('li',{class: 'list-5' }, ['Dva']) 
 
])
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)
   fixPlace(RealDom,diffs)
}
复制代码

before

4b3f7c34e38ef797ae85e2504980ca30.png

diff after

1cca007e071a5bfa304632364e406f58.png

嘻嘻完美

通过这几个例子下来,其实虚拟 dom 的思想就已经可以实现了,我们在使用框架的过程中如果可以梳理清楚其中的核心概念,一定会走的更加踏实。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值