【Vue2.0源码学习】虚拟DOM篇-Vue中的DOM-优化更新子节点

news2024/11/26 21:52:37

1. 前言

在上一篇文章中,我们介绍了当新的VNode与旧的oldVNode都是元素节点并且都包含子节点时,Vue对子节点是

先外层循环newChildren数组,再内层循环oldChildren数组,每循环外层newChildren数组里的一个子节点,就去内层oldChildren数组里找看有没有与之相同的子节点,最后根据不同的情况作出不同的操作。

在上一篇文章的结尾我们也说了,这种方法虽然能够解决问题,但是还存在可优化的地方。比如当包含的子节点数量很多时,这样循环算法的时间复杂度就会变的很大,不利于性能提升。当然,Vue也意识到了这点,并对此也进行了优化,那么本篇文章,就来学习一下关于子节点更新的优化问题Vue是如何做的。

2. 优化策略介绍

假如我们现有一份新的newChildren数组和旧的oldChildren数组,如下所示:

newChildren = ['新子节点1','新子节点2','新子节点3','新子节点4']
oldChildren = ['旧子节点1','旧子节点2','旧子节点3','旧子节点4']

如果按照优化之前的解决方案,那么我们接下来的操作应该是这样的:先循环newChildren数组,拿到第一个新子节点1,然后用第一个新子节点1去跟oldChildren数组里的旧子节点逐一对比,如果运气好一点,刚好oldChildren数组里的第一个旧子节点1与第一个新子节点1相同,那就皆大欢喜,直接处理,不用再往下循环了。那如果运气坏一点,直到循环到oldChildren数组里的第四个旧子节点4才与第一个新子节点1相同,那此时就会多循环了4次。我们不妨把情况再设想的极端一点,如果newChildren数组和oldChildren数组里前三个节点都没有变化,只是第四个节点发生了变化,那么我们就会循环16次,只有在第16次循环的时候才发现新节点4与旧节点4相同,进行更新,如下图所示:
在这里插入图片描述

上面例子中只有四个子节点,好像还看不出来有什么缺陷,但是当子节点数量很多的时候,算法的时间复杂度就会非常高,很不利于性能提升。

那么我们该怎么优化呢?其实我们可以这样想,我们不要按顺序去循环newChildrenoldChildren这两个数组,可以先比较这两个数组里特殊位置的子节点,比如:

  • 先把newChildren数组里的所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作;
  • 如果不同,再把newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作;
  • 如果不同,再把newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;
  • 如果不同,再把newChildren数组里所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;
  • 最后四种情况都试完如果还不同,那就按照之前循环的方式来查找节点。

其过程如下图所示:

在这里插入图片描述

在上图中,我们把:

  • newChildren数组里的所有未处理子节点的第一个子节点称为:新前;
  • newChildren数组里的所有未处理子节点的最后一个子节点称为:新后;
  • oldChildren数组里的所有未处理子节点的第一个子节点称为:旧前;
  • oldChildren数组里的所有未处理子节点的最后一个子节点称为:旧后;

OK,有了以上概念以后,下面我们就来看看其具体是如何实施的。

3. 新前与旧前

newChildren数组里的所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那好极了,直接进入之前文章中说的更新节点的操作并且由于新前与旧前两个节点的位置也相同,无需进行节点移动操作;如果不同,没关系,再尝试后面三种情况。在这里插入图片描述

4. 新后与旧后

newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作并且由于新后与旧后两个节点的位置也相同,无需进行节点移动操作;如果不同,继续往后尝试。在这里插入图片描述

5. 新后与旧前

newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;
在这里插入图片描述

此时,出现了移动节点的操作,移动节点最关键的地方在于找准要移动的位置。我们一再强调,更新节点要以新VNode为基准,然后操作旧的oldVNode,使之最后旧的oldVNode与新的VNode相同。那么现在的情况是:newChildren数组里的最后一个子节点与oldChildren数组里的第一个子节点相同,那么我们就应该在oldChildren数组里把第一个子节点移动到最后一个子节点的位置,如下图:

在这里插入图片描述

从图中不难看出,我们要把oldChildren数组里把第一个子节点移动到数组中所有未处理节点之后

如果对比之后发现这两个节点仍不是同一个节点,那就继续尝试最后一种情况。

6. 新前与旧后

newChildren数组里所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;

在这里插入图片描述

同样,这种情况的节点移动位置逻辑与“新后与旧前”的逻辑类似,那就是newChildren数组里的第一个子节点与oldChildren数组里的最后一个子节点相同,那么我们就应该在oldChildren数组里把最后一个子节点移动到第一个子节点的位置,如下图:

在这里插入图片描述

从图中不难看出,我们要把oldChildren数组里把最后一个子节点移动到数组中所有未处理节点之前

OK,以上就是子节点对比更新优化策略种的4种情况,如果以上4种情况逐个试遍之后要是还没找到相同的节点,那就再通过之前的循环方式查找。

7. 回到源码

思路分析完,逻辑理清之后,我们再回到源码里看看,验证一下源码实现的逻辑是否跟我们分析的一样。源码如下:

// 循环更新子节点
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0               // oldChildren开始索引
    let oldEndIdx = oldCh.length - 1   // oldChildren结束索引
    let oldStartVnode = oldCh[0]        // oldChildren中所有未处理节点中的第一个
    let oldEndVnode = oldCh[oldEndIdx]   // oldChildren中所有未处理节点中的最后一个

    let newStartIdx = 0               // newChildren开始索引
    let newEndIdx = newCh.length - 1   // newChildren结束索引
    let newStartVnode = newCh[0]        // newChildren中所有未处理节点中的第一个
    let newEndVnode = newCh[newEndIdx]  // newChildren中所有未处理节点中的最后一个

    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    // 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // 如果oldStartVnode不存在,则直接跳过,比对下一个
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 如果新前与旧前节点相同,就把两个节点进行patch更新
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 如果新后与旧后节点相同,就把两个节点进行patch更新
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 如果不属于以上四种情况,就进行常规的循环比对patch
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 如果在oldChildren里找不到当前循环的newChildren里的子节点
        if (isUndef(idxInOld)) { // New element
          // 新增节点并插入到合适位置
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 如果在oldChildren里找到了当前循环的newChildren里的子节点
          vnodeToMove = oldCh[idxInOld]
          // 如果两个节点相同
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 调用patchVnode更新节点
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            // canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      /**
       * 如果oldChildren比newChildren先循环完毕,
       * 那么newChildren里面剩余的节点都是需要新增的节点,
       * 把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
       */
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      /**
       * 如果newChildren比oldChildren先循环完毕,
       * 那么oldChildren里面剩余的节点都是需要删除的节点,
       * 把[oldStartIdx, oldEndIdx]之间的所有节点都删除
       */
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

读源码之前,我们先有这样一个概念:那就是在我们前面所说的优化策略中,节点有可能是从前面对比,也有可能是从后面对比,对比成功就会进行更新处理,也就是说我们有可能处理第一个,也有可能处理最后一个,那么我们在循环的时候就不能简单从前往后或从后往前循环,而是要从两边向中间循环。

那么该如何从两边向中间循环呢?请看下图:

在这里插入图片描述

首先,我们先准备4个变量:

  • newStartIdx:newChildren数组里开始位置的下标;
  • newEndIdx:newChildren数组里结束位置的下标;
  • oldStartIdx:oldChildren数组里开始位置的下标;
  • oldEndIdx:oldChildren数组里结束位置的下标;

在循环的时候,每处理一个节点,就将下标向图中箭头所指的方向移动一个位置,开始位置所表示的节点被处理后,就向后移动一个位置;结束位置所表示的节点被处理后,就向前移动一个位置;由于我们的优化策略都是新旧节点两两更新的,所以一次更新将会移动两个节点。说的再直白一点就是:newStartIdxoldStartIdx只能往后移动(只会加),newEndIdxoldEndIdx只能往前移动(只会减)。

当开始位置大于结束位置时,表示所有节点都已经遍历过了。

OK,有了这个概念后,我们开始读源码:

  1. 如果oldStartVnode不存在,则直接跳过,将oldStartIdx加1,比对下一个

    // 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
     if (isUndef(oldStartVnode)) {
       oldStartVnode = oldCh[++oldStartIdx]
     }
    }
    
  2. 如果oldEndVnode不存在,则直接跳过,将oldEndIdx减1,比对前一个

    else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
    }
    
  3. 如果新前与旧前节点相同,就把两个节点进行patch更新,同时oldStartIdxnewStartIdx都加1,后移一个位置

    else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
    }
    
  4. 如果新后与旧后节点相同,就把两个节点进行patch更新,同时oldEndIdxnewEndIdx都减1,前移一个位置

    else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
    }
    
  5. 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后,最后把oldStartIdx加1,后移一个位置,newEndIdx减1,前移一个位置

    else if (sameVnode(oldStartVnode, newEndVnode)) {
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
    }
    
  6. 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前,最后把newStartIdx加1,后移一个位置,oldEndIdx减1,前移一个位置

    else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
    }
    
  7. 如果不属于以上四种情况,就进行常规的循环比对patch

  8. 如果在循环中,oldStartIdx大于oldEndIdx了,那就表示oldChildrennewChildren先循环完毕,那么newChildren里面剩余的节点都是需要新增的节点,把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM

    if (oldStartIdx > oldEndIdx) {
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    }
    
  9. 如果在循环中,newStartIdx大于newEndIdx了,那就表示newChildrenoldChildren先循环完毕,那么oldChildren里面剩余的节点都是需要删除的节点,把[oldStartIdx, oldEndIdx]之间的所有节点都删除

    else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
    

OK,处理完毕,可见源码中的处理逻辑跟我们之前分析的逻辑是一样的。

8. 总结

本篇文章中,我们介绍了Vue中子节点更新的优化策略,发现Vue为了避免双重循环数据量大时间复杂度升高带来的性能问题,而选择了从子节点数组中的4个特殊位置互相比对,分别是:新前与旧前,新后与旧后,新后与旧前,新前与旧后。对于每一种情况我们都通过图文的形式对其逻辑进行了分析。最后我们回到源码,通过阅读源码来验证我们分析的是否正确。幸运的是我们之前每一步的分析都在源码中找到了相应的实现,得以验证我们的分析没有错。以上就是Vue中的patch过程,即DOM-Diff算法所有内容了,到这里相信你再读这部分源码的时候就有比较清晰的思路了。

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

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

相关文章

《HelloGitHub》第 86 期

兴趣是最好的老师&#xff0c;HelloGitHub 让你对编程感兴趣&#xff01; 简介 HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。 https://github.com/521xueweihan/HelloGitHub 这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等&#xff0c;涵盖多种编程语言 …

华为OD机试真题B卷 Java 实现【旋转数组的最小数字】,附详细解题思路

一、题目描述 有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。 二、输入描述 3,4,5,1,2 …

MyBatis缓存和二级缓存整合Redis

MyBatis缓存和二级缓存整合Redis ⼀级缓存缓存验证在⼀个sqlSession中&#xff0c;对user表根据username进⾏两次查询&#xff0c;查看他们发出sql语句的情况同样是对user表进⾏两次查询&#xff0c;只不过两次查询之间进⾏了⼀次update操作。总结 ⼀级缓存原理探究与源码分析 …

安装 Nginx 修改默认端口

用远程工具连接我们上次购买的机器&#xff0c;这里我要介绍一个知识点&#xff0c;博主使用的工具是 MobaXterm&#xff0c;这个工具有一个多操作的功能&#xff0c;在下图的位置可以开启多操作&#xff0c;然后连接你的服务器机子即可&#xff1a; 首先我们将机子里面的依赖源…

【JavaSE】Java基础语法(三十四):实现多线程

文章目录 1. 简单了解多线程2. 并发和并行3. 进程和线程4. 实现多线程方式一&#xff1a;继承Thread类【应用】5. 实现多线程方式二&#xff1a;实现Runnable接口【应用】6. 实现多线程方式三: 实现Callable接口【应用】7. 设置和获取线程名称【应用】8. 线程休眠【应用】9. 线…

[PyTorch][chapter 36][经典卷积神经网络-1 ]

前言&#xff1a; ILSVRC&#xff08;ImageNet Large Scale Visual Recognition Challenge&#xff09;是近年来机器视觉领域最受追捧也是最具权威的学术竞赛之一&#xff0c;代表了图像领域的最高水平。 ImageNet数据集是ILSVRC竞赛使用的是数据集&#xff0c;由斯坦福大学李…

多线程屏障CyclicBarrier

文章目录 前言一、CyclicBarrier可以做什么&#xff1f;二、使用步骤1 单参数CyclicBarrier2 多参数 CyclicBarrier3 与CyclicBarrier类似的Exchanger 总结 前言 多线程中的CyclicBarrier,同样也是juc包下的一个工具类; 一、CyclicBarrier可以做什么&#xff1f; CyclicBarri…

C#,码海拾贝(28)——求解“对称正定方程组”的“平方根法”之C#源代码,《C#数值计算算法编程》源代码升级改进版

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 求解线性方程组的类 LEquations /// 原作 周长发 /// 改编 深度混淆 /// </summary> public static partial class LEquations { /// <summary> /…

【译】Google Guava 的 Table 接口介绍

原文&#xff1a;https://www.baeldung.com/guava-table 1. 概述 在本教程中&#xff0c;我们将展示如何使用 Google Guava 的 Table 接口及其多个实现。 Guava 的 Table 是一种集合&#xff0c;表示包含行、列和相关单元格值的表结构&#xff0c;行和列充当有序的键对。 2…

React Native开发速记

文章目录 引子React Native适用场景React基础JSX 组件的定义基础APIFlex弹性布局例子: Flex布局实现多行多列 常用UI组件几个核心钩子函数useState用法useEffect典型用法 和原生模块交互调用原生模块方法 调试其它工具UI框架参考资源 引子 软件开发&#xff0c;移动优先&#…

webAJAX概述.

1.1什么是AJAX. Ajax即AsynchronousJavascript And XML&#xff1a;异步数据回调。 使用Ajax技术网页应用能够快速地将更新呈现在用户界面上&#xff0c;不需要重载&#xff08;刷新&#xff09;整个页面【只刷新局部】&#xff0c;这使得程序能够更快地回应用户的操作。、 1…

使用Node. js输出到命令行

目录 1、使用控制台模块的基本输出 2、清除控制台 3、计数元素 4、复位计数 5、打印堆栈跟踪 6、计算花费的时间 7、stdout和stderr 8、为输出着色 9、创建进度条 1、使用控制台模块的基本输出 Node.js提供了一个console模块&#xff0c;它提供了大量非常有用的与命令…

Qt Quick系列(4)—定位元素

&#x1f680;作者&#xff1a;CAccept &#x1f382;专栏&#xff1a;Qt Quick 文章目录 前言相对布局代码示例示例一示例二示例三示例四示例五示例六 简单"布局器"ColumnRowGridFlow 结语 前言 在Qt Quick中&#xff0c;可以使用以下方式来定位元素&#xff1a;…

需要建立强大的网络响应框架

由于头条新闻充斥着网络攻击&#xff0c;因此企业制定网络响应框架变得前所未有的重要。当今的网络安全形势继续快速发展&#xff0c;黑客行动主义、民族国家支持的网络攻击、勒索软件和其他攻击策略变得更加危险、复杂&#xff0c;组织的防御成本也越来越高。随着企业进行数字…

华为OD机试真题B卷 Java 实现【名字的漂亮度】,附详细解题思路

一、题目描述 给出一个字符串&#xff0c;该字符串仅由小写字母组成&#xff0c;定义这个字符串的“漂亮度”是其所有字母“漂亮度”的总和。 每个字母都有一个“漂亮度”&#xff0c;范围在1到26之间。没有任何两个不同字母拥有相同的“漂亮度”。字母忽略大小写。 给出多个…

Ex-ChatGPT本地部署+Azure OpenAI接口配置+docker部署服务

Ex-ChatGPT项目分为 Ex-ChatGPT 和 WebChatGPTEnhance 两部分&#xff0c;Ex-ChatGPT启动后是个web服务&#xff0c;通过访问ip端口体验&#xff1b; WebChatGPTEnhance可编译生成一个浏览器插件&#xff0c;Chrome或者Microsoft edge浏览器可以安装该插件&#xff0c;点击该插…

Golang中文件目录操作的实现

目录 文件 文件目录 文件目录操作 读取文件 一、方法一 (file.Read()) 二、方法二 (bufio读取文件) 三、方法三 (ioutil 读取方法) 写入文件 一、方法一 二、方法二 三、方法三 (ioutil写入文件) 复制文件 一、方法一 二、方法二 文件 Golang中&#xff0c;文件是…

python异常处理速通

一.异常处理认识 1.基础认识 开发人员在编写程序时&#xff0c;难免会遇到错误&#xff0c;有的是编写人员疏忽造成的语法错误&#xff0c;有的是程序内部隐含逻辑问题造成的数据错误&#xff0c;还有的是程序运行时与系统的规则冲突造成的系统错误&#xff0c;等等。总的来说…

什么是肖特基二极管

普通二极管是由N型半导体和P型半导体接触制成&#xff0c;交界面形成PN结。 肖特基二极管是由N型半导体和金属接触制成&#xff0c;交界面形成肖特基结。 肖特基结的形成主要是因为N型半导体中的电子更容易逸出进入到金属&#xff0c;从而在接触面N型半导体失去电子形成正离子区…

python:绘制GAM非线性回归散点图和拟合曲线

作者&#xff1a;CSDN _养乐多_ 本文将介绍使用python语言绘制广义线性模型&#xff08;Generalized Additive Model&#xff0c;GAM&#xff09;非线性回归散点图和拟合曲线。并记录了计算RMSE、ubRMSE、R2、Bias的代码。 文章目录 一、GAM非线性回归详解二、代码三、计算RM…