双端Diff算法

双端Diff算法

双端Diff算法指的是,在新旧两组子节点的四个端点之间分别进行比较,并试图找到可复用的节点。相比简单Diff算法,双端Diff算法的优势在于,对于同样的更新场景,执行的DOM移动操作次数更少。

  1. 简单 Diff 算法(单向 Diff):
  • 工作原理:简单 Diff 算法从一个序列的起始位置开始,逐个比较元素,找出不同之处。

  • 特点:

    • 顺序性:仅从一个方向进行比较,一旦发现不同之处,就会停止继续比较。

    • 复杂度:时间复杂度为 O(n),其中 n 是序列的长度。

    • 结果不唯一:因为只按照一个方向进行比较,可能会忽略一些潜在的更优的匹配方式。

  1. 双端 Diff 算法(双向 Diff):
  • 工作原理:双端 Diff 算法同时从两个序列的起始位置开始,向中间移动,逐个比较元素,找出不同之处。
  • 特点:
    - 双向性:同时从两个方向进行比较,可以更全面地考虑匹配情况。
    - 优化效率:通过跳过相同的前缀和后缀部分,减少了比较的次数,提高了效率。
    - 复杂度:时间复杂度为 O(n+m),其中 n 和 m 分别是两个序列的长度。
    - 结果更准确:考虑了更多的匹配可能性,得到更准确的差异结果。

双端Diff算法的比较方式

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

双端Diff算法是一种同时对新旧两组子节点的两个端点进行比较的算法,因此我们需要四个索引值,分别指向新旧两组节点的端点。

function patchChildren(n1, n2, container) {
  if (typeof n2.children === 'string') {
    // 省略部分代码
  } else if (Array.isArray(n2.children)) {
    // 封装 patchKeyedChildren 函数处理两组子节点
    patchKeyedChildren(n1, n2, container);
  } else {
    // 省略部分代码
  }
}
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引值
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引指向的 vnode 节点
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];
}

上面的代码中,我们将两组子节点的打补丁工程封装到了 patchKeyedChildren 函数中。该函数中,先获取两组新旧子节点 oldChildren 和 newChildren,然后创建四个索引值,分别指向新旧两组子节点的收尾,即 oldStartIdx(简称为旧前)、oldEndIdx(简称为旧后)、newStartIdx(简称为新前)、newEndIdx(简称为新后),以及四个索引值对应的 vnode。其中 oldStartVNode 和 oldEndVNode 是旧的一组子节点的第一个节点和最后一个节点,newStartVNode 和 newEndVNode 则是新的一组子节点的第一个子节点和最后一个子节点。

双端比较,每一轮均分为四个步骤:

  1. 比较旧的一组子节点的第一个子节点p-1(简称为旧前)于新的一组子节点的第一个子节点p-4(简称为新前),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  2. 比较旧的一组子节点的最后一个子节点p-4(简称为旧后)于新的一组子节点的最后一个子节点p-3(简称为新后),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  3. 比较旧的一组子节点的第一个子节点p-1(简称为旧前)于新的一组子节点的最后一个子节点p-3(简称为新后),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  4. 比较旧的一组子节点的最后一个子节点p-4(简称为旧后)于新的一组子节点的第一个子节点p-4(简称为新前)。由于他们的 key 相同,因此可以进行DOM复用。

四个步骤命中任何一步均说明命中的节点可以进行DOM复用,因此后续只需要进行DOM移动操作完成更新即可。

function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引值
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引指向的 vnode 节点
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];
  while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
    if (oldStartVNode.key === newStartVNode.key) {
      // 步骤一:oldStartVNode 和 newStartVNode 比较
      // 调用 patch 函数在 oldStartIdx 和 newStartIdx 之间打补丁
      patch(oldStartVNode, newStartVNode, container);
      // 更新相关索引到下一个位置
      oldStartVNode = oldChildren[++oldStartIdx];
      newStartVNode = newEndVNode[++newEndIdx];
    } else if(oldEndVNode.key === newEndVNode.key) {
      // 步骤二:oldEndVNode 和 newEndVNode 比较
      // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
      patch(oldEndVNode, newEndVNode, container);
      // 更新索引和头尾部节点变量
      newEndVNode = newChildren[--newEndIdx];
      oldEndVNode = oldChildren[--oldEndIdx];
    } else if(oldStartVNode.key === newEndVNode.key) {
      // 步骤三:oldStartVNode 和 newEndVNode 比较
      // 调用 patch 函数在 oldStartIdx 和 newEndIdx之间打补丁
      patch(oldStartVNode, newEndVNode, container);
      // 将旧的一组子节点的头部节点对应的真是节点 DOM 节点 oldStartVNode.el 移动
      // 到旧一组子节点的尾部节点对应的真实 DOM 节点后面
      insert(oldStartVNode.el, container, oldEndVNode.el.nextSibiling);
      // 更新相关索引到下一个位置
      oldStartVNode = oldChildren[++oldStartIdx];
      newEndVNode = newChildren[--newEndIdx];
    } else if(oldEndVNode.key === newStartVNode.key) {
      // 步骤四:oldEndVNode 和 newStartVNode 比较
      // 仍然需要调用 patch 函数进行打补丁
      patch(oldEndVNode, newStartVNode, container);
      // 移动 DOM 操作
      // oldEndVNode.el 移动到 oldStartVNode.el 前面
      insert(oldEndVNode.el, container, oldStartVNode.el);
      // 移动 DOM 完成后,更新索引值,并指向下一个位置
      oldEndVNode = oldChildren[--oldEndIdx];
      newStartVNode = newChildren[++newStartIdx];
    }
  }
}

上述代码:

  • 步骤四中找到了具有相同key值的节点,说明原来处于尾部的节点在新的顺序周中应该处于头部。因此我们只需要以头部元素 oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可,移动前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。然后还需要更新索引,oldEndIdx 向上移动,newStartIdx 向下移动,同时更新 oldEndVNode 和 newStartVNode。
  • 步骤三中找到了具有相同key值的节点,说明原来处于头部的节点在新的顺序中应该处于尾部。因此我们需要以 oldEndVNode.el.nextSibiling(旧一组节点的尾部节点对应的真实 DOM 节点的兄弟节点) 作为锚点,将头部元素 oldStartVNode.el 移动到锚点之前即可,移动前需要调用 patch 函数对新旧虚拟节点进行打补丁,然后更新相关的索引,oldStartIdx 向下移动,newEndIdx 向上移动,同时更新 oldStartVNode 和 newEndVNode。
  • 步骤二中找到了具有相同key值的节点,说明原来处于尾部的节点在新的顺序中仍然处于尾部,因此不需要进行移动,调用 patch 函数进行新旧虚拟节点打补丁,然后更新相关索引,newEndIdx 和 oldEndIdx 均向上移动,同时更新 newEndVNode 和 oldEndVNode。
  • 步骤一中找到了具有相同key值的节点,说明原来处于头部的节点在新的顺序中仍然处于头部,因此不需要进行移动,调用 patch 函数进行新旧虚拟节点打补丁,然后更新相关索引,oldStartIdx 和 newEndIdx 均向下移动,同时更新 oldStartVNode 和 newStartVNode。

非理想状况的处理方式

在这里插入图片描述

在四个步骤的比较过程中,都无法找到可复用的节点,此时我们通过增加额外的处理步骤(盘外招)来处理这种情况:拿新的一组子节点中的头部节点去旧的一组子节点中寻找,如下代码所示:

while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
  if (oldStartVNode.key === newStartVNode.key) {
    // 省略部分代码
  } else if(oldEndVNode.key === newEndVNode.key) {
    // 省略部分代码
  } else if(oldStartVNode.key === newEndVNode.key) {
    // 省略部分代码
  } else if(oldEndVNode.key === newStartVNode.key) {
    // 省略部分代码
  } else {
    // 遍历旧的一组节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
    // idxInOld 就是新的一组子节点的头部节点在旧的一组节点中的索引
    const idxInOld = oldChildren.findIndex(ele => ele.key === newStartVNode.key);
  }
}

在这里插入图片描述

上图中我们拿新的一组子节点的头部节点p-2去旧的一组子节点中查找,在索引为1的位置找到了可复用的节点。然后将节点p-2对应的真实 DOM 节点移动到当前旧的一组子节点的头部节点p-1所对应的真实 DOM 节点之前。

while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
  if (oldStartVNode.key === newStartVNode.key) {
    // 省略部分代码
  } else if(oldEndVNode.key === newEndVNode.key) {
    // 省略部分代码
  } else if(oldStartVNode.key === newEndVNode.key) {
    // 省略部分代码
  } else if(oldEndVNode.key === newStartVNode.key) {
    // 省略部分代码
  } else {
    // 遍历旧的一组节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
    // idxInOld 就是新的一组子节点的头部节点在旧的一组节点中的索引
    const idxInOld = oldChildren.findIndex(ele => ele.key === newStartVNode.key);
    // idxInOld > 0,说明找了可复用的节点,并且需要将其对应的真实 DOM 移动到头部
    if (idxInOld) {
      // idxInOld 位置对应的 vnode 就是需要移动的节点
      const vnodeToMove = oldChildren[idxInOld];
      // 不要忘记除移动操作之外还应该打补丁
      patch(vnodeToMove, newStartVNode, container);
      // 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此将后者作为锚点
      insert(vnodeToMove.el, container, oldStartVNode.el);
      // 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动了别处,因此将其设置为 undefined
      oldChildren[idxInOld] = undefined;
      // 最后更新 newStartIdx 到下一个位置
      newStartVNode = newChildren[++newStartIdx];
    }
  }
}

上面代码中,首先判断 idxInOld是否大于0,条件成立,说明找到了可复用的节点,然后将该节点进行移动。先获取需要移动的节点(oldChildren[idxInOld]),移动前进行打补丁,然后找到锚点(oldStartVNode.el),调用 insert 函数完成节点移动。移动完成后,需要将 oldChildren[idxInOld] 设置为undefined,同时更新 newStartIdx (向下移动)。

在这里插入图片描述

然后再进行双端Diff的四个步骤,进行节点移动和更新操作。由于旧的一组子节点中存在 undefined(说明该节点已经被处理过,直接跳过即可)因此我们需要对代码进行补充。

while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
  // 增加两个判断分支,如果头部节点为undefined,说明该节点被处理过,直接跳到下一个位置
  if (!oldStartVNode) {
    oldStartVNode = oldChildren[++oldStartIdx];
  } else if (!oldEndVNode) {
    oldEndVNode = oldChildren[--endStartIdx];
  } else if (oldStartVNode.key === newStartVNode.key) {
    // 省略部分代码
  } else if(oldEndVNode.key === newEndVNode.key) {
    // 省略部分代码
  } else if(oldStartVNode.key === newEndVNode.key) {
    // 省略部分代码
  } else if(oldEndVNode.key === newStartVNode.key) {
    // 省略部分代码
  } else {
    // 遍历旧的一组节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
    // idxInOld 就是新的一组子节点的头部节点在旧的一组节点中的索引
    const idxInOld = oldChildren.findIndex(ele => ele.key === newStartVNode.key);
    // idxInOld > 0,说明找了可复用的节点,并且需要将其对应的真实 DOM 移动到头部
    if (idxInOld) {
      // idxInOld 位置对应的 vnode 就是需要移动的节点
      const vnodeToMove = oldChildren[idxInOld];
      // 不要忘记除移动操作之外还应该打补丁
      patch(vnodeToMove, newStartVNode, container);
      // 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此将后者作为锚点
      insert(vnodeToMove.el, container, oldStartVNode.el);
      // 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动了别处,因此将其设置为 undefined
      oldChildren[idxInOld] = undefined;
      // 最后更新 newStartIdx 到下一个位置
      newStartVNode = newChildren[++newStartIdx];
    }
  }
}

添加新元素

前面说了非理想情况的处理,即在一轮比较过程中,不会命中四个步骤中的任何一步。这时我们会拿新的一组子节点中的头部节点去旧的一组子节点中寻找可复用的节点,然而并非总能找到。如下所示:

在这里插入图片描述

我们发现经过四个步骤,均没有命中且p-4节点在旧的一组也没有相同的 key 值对应的节点,因此可得p-4节点是一个新增节点,我们应该将该节点挂载到正确的位置上。因为p-4节点是新的一组子节点中的头部节点,所以需要将它挂载到当前头部节点(旧的一组子节点的头部节点p-1)的前面即可。

while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
  // 增加两个判断分支,如果头部节点为undefined,说明该节点被处理过,直接跳到下一个位置
  if (!oldStartVNode) {
    oldStartVNode = oldChildren[++oldStartIdx];
  } else if (!oldEndVNode) {
    oldEndVNode = oldChildren[--endStartIdx];
  } else if (oldStartVNode.key === newStartVNode.key) {
    // 省略部分代码
  } else if(oldEndVNode.key === newEndVNode.key) {
    // 省略部分代码
  } else if(oldStartVNode.key === newEndVNode.key) {
    // 省略部分代码
  } else if(oldEndVNode.key === newStartVNode.key) {
    // 省略部分代码
  } else {
    // 遍历旧的一组节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
    // idxInOld 就是新的一组子节点的头部节点在旧的一组节点中的索引
    const idxInOld = oldChildren.findIndex(ele => ele.key === newStartVNode.key);
    // idxInOld > 0,说明找了可复用的节点,并且需要将其对应的真实 DOM 移动到头部
    if (idxInOld) {
      // idxInOld 位置对应的 vnode 就是需要移动的节点
      const vnodeToMove = oldChildren[idxInOld];
      // 不要忘记除移动操作之外还应该打补丁
      patch(vnodeToMove, newStartVNode, container);
      // 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此将后者作为锚点
      insert(vnodeToMove.el, container, oldStartVNode.el);
      // 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动了别处,因此将其设置为 undefined
      oldChildren[idxInOld] = undefined;
      // 最后更新 newStartIdx 到下一个位置
      newStartVNode = newChildren[++newStartIdx];
    } else {
      // 将 newStartVNode 作为新的节点挂载到头部,使用当前头部节点 oldStartVNode.el 作为锚点
      patch(null, newStartVNode, container, oldStartVNode.el);
    }
    newStartVNode = newChildren[++newStartIdx];
  }
}

上面的代码所示,当条件 idxInOld > 0 不成立时,说明 newStartVNode 节点是全新的节点。又由于

newStartVNode 节点是头部节点,因此我们应该将其作为新的头部节点进行挂载。因此我们在调用 patch 函数挂载节点时,我们使用 oldStartVNode.el 作为锚点。这步操作完成之后,新旧两组子节点以及真实 DOM 节点如下图所示。

在这里插入图片描述

除了上述新增节点的方式,还有另外一种情况,如下所示:

在这里插入图片描述

在经历三轮更新之后,新旧两组子节点以及真实 DOM 节点的状态如下图所示:

在这里插入图片描述

可以发现,由于变量 oldStartIdx 的值大于 oldEndIdx 的值,满足更新停止的条件,因此更新停止。但是观察发现,节点p-4在整个更新过程中被遗漏了,因此我们添加额外的代码,代码如下:

while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
  // 省略部分代码
}
// 循环结束后检查索引值的情况
if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
  // 如果满足条件,则说明有新的节点遗留,需要挂载他们
  for (let i = newStartIdx; i <= newEndIdx; i++) {
    const anchor = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null;
    patch(null, newChildren[i], container, anchor);
  }
}

我们在while循环之后,增加了一个if条件语句,检查四个索引值的情况。如果满足oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx条件,说明有新的一组子节点中有遗留的节点需要作为新节点挂载。其中索引值位于 newStartIdx 和 newEndIdx 之间的节点都是新节点。于是我们开启一个for循环遍历这个区间的节点,并逐一进行挂载。挂载时的锚点仍然使用当前的头部节点 oldStartVNode.el。

移除不存在的元素

解决了新增节点的问题后,我们来讨论关于移除元素的情况,如下所示:

在这里插入图片描述

经过两轮更新后,如下所示:

在这里插入图片描述

我们可以发现:此时变量 newStartIdx 的值大于变量 newEndIdx 的值,满足更新停止的条件,于是更新结束。但是旧的一组子节点中存在未被处理的节点,应该将其移除,我们新增额外的代码处理,代码如下:

while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
  // 省略部分代码
}
if (oldEndIdx < oldStartIdx && newStart <= newEndIdx) {
  // 添加新节点
  // 省略部分代码
} else if (oldEndIdx >= oldStartIdx && newStart > newEndIdx) {
  // 移除操作
  for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    unmount(oldChildren[i]);
  }
}

与处理新增节点类似,我们在while循环结束后又增加了一个 else…if 分支,用于卸载已经不存在的节点。索引值位于 oldStartIdx 和 oldEndIdx 区间的节点都应该被卸载,于是我们开启一个 for 循环将他们逐一卸载。

完整的代码如下:

function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引值
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引指向的 vnode 节点
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];
  
  while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
    // 增加两个判断分支,如果头部节点为undefined,说明该节点被处理过,直接跳到下一个位置
    if (!oldStartVNode) {
      oldStartVNode = oldChildren[++oldStartIdx];
    } else if (!oldEndVNode) {
      oldEndVNode = oldChildren[--endStartIdx];
    } else if (oldStartVNode.key === newStartVNode.key) {
      // 第一步:oldStartVNode 和 newStartVNode 比较
      // 调用 patch 函数在 oldStartIdx 和 newStartIdx 之间打补丁
      patch(oldStartVNode, newStartVNode, container);
      // 更新相关索引到下一个位置
      oldStartVNode = oldChildren[++oldStartIdx];
      newStartVNode = newEndVNode[++newEndIdx];
    } else if(oldEndVNode.key === newEndVNode.key) {
      // 第二步:oldEndVNode 和 newEndVNode 比较
      // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
      patch(oldEndVNode, newEndVNode, container);
      // 更新索引和头尾部节点变量
      newEndVNode = newChildren[--newEndIdx];
      oldEndVNode = oldChildren[--oldEndVNode];
    } else if(oldStartVNode.key === newEndVNode.key) {
      // 第三步:oldStartVNode 和 newEndVNode 比较
      // 调用 patch 函数在 oldStartIdx 和 newEndIdx之间打补丁
      patch(oldStartVNode, newEndVNode, container);
      // 将旧的一组子节点的头部节点对应的真是节点 DOM 节点 oldStartVNode.el 移动
      // 到旧一组子节点的尾部节点对应的真实 DOM 节点后面
      insert(oldStartVNode.el, container, oldEndVNode.el.nextSibiling);
      // 更新相关索引到下一个位置
      oldStartVNode = oldChildren[++oldStartIdx];
      newEndVNode = newChildren[--newEndIdx];
    } else if(oldEndVNode.key === newStartVNode.key) {
      // 第四步:oldEndVNode 和 newStartVNode 比较
      // 仍然需要调用 patch 函数进行打补丁
      patch(oldEndVNode, newStartVNode, container);
      // 移动 DOM 操作
      // oldEndVNode.el 移动到 oldStartVNode.el 前面
      insert(oldEndVNode.el, container, oldStartVNode.el);
      // 移动 DOM 完成后,更新索引值,并指向下一个位置
      oldEndVNode = oldChildren[--oldEndIdx];
      newStartVNode = newChildren[++newStartIdx];
    } else {
      // 遍历旧的一组节点,试图寻找与 newStartVNode 拥有相同 key 值的节点
      // idxInOld 就是新的一组子节点的头部节点在旧的一组节点中的索引
      const idxInOld = oldChildren.findIndex(ele => ele.key === newStartVNode.key);
      // idxInOld > 0,说明找了可复用的节点,并且需要将其对应的真实 DOM 移动到头部
      if (idxInOld) {
        // idxInOld 位置对应的 vnode 就是需要移动的节点
        const vnodeToMove = oldChildren[idxInOld];
        // 不要忘记除移动操作之外还应该打补丁
        patch(vnodeToMove, newStartVNode, container);
        // 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此将后者作为锚点
        insert(vnodeToMove.el, container, oldStartVNode.el);
        // 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动了别处,因此将其设置为 undefined
        oldChildren[idxInOld] = undefined;
        // 最后更新 newStartIdx 到下一个位置
        newStartVNode = newChildren[++newStartIdx];
      } else {
        // 将 newStartVNode 作为新的节点挂载到头部,使用当前头部节点 oldStartVNode.el 作为锚点
        patch(null, newStartVNode, container, oldStartVNode.el);
      }
      newStartVNode = newChildren[++newStartIdx];
    }
  	// 循环结束后检查索引值的情况
  	if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
      // 如果满足条件,则说明有新的节点遗留,需要挂载他们
      for (let i = newStartIdx; i <= newEndIdx; i++) {
        const anchor = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null;
        patch(null, newChildren[i], container, anchor);
      }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

光学期刊1

光学领域的你&#xff0c;如何评价最近发布的光学期刊分区&#xff1f; 如题&#xff0c;附分区表 20240122 知乎 同样先写结论&#xff1a;时代变了&#xff0c;发国产没错的。参考light当年开局多艰难&#xff0c;被各种diss口碑差&#xff0c;很多投一区守门员不中的也…

二进制部署高可用k8s集群V1.20.11版本

文章目录 一、操作系统初始化配置&#xff08;所有节点均执行&#xff09;1、关闭防火墙2、关闭selinux3、关闭swap4、根据规划修改主机名5、在master节点上添加host6、将桥接的IPv4流量传递到iptables的链7、时间同步 二、部署Etcd集群1、准备cfssl证书生成工具2、生成Etcd证书…

2024年软件测试面试题大全【含答案】

Part1 1、你的测试职业发展是什么&#xff1f;【文末有面试文档免费领取】 测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师奔去。而且我也有初步的职业规划&#xff0c;前3年积累测试经验&#xff0c;按如何做…

tableau mysql 驱动安装

最便捷&#xff0c;最快速的方式。 整体流程&#xff1a;首先得知道你电脑mysql的版本&#xff0c;然后去官网下载ODBC驱动。 mysql版本&#xff1a;浏览器搜一些。 高版本驱动应该是兼容低版本的。 ODBC驱动&#xff1a; 选择第一个&#xff0c;下载&#xff0c;直接msi安装…

学习笔记之 机器学习之预测雾霾

文章目录 Encoder-DecoderSeq2Seq (序列到序列&#xff09; Encoder-Decoder 基础的Encoder-Decoder是存在很多弊端的&#xff0c;最大的问题就是信息丢失。Encoder将输入编码为固定大小的向量的过程实际上是一个“信息有损的压缩过程”&#xff0c;如果信息量越大&#xff0c;…

POKT Network (POKT) :进军百亿美元市场规模的人工智能推理市场

POKT Network&#xff08;又称 Pocket Network&#xff09;是一个去中心化的物理基础设施网络&#xff08;DePIN&#xff09;&#xff0c;它能够协调并激励对任何开放数据源的访问&#xff0c;最初专注于向应用程序和服务提供商提供区块链数据。 自 2020 年主网上线以来&#x…

MSG3D

论文在stgcn与sta-lstm基础上做的。下面讲一下里面的方法&#xff1a; 1.准备工作 符号。这里是对符号进行解释。 一个人体骨骼图被记为G(v,E) 图卷积&#xff1a; 图卷积定义 考虑一种常用于处理图像的标准卷积神经网络 (CNN)。输入是像素网格。每个像素都有一个数据值向…

大数据开发之电商数仓(hadoop、flume、hive、hdfs、zookeeper、kafka)

第 1 章&#xff1a;数据仓库 1.1 数据仓库概述 1.1.1 数据仓库概念 1、数据仓库概念&#xff1a; 为企业制定决策&#xff0c;提供数据支持的集合。通过对数据仓库中数据的分析&#xff0c;可以帮助企业&#xff0c;改进业务流程、控制成本&#xff0c;提高产品质量。 数据…

网络:FTP

1. FTP 文件传输协议&#xff0c;FTP是用来传输文件的协议。使用FTP实现远程文件传输的同时&#xff0c;还可以保证数据传输的可靠性和高效性。 2. 特点 明文传输。 作用&#xff1a;可以从服务器上下载文件&#xff0c;或将本地文件上传到服务器。 3. FTP原理 FTP有控制层面…

Java技术栈 —— JVM虚拟机

JVM虚拟机 一、字节码(Byte-Code)1.1 如何查看字节码&#xff1f;1.2 如何理解字节码的作用&#xff1f; 二、JVM内存模型&#xff08;极其重点&#xff0c;必须牢牢把握住&#xff09;2.1 方法区2.2 虚拟机栈2.3 本地方法栈2.4 堆2.5 程序计数器2.6 面试必问 三、GC机制四、JV…

【华为 ICT HCIA eNSP 习题汇总】——题目集7

1、一台 PC 的 MAC 地址是 5489-98FB-65D8 &#xff0c;管理员希望该 PC 从 DHCP 服务器获得指定的 IP 地址为192.168.1.11/24&#xff0c;以下命令配置正确的是&#xff08;&#xff09;。 A、dhcp static-bind ip-address 192.168.1.11 24 mac- address 5489-98FB-65D8 B、dh…

力扣740. 删除并获得点数

动态规划 思路&#xff1a; 选择元素 x&#xff0c;获得其点数&#xff0c;删除 x 1 和 x - 1&#xff0c;则其他的 x 的点数也会被获得&#xff1b;可以将数组转换成一个有序 map&#xff0c;key 为 x&#xff0c; value 为对应所有 x 的和&#xff1b;则问题转换成了不能同…

中间件-缓存、索引、日志

文章目录 缓存中间件本地缓存中间件分布式缓存中间件全文索引中间件分布式日志中间件小结 缓存中间件 缓存是性能优化的一大利器 我们先一起来看一个用户中心查询用户信息的基本流程 这时候&#xff0c;如果查找用户信息这个 API 的调用频率增加&#xff0c;并且在整个业务流…

怎么使用【jmeter正则表达式提取器】解决返回值作参数的问题

前言 我们在使用jmeter做接口测试时&#xff0c;常常会碰到上个接口的返回值会作为下个接口的参数来进行请求。这时候&#xff0c;就需要用到jmeter的正则表达式提取器了。 添加正则表达式提取器步骤&#xff1a; “选择要添加提取器的接口右键”——add——post processors—…

线程池--JAVA

虽然线程是轻量级进程&#xff0c;但是如果当创建和销毁的的频率非常之高&#xff0c;那么它也就会消耗很多的资源。 而线程池就是用来优化线程频繁创建和销毁的场景&#xff0c;减少线程创建、销毁的频率。 ExecutorService JAVA标准库为我们实现了线程池&#xff0c;Execu…

archlinux 如何解决安装以后没有声音的问题

今天安装完archlinux以后发现看视频没声音 检查一下是否有 /lib/firmware/intel/sof 发现没有 如果你也是这样的话&#xff0c;可以尝试安装&#xff1a; sudo pacman -S sof-firmware 重启后再看看有没有声音&#xff1a; reboot 反正我有声音了

跟着我学Python进阶篇:03. 面向对象(下)

往期文章 跟着我学Python基础篇&#xff1a;01.初露端倪 跟着我学Python基础篇&#xff1a;02.数字与字符串编程 跟着我学Python基础篇&#xff1a;03.选择结构 跟着我学Python基础篇&#xff1a;04.循环 跟着我学Python基础篇&#xff1a;05.函数 跟着我学Python基础篇&#…

【ARM 嵌入式 编译系列 7.3 -- GCC 链接脚本中 DISCARD 与 .ARM.exidx】

请阅读【嵌入式开发学习必备专栏 之 ARM GCC 编译专栏】 文章目录 背景.ARM.exidx方法一:使用链接器脚本方法二:使用链接器选项注意事项背景 在移植 RT-Thread 到 cortex-m33(RA4M2)上的时候,在编译的时候遇到下面问题: Building target: ra4m2.elf arm

Vue2基础-Vue对象进阶介绍2

文章目录 一、自定义指令1、用法2、注意点 二、生命周期1、概念2、示意图3、分析 一、自定义指令 本质上是封装了DOM元素的具体操作 1、用法 <div >放大十倍后的值是&#xff1a;<span v-big"n"></span></div> <input type"text&…

【Linux】—— 共享内存

本期我将要带大家学习的是有关进程间通信的另一种方式——共享内存。共享内存是一种用于进程间通信的高效机制&#xff0c;允许多个进程访问和操作同一块内存区域。 目录 &#xff08;一&#xff09;深刻理解共享内存 1.1 概念解释 1.2 共享内存原理 1.3 共享内存数据结构 …