返回顶部
首页 > 资讯 > 前端开发 > JavaScript >Vue的diff算法原理你真的了解吗
  • 642
分享到

Vue的diff算法原理你真的了解吗

2024-04-02 19:04:59 642人浏览 安东尼
摘要

目录思维导图0. 从常见问题引入1. 生成虚拟dom1. h方法实现2. render方法实现3. 再次渲染2. diff算法1. 对常见的dom做优化情况1:末尾追加一个元素(头和

思维导图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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:

<div id="wrapper" class="1">
    <span style="color:red">hello</span>
    world
</div> 

如果利用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.parentNode.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.innerhtml = ''
    } 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
}
// diff
function 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树同层的比较,先对比父节点是否相同,然后对比儿子节点是否相同,相同的话对比孙子节点是否相同

在这里插入图片描述

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程网的更多内容!    

--结束END--

本文标题: Vue的diff算法原理你真的了解吗

本文链接: https://lsjlt.com/news/142944.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Vue的diff算法原理你真的了解吗
    目录思维导图0. 从常见问题引入1. 生成虚拟dom1. h方法实现2. render方法实现3. 再次渲染2. diff算法1. 对常见的dom做优化情况1:末尾追加一个元素(头和...
    99+
    2024-04-02
  • Vue的虚拟DOM和diff算法你了解吗
    目录什么是虚拟DOM?为什么需要虚拟DOM?总结在vue 中 数据改变 -> 虚拟DOM(计算变更)-> 操作DOM -> 视图更新 虚拟DOM: js执行速度比较...
    99+
    2024-04-02
  • React中的Diff算法你了解吗
    目录一、Diff算法的作用二、React的Diff算法  1、什么是调和?2、什么是React diff算法?3、diff策略4、tree diff:5、comp...
    99+
    2024-04-02
  • Vue的diff算法原理是什么
    这篇文章将为大家详细讲解有关Vue的diff算法原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。思维导图0. 从常见问题引入虚拟dom是什么如何创建虚拟dom虚拟dom如何渲染成真是dom虚拟do...
    99+
    2023-06-29
  • Vue的过滤器你真了解吗
    目录1.过滤器1.1对过滤器的理解1.2全局过滤器:1.3局部过滤器:1.4过滤器的案例总结1. 过滤器 案例中使用到时间格式相关API 1.1 对过滤器的理解 定义:对要显示的数据...
    99+
    2024-04-02
  • C++的运算符你真的了解吗
    目录前言1 算术运算符2 赋值运算符3 比较运算符4 逻辑运算符总结前言 运算符的作用:用于执行代码的运算 主要有: 1 算术运算符 用于处理四则运算 对于前置递增:将递增运算前...
    99+
    2024-04-02
  • 你真的了解 Java 分布式编程算法吗?
    Java分布式编程算法是一种处理分布式计算的技术,它可以通过不同的节点分布计算任务,将计算结果整合在一起。在本文中,我们将深入了解Java分布式编程算法,并提供一些示例代码来帮助您更好地理解。 Java分布式编程算法的基础概念 Java分布...
    99+
    2023-06-20
    教程 分布式 编程算法
  • Vue2 的 diff 算法规则原理详解
    目录前言算法规则diff 优化策略老数组的开始与新数组的开始老数组的结尾与新数组的结尾老数组的开始与新数组的结尾老数组的结尾与新数组的开始以上四种情况都没对比成功推荐在渲染列表时为节...
    99+
    2024-04-02
  • C++重载运算符你真的了解吗
    目录1.重载运算符的必要性2.重载运算符的形式与规则3.重载运算符的运算4.转义运算符总结运算符实际上是一个函数,所以运算符的重载实际上是函数的重载,。编译程序对运算符的重载的选择,...
    99+
    2024-04-02
  • 你真的掌握了Java的自然语言处理算法吗?
    Java自然语言处理(NLP)算法在现代计算机领域中发挥着越来越重要的作用。随着人们对语言处理技术的需求越来越高,Java NLP算法的应用也变得越来越普遍。然而,想要真正掌握Java自然语言处理算法并不是一件容易的事情。在本文中,我们将深...
    99+
    2023-09-04
    自然语言处理 编程算法 开发技术
  • C++中的函数你真的理解了吗
    目录1 概述2 函数的定义及调用3 值传递4 函数的常见形式5 函数的声明6 函数的分文件编写作用:让代码结构更加清晰1.2.3.4.总结1 概述 作用:将一段经常使用的代码进行封装...
    99+
    2024-04-02
  • C++中的数组你真的理解了吗
    目录1 概述2 一维数组2.1 一维数组定义方式2.2 一维数组组名2.3 冒泡排序3 二维数组3.1 二维数组定义方式3.2 二维数组数组名3.3二维数组应用举例总结1 概述 所谓...
    99+
    2024-04-02
  • Java的代理模式你真的了解吗
    目录代理模式原理解析动态代理的原理解析代理模式的应用场景代理模式原理解析 代理模式(Proxy Design Pattern),它在不改变原始类(或者叫被代理类)代码的情况下,通过引...
    99+
    2024-04-02
  • 你真的了解Python日志打包load的工作原理吗?
    Python是一门广泛应用于各种领域的编程语言。在日志处理方面,Python也提供了很多的库和工具。其中,日志打包和load是日志处理中常用的操作之一。本文将深入讲解Python中日志打包和load的工作原理,并演示一些实用的代码。 一、日...
    99+
    2023-10-29
    日志 打包 load
  • Python的语法基础你真的了解吗
    目录Python语法基础01-Python快速入门U1-定义变量U2-判断语句U3-循环U4-定义函数U5-面向对象U6-引入python文件02-python的三大优点、七大特色U...
    99+
    2024-04-02
  • 你真的了解Linux下API的用法吗?
    Linux下的API是指应用程序接口,是操作系统提供给应用程序的一组接口,它们允许应用程序与操作系统进行交互和通信。对于Linux开发者来说,熟练掌握Linux下API的用法是非常重要的,本文将为大家介绍Linux下API的用法及其实例演示...
    99+
    2023-09-30
    数组 linux api
  • vue.js diff算法原理详细解析
    目录diff算法的概念虚拟Domh函数diff对比规则patchpatchVnodeupdateChildren总结diff算法的概念 diff算法可以看作是一种对比算法,...
    99+
    2024-04-02
  • Vue的computed计算属性你了解吗
    目录computed计算属性1、什么是计算属性2、为什么要用计算属性3、compute、methods和watch三者的区别4、案例:遍历数组对象的时候进行监视总结computed计...
    99+
    2024-04-02
  • Java 数组编程算法,你真的掌握了吗?
    Java 数组是一种非常重要的数据结构,它可以在程序中存储和操作一系列相同类型的数据。然而,Java 数组编程算法并不是所有程序员都能够完全掌握的。在本文中,我们将探讨几个常见的 Java 数组编程算法,并提供一些示例代码来帮助你更好地理...
    99+
    2023-10-13
    数组 编程算法 学习笔记
  • ASP 框架教程:你真的理解了吗?
    ASP框架教程:你真的理解了吗? ASP框架是一个非常流行的Web应用程序开发框架,使用了Microsoft ASP.NET技术。这个框架的目的是为了让开发人员更快速、更容易地创建Web应用程序。但是,ASP框架的概念并不容易理解,因此在这...
    99+
    2023-08-05
    框架 教程 学习笔记
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作