vlambda博客
学习文章列表

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

  • 本文已获得原作者的独家授权,有想转载的朋友们可以在后台联系我申请开白哦!
  • PS:欢迎掘友们向我投稿哦,被采用的文章还可以送你掘金精美周边!

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

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

虚拟 DOM

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

如何表示 DOM 结构

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

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

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

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

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

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

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

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

  • tagName 对应真实的标签类型
  • attrs 表示节点上的所有属性
  • child 表示该节点的孩子节点

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

分析diff算法与虚拟dom(理解现代前端框架)
function newElement(tag,attr,child){ //创建对象函数
    return new Element(tag,attr,child)
}
复制代码

测试一下

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

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

生成真实 DOM 节点

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

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

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

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

到这里我们就可以通过使用 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>
复制代码

结果如下:

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

虚拟 DOM diff

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

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

我们通过给 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
    }
}


复制代码

测试结果如下:

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

更新 dom

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

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

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

const doFix = (node,difference) =>{
     difference.forEach(item=>{
         switch (item.type){
             case 'change_attrs':
                 const attrs = item.value
                 forlet 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

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

diff after

嘻嘻完美

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