千家信息网

Vue的diff算法原理是什么

发表于:2026-11-03 作者:千家信息网编辑
千家信息网最后更新 2026年11月03日,这篇文章将为大家详细讲解有关Vue的diff算法原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。思维导图0. 从常见问题引入虚拟dom是什么?如何创建虚拟d
千家信息网最后更新 2026年11月03日Vue的diff算法原理是什么

这篇文章将为大家详细讲解有关Vue的diff算法原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。

思维导图

0. 从常见问题引入

  • 虚拟dom是什么?

  • 如何创建虚拟dom?

  • 虚拟dom如何渲染成真是dom?

  • 虚拟dom如何patch(patch)

  • 虚拟DOM的优势?(性能)

  • Vue中的key到底有什么用,为什么不能用index?

  • Vue中的diff算法实现

  • diff算法是深度还是广度优先遍历

1. 生成虚拟dom

1. h方法实现

virtual dom ,也就是虚拟节点

1.它通过js的Object对象模拟dom中的节点

2.再通过特定的render方法将其渲染成真实的dom节点

eg:

hello world

如果利用h方法生成虚拟dom的话:

h('div', { id: 'wrapper', class: '1' }, h('span', { style: { color: 'red' } }, 'hello'), 'world');

对应的js对象如下:

let vd = {    type: 'div',    props: { id: 'wrapper', class: '1' },    children: [        {            type: 'span',            props: { color: 'red' },            children: [{}]        },        {            type: '',            props: '',            text: 'world'        }    ]}

自己实现一个h方法

 function createElement(type, props = {}, ...children) {    // 防止没有传值的话就赋值一个初始值    let key;    if (props.key) {        key = props.key        delete props.key    }    // 如果孩子节点有字符串类型的,也需要转化为虚拟节点    children = children.map(child => {        if (typeof child === 'string') {            // 把不是节点类型的子节点包装为虚拟节点            return vNode(undefined, undefined, undefined, undefined, child)        } else {            return child        }    })    return vNode(type, props, key, children)}function vNode(type, props, key, children, text = undefined) {    return {        type,        props,        key,        children,        text    }}

2. render方法实现

render的作用:把虚拟dom转化为真实dom渲染到container容器中去

export function render(vnode, container) {    let ele = createDomElementFrom(vnode) //通过这个方法转换真实节点    if (ele) container.appendChild(ele)}

把虚拟dom转化为真实dom,插入到容器中,如果虚拟dom对象包含type值,说明为元素(createElement),否则为节点类型(createTextnode),并把真实节点赋值给虚拟节点,建立起两者之间的关系

function createDomElementFrom(vnode) {    let { type, key, props, children, text } = vnode    if (type) {//说明是一个标签        // 1. 给虚拟元素加上一个domElemnt属性,建立真实和虚拟dom的联系,后面可以用来跟新真实dom        vnode.domElement = document.createElement(type)        // 2. 根据当前虚拟节点的属性,去跟新真实dom的值        updateProperties(vnode)        // 3. children中方的也是一个个的虚拟节点(就是递归把儿子追加到当前元素里)        children.forEach(childVnode => render(childVnode, vnode.domElement))    } else {//说明是一个文本    }    return vnode.domElement}function updateProperties(newVnode, oldProps = {}) {    let domElement = newVnode.domElement //真实dom,    let newProps = newVnode.props; //当前虚拟节点中的属性    // 如果老的里面有,新的里面没有,说明这个属性被移出了    for (let oldPropName in oldProps) {        if (!newProps[oldPropName]) {            delete domElement[oldPropName] //新的没有,为了复用这个dom,直接删除        }    }    // 如果新的里面有style,老的里面也有style,style可能还不一样    let newStyleObj = newProps.style || {}    let oldStyleObj = oldProps.style || {}    for (let propName in oldStyleObj) {        if (!newStyleObj[propName]) {            domElement.style[propName] = ''        }    }    // 老的里面没有,新的里面有    for (let newPropsName in newProps) {        // 直接用新节点的属性覆盖老节点的属性        if (newPropsName === 'style') {            let styleObj = newProps.style;            for (let s in styleObj) {                domElement.style[s] = styleObj[s]            }        } else {            domElement[newPropsName] = newProps[newPropsName]        }    }}

根据当前虚拟节点的属性,去更新真实dom的值
由于还有子节点,所以还需要递归,生成子节点虚拟dom的真实节点,插入当前的真实节点里去

3. 再次渲染

刚刚可能会有点不解,为什么要把新的节点和老的节点属性进行比对,因为刚刚是首次渲染,现在讲一下二次渲染

比如说现在构建了一个新节点newNode,我们需要和老节点进行对比,然而并不是简单的替换,而是需要尽可能多地进行复用

首先判断父亲节点的类型,如果不一样就直接替换

如果一样

1.文本类型,直接替换文本值即可

2.元素类型,需要根据属性来替换

这就证明了render方法里我们的oldProps的必要性,所以这里把新节点的真实dom赋值为旧节点的真实dom,先复用一波,待会再慢慢修改

updateProperties(newVnode, oldVNode.props)

export function patch(oldVNode, newVnode) {    // //判断类型是否一样,不一样直接用新虚拟节点替换老的    if (oldVNode.type !== newVnode.type) {        return oldVNode.domElement[xss_clean].replaceChild(            createDomElementFrom(newVnode), oldVNode.domElement        )    }    // 类型相同,且是文本    if (oldVNode.text) {        return oldVNode.document.textContent = newVnode.text    }    // 类型一样,不是文本,是标签,需要根据新节点的属性更新老节点的属性    // 1. 复用老节点的真实dom    let domElement = newVnode.domElement = oldVNode.domElement    // 2. 根据最新的虚拟节点来更新属性    updateProperties(newVnode, oldVNode.props)    // 比较儿子    let oldChildren = oldVNode.children    let newChildren = newVnode.children    // 1. 老的有儿子,新的有儿子    if (oldChildren.length > 0 && newChildren.length > 0) {        // 对比两个儿子(很复杂)    } else if (oldChildren.length > 0) {        // 2. 老的有儿子,新的没儿子        domElement[xss_clean] = ''    } else if (newChildren.length > 0) {        // 3. 新增了儿子        for (let i = 0; i < newChildren.length; i++) {            // 把每个儿子加入元素里            let ele = createDomElementFrom(newChildren[i])            domElement.appendChild(ele)        }    }}

2. diff算法

刚刚的渲染方法里,首先是对最外层元素进行对比,对于儿子节点,分为三种情况

1.老的有儿子,新的没儿子(那么直接把真实节点的innerHTML设置为空即可)

2.老的没儿子,新的有儿子(那么遍历新的虚拟节点的儿子列表,把每一个都利用createElementFrom方法转化为真实dom,append到最外层真实dom即可)

3.老的有儿子,新的有儿子,这个情况非常复杂,也就是我们要提及的diff算法

1. 对常见的dom做优化

  • 前后追加元素

  • 正序和倒序元素

  • 中间插入元素

以最常见的ul列表为例子

旧的虚拟dom

let oldNode = h('div', {},    h('li', { style: { background: 'red' }, key: 'A' }, 'A'),    h('li', { style: { background: 'blue' }, key: 'B' }, 'A'),    h('li', { style: { background: 'yellow' }, key: 'C' }, 'C'),    h('li', { style: { background: 'green' }, key: 'D' }, 'D'),);
情况1:末尾追加一个元素(头和头相同)

新的虚拟节点

let newVnode = h('div', {},    h('li', { style: { background: 'red' }, key: 'A' }, 'A'),    h('li', { style: { background: 'blue' }, key: 'B' }, 'B'),    h('li', { style: { background: 'yellow' }, key: 'C' }, 'C1'),    h('li', { style: { background: 'green' }, key: 'D' }, 'D1'),    h('li', { style: { background: 'black' }, key: 'D' }, 'E'),);

eg:

// 比较是否同一个节点function isSameVnode(oldVnode, newVnode) {    return oldVnode.key == newVnode.key && oldVnode.type == newVnode.type}// difffunction updateChildren(parent, oldChildren, newChildren) {    // 1. 创建旧节点开头指针和结尾    let oldStartIndex = 0    let oldStartVnode = oldChildren[oldStartIndex];    let oldEndIndex = oldChildren.length - 1    let oldEndVnode = oldChildren[oldEndIndex];    // 2. 创建新节点的指针    let newStartIndex = 0    let newStartVnode = newChildren[newStartIndex];    let newEndIndex = newChildren.length - 1    let newEndVnode = newChildren[newEndIndex];    // 1. 当从后面插入节点的时候,希望判断老的孩子和新的孩子 循环的时候,谁先结束就停止循环    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {        // 注意:比较对象是否相等,你不能用==,因为指向的位置可能不一样,可以用type和key        if (isSameVnode(oldStartVnode, newStartVnode)) {            //patch比对更新            patch(oldStartVnode, newStartVnode)            // 移动指针            oldStartVnode = oldChildren[++oldStartIndex]            newStartVnode = newChildren[++newStartIndex]        }    }    if (newStartIndex <= newEndIndex) {        for (let i = newStartIndex; i <= newEndIndex; i++) {            parent.appendChild(createDomElementFrom(newChildren[i]))        }    }}

情况2:队首添加一个节点(尾和尾)

头和头+尾和尾的处理方法:

我们通过parent.insertBefore(createDomElementFrom(newChildren[i]), beforeElement)使得末尾添加和头部添加采用同一种处理方法

    // 如果是从前往后遍历说明末尾新增了节点,会比原来的儿子后面新增了几个    // 也可以时从后往前遍历,说明比原来的儿子前面新增了几个    if (newStartIndex <= newEndIndex) {        for (let i = newStartIndex; i <= newEndIndex; i++) {            // 取得第一个值,null代表末尾            let beforeElement = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].domElement   parent.insertBefore(createDomElementFrom(newChildren[i]), beforeElement)        }    }

图解:

MVVM=>数据一变,就调用patch

情况3:翻转类型(头和尾)

尾和头就不画图了

else if (isSameVnode(oldStartVnode, newEndVnode)) {                // 头和尾巴都不一样,拿老的头和新的尾巴比较                patch(oldStartVnode, newEndVnode)                // 把旧节点的头部插入到旧节点末尾指针指向的节点之后一个                parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSibling)                // 移动指针                oldStartVnode = oldChildren[++oldStartIndex]                newEndVnode = newChildren[--newEndIndex]            } else if (isSameVnode(oldEndVnode, newStartVnode)) {                // 头和尾巴都不一样,拿老的头和新的尾巴比较                patch(oldEndVnode, newStartVnode)                // 把旧节点的头部插入到旧节点末尾指针指向的节点之后一个                parent.insertBefore(oldEndVnode.domElement, oldStartVnode.domElement)                // 移动指针                oldEndVnode = oldChildren[--oldEndIndex]                newStartVnode = newChildren[++newStartIndex]            } else {
情况4: 暴力比对复用

else {                // 都不一样,就暴力比对                // 需要先拿到新的节点去老的节点查找是否存在相同的key,存在则复用,不存在就创建插入即可                // 1. 先把老的哈希                let index = map[newStartVnode.key]//看看新节点的key在不在这个map里                console.log(index);                if (index == null) {//没有相同的key                    // 直接创建一个,插入到老的前面即可                    parent.insertBefore(createDomElementFrom(newStartVnode),                        oldStartVnode.domElement)                } else {//有,可以复用                    let toMoveNode = oldChildren[index]                    patch(toMoveNode, newStartVnode)//复用要先patch一下                    parent.insertBefore(toMoveNode.domElement, oldStartVnode.domElement)                    oldChildren[index] = undefined                    // 移动指正                }                newStartVnode = newChildren[++newStartIndex]            }// 写一个方法,做成一个哈希表{a:0,b:1,c:2}function createMapToIndex(oldChildren) {    let map = {}    for (let i = 0; i < oldChildren.length; i++) {        let current = oldChildren[i]        if (current.key) {            map[current.key] = i        }    }    return map}

对于key的探讨

1. 为什么不能没有key

2. 为什么key不能是index

3. diff的遍历方式

采用的是深度优先,只会涉及到dom树同层的比较,先对比父节点是否相同,然后对比儿子节点是否相同,相同的话对比孙子节点是否相同

关于"Vue的diff算法原理是什么"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。

节点 儿子 属性 方法 元素 类型 相同 复用 指针 算法 情况 末尾 文本 对象 尾巴 更新 移动 头部 孩子 常见 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 小米移动互联网科技园 中关村互联网科技集团 浩特互联网科技 唐信互联网科技 大连 长亮科技互联网金融 左右互联网络科技 汉立互联网科技有限公司联系电话 湖南洣水互联网科技有限公司扣款 昭通互联网科技选哪家 互联网科技革命什么开始 坤平互联网科技 互联网科技扶贫时间 工业互联网科技成果转化 半导体行业属于互联网高科技类吗 宁波跨境商互联网科技有限公司 互联网科技平台产品运营 互联网科技的前景 宁波允成互联网科技有限公司 山东亿川互联网科技有限责任公司 如何强化互联网金融科技 科技与互联网发展 深圳齐家互联网科技 互联网金融科技委员会 四个字互联网科技公司取名 成都玖崟互联网科技有限公司乐商圈怎么样 济南华为互联网科技产业总部基地 华服互联网科技官网 高科技互联网套路 元气玛特互联网科技公司是外包吗 中国主要互联网科技公司
0