手写一个vue2的diff案例

一、Vue为什么需要采用虚拟DOM?

虚拟 DOM 在 Vue 中起到了优化性能、提供跨平台兼容性
以及简化开发流程的作⽤。

  • 虚拟 DOM 可以减少直接操作实际 DOM 的次数。
  • 虚拟 DOM 是⼀个抽象层,将实际 DOM 抽象为⼀个跨平台
    的表示形式。使得vue 可以在不同的平台上运⾏。
  • Vue 会通过⽐较新旧虚拟 DOM 树的差异(Diff算法),找
    出需要更新的部分进⾏更新。

二、Vue中key的作⽤?

在 Vue 中,key 是用于识别 Vue 中的列表(例如使用 v-for 指令)中每个子节点的特殊属性。key 的作用主要有两个方面:

  • 用于 Vue 的列表渲染时的性能优化:
    当 Vue 用 v-for 指令渲染列表时,它会尽可能地复用已经存在的元素,而不是重新渲染所有元素。Vue 会尽量高效地更新 DOM,以确保与虚拟 DOM 中的数据一致。

  • 当列表中的元素没有 key 时,Vue 会采用就地更新策略(in-place patch),也就是会尽量复用已有的 DOM 元素。但是当列表项的顺序发生变化时,或者有动态的增减操作时,Vue 可能无法正确识别哪个元素对应哪个数据项,导致错误的渲染结果。

  • 而当列表中的元素有 key 时,Vue 会基于 key 的变化重新排序和更新元素,这样可以确保列表的变化能够正确地映射到数据的变化上,避免出现意外的渲染结果。

  • 用于确保组件状态的完整性:
    在某些情况下,如果同一组件在不同的渲染中,存在相同的 key,Vue 可能会复用该组件的状态。这在一些特定场景下可能会导致状态混乱。因此,给组件设置唯一的 key 可以确保组件状态的完整性,每个组件都是独立的,不会被复用之前的状态。

因此,key 在 Vue 中是一个非常重要的属性,它能够确保列表渲染的正确性和性能优化,以及确保组件状态的完整性。

三、Vue2中diff算法的实现原理

Vue.js 2.x 中的 Virtual DOM diff 算法的实现原理主要依赖于 Snabbdom 这个虚拟 DOM 库。Snabbdom 是一个非常轻量级且高效的虚拟 DOM 库,Vue.js 在其基础上进行了适当的改进和定制以满足自身的需求。

下面是 Vue.js 2.x 中 Virtual DOM diff 算法的简要实现原理:

  • 虚拟 DOM 的生成:首先,Vue.js 会根据模板或者 render 函数生成当前状态下的虚拟 DOM 树。

  • 新旧虚拟 DOM 树的对比:然后,当状态发生变化时,Vue.js 会生成一个新的虚拟 DOM 树。接着,Vue.js 使用 diff 算法比较新旧虚拟 DOM 树的差异。

  • 差异的标记:在比较过程中,如果发现节点类型相同但是内容不同,那么就会更新该节点的内容;如果节点类型不同,直接将旧节点替换为新节点;如果节点位置发生变化,那么就会将节点移动到新的位置,而不是销毁并重新创建。

  • 差异的应用:最后,Vue.js 根据这些差异使用最小的操作数来更新真实 DOM。这样可以最大程度地减少真实 DOM 操作,提高渲染效率。

总体来说,Vue.js 2.x 中的 Virtual DOM diff 算法主要通过创建新旧虚拟 DOM 树的比较,并根据差异进行最小化的更新来实现高效的页面更新。这种方式能够尽量减少对真实 DOM 的操作,从而提升页面渲染的性能。

四、项目的搭建

第一步:配置package.json

//新建一个文件夹为vue2-diff,在对应的文件夹中执行下面的命令
npm init -y

第二步:新建index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="app"></div><script type="module">import { createElement, createTextNode } from './h.js'import { patch } from './patch.js'const vnode1 = createElement('div',{ style: { color: 'red',background:'purple' }, key: 'a' },createElement('li',{key:'a'},createTextNode('a')),createElement('li',{key:'b'},createTextNode('b')),createElement('li',{key:'c'},createTextNode('c')),createElement('li',{key:'d'},createTextNode('d')));// 虚拟节点就是一个对象来描述我们真实的节点patch(app, vnode1); // 初次渲染const vnode2 = createElement('div',{ style: { color: 'blue' }, key: 'a' },createElement('li',{key:'b'},createTextNode('b')),createElement('li',{key:'m'},createTextNode('m')),createElement('li',{key:'a'},createTextNode('a')),createElement('li',{key:'c'},createTextNode('c')),createElement('li',{key:'q'},createTextNode('q')),);setTimeout(()=>{patch(vnode1,vnode2); // 用vnode2 和 vnode1 做diff 更新vnode1上的元素},1000)</script>
</body></html>

第三部:新建一个patch.js文件

export function patch(oldVnode, vnode) {// 判断oldVnode是一个元素节点?if (oldVnode.nodeType) { // 元素const el = createElm(vnode);oldVnode.appendChild(el);} else {patchVnode(oldVnode, vnode); // 从根开始比较的}
}
function isSameVnode(oldVnode, vnode) { // 必须标签一样key 一样才是同一个元素return (oldVnode.tag === vnode.tag) && (oldVnode.key === vnode.key)
}
function patchVnode(oldVnode, vnode) {// 比较两个节点 (节点需要能复用)if (!isSameVnode(oldVnode, vnode)) {// 如果不是相同节点,将老dom元素直接替换成新元素即可return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)}// 走到这里说明之前和现在的节点是同一个节点, 要复用节点const el = vnode.el = oldVnode.elif (!oldVnode.tag) { // 文本比较文本内容,有变化复用文本节点更新内容if (oldVnode.text !== vnode.text) {el.textContent = vnode.text}}// 除了文本那就是元素了, 元素的话需要比较自己的属性和儿子updateProperties(vnode, oldVnode.data); // 更新属性,需要和老的比对// 比较双方儿子let oldChildren = oldVnode.children || [];let newChildren = vnode.children || [];// 双方都有儿子if (oldChildren.length > 0 && newChildren.length > 0) {// 比较双方的儿子updateChildren(el, oldChildren, newChildren); // 交给此方法来更新} else if (oldChildren.length > 0) {el.innerHTML = '';} else if (newChildren.length > 0) {for (let i = 0; i < newChildren.length; i++) {el.appendChild(createElm(newChildren[i]))}}// 之前有儿子 现在没儿子 把以前的儿子删除掉// 之前的没儿子 现在有儿子 直接将现在的儿子插入即可
}
// 给dom元素添加样式
function updateProperties(vnode, oldProps = {}) {const newProps = vnode.data || {}const el = vnode.el;// 对于属性来说新的要直接生效 但是老的里面有的新的没有还要移除let newStyle = newProps.style || {}let oldStyle = oldProps.style || {};for (let key in oldStyle) { // 老的样式有,新的没有要删除dom元素的样式if (!newStyle[key]) {el.style[key] = ''}}for (let key in oldProps) { // 老的属性有新的没有 移除这个属性if (!newProps[key]) {el.removeAttribute(key)}}for (let key in newProps) {if (key === 'style') {for (let styleName in newProps.style) {el.style[styleName] = newProps.style[styleName]}} else {el.setAttribute(key, newProps[key])}}
}
// 递归创建节点
function createElm(vnode) {let { tag, children, text } = vnode// 如果标签名是字符串说明是一个元素节点if (typeof tag === 'string') {// createElement DOMapivnode.el = document.createElement(tag);updateProperties(vnode)children.forEach(child => vnode.el.appendChild(createElm(child)))} else {vnode.el = document.createTextNode(text)}return vnode.el
}
function updateChildren(el, oldChildren, newChildren) {// 对dom操作的常见优化 // 给你一个列表  增加一个 删除一个 倒序 反序// 双端比对let oldStartIndex = 0;let oldStartVnode = oldChildren[0];let oldEndIndex = oldChildren.length - 1;let oldEndVnode = oldChildren[oldEndIndex];let newStartIndex = 0;let newStartVnode = newChildren[0];let newEndIndex = newChildren.length - 1;let newEndVnode = newChildren[newEndIndex]function makeIndexByKey(children) {let map = {};children.forEach((child, index) => {map[child.key] = index; // 老的key 和索引的映射表})return map;}const map = makeIndexByKey(oldChildren)// 一直比较直到一方指针重合就停止while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {// 如果头指针指向的结点是同一个节点,要复用这个节点if (!oldStartVnode) { // 比对的时候跳过空节点oldStartVnode = oldChildren[++oldStartIndex];} else if (!oldEndVnode) {oldEndVnode = oldChildren[--oldEndIndex];} else if (isSameVnode(oldStartVnode, newStartVnode)) { // 从头往后比patchVnode(oldStartVnode, newStartVnode)oldStartVnode = oldChildren[++oldStartIndex];newStartVnode = newChildren[++newStartIndex]} else if (isSameVnode(oldEndVnode, newEndVnode)) { // 从尾往前比patchVnode(oldEndVnode, newEndVnode)oldEndVnode = oldChildren[--oldEndIndex];newEndVnode = newChildren[--newEndIndex]} else if (isSameVnode(oldEndVnode, newStartVnode)) {// 尾部和头部比较patchVnode(oldEndVnode, newStartVnode); // 递归比较el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 把尾部移动到头部oldEndVnode = oldChildren[--oldEndIndex]; // 老的往前移动newStartVnode = newChildren[++newStartIndex]; // 新的往后移动} else if (isSameVnode(oldStartVnode, newEndVnode)) {patchVnode(oldStartVnode, newEndVnode);el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); // 把尾部移动到头部oldStartVnode = oldChildren[++oldStartIndex];newEndVnode = newChildren[--newEndIndex]}// 优化diff算法, 通过dom常见操作优化出来的 else {// 用新的节点去老的里面找,如果找的到则移动复用// 如果找不到则创建插入,// 如果新的都判断完了,老的中多的就删除即可let moveIndex = map[newStartVnode.key]; //  用新的节点去老的里面找索引if (moveIndex == undefined) { // null == undefiendel.insertBefore(createElm(newStartVnode), oldStartVnode.el); // 老的中没有} else {let moveVnode = oldChildren[moveIndex]; // 找到要移动的节点el.insertBefore(moveVnode.el, oldStartVnode.el); // 将节点移动到头指针的前面oldChildren[moveIndex] = null;patchVnode(moveVnode, newStartVnode); // 比对属性和子节点}newStartVnode = newChildren[++newStartIndex]}}console.log(oldStartIndex,oldEndIndex)if (oldStartIndex <= oldEndIndex) { // 老的对于的要删除掉for (let i = oldStartIndex; i <= oldEndIndex; i++) {let child = oldChildren[i]if (child) {el.removeChild(child.el)}}}if (newStartIndex <= newEndIndex) { // 新的比老的多for (let i = newStartIndex; i <= newEndIndex; i++) {let ele = newChildren[i]let anchor = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].elel.insertBefore(createElm(ele), anchor);//  el.insertBefore(createElm(ele),null)  === el.appendChild(createElm(ele))}}// newStartIndex >= newEndIndex}
// 初次渲染
// 比对的核心是从patch开始的  patch(真实的容器,虚拟节点)
//  - 根据虚拟节点创建成真实节点插入到容器中 (创建真实节点采用的是createElm) 
//  - 根据虚拟节点属性创建真实的属性updateProperties
// diff算法 
// 从patch开始的  patch(老的虚拟节点,新的虚拟节点) 
// patchVnode 比较两个节点的差异做更新的 文本、孩子、属性。。。
//  - isSameVnode 看两个节点是不是同一个节点,如果不相同删除替换即可 
//  - 复用之前的dom元素
//  - 如果是文本看文本内容是否有差异
//  - 如果是元素更新属性
//  - 如果是元素在更新儿子
//  - 更新儿子的三种情况 (updateChildren 两方都有儿子如何更新)

第三步:新建一个h.js文件

export function createElement(tag, data = {},...children) {// 创建元素节点let key = data.key; // key属性if (key) {delete data.key}return vnode(tag,data,key,children)
}export function createTextNode(text) {// 创建文本节点return vnode(undefined,undefined,undefined,undefined,text)
}function vnode(tag,data,key,children,text) {return { // -> vnode.key  // vnode.data.key 不存在tag,data,key,children,text}
}

将对应的文件引入,然后执行对应的命令启动,就能看到对应的效果了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/19719.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

yangwebrtc x86_64环境搭建

版本&#xff1a;5.0.099 sudo apt-get install libxext-dev sudo apt-get install x11proto-xext-dev sudo apt-get install libxi-dev sudo apt install libasound2-dev sudo apt install libgl1-mesa-dev sudo apt-get install libxtst-dev 用qt打开以下两个项目的.pro met…

WPF快速学习入门(8.视觉树和逻辑树)

WPF视觉树和逻辑树学习教程 WPF&#xff08;Windows Presentation Foundation&#xff09;是一种用于构建桌面应用程序的UI框架。在WPF中&#xff0c;有两种重要的树结构&#xff1a;视觉树和逻辑树。本文将介绍这两种树结构的概念、用途以及如何在实际项目中使用它们。 1. 视…

探索UWB模块的多功能应用——UWB技术赋能智慧生活

超宽带&#xff08;Ultra-Wideband, UWB&#xff09;技术&#xff0c;凭借其高精度、低功耗和强抗干扰能力&#xff0c;正在成为智能家居领域的一项关键技术。UWB模块的应用不仅提高了智能家居设备的性能&#xff0c;还为家庭安全、设备管理和用户体验带来了显著的改善。 UWB模…

pycharm 上一次编辑位置不见了

目录 pycharm2024版 上一次编辑位置不见了&#xff0c;研究发现移到了左下角了&#xff0c;如下图所示&#xff1a; 上一次编辑位置快捷键&#xff1a; pycharm2024版 上一次编辑位置不见了&#xff0c;研究发现移到了左下角了&#xff0c;如下图所示&#xff1a; 上一次编辑…

Windows 80端口占用解决办法

启动软件系统前&#xff0c;有时遇见端口被其他程序占用&#xff0c;导致无法启动系统 解决办法: # 查看端口占用情况 > netstat -ano | findstr 端口号通常端口占用&#xff0c;通过任务管理器查看PID&#xff0c;结束任务可以完成。System进程占用&#xff0c;结束不了&…

惯性动作捕捉与数字人实时交互/运营套装,对高校元宇宙实训室有何作用?

惯性动作捕捉与数字人实时交互/运营套装&#xff0c;可以打破时空限制&#xff0c;通过动捕设备写实数字人软件系统动捕设备系统定制化数字人短视频渲染平台&#xff0c;重塑课程教学方式&#xff0c;开展元宇宙沉浸式体验教学活动和参观交流活动。 写实数字人软件系统内置丰富…

AI预测福彩3D采取888=3策略+和值012路一缩定乾坤测试5月31日预测第7弹

昨天的3D已命中&#xff01;今天继续基于8883的大底&#xff0c;使用尽可能少的条件进行缩号。好了&#xff0c;直接上结果吧~ 首先&#xff0c;888定位如下&#xff1a; 百位&#xff1a;7,6,5,8,9,3,2,0 十位&#xff1a;3,4,5,2,1,7,8,9 …

视频汇聚EasyCVR平台GA/T 1400视图库应用:助力社会治安防控效能提升

在信息化、智能化的时代浪潮下&#xff0c;公安视频图像信息应用系统的发展与应用显得尤为重要。GA/T 1400标准&#xff0c;全称为《公安视频图像信息应用系统》&#xff0c;作为公安行业的一项重要标准&#xff0c;其视图库的应用在提升公安工作效能、加强社会治安防控等方面发…

C#中的空合并运算符与空合并赋值运算符:简化空值处理

在C#编程中&#xff0c;处理可能为null的值是一项常见的任务&#xff0c;尤其是在涉及数据库查询、Web服务调用或任何可能返回缺失数据的场景中。为了简化这类操作并提高代码的可读性&#xff0c;C# 8 引入了两个非常实用的运算符&#xff1a;空合并运算符 (??) 和 空合并赋值…

小白跟做江科大32单片机之按键控制LED

原理部分 1.LED部分使用的是这样的连接方式 2.传感器模块的电路图 滤波电容如果接地&#xff0c;一般用于滤波&#xff0c;在分析电路时就不用考虑。下面这个电路就是看A端和B端哪端的拉力大&#xff0c;就能把电压值对应到相应的电压值 比较器部分 如果A端电压>B端电压&am…

深入Kafka消息分区机制:从原理到实践

深入Kafka消息分区机制&#xff1a;从原理到实践 在现代分布式系统中&#xff0c;如何高效地处理海量数据是一个至关重要的问题。Apache Kafka作为一种高吞吐量的分布式消息系统&#xff0c;广泛应用于日志收集、实时分析等场景。为了保证数据的高效处理和系统的高可扩展性&am…

【C++奇技淫巧】CRTP(奇特重现模板模式)

CRTP&#xff08;Curiously Recurring Template Pattern&#xff0c;奇特重现模版模式&#xff09;,是一种在C中使用模板来实现的设计模式&#xff0c;主要用于实现编译时多态性&#xff08;静态多态&#xff09;。这种模式通过类模板和模板继承机制来实现&#xff0c;使得派生…

【html知识】html中常用的表单元素+css格式美化

创作背景与目的&#xff1a; 随着互联网的飞速发展&#xff0c;表单作为网页交互的重要组成部分&#xff0c;扮演着收集用户信息、进行用户反馈的关键角色。本作品旨在总结并展示HTML中常用的表单元素&#xff0c;帮助开发者快速了解并应用这些元素&#xff0c;以优化网页的交…

自学成才Flutter 弹性布局、线性布局

本文我们要介绍 Flutter 中布局 Widget&#xff0c;包括弹性布局、线性布局 流式布局和层叠布局。 Flutter中文网 Flutter开发 一、弹性布局--Flex Flex 类似 Android 中的 FlexboxLayout&#xff0c;和 Expanded 配合使用可以实现子Widget 按照一定比例来分配父容器空间。 使…

数仓建模—企业数字化转型的本质

数仓建模—企业数字化转型的本质 数字化转型与数字化、数字化建设、数字化管理到底有什么差别?企业的数字化建设重点关注哪两个层面的实现?数字化转型中的“转型”到底指的是什么?数字化转型对企业有哪些最重要的挑战?转型的终极目标是什么?客户价值实现是转型的结果还是逻…

Web前端三大主流框架:React、Vue和Angular

在当前的Web开发领域&#xff0c;前端框架的选择对于项目的成功至关重要。作为一名资深的IT技术员&#xff0c;我对前端技术的发展和行业趋势保持着持续的关注。本文将介绍当前Web前端三大主流框架&#xff1a;React、Vue和Angular&#xff0c;并分析它们各自的优势。 React&a…

MyBatis通用Mapper:简化数据库操作的利器

引言 在软件开发中&#xff0c;数据库操作是不可或缺的一部分。通常我们会使用mybatis&#xff0c;的MBG插件&#xff0c;自动生成表对应的基本操作语句xml。 当我们的表字段发生变化的时候&#xff0c;我们需要修改实体类和Mapper文件定义的字段和方法。如果是增量维护&…

【学习笔记】数据结构(一)

基本概念和术语 &#x1f449;数据&#xff1a;所有能被输入到计算机中&#xff0c;且被计算机处理的符号的集合&#xff1b; 是计算机操作对象的总称&#xff1b;是计算机处理信息的载体&#xff1b;是信息的某一种特定的符号表示形式包括数值型数据、非数值型数据 &#x1…

git生成密钥(免密)

生成SSH密钥对的方法如下&#xff1a; 打开Git Bash。 输入以下命令生成新的SSH密钥对&#xff1a; ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 这里的 -C 参数后面跟的是你的邮箱地址&#xff0c;通常用于标识这个密钥。 当系统提示你“Enter a fil…

汇编概论和实践

一 汇编第一例 C代码 #include <stdio.h>int main() {printf("Hello, World!\n");return 0; }对应的汇编 .LC0:.string "Hello, World!"main:pushq %rbpmovq %rsp, %rbpleaq .LC0(%rip), %rdicall puts@PLTmovl $0, %eaxpopq %rbpret 二 CPU架构…