返回顶部
首页 > 资讯 > 前端开发 > JavaScript >vue.js diff算法原理详细解析
  • 150
分享到

vue.js diff算法原理详细解析

2024-04-02 19:04:59 150人浏览 独家记忆
摘要

目录diff算法的概念虚拟Domh函数diff对比规则patchpatchVnodeupdateChildren总结diff算法的概念 diff算法可以看作是一种对比算法,

diff算法的概念

diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom

虚拟Dom

上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟Dom,而不是直接去操作真实Dom。

虚拟Dom,其实很简单,就是一个用来描述真实Dom的对象

它有六个属性,sel表示当前节点标签名,data内是节点的属性,children表示当前节点的其他子标签节点,elm表示当前虚拟节点对应的真实节点(这里暂时没有),key即为当前节点的key,text表示当前节点下的文本,结构类似这样。

let vnode = {
    sel: 'ul', 
    data: {},
    children: [ 
        {
            sel: 'li', data: { class: 'item' }, text: 'son1'
        },
        {
            sel: 'li', data: { class: 'item' }, text: 'son2'
        },    
    ],
    elm: undefined,
    key: undefined,
    text: undefined
}

那么虚拟Dom有什么用呢。我们其实可以把虚拟Dom理解成对应真实Dom的一种状态。当真实Dom发生变化后,虚拟Dom可以为我们提供这个真实Dom变化之前和变化之后的状态,我们通过对比这两个状态,即可得出真实Dom真正需要更新的部分,即可实现最小量更新。在一些比较复杂的Dom变化场景中,通过对比虚拟Dom后更新真实Dom会比直接更新真实Dom的效率高,这也就是虚拟Dom和diff算法真正存在的意义。

h函数

在介绍diff算法原理之前还需要简单让大家了解一下h函数,因为我们要靠它为我们生成虚拟Dom。这个h函数大家应该也比较熟悉,就是render函数里面传入的那个h函数

h函数可以接受多种类型的参数,但其实它内部只干了一件事,就是执行vnode函数。根据传入h函数的参数来决定执行vnode函数时传入的参数。那么vnode函数又是干什么的呢?vnode函数其实也只干了一件事,就是把传入h函数的参数转化为一个对象,即虚拟Dom。

// vnode.js
export default function (sel, data, children, text, elm) {
    const key = data.key 
    return {sel, data, children, text, elm, key}
}

执行h函数后,内部会通过vnode函数生成虚拟Dom,h函数把这个虚拟在return出去。

diff对比规则

明确了h函数是干什么的,我们可以简单用h函数生成两个不同的虚拟节点,我们将通过一个简易版的diff算法代码介绍diff对比的具体流程。

// 第一个参数是sel 第二个参数是data 第三个参数是children
const myVnode1 = h("h1", {}, [
  h("p", {key: "a"}, "a"),
  h("p", {key: "b"}, "b"),
]);
const myVnode2 = h("h1", {}, [
  h("p", {key: "c"}, "c"),
  h("p", {key: "d"}, "d"),
]);

patch

比较的第一步就是执行patch,它相当于对比的入口。既然是对比两个虚拟Dom,那么就将两个虚拟Dom作为参数传入patch中。patch的主要作用是对比两个虚拟Dom的根节点,并根据对比结果操作真实Dom。

patch函数的核心代码如下,注意注释。

// patch.js
import vnode from "./vnode"
import patchDetails from "./patchVnode"
import createEle from "./createEle"

export function patch(oldVnode, newVnode) {
  // 1.判断oldVnode是否为虚拟节点,不是的话转化为虚拟节点
  if(!oldVnode.sel) {
    // 转化为虚拟节点
    oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
  }
  // 2.判断oldVnode和newVnode是否为同一个节点
  if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
    console.log('是同一个节点')
    // 比较子节点
    patchDetails(oldVnode, newVnode)
  }else {
    console.log('不是同一个节点')
    // 插入newVnode 
    const newNode = createEle(newVnode) // 插入之前需要先将newVnode转化为dom
    oldVnode.elm.parentNode.insertBefore(newNode, oldVnode.elm) // 插入操作
    // 删除oldVnode
    oldVnode.elm.parentNode.removeChild(oldVnode.elm)
  }
}
// createEle.js

export default function createEle (vnode) {
  const realNode = document.createElement(vnode.sel)
​
  // 子节点转换
  if(vnode.text && (vnode.children == undefined || (vnode.children && vnode.children.length == 0)) ) {
    // 子节点只含有文本
    realNode.innerText = vnode.text  
  }else if(Array.isArray(vnode.children) && vnode.children.length > 0) {
    // 子节点为其他虚拟节点 递归添加node
    for(let i = 0; i < vnode.children.length; i++) {
      const childNode = createEle(vnode.children[i])
      realNode.appendChild(childNode)
    }
  }
  // 补充vnode的elm属性
  vnode.elm = realNode
  return vnode.elm
}

patchVnode

patchVnode用来比较两个虚拟节点的子节点并更新其子节点对应的真实Dom节点

// patchVnode.js
​
import updateChildren from "./updateChildren"
import createEle from "./createEle"
​

export function patchDetails(oldVnode, newVnode) {
  // 判断oldVnode和newVnode是否为同一个对象, 是的话直接不用比了
  if(oldVnode == newVnode) return 
​
  // 默认newVnode和oldVnode只有text和children其中之一,真实的源码这里的情况会更多一些,不过大同小异。
​
  if(hasText(newVnode)) {
    // newVnode有text但没有children
​
    
    if(newVnode.text !== oldVnode.text) {
      oldVnode.elm.innerText = newVnode.text
    }
  }else if(hasChildren(newVnode)) {
    // newVnode有children但是没有text
    
    if(hasText(oldVnode)) {
      // oldVnode有text但是没有children
      
      oldVnode.elm.innerText = '' // 删除oldVnode的text
      // 添加newVnode的children
      for(let i = 0; i < newVnode.children.length; i++) {
        oldVnode.elm.appendChild(createEle(newVnode.children[i]))
      }
    }else if(hasChildren(oldVnode)) {
      // oldVnode有children但是没有text
​
      // 对比两个节点的children 并更新对应的真实dom节点
      updateChildren(oldVnode.children, newVnode.children, oldVnode.elm)
    }
  }
}
// 有children没有text
function hasChildren(node) {
  return !node.text && (node.children && node.children.length > 0)
} 
// 有text没有children
function hasText(node) {
  return node.text && (node.children == undefined || (node.children && node.children.length == 0))
} 

updateChildren

该方法是diff算法中最复杂的方法(大的要来了)。对应上面patchVnodeoldVnodenewVnode都有children的情况。

首先我们需要介绍一下这里的对比规则。

对比过程中会引入四个指针,分别指向oldVnode子节点列表中的第一个节点和最后一个节点(后面我们简称为旧前旧后)以及指向newVnode子节点列表中的第一个节点和最后一个节点(后面我们简称为新前新后

对比时,每一次对比按照以下顺序进行命中查找

  • 旧前与新前节点对比(1)
  • 旧后与新后节点对比(2)
  • 旧前与新后节点对比(3)
  • 旧后与新前节点对比(4)

上述四种情况,如果某一种情况两个指针对应的虚拟Dom相同,那么我们称之为命中。命中后就不会接着查找了,指针会移动,(还有可能会操作真实Dom,3或者4命中时会操作真实Dom移动节点)之后开始下一次对比。如果都没有命中,则去oldVnode子节点列表循环查找当前新前指针所指向的节点,如果查到了,那么操作真实Dom移动节点,没查到则新增真实Dom节点插入。

这种模式的对比会一直进行,直到满足了终止条件。即旧前指针移动到了旧后指针的后面或者新前指针移动到了新后指针的后面,我们可以理解为旧子节点先处理完毕新子节点处理完毕。那么我们可以预想到新旧子节点中总会有其一先处理完,对比结束后,我们会根据没有处理完子节点的那一对前后指针决定是要插入真实Dom还是删除真实Dom。

  • 如果旧子节点先处理完了,新子节点有剩余,说明有要新增的节点。将根据最终新前新后之间的虚拟节点执行插入操作
  • 如果新子节点先处理完了,旧子节点有剩余,说明有要删除的节点。将根据最终旧前旧后之间的虚拟节点执行删除操作

下面将呈现代码,注意注释:

// updateChildren.js
import patchDetails from "./patchVnode"
import createEle from "./createEle";

export default function updateChildren(oldCh, newCh, parent) {
  // 定义四个指针 旧前 旧后 新前 新后 (四个指针两两一对,每一对前后指针所指向的节点以及其之间的节点为未处理的子节点)
  let oldStartIndex = 0;
  let oldEndIndex = oldCh.length - 1;
  let newStartIndex = 0;
  let newEndIndex = newCh.length - 1;
​
  // 四个指针对应的节点
  let oldStartNode = oldCh[oldStartIndex];
  let oldEndNode = oldCh[oldEndIndex];
  let newStartNode = newCh[newStartIndex];
  let newEndNode = newCh[newEndIndex];
​
  // oldCh中每个子节点 key 与 index的哈希表 用于四种对比规则都不匹配的情况下在oldCh中寻找节点
  const keyMap = new Map();
​
  
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // 这四种情况是为了让指针在移动的过程中跳过空节点
    if (oldStartNode == undefined) {
      oldStartNode = oldCh[++oldStartIndex];
    } else if (oldEndNode == undefined) {
      oldEndNode = oldCh[--oldEndIndex];
    } else if (newStartNode == undefined) {
      newStartNode = newCh[++newStartIndex];
    } else if (newEndNode == undefined) {
      newEndNode = newCh[--newEndIndex];
    } else if (isSame(oldStartNode, newStartNode)) {
      console.log("method1");
      // 旧前-新前是同一个虚拟节点
​
      // 两个子节点再对比他们的子节点并更新dom (递归切入点)
      patchDetails(oldStartNode, newStartNode);
      // 指针移动
      oldStartNode = oldCh[++oldStartIndex];
      newStartNode = newCh[++newStartIndex];
    } else if (isSame(oldEndNode, newEndNode)) {
      console.log("method2");
      // 旧后-新后是同一个虚拟节点
​
      // 两个子节点再对比他们的子节点并更新dom (递归切入点)
      patchDetails(oldEndNode, newEndNode);
      // 指针移动
      oldEndNode = oldCh[--oldEndIndex];
      newEndNode = newCh[--newEndIndex];
    } else if (isSame(oldStartNode, newEndNode)) {
      console.log("method3");
      // 旧前-新后是同一个虚拟节点
​
      // 两个子节点再对比他们的子节点并更新dom (递归切入点)
      patchDetails(oldStartNode, newEndNode);
​
      
      parent.insertBefore(oldStartNode.elm, oldEndNode.elm.nextSibling);
​
      // 指针移动
      oldStartNode = oldCh[++oldStartIndex];
      newEndNode = newCh[--newEndIndex];
    } else if (isSame(oldEndNode, newStartNode)) {
      console.log("method4");
      // 旧后-新前 是同一个虚拟节点
​
      // 两个子节点再对比他们的子节点并更新dom (递归切入点)
      patchDetails(oldEndNode, newStartNode);
      
      parent.insertBefore(oldEndNode.elm, oldCh[oldStartIndex].elm);
​
      // 指针移动
      oldEndNode = oldCh[--oldEndIndex];
      newStartNode = newCh[++newStartIndex];
    } else {
      console.log("does not match");
      // 四种规则都不匹配
​
      // 生成keyMap
      if (keyMap.size == 0) {
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
          if (oldCh[i].key) keyMap.set(oldCh[i].key, i);
        }
      }
​
      // 在oldCh中搜索当前newStartIndex所指向的节点
      if (keyMap.has(newStartNode.key)) {
        // 搜索到了
​
        // 先获取oldCh中该虚拟节点
        const oldMoveNode = oldCh[keyMap.get(newStartNode.key)];
        // 两个子节点再对比他们的子节点并更新dom (递归切入点)
        patchDetails(oldMoveNode, newStartNode);
​
        // 移动这个节点(移动的是真实节点)
        parent.insertBefore(oldMoveNode.elm, oldStartNode.elm);
​
        // 该虚拟节点设置为undefined(还记得最开始的四个条件吗,因为这里会将子节点制空,所以加了那四个条件)
        oldCh[keyMap.get(newStartNode.key)] = undefined;
          
      } else {
        // 没搜索到 直接插入
        parent.insertBefore(createEle(newStartNode), oldStartNode.elm);
      }
​
      // 指针移动
      newStartNode = newCh[++newStartIndex];
    }
  }
​
  
  if (oldStartIndex <= oldEndIndex) {
    // 删除
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
      // 加判断是因为oldCh[i]有可能为undefined
      if(oldCh[i]) parent.removeChild(oldCh[i].elm);
    }
  } else if (newStartIndex <= newEndIndex) {
    
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      // oldCh[oldStartIndex]存在是从头部插入
      parent.insertBefore(createEle(newCh[i]), oldCh[oldStartIndex] ? oldCh[oldStartIndex].elm : undefined);
    }
  }
}
​
// 判断两个虚拟节点是否为同一个虚拟节点
function isSame(a, b) {
  return a.sel == b.sel && a.key == b.key;
}

这里的逻辑稍微比较复杂,需要大家多理几遍,必要的话,自己手画一张图自己移动一下指针。着重需要注意的地方是操作真实Dom时,插入、移动节点应该将节点从哪里插入或者移动到哪里,其实基本插入到oldStartIndex对应的真实Dom的前面,除了第三种命中后的移动节点操作,是移动到oldEndIndex所对应真实节点之后

总结

由于diff算法对比的是虚拟Dom,而虚拟Dom是呈树状的,所以我们可以发现,diff算法中充满了递归。总结起来,其实diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。

这里再提一嘴key,我们面试中经常会被问到Vuekey的作用。根据上面我们分析的,key的主要作用其实就是对比两个虚拟节点时,判断其是否为相同节点。加了key以后,我们可以更为明确的判断两个节点是否为同一个虚拟节点,是的话判断子节点是否有变更(有变更更新真实Dom),不是的话继续比。如果不加key的话,如果两个不同节点的标签名恰好相同,那么就会被判定为同一个节点(key都为undefined),结果一对比这两个节点的子节点发现不一样,这样会凭空增加很多对真实Dom的操作,从而导致页面更频繁地重绘和回流。

所以我认为合理利用key可以有效减少真实Dom的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。

到此这篇关于vue.js diff算法原理详细解析的文章就介绍到这了,更多相关vue.js diff算法内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: vue.js diff算法原理详细解析

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

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

猜你喜欢
  • vue.js diff算法原理详细解析
    目录diff算法的概念虚拟Domh函数diff对比规则patchpatchVnodeupdateChildren总结diff算法的概念 diff算法可以看作是一种对比算法,...
    99+
    2024-04-02
  • Vue2 的 diff 算法规则原理详解
    目录前言算法规则diff 优化策略老数组的开始与新数组的开始老数组的结尾与新数组的结尾老数组的开始与新数组的结尾老数组的结尾与新数组的开始以上四种情况都没对比成功推荐在渲染列表时为节...
    99+
    2024-04-02
  • React diff算法超详细讲解
    目录diff 算法介绍diff 策略tree diffcomponent diffelement diff结合源码看 diff整体流程新内容为 REACT_ELEMENT_TYPE新...
    99+
    2022-11-13
    React diff算法原理 React diff算法源码
  • react diff 算法实现思路及原理解析
    目录事例分析diff 特点diff 思路实现 diff 算法修改入口文件实现 React.Fragment我们需要修改 children 对比前面几节我们学习了解了 react 的渲...
    99+
    2024-04-02
  • JavaHashMap算法原理详细讲解
    目录前言一、HashMap概述二、HashMap的数据结构三、HashMap源码分析3.1 关键属性3.2 构造方法3.3 存储数据3.4 调整大小3.5 数据读取3.6 HashM...
    99+
    2023-02-08
    Java HashMap原理 Java HashMap
  • vue diff算法全解析
    目录前言 Vue 更新视图 patch sameVnode patchVnodeupdateChildren 前言 我们知道 Vue 使用的是虚拟 DOM 去减少对真实 DOM 的...
    99+
    2024-04-02
  • React diff算法面试考点超详细讲解
    目录前言diff算法的概念手写diff算法的过程前言 在前端工程上,日益复杂的今天,性能优化已经成为必不可少的环境。前端需要从每一个细节的问题去优化。那么如何更优,当然与他的如何怎么...
    99+
    2022-12-19
    React diff算法原理 React diff算法源码
  • react diff算法源码解析
    目录单节点Diff reconcileSingleElement多节点Diff reconcileChildrenArray如何判断节点移动了? 总结 React中Diff算法又称为...
    99+
    2024-04-02
  • Vue3diff算法之双端diff算法详解
    目录双端Diff算法双端比较的原理简单Diff的不足双端Diff介绍Diff流程第一次diff第二次diff第三次diff第四次diff双端Diff的优势非理想情况的处理方式添加新元...
    99+
    2024-04-02
  • Vue的diff算法原理是什么
    这篇文章将为大家详细讲解有关Vue的diff算法原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。思维导图0. 从常见问题引入虚拟dom是什么如何创建虚拟dom虚拟dom如何渲染成真是dom虚拟do...
    99+
    2023-06-29
  • Vue的diff算法原理你真的了解吗
    目录思维导图0. 从常见问题引入1. 生成虚拟dom1. h方法实现2. render方法实现3. 再次渲染2. diff算法1. 对常见的dom做优化情况1:末尾追加一个元素(头和...
    99+
    2024-04-02
  • Golang Mutex 原理详细解析
    目录前言Lock单协程加锁加锁被阻塞Unlock无协程阻塞下的解锁解锁并唤醒协程自旋什么是自旋自旋条件自旋的优势自旋的问题Mutex 的模式Normal 模式Starving 模式W...
    99+
    2024-04-02
  • LRU算法原理解析
    LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。 现代操作系统提供了一种对主存的抽象概念虚拟内存,来对主存进行更好地管理。他将主存看成是一个存储在磁盘上的地址空间的高速...
    99+
    2023-01-31
    算法 原理 LRU
  • golang切片原理详细解析
    目录切片的解析切片的初始化字面量初始化make初始化切片的截取切片的复制切片的扩容总结切片的解析 当我们的代码敲下[]时,便会被go编译器解析为抽象语法树上的切片节点, 被初始化为切...
    99+
    2024-04-02
  • 详解react应用中的DOM DIFF算法
    目录前言 什么是Virtual DOM? 使用Virtual DOM的原因 DOM 渲染页面的操作流程 Virtual DOM的优势如何将DOM用virtual DOM 来表...
    99+
    2024-04-02
  • 一文详解Vue 的双端 diff 算法
    目录前言diff 算法简单 diff双端 diff总结前言 Vue 和 React 都是基于 vdom 的前端框架,组件渲染会返回 vdom,渲染器再把 vdom 通过增删改的 ap...
    99+
    2024-04-02
  • React Diff算法不采用Vue的双端对比原因详解
    目录前言React 官方的解析Fiber 的结构Fiber 链表的生成React 的 Diff 算法第一轮,常见情况的比对第二轮,不常见的情况的比对重点如何协调更新位置信息小结图文解...
    99+
    2024-04-02
  • 怎么深入解析Vue3中的diff 算法
    今天给大家介绍一下怎么深入解析Vue3中的diff 算法。文章的内容小编觉得不错,现在给大家分享一下,觉得有需要的朋友可以了解一下,希望对大家有所帮助,下面跟着小编的思路一起来阅读吧。1.0  diff 无key子节点在处理被标记...
    99+
    2023-06-26
  • react中的虚拟dom和diff算法详解
    虚拟DOM的作用 首先我们要知道虚拟dom的出现是为了解决什么问题的,他解决我们平时频繁的直接操作DOM效率低下的问题。那么为什么我们直接操作DOM效率会低下呢? 比如我们创建一个d...
    99+
    2024-04-02
  • 快速搞懂Vue2 diff算法(图文详解)
    前置内容既然是对比新旧 VNode 数组,那么首先肯定有 对比 的判断方法:sameNode(a, b)、新增节点的方法 addVnodes、移除节点的方法 removeVnodes,当然,即使 sameNode 判断了 VNode 一致之...
    99+
    2023-05-14
    Vue2 Vue diff算法
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作