vue2 diff算法及虚拟DOM

news2024/12/25 13:02:17

概括:diff算法,虚拟DOM中采用的算法,把树形结构按照层级分解,只比较同级元素,不同层级的节点只有创建和删除操作。

一、虚拟DOM

(1) 什么是虚拟DOM?

虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。这个概念是由 React 率先开拓,随后在许多不同的框架中都有不同的实现,当然也包括 Vue。

虚拟DOM本质上就是用来描述真实DOM的JavaScript对象。

举例:

    <div id="hello">我是奥特曼</div>

编译成虚拟DOM

const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* 更多 vnode */
  ],
  text:'我是奥特曼'
}

(2)为什么要有虚拟DOM

1.能够减少操作真实DOM,对于浏览器而言有很大的性能提升,对于开发而言有很大的工作效率(同时避免了代码繁琐)。

2.想要修改数据 直接修改js对象即可,当然我们基于框架去开发也不用关心数据如何去更新视图。

3.例如新节点中,只有一部分节点进行变动时,只需要更新要变更的一部分,另一部分完全可以使用之前的节点。也避免了全部更新渲染。

最终目的:新虚拟DOM和旧虚拟DOM 进行diff计算,(精细化比较),算出应该如何最小量更新,最后反应到真正的DOM上。

二、snabbdom

在vue2中diff算法借鉴了snabbdom库,这个库也就包含着如何生成虚拟DOM和diff算法。

方法包括:

h( ) 函数用来生成虚拟DOM

vnode( ) 函数 通过h调用vnode 达到返回虚拟DOM的格式

patch( ) 函数用来比较新旧节点是否是一个节点,如果不是进行暴力更新,如果是进行对比

patchVnode( ) 函数用来详细对比 当遇见新旧节点都是数组时调用updataChildren

updataChildren( ) 函数用来对比新旧节点都是数组的情况

createElement( ) 讲虚拟dom创建出真实dom

以下函数均为简单版实现核心  并为源码 

(1)h( )函数

h函数的使用方式:

import { h } from 'vue'

// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })

// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })

// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })

// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')

// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])

// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])

 当然传参的方式有很多,最普通的写法

 h(a,{},'文字')

当然第二个就可以去表示是参数的一些属性,当没有属性的时候你也可以不传,或者第二个参数可以传子集元素的集合等,  snabbdom就是在这方面做了很多个判断。

 h(a,'文字')

实现一个简单的h函数

当然如果你想了解的更透彻可以 clone snabbdom库的代码进行查看。

git clone https://github.com/snabbdom/snabbdom.git

由于snabbdom传参形式较多, 这里就实现一个简单的h函数,只实现三种场景

  ① h('div', {}, '文字')
  ② h('div', {}, [h()])
  ③ h('div', {}, h())

vnode( ) 函数

export default function vnode(sel, data, children, text, elm) {
  const key = data == undefined ? undefined : data.key;
  return { sel, data, children, text, elm, key };
}

vnode就是把传进来的参数以对象的形式返回出去,返回出一个vnode的形式

sel: 元素/标签

data:属性

children:子元素vnode的集合

text:文字

elm:节点

h( )函数

import vnode from "./vnode";
/**
 * 产生虚拟DOM树 返回的是一个对象
 * 低配版本的h函数,这个函数必须接受三个参数,缺一不可
 * @param {*} sel
 * @param {*} data
 * @param {*} c
 * 调用只有三种形态
 * ① h('div', {}, '文字')
 * ② h('div', {}, [h()])
 * ③ h('div', {}, h())
 */
export default function (sel, data, c) {
  // 检查参数个数
  if (arguments.length !== 3) {
    throw new Error("请传入只三个参数!");
  }
  // 检查参数c的类型
  if (typeof c === "string" || typeof c === "number") {
    // 说明现在是 ① h('div', {}, '文字')
    return vnode(sel, data, undefined, c, undefined);
  } else if (Array.isArray(c)) {
    // 说明是 ② h('div', {}, [])
    let children = [];
    // 遍历 数组 c
    for (let item of c) {
      // 如果是数组传入的项一定有sel属性 也就必须是h函数的格式 不考虑数组中有包含文字的情况
      if (!(typeof item === "object" && item.hasOwnProperty("sel"))) {
        throw new Error("传入的数组有不是h函数的项");
      }
      // 不用执行c[i], 调用的时候执行了,只要收集
      children.push(item);
    }
    return vnode(sel, data, children, undefined, undefined);
  } else if (typeof c === "object" && c.hasOwnProperty("sel")) {
    // 说明是 ③ h('div', {}, h())
    let children = [c];
    return vnode(sel, data, children, undefined, undefined);
  } else {
    throw new Error("传入的参数类型不对!");
  }
}

(2)createElement( )函数

把虚拟节点从(vnode)变成真实DOM元素

/**
 * 创建节点。将vnode虚拟节点创建为DOM节点
 * 是孤儿节点,不进行插入操作
 * @param {object} vnode 
 * @returns {object} domNode 返回DOM节点
 */
export default function createElement(vnode) {
  // 创建一个DOM节点,这个节点现在是孤儿节点,最后返回这个DOM节点
  let domNode = document.createElement(vnode.sel);
  // 判断是有子节点还是有文本
  if (
    vnode.text !== "" &&
    (vnode.children === undefined || vnode.children.length === 0)
  ) {
    // 说明没有子节点,内部是文本
    domNode.innerText = vnode.text;
  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
    // 说明内部是子节点,需要递归创建节点 
    // 遍历vnode.children数组
    for (let ch of vnode.children) {
      // 递归调用 创建出它的DOM,一旦调用createElement意味着创建出DOM了。并且它的elm属性指向了创建出的dom,但是没有上树,是一个孤儿节点
      let chDOM = createElement(ch);
      // console.log(ch);
      // 上树
      domNode.appendChild(chDOM);
    }
  }
  // 补充elm属性
  vnode.elm = domNode;
  // 返回domNode DOM对象
  return domNode;
}

(3)patch( )函数

patch函数基本上也是diff算法的核心,比较这新虚拟节点和旧虚拟节点的不同。

export default function patch(oldVnode, newVnode) {
  // 判断传入的第一个参数是 DOM节点 还是 虚拟节点
  if (oldVnode.sel == "" || oldVnode.sel === undefined) {
    // 说明是DOM节点,此时要包装成虚拟节点
    oldVnode = vnode(
      oldVnode.tagName.toLowerCase(), // sel
      {}, // data
      [], // children
      undefined, // text
      oldVnode // elm
    );
  }
  // 此时新旧节点都是虚拟节点了
  // 判断 oldVnode 和 newVnode 是不是同一个节点
  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
    console.log("是同一个节点,需要精细化比较");
    patchVnode(oldVnode, newVnode);
  } else {
    console.log("不是同一个节点,暴力插入新节点,删除旧节点");
    // 创建 新虚拟节点 为 DOM节点
    let newVnodeElm = createElement(newVnode);
    // 获取旧虚拟节点真正的DOM节点
    let oldVnodeElm = oldVnode.elm;
    // 判断newVnodeElm是存在的
    if (newVnodeElm) {
      // 插入 新节点 到 旧节点 之前
      oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm);
    }
    // 删除旧节点
    oldVnodeElm.parentNode.removeChild(oldVnodeElm);
  }
}

注释写的比较详细,大致三部分:

1. 第一次有可能传入进来的是一个元素,因为第一次还没有节点,需要把元素包装成虚拟节点 vnode

2. 如果 旧虚拟节点和新虚拟节点key和元素标签一样时 可以进入深层比较 调用patchVnode函数

3. 否则把新节点创建成dom元素 插入到旧节点之前 并移除旧节点

(4)patchVnode( )函数

import updateChild from "./updateChildren";
import createElement from "./createElement";
export default function patchVnode(oldVnode, newVnode) {
  // 第一种情况 全都一样 不需要操作
  if (oldVnode == newVnode) return;

  // 第二种情况新节点是文字
  if (  newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0) ) {
    // 如果旧节点文字不等于新节点文字 直接替换
    if (oldVnode.text != newVnode.text) {
      oldVnode.elm.innerText = newVnode.text;
    }
  // 第三种情况 新节点是数组
  } else {
    // 如果新旧节点都是数组  也是最复杂的情况
    if (oldVnode.children && oldVnode.children.length > 0) {
      updateChild(oldVnode.elm, oldVnode.children, newVnode.children);
      //   如果旧节点是文字 新节点是数组
    } else {
      // 清空旧节点文字 把子节点给旧节点的elm元素上
      oldVnode.elm.innerText = "";
      for (let i = 0; i < newVnode.children; i++) {
        const childDom = newVnode.children[i];
        childDom.elm = createElement(childDom);
        oldVnode.elm.appendChild(childDom.elm);
      }
    }
  }
  newVnode.elm = oldVnode.elm;
}

(5)updateChild( )函数

这也是函数中最复杂的部分了,如果新旧节点均为数组,我们要做最细致的比较。

例如:旧节点 A,B,C  新节点 A,C,B  我们是肯定希望去移动它,并不是删除B,C重新创建出C,B

例如: 旧节点 A,B,C  新节点 D,A,B,C,E  同样我们需要在A前面创建出D  在C后面创建出E 

当然复杂的场景还有很多很多,这也是我们都要考虑的,这时候就需要用到snabbdom中的四个指针概念了。

在这里插入图片描述

 那什么是新前旧前、新后旧后呢?

 新前就是新节点(newVnode)中第一个,旧前就是旧节点(oldVnode)中的第一个, 新后旧后同理。

 遵循四种查找方式

1. 例如 第一种查询方式  新前和旧前(A==B)匹配上了  newStartIdx 和 oldStartIdx 都进行 +1  这时候新前旧前指针进行移动,移动后重新执行四种命中方式,   直到四种命中方式都找不到为止。

2. 若四种命中方式都查不着不到,则需要在oldVnode中进行循环查找,找到则插入到oldStartIdx之前,若循环也找不到则 创建出新元素 插入到oldEenIdx之前。 

3. 若新节点中有剩余 代表要新增元素 把新前和新后中间的元素 创建出真实DOM 插入到新后位置的之前。

4.若旧节点中有剩余 代表要删除元素 把旧前和旧后中间的元素 进行删除。

注意:当命中③时 需要移动节点 将当前新指向节点移动到旧节点之后,当命中④时,需要移动节点 将新指向节点移到到旧节点之前。

下面来举一个全面的 场景 以此用到所有命中方便理解

当然 图中最后一步是找不到的场景,当然也有找到的场景,在代码中 在 keyMap 中进行查找 

例如 keyMap  = { A: 0,B:1,C:2 }  若匹配到了需要调用patchVnode函数 并且  需要移到指定的位置上,若匹配不上则插入到旧前节点之前。

export default function updataChild(parentElm, oldCh, newCh) {
  // 旧前
  let oldStartIdx = 0;
  // 新前
  let newStartIdx = 0;
  // 新后
  let newEndIdx = newCh.length - 1;
  // 旧后
  let oldEndIdx = oldCh.length - 1;
  // 旧前节点
  let oldStartVnode = oldCh[0];
  // 旧后节点
  let oldEndVnode = oldCh[oldEndIdx];
  // 新前节点
  let newStartVnode = newCh[0];
  // 新后节点
  let newEndVnode = newCh[newEndIdx];

  let keyMap = null;
  // 进入循环
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // debugger
    // 新前 旧前
    // 1. 如果相同在
    // 首先应该不是判断四种命中,而是略过已经加了undefined标记的项
     if (oldStartVnode === null || oldCh[oldStartIdx] === undefined) {
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (oldEndVnode === null || oldCh[oldEndIdx] === undefined) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (newStartVnode === null || newCh[newStartIdx] === undefined) {
      newStartVnode = newCh[++newStartIdx];
    } else if (newEndVnode === null || newCh[newEndIdx] === undefined) {
      newEndVnode = newCh[--newEndIdx];
    }  else if (checkSameVnode(oldStartVnode, newStartVnode)) {
      // 新前与旧前
      console.log(" ①1 新前与旧前 命中");
      // 精细化比较两个节点 oldStartVnode现在和newStartVnode一样了
      patchVnode(oldStartVnode, newStartVnode);
      // 移动指针,改变指针指向的节点,这表示这两个节点都处理(比较)完了
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (checkSameVnode(oldEndVnode, newEndVnode)) {
      // 新后与旧后
      console.log(" ②2 新后与旧后 命中");
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(oldStartVnode, newEndVnode)) {
      // 新后与旧前
      console.log(" ③3 新后与旧前 命中");
      patchVnode(oldStartVnode, newEndVnode);
      // 当③新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的 旧后的后面
      // 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
      // 新前与旧后
      console.log(" ④4 新前与旧后 命中");
      patchVnode(oldEndVnode, newStartVnode);
      // 当④新前与旧后命中的时候,此时要移动节点。移动 新前(旧后) 指向的这个节点到老节点的 旧前的前面
      // 移动节点:只要插入一个已经在DOM树上的节点,就会被移动
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 四种都没有匹配到,都没有命中
      console.log("四种都没有命中");
      // 寻找 keyMap 一个映射对象, 就不用每次都遍历old对象了
      if (!keyMap) {
        keyMap = {};
        // 记录oldVnode中的节点出现的key
        // 从oldStartIdx开始到oldEndIdx结束,创建keyMap
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
          const key = oldCh[i].key;
          if (key !== undefined) {
            keyMap[key] = i;
          }
        }
      }
      console.log(keyMap);
      // 寻找当前项(newStartIdx)在keyMap中映射的序号
      const idxInOld = keyMap[newStartVnode.key];
      if (idxInOld === undefined) {
        // 如果 idxInOld 是 undefined 说明是全新的项,要插入
        // 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
        parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
      } else {
        // 说明不是全新的项,要移动
        const elmToMove = oldCh[idxInOld];
        patchVnode(elmToMove, newStartVnode);
        // 把这项设置为undefined,表示我已经处理完这项了
        oldCh[idxInOld] = undefined;
        // 移动,调用insertBefore也可以实现移动。
        parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
      }

      // newStartIdx++;
      newStartVnode = newCh[++newStartIdx];
    }
  }

  // 如果新节点还有剩余  要新增
  if (newStartIdx <= newEndIdx) {
    console.log('新节点有剩余',newCh[newEndIdx+1]);
    let before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      parentElm.insertBefore(createElement(newCh[i]), before);
    // parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
    }
  }else if (oldStartIdx <= oldEndIdx) {
    console.log("旧节点有剩余");
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      parentElm.removeChild(oldCh[i].elm);
    }
  }
}

function checkSameVnode(oldStartVnode, newStartVnode) {
  return (
    oldStartVnode.key == newStartVnode.key &&
    oldStartVnode.sel == newStartVnode.sel
  );
}

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

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

相关文章

KDDAC-10kV 电缆交流耐压及振荡波局放测试系统

一、概述 目前对电力电缆检修的管理&#xff0c;主要是依据《电力设备交接和预防性试验规程》所规定的项目和试验周期&#xff0c;定期在停电状态下进行绝缘性能试验。其中变频串联谐振试验由于试验状况接近电缆的运行工况&#xff0c;因此成为国内目前应用广泛的试验方法。 …

社科院与杜兰大学中外合作办学金融管理硕士——30+的年龄在职读研有必要吗?

说起读研&#xff0c;年龄在什么区间最合适呢&#xff1f;上次有位咨询的同学反馈年龄已经快35岁了&#xff0c;有一份不错的工作&#xff0c;但又不甘心止步于此&#xff0c;想要通过提升学历升职加薪&#xff0c;但又纠结自己是否能静下心来学习、是否能顺利毕业、拿到的证书…

[HSCSEC 2023] rev,pwn,crypto,Ancient-MISC部分

比赛后有讲解&#xff0c;没赶上&#xff0c;前20比赛完1小时提交WP&#xff0c;谁会大半夜的起来写WP。总的感觉pwn,crypto过于简单&#xff0c;rev有2个难的不会&#xff0c;其它不是我的方向都感觉过于难&#xff0c;一个都没作。revDECOMPILEONEOONE入门题&#xff0c;一个…

12 循环神经网络(基础篇) Basic RNN

文章目录问题引入关于权重权重共享RNN CellRNN原理RNN计算过程代码实现RNN Cell维度说明代码RNN维度说明NumLayers说明计算过程代码参考实例问题分析多分类问题代码RNN CellRNN改进Embedding网络结构Embedding说明Linear说明代码课程来源&#xff1a; 链接课程文本参考&#xf…

前端react面试题指南

概述下 React 中的事件处理逻辑 抹平浏览器差异&#xff0c;实现更好的跨平台。避免垃圾回收&#xff0c;React 引入事件池&#xff0c;在事件池中获取或释放事件对象&#xff0c;避免频繁地去创建和销毁。方便事件统一管理和事务机制。 为了解决跨浏览器兼容性问题&#xff0…

CSS基础:盒子模型和浮动

盒子模型 所有HTML元素可以看作盒子&#xff0c;在CSS中&#xff0c;"box model"这一术语是用来设计和布局时使用 CSS盒模型本质上是一个盒子&#xff0c;封装HTML元素。 它包括&#xff1a;外边距&#xff08;margin&#xff09;&#xff0c;边框&#xff08;bord…

操作系统考试突击复习笔记

0 基础概念补充特权命令&#xff1a;有特殊权限的指令&#xff0c;比如清内存、置时钟、分配系统资源、修改虚拟内存的段表和页表&#xff0c;修改用户的访问权限。系统调用&#xff1a;操作系统为应用程序提供的使用接口&#xff0c;可以理解为一种可供应用程序调用的特殊函数…

Elasticsearch7.8.0版本进阶——数据写流程

目录一、数据写流程概述二、数据写流程步骤2.1、数据写流程图2.2、数据写流程步骤&#xff08;新建索引和删除文档所需要的步骤顺序&#xff09;2.3、数据写流程的请求参数一、数据写流程概述 新建、删除索引和新建、删除文档的请求都是写操作&#xff0c; 必须在主分片上面完…

http409报错原因

今天一个同事的接口突然报409,大概百度了一下,不是很清楚,谷歌也没找到特别好的解释 因为是直接调用的gitlab,就直接看了下gitlab的api The following table shows the possible return codes for API requests. Return valuesDescription200 OKThe GET, PUT or DELETE request…

【halcon】灰度直方图直观理解与应用

灰度直方图 横坐标&#xff1a;是 0~255 表示灰度值的范围 纵坐标&#xff1a;是在不同灰度值下像素的个数&#xff01; 那么灰度直方图的本质就是统计不同灰度下像素的个数&#xff01; 它的直观目的&#xff0c;就是查看灰度的分布情况&#xff01; 与之相关的函数&#xff…

宝塔搭建实战php开源likeadmin通用管理pc端nuxt3源码(三)

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 昨天给大家分享了admin前端的搭建部署方式&#xff0c;今天来给大家分享pc端在本地搭建&#xff0c;与打包发布到宝塔的方法&#xff0c;希望能够帮助到大家&#xff0c;感兴趣的朋友可以自行下载学习。 技术架构…

C语言在游戏中播放音乐

使用 mciSendString 播放音乐 mciSendString 支持 mp3、wma、wav、mid 等多种媒体格式&#xff0c;使用非常简单。这里做一个简单的范例&#xff0c;用 mciSendString 函数播放 MP3 格式的音乐&#xff0c;代码如下&#xff1a; // 编译该范例前&#xff0c;请把 music.mp3 放…

Kotlin实现简单音乐播放器

关于音乐播放器&#xff0c;我真的是接触比较多&#xff0c;听歌作为我第一大爱好&#xff0c;之前也用Java设计过音乐播放器&#xff0c;感兴趣的同学可以阅读&#xff1a;Android Studio如何实现音乐播放器&#xff08;简单易上手&#xff09;和 Android Studio实现音乐播放器…

全链路异步,让你的 SpringCloud 性能优化10倍+

背景 随着业务的发展&#xff0c;微服务应用的流量越来越大&#xff0c;使用到的资源也越来越多。 在微服务架构下&#xff0c;大量的应用都是 SpringCloud 分布式架构&#xff0c;这种架构&#xff0c;总体是全链路同步模式。 同步编程模式不仅造成了资源的极大浪费&#x…

设计模式:适配器模式(c++实现案例)

适配器模式 适配器模式是将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。好比日本现在就只提供110V的电压&#xff0c;而我的电脑就需要220V的电压&#xff0c;那怎么办啦?适配器就是干这活的&#xff0…

CVE-2019-2725

//去年存货 前言 在学习内网过程中遇到了weblogic比较常见的漏洞&#xff0c;编号是cve-2019-2725,之前没有总结过&#xff0c;于是本篇文章给大家总结归纳一下该漏洞的利用方法与原理。 基础知识 cve-2019-2725漏洞的核心利用点是weblogic的xmldecoder反序列化漏洞&#x…

Bean(Spring)的执行流程和生命周期

Bean&#xff08;Spring&#xff09;的执行流程具体的流程就和我们创建Spring基本相似。启动 Spring 容器 -> 实例化 Bean&#xff08;分配内存空间&#xff0c;从无到有&#xff09; -> Bean 注册到 Spring 中&#xff08;存操作&#xff09; -> 将 Bean 装配到需要的…

基于微信小程序的医院挂号系统小程序

文末联系获取源码 开发语言&#xff1a;Java 框架&#xff1a;ssm JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏览器…

C# 泛型集合中的排序

集合排序问题与ICompareble对于C#最常见的集合List<T>&#xff0c;有时候需要进行排序&#xff0c;而List是直接有Sort方法&#xff0c;因此对于一个简单地整数集合排序&#xff0c;很简单&#xff1a;List<int> ints new List<int>() { 1, 3, 1, 4, 5, 9, …

LeetCode-222. 完全二叉树的节点个数

目录递归法改进题目来源222. 完全二叉树的节点个数递归法 递归三步曲 1.确定递归函数的参数和返回值 参数就是传入树的根节点&#xff0c;返回就返回以该节点为根节点二叉树的节点数量&#xff0c;所以返回值为int类型。 int countNodes(TreeNode root)2.确定终止条件 如果…