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

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

虚拟DOM

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

如何表示DOM结构

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

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

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

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

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

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

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

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

export class Element {
	constructor(tagName, attrs = {}, child = []) {
		this.tagName = tagName;
		this.attrs = attrs;
		this.child = child;
	}
}
function newElement(tag,attr,child){ //创建对象函数
    return new Element(tag,attr,child)
}

测试一下

export class Element {
	constructor(tagName, attrs = {}, child = []) {
		this.tagName = tagName;
		this.attrs = attrs;
		this.child = child;
	}
}
function newElement (tag,attr,child) {
	return new Element(tag,attr,child)
}

const vdObj1 = newElement('ul', { id: 'list'}, [
	new Element('li', { class: 'list-1', style: 'color: red;' }, ['me']),
	new Element('li', { class: 'list-2' }, ['virtual dom']),
	new Element('li', { class: 'list-3' }, ['react']),
	new Element('li', { class: 'list-4' }, ['vue']),
]);
console.log(vdObj1);

输出:

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

生成真实DOM节点

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


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') {
				node.value  = value;
			} else {
				node.setAttribute(key, value);
			}
			break;
		default:
			node.setAttribute(key, value);
			break;
	}
}

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

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 = thsi.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;
	}
}

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

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

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

//vdmock.js 部分

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') {
				node.value  = value;
			} else {
				node.setAttribute(key, value);
			}
			break;
		default:
			node.setAttribute(key, value);
			break;
	}
}

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;
	}
}

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

const vdObj1 = newElement('ul', { id: 'list'}, [
	new Element('li', { class: 'list-1', style: 'color: red;' }, ['me']),
	new Element('li', { class: 'list-2' }, ['virtual dom']),
	new Element('li', { class: 'list-3' }, ['react']),
	new Element('li', { class: 'list-4' }, ['vue']),
]);
console.log(vdObj1);
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 name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="module" src="./vdmock.js"  ></script>
    
    <title>Document</title>
</head>
<body >
    <script type="module" >
        import start from './vdmock.js'
        start()
    </script>
</body>
</html>

结果如下:

虚拟DOM diff

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

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

测试结果如下:

更新dom

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

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' }, ['me']),
    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)
}

完美

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

vdmock.js


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){
        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
    }
}


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' }, ['me']),
    newElement('li',{class: 'list' }, ['virtual dom']),
    newElement('li',{class: 'list' }, ['React']),  
    newElement('li',{class: 'list' }, ['Vue']) 
])
const VdObj = newElement('ul',{id: 'list'},[
    newElement('li',{class: 'list',style:'color:red' }, ['mee']),
    newElement('li',{class: 'list' }, ['virtual dom']),
    newElement('li',{class: 'list' }, ['React']),  
    newElement('li',{class: 'list-1' }, ['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)
}

添加好友备注【进阶学习】拉你进技术交流群

  • 31
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值