Vue深入-21【虚拟节点与DOM Diff算法源码实现】

本文围绕Vue展开,介绍了diff算法特征,如平级对比、深度优先等。阐述了虚拟节点的构成,详细说明了从创建虚拟节点,到转换为真实DOM、分析虚拟节点差异创建补丁包,最后给真实DOM打补丁的完整流程,总结了各步骤的操作要点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(1).了解diff算法特征、虚拟节点、创建项目

1.算法特征

只会平级对比

不会跨级对比

直接替换,不会再去操作Li标签

深度优先,不是广度优先

2.虚拟节点

构成虚拟节点的对象

落实在控制台

3.建立vdom文件

建立文件,执行命令

npm init -y 
npm i webpack@4.44.1 webpack-cli@3.3.12 webpack-dev-server@3.11.0
yarn add html-webpack-plugin@4.4.1

具体文件配置

根目录新建文件webpack.config.js

const path = require('path'),
      HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports={
    entry:'./src/js/index.js',
    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'dist')
    },
    devtool:'source-map',
    resolve:{
        modules:[path.resolve(__dirname,''),path.resolve(__dirname,'node_modules')]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:path.resolve(__dirname,'src/index.html')
        })
    ],
    // devServer:{
    //     contentBase:'./'
    // }
}      

   

package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev":"webpack-dev-server",
    "build":"webpack"
  },

src目录中建立js文件夹和index.html

index.js

const vDom = createElement('ul',{
    class:'list',
    style:'width:300px;height:300px;background-color:#f40'
},[
    createElement('li',{
        class:"item",
        'data-index':0
    },[
        createElement('p',{class:'text'},['第一个列表项'])
    ]),
    createElement('li',{
        class:"item",
        'data-index':1
    },[
        createElement('p',{class:'text'},[
            createElement('span',{class:'title'},['第二个列表项'])
        ])
    ]),
    createElement('li',{
        class:'item',
        'data-index':2
    },[
        '第三个列表项'
    ])
])

(2).构建虚拟节点、转换真实DOM、渲染DOM节点

1.构建虚拟节点

Element.js

class Element{
    constructor(type,props,children){
        this.type = type;
        this.props = props;
        this.children = children;
    }
}
export default Element;

virtualDom.js

import Element from './Element';
function createElement(type,props,children){
    return new Element(type,props,children)
}
export {
    createElement
}

index.js

import {createElement} from './virtualDom'
const vDom = createElement('ul',{
    class:'list',
    style:'width:300px;height:300px;background-color:#f40'
},[
    createElement('li',{
        class:"item",
        'data-index':0
    },[
        createElement('p',{class:'text'},['第一个列表项'])
    ]),
    createElement('li',{
        class:"item",
        'data-index':1
    },[
        createElement('p',{class:'text'},[
            createElement('span',{class:'title'},['第二个列表项'])
        ])
    ]),
    createElement('li',{
        class:'item',
        'data-index':2
    },[
        '第三个列表项'
    ])
])
console.log(vDom)

2.转换真实DOM/渲染dom

index.js

import {createElement,render,renderDom} from './virtualDom'
...
const rDom = render(vDom);
renderDom(
    rDom,
    document.getElementById('app')
)
console.log(rDom)

virtualDom.js

import Element from './Element';
function createElement(type,props,children){
    return new Element(type,props,children)
}
function setAttrs(node,prop,value){
    switch(prop){
        case 'value':
            if(node.tagName === 'INPUT' || node.tagName=== 'TEXTAREA'){
                node.value = value;
            }else{
                node.setAttribute(prop,value)
            }
        break;
        case 'style':
            node.style.cssText = value;
        break;    
        default:
            node.setAttribute(prop,value);
            break;  
    }
}
function render(vDom){
    const {type,props,children} = vDom,
         el = document.createElement(type);
    for(let key in props){
        setAttrs(el,key,props[key]);
    }     
    children.map((c)=>{
        c = c instanceof Element
            ? render(c)
            : document.createTextNode(c)
        el.appendChild(c)    
    })
    return el
}
function renderDom(el,rootEl){
    rootEl.appendChild(el)
}
export {
    createElement,
    render,
    renderDom,
    setAttrs
}

(3).虚拟节点差异分析、创建补丁包

1.虚拟节点差异分析

index.js

import {createElement,render,renderDom} from './virtualDom';
import domDiff from './domDiff';
import doPatch from './doPatch';

const vDom1 = createElement('ul', { 
    class: 'list', 
    style: 'width: 300px; height: 300px; background-color: orange'
  }, [
    createElement('li', { 
      class: 'item', 
      'data-index': 0 
    }, [
      createElement('p', { 
        class: 'text'
      }, [
        '第1个列表项'
      ])
    ]),
    createElement('li', { 
      class: 'item', 
      'data-index': 1
    }, [
      createElement('p', { 
        class: 'text'
      }, [
        createElement('span', { 
          class: 'title' 
        }, [])
      ])
    ]),
    createElement('li', { 
      class: 'item', 
      'data-index': 2
    }, [
      '第3个列表项'
    ])
  ]);
  
const vDom2 = createElement('ul', {
  class: 'list-wrap',
  style: 'width: 300px; height: 300px; background-color: orange'
}, [
  createElement('li', {
    class: 'item',
    'data-index': 0
  }, [
    createElement('p', {
      class: 'title'
    }, [
      '特殊列表项'
    ])
  ]),
  createElement('li', {
    class: 'item',
    'data-index': 1
  }, [
    createElement('p', {
      class: 'text'
    }, [])
  ]),
  createElement('div', {
    class: 'item',
    'data-index': 2
  }, [
    '第3个列表项'
  ])
]);

const rDom = render(vDom1);
renderDom(
    rDom,
    document.getElementById('app')
)
const patches = domDiff(vDom1,vDom2);
// doPatch(rDom,patches);
console.log(patches);

具体差异

2.创建补丁包

补丁的形式

const patches = {
    0:[
        {
            type:'ATTR',
            attr:{}
        }
    ],
    2:[
        {
            type:'ATTR',
            attr:{}
        }
    ],
    3:[
        {
            type:'TEXT',
            text:'特殊列表项'
        }
    ],
    6:[
        {
            type:'REMOVE',
            index:6
        }
    ],
    7:[
        {
            type:'REPLACE',
            newNode:newNode
        }
    ]

}

还有新增牵扯到索引增加,以及替换时直接替换不做演示了

index.js

import domDiff from './domDiff'
...
const patches = domDiff(vDom1,vDom2);
console.log(patches);

patchType.js

export const ATTR = 'ATTR';
export const TEXT = 'TEXT';
export const REPLACE = 'REPLACE';
export const REMOVE = 'REMOVE';

domDiff.js

import {ATTR,TEXT,REPLACE,REMOVE} from './patchType';
let patches = {},
    vnIndex = 0;
function domDiff(oldVDom,newVDom){
    let index = 0;
    vNodeWalk(oldVDom,newVDom,index);
    return patches;
}    
function vNodeWalk(oldNode,newNode,index){
    let vnPatch = [];
    if(!newNode){
        vnPatch.push({
            type:REMOVE,
            index
        })
    }else if(typeof oldNode === 'string' && typeof newNode === 'string'){
        if(oldNode !== newNode){
            vnPatch.push({
                type:TEXT,
                text:newNode
            })
        }
    }else if(oldNode.type === newNode.type){
        const attrPatch = attrsWalk(oldNode.props,newNode.props);
        if(Object.keys(attrPatch).length>0){
            vnPatch.push({
                type:ATTR,
                attrs:attrPatch
            })
        }
        childrenWalk(oldNode.children,newNode.children)
    }else{
        vnPatch.push({
            type:REPLACE,
            newNode:newNode
        })
    }
    if(vnPatch.length > 0){
        patches[index] = vnPatch
    }
}
function attrsWalk(oldAttrs,newAttrs){
    let attrPatch = {};
    for(let key in oldAttrs){
        if(oldAttrs[key] !== newAttrs[key]){
            attrPatch[key] = newAttrs[key]
        }
    }
    for(let key in newAttrs){
        if(!oldAttrs.hasOwnProperty(key)){
            attrPatch[key] = newAttrs[key];
        }
    }
    return attrPatch;
}
function childrenWalk(oldChildren,newChildren){
    oldChildren.map((c,idx)=>{
       
        vNodeWalk(c,newChildren[idx],++vnIndex)
    })
}
export default domDiff

(4).给真实DOM打补丁

index.js

import doPatch from './doPatch';
...
const rDom = render(vDom1);
renderDom(
    rDom,
    document.getElementById('app')
)
const patches = domDiff(vDom1,vDom2);
doPatch(rDom,patches);
console.log(patches);

doPatch.js

import {ATTR,TEXT,REPLACE,REMOVE} from './patchType';
import {setAttrs,render} from './virtualDom'
import Element from './Element';
let finalPatches = {},
    rnIndex = 0;
function doPatch(rDom,patches){
    finalPatches = patches;
    rNodeWalk(rDom);
}    
function rNodeWalk(rNode){
    const rnPatch = finalPatches[rnIndex++],
    childNodes = rNode.childNodes;
    [...childNodes].map((c)=>{
        rNodeWalk(c);
    })
    if(rnPatch){
        patchAction(rNode,rnPatch);
    }
}
function patchAction(rNode,rnPatch){
    rnPatch.map((p)=>{
        switch(p.type){
            case ATTR:
                for(let key in p.attrs){
                    const value = p.attrs[key];
                    if(value){
                        setAttrs(rNode,key,value)
                    }else{
                        rNode.removeAttribute(key)
                    }
                }
            break;
            case TEXT:
                rNode.textContent = p.text;
            break;
            case REPLACE:
                const newNode = (p.newNode instanceof Element) 
                              ? render(p.newNode)
                              :document.createTextNode(p.newNode)
                rNode.parentNode.replaceChild(newNode,rNode);              
            break;
            case REMOVE:
                rNode.parentNode.removeChild(rNode)
            break;
            default:
            break;                    
        }
    })
}
export default doPatch;

(5).流程总结

概念总结

  首先要创建好虚拟节点,再通过虚拟节点转化为真实的dom,

接下来判断两个虚拟dom的不同从而创建补丁包,最后将补丁包打入即可

流程总结

1.(dom元素信息对象传入)

  首先通过函数创建的形式将dom元素相关信息以对象和数组的形式传入,

如果有子节点则再调用创建的函数放置到第三个参数

2.(构成虚拟节点)

  通过类的实例化将传入的dom元素信息对象化,此时就构成了虚拟节点

3.(转换为真实dom)

  接下来通过函数判断传入的第二个参数的类型是style、class、value,或者是data-

等等。

  如果是value则要判断标签是否为input或textarea,如果是则通过.value设置,不是则通过setAttribute进行设置,如果是style则通过.style.cssText进行设置,其他剩下的也都已setAttrbute进行设置

  处理完标签参数后 循环第三个参数children,判断是否为类构建,如果是则进行递归处理,不是则证明只是普通文本,通过createTextNode进行创建即可

此时只需要将处理好的dom节点挂载在指定dom节点上即可完成虚拟dom转换真实dom然后渲染节点

4.(虚拟节点差异分析创建补丁包)

  差异比较时会深度比较而不是广度比较,有标签的删除 替换 修改 新增、以及标签参数变更的操作,其中特殊的两个就是

替换不需要删除而是直接替换发生索引移步,新增会出现索引增加等情况。在比较差异的时候就会创建对象形式的补丁包,其中记录了操作类型,具体操作值与索引,这就是diff算法,具体操作流程如下

 首先将新的虚拟dom和旧的虚拟dom传入,

如果不存在newdom则创建删除补丁,

如果new/old dom类型都为字符串且文本不相同则创建文本变更补丁,

如果new/olddom的标签相同且通过循环判断出标签中的参数有增加或修改则创建参数变更的补丁

其余则都为替换补丁,新增补丁过于复杂就不说了

其中在发生标签相同的情况下还要将子节点传入这个函数递归进行处理

完成后就创建好了差异补丁包

5.(给创建好的真实dom打补丁)

将真实dom和补丁包传入函数

循环补丁包,判断其中的补丁类型,

补丁类型如果是标签变更,有变更值则通过函数重新给dom节点赋值参数内容,如果没有则删除

补丁类型如果是文本变更,则通过dom元素.textContent直接替换

补丁类型如果是替换补丁,则判断是否为element构建,如果是则创建dom替换,如果不是则为普通文本,通过createTextNode创建

补丁类型如果是删除补丁,则直接通过removeChild进行移除

同时以上操作全部递归处理所有子节点 最终完成补丁打入

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值