什么是虚拟DOM

DOM是很慢的,其元素非常庞大,当我们频繁的去做 DOM更新,会产生一定的性能问题,我们可以直观感受一下 div元素包含的海量属性



在Javascript对象中,虚拟DOM 表现为一个 Object对象(以VNode 节点作为基础的树)。并且最少包含标签名tag、属性attrs和子元素对象children三个属性,不同框架对这三个属性的名命可能会有差别。

<ul style="color: #de5e60; border: 1px solid #de5e60">
<li key="a">a</li>
<li key="b">b</li>
<li key="c">c</li>
</ul>

真实节点对应的虚拟DOM:

const VDOM = {
tag: 'ul',
data: {
style: { color: '#de5e60', border: '1px solid #de5e60' },
},
children: [
{
tag: 'li',
key: 'a',
data: {},
children: [{ text: 'a' }],
},
{
tag: 'li',
key: 'b',
data: {},
children: [{ text: 'b'}],
},
{
tag: 'li',
key: 'c',
data: {},
children: [{ text: 'c'}],
},
],
}

我们常说虚拟DOM可以提升效率。这句话是不严谨的

通过虚拟DOM改变真正的 DOM并不比直接操作 DOM效率更高。恰恰相反,我们仍需要调用DOM API去操作 DOM,并且虚拟DOM还会额外占用内存!



but!!!我们可以通过 虚拟DOM + diff算法,找到需要更新的最小单位,最大限度地减少DOM操作,从而提升性能。

什么是Diff

Dom 是多叉树结构,完整对比两棵树的差异,时间复杂度是O(n³),这个复杂度会导致比对性能很差!

为了优化,Diff 算法约定只做同层级节点比对,而不是跨层级节点比对,即深度优先遍历算法,其复杂度为O(n)

Diff原理

当数据修改后会触发setter劫持操作,我们在setter中执行dep.notity(),通知所有的订阅者watcher重新渲染。

订阅者watcher这时会在回调内部,通过vm._render()获取最新的虚拟DOM;然后通过patch方法比对新旧虚拟DOM,给真实元素打补丁,更新视图

createElm

利用vnode创建真实元素,有一个巧妙的地方是,我们把真实元素挂载到了vnode上,便于我们后续通过虚拟节点去操作对应的真实元素

export function createElm(vnode) {
let { tag, data, children, text } = vnode
// 标签
if (typeof tag === 'string') {
// 将真实节点挂载到虚拟节点上
vnode.el = document.createElement(tag)
patchProps(vnode.el, {}, data)
children.forEach(child => {
vnode.el.appendChild(createElm(child))
})
} else {
// 文本
vnode.el = document.createTextNode(text)
}
return vnode.el
}

sameVnode

判断是否是相同节点,节点的tag和节点的key都相同

export function isSameVnode(vnode1, vnode2) {
return vnode1.tag === vnode2.tag && vnode1.key === vnode2.key
}

patch

patch方法有两大作用,一个是初始化元素 ,另一个是更新元素

export function patch(oldVNode, vnode) {
const isRealElement = oldVNode.nodeType
// 初渲染元素
if (isRealElement) {
const elm = oldVNode // 获取真实元素
const parentElm = elm.parentNode // 拿到父元素
let newElm = createElm(vnode) // 根据vnode创建元素 parentElm.insertBefore(newElm, elm.nextSibling) // 插入刚刚创建的元素
parentElm.removeChild(elm) // 删除旧节点
return newElm
} else {
// 更新元素
return patchVnode(oldVNode, vnode)
}
}

patchVnode

比对新旧虚拟节点打补丁,diff比对规则如下:

  1. 新旧节点不相同(判断节点的tag和节点的key),直接用新节点替换旧节点,无需比对
  2. 新旧节点相同,且都是文本节点,更新文本内容即可
  3. 新旧节点是同一个节点,比较两个节点的属性是否有差异,复用旧的节点,将差异的属性更新
  4. 节点比较完毕后,需要比较两个节点的儿子
    1. 新旧节点都有儿子,调用updateChildren(),这里是diff算法核心逻辑!后面会详细讲解
    2. 新节点有儿子,旧节点没有儿子,将新的子节点挂载到oldVNode.el
    3. 旧节点有儿子,新节点没有儿子,删除oldVNode.el的所有子节点
function patchVnode(oldVNode, vnode) {
// 1. 新旧节点不相同(判断节点的tag和节点的key),直接用新节点替换旧节点,无需比对
if (!isSameVnode(oldVNode, vnode)) {
let el = createElm(vnode)
oldVNode.el.parentNode.replaceChild(el, oldVNode.el)
return el
}
let el = (vnode.el = oldVNode.el) // 2. 新旧节点相同,且是文本 (判断节点的tag和节点的key),比较文本内容
if (!oldVNode.tag) {
if (oldVNode.text !== vnode.text) {
el.textContent = vnode.text // 用新的文本覆盖掉旧的
}
} // 3. 新旧节点相同,且是标签 (判断节点的tag和节点的key)
// 3.1 比较标签属性
patchProps(el, oldVNode.data, vnode.data) let oldChildren = oldVNode.children || []
let newChildren = vnode.children || []
// 4 比较两个节点的儿子
// 4.1 新旧节点都有儿子
if (oldChildren.length > 0 && newChildren.length > 0) {
// diff算法核心!!!
updateChildren(el, oldChildren, newChildren)
}
// 4.2 新节点有儿子,旧节点没有儿子,挂载
else if (newChildren.length > 0) {
mountChildren(el, newChildren)
}
// 4.3 旧节点有儿子,新节点没有儿子,删除
else if (oldChildren.length > 0) {
el.innerHTML = ''
}
}

updateChildren(Diff核心算法)

这个方法是diff比对的核心!

vue2中采用了头尾双指针的方式,通过头头、尾尾、头尾、尾头、乱序五种比对方式,进行新旧虚拟节点的依次比对

在比对过程中,我们需要四个指针,分别指向新旧列表的头部和尾部。为了方便我们理解,我使用了不同颜色和方向的箭头加以区分,图例如下:

双端比对

头头比对

旧孩子的头 比对 新孩子的头

如果是相同节点,则调用patchVnode打补丁并递归比较子节点;然后将 新旧列表的头指针 都向后移动

终止条件:双方有一方头指针大于尾指针,则停止循环

if (isSameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
}

尾尾比对

旧孩子的尾 和 新孩子的尾比较

如果是相同节点,则调用patchVnode打补丁并递归比较子节点;然后将 新旧列表的尾指针 都向前移动

终止条件:双方有一方头指针大于尾指针,则停止循环

else if (isSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
}

头尾比对

旧孩子的头 和 新孩子的尾比较

如果是相同节点,则调用patchVnode打补丁并递归比较子节点;然后将 oldStartVnode 移动到 oldEndVnode 的后面(把 旧列表头指针指向的节点 移动到 旧列表尾指针指向的节点 后面)

最后把 旧列表头指针 向后移动,新列表尾指针 向前移动

终止条件:双方有一方头指针大于尾指针,则停止循环

else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
oldStartVnode = oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex]
}

尾头比对

旧孩子的尾 和 新孩子的头比较

如果是相同节点,则调用patchVnode打补丁并递归比较子节点;然后将 oldEndVnode 移动到 oldStartVnode 的前面(把 旧列表尾指针指向的节点 移动到 旧列表头指针指向的节点 前面)

最后把 旧列表尾指针 向前移动,新列表头指针 向后移动

终止条件:双方有一方头指针大于尾指针,则停止循环

else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
el.insertBefore(oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldChildren[--oldEndIndex]
newStartVnode = newChildren[++newStartIndex]
}

乱序比对

每次比对时,优先进行头头、尾尾、头尾、尾头的比对尝试,如果都没有命中才会进行乱序比较

  1. 我们根据旧的列表创建一个 key -> index 的映射表,拿新的儿子去映射关系里查找。注意:查找时只能找得到key相同的老节点,并没判断tag
  2. 若找的到相同key的老节点并且是相同节点,则复用节点移动到 oldStartVnode(旧列表头指针指向的节点)的前面,然后调用 patchVnode 打补丁递归比较子节点(移动走的老位置要做空标记,表示这个旧节点已经被移动过了,后续比对时可直接跳过此节点)
  3. 否则,创建节点并移动到 oldStartVnode(旧列表头指针指向的节点)的前面
  4. 只需将新列表头指针 向后移动即可
  5. 最后删除老列表中多余的节点,此过程在下一章挂载卸载阶段删除掉

终止条件:双方有一方头指针大于尾指针,则停止循环

----------------- 创建映射关系 -----------------------
function makeIndexByKey(children) {
let map = {}
children.forEach((child, index) => {
map[child.key] = index
})
return map
}
// 旧孩子映射表(key-index),用于乱序比对
let map = makeIndexByKey(oldChildren) -------------------- 乱序比对 -------------------------
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
continue
}
if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
continue
} let moveIndex = map[newStartVnode.key]
// 找的到相同key的老节点,并且是相同节点
if (moveIndex !== undefined && isSameVnode(oldChildren[moveIndex], newStartVnode)) {
let moveVnode = oldChildren[moveIndex] // 复用旧的节点
el.insertBefore(moveVnode.el, oldStartVnode.el) // 将 moveVnode 移动到 oldStartVnode的前面(把复用节点 移动到 旧列表头指针指向的节点 前面)
oldChildren[moveIndex] = undefined // 表示这个旧节点已经被移动过了
patchVnode(moveVnode, newStartVnode) // 递归比较子节点
} // 找不到相同key的老节点 or 找的到相同key的老节点但tag不相同
else {
el.insertBefore(createElm(newStartVnode), oldStartVnode.el) // 将 创建的节点 移动到 oldStartVnode的前面(把创建的节点 移动到 旧列表头指针指向的节点 前面)
}
newStartVnode = newChildren[++newStartIndex]

挂载卸载

终止条件:双方有一方头指针大于尾指针,则停止循环。当循环比对结束后,我们需要将新列表中多余的节点插入到oldVNode.el中,并将老列表中多余的节点删除掉。

我们将其划分为4种场景,可参考头头比对、尾尾比对章节的图辅助理解

  • 同序列尾部挂载:新列表头指针新列表尾指针 的节点需要挂载新增,向后追加
  • 同序列头部挂载:新列表头指针新列表尾指针 的节点需要挂载新增,向前追加
  • 同序列尾部卸载:旧列表头指针旧列表尾指针 的节点需要卸载删除
  • 同序列头部卸载: 和 同序列尾部卸载 逻辑一致

tip:何时向后追加,何时向前追加,我们根据什么去判断的呢?

新列表尾指针指向的节点 的下一个节点存在,则向前追加,插入到newChildren[newEndIndex + 1].el的前面;若不存在,则向后追加,插入到oldVNode.el子节点列表的末尾处

// 同序列尾部挂载,向后追加
// a b c d
// a b c d e f
// 同序列头部挂载,向前追加
// a b c d
// e f a b c d
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
let childEl = createElm(newChildren[i])
// 这里可能是向后追加 ,也可能是向前追加
let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null
el.insertBefore(childEl, anchor) // anchor为null的时候等同于 appendChild
}
} // 同序列尾部卸载,删除尾部多余的旧孩子
// a b c d e f
// a b c d
// 同序列头部卸载,删除头部多余的旧孩子
// e f a b c d
// a b c d
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (oldChildren[i]) {
let childEl = oldChildren[i].el
el.removeChild(childEl)
}
}
}

总结

vue2采用了头尾双指针的方法,每次比对时,优先进行头头、尾尾、头尾、尾头的比对尝试,如果都没有命中才会进行乱序比对

当比对命中时(新旧节点是相同的),则调用patchVnode打补丁并递归比较子节点;打完补丁后呢,如果该节点是头指针指向的节点就向后移动指针,是尾指针指向的节点则向前移动指针

终止条件:双方有一方头指针大于尾指针,则停止循环

如果双端比对中的头尾、尾头命中了节点,也需要进行节点移动操作,为什么不直接用乱序比对呢,没理解其优势在哪?

但是双端diff相比于简单diff性能肯定会更好一些,例如:从 ABCDDABC简单diff需要移动 ABC 三个节点,但是双端diff只需要移动 D 一个节点

关于简单diff的介绍可移步此文章 - 聊聊 Vue 的双端 diff 算法

tip:vue3中并没有头尾、尾头比对的概念;新增了最长递增子序列算法去优化乱序比对,减少了乱序比对中节点的移动次数

updateChildren 核心代码如下:

function updateChildren(el, oldChildren, newChildren) {
let oldStartIndex = 0
let newStartIndex = 0
let oldEndIndex = oldChildren.length - 1
let newEndIndex = newChildren.length - 1 let oldStartVnode = oldChildren[0]
let newStartVnode = newChildren[0] let oldEndVnode = oldChildren[oldEndIndex]
let newEndVnode = newChildren[newEndIndex] function makeIndexByKey(children) {
let map = {}
children.forEach((child, index) => {
map[child.key] = index
})
return map
}
// 旧孩子映射表(key-index),用于乱序比对
let map = makeIndexByKey(oldChildren) // 双方有一方头指针大于尾部指针,则停止循环
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
continue
}
if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
continue
} // 双端比较_1 - 旧孩子的头 比对 新孩子的头;
// 都从头部开始比对(对应场景:同序列尾部挂载-push、同序列尾部卸载-pop)
if (isSameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode) // 如果是相同节点,则打补丁,并递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
}
// 双端比较_2 - 旧孩子的尾 比对 新孩子的尾;
// 都从尾部开始比对(对应场景:同序列头部挂载-unshift、同序列头部卸载-shift)
else if (isSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode) // 如果是相同节点,则打补丁,并递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
}
// 双端比较_3 - 旧孩子的头 比对 新孩子的尾;
// 旧孩子从头部开始,新孩子从尾部开始(对应场景:指针尽可能向内靠拢;极端场景-reverse)
else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling) // 将 oldStartVnode 移动到 oldEndVnode的后面(把当前节点 移动到 旧列表尾指针指向的节点 后面)
oldStartVnode = oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex]
}
// 双端比较_4 - 旧孩子的尾 比对 新孩子的头;
// 旧孩子从尾部开始,新孩子从头部开始(对应场景:指针尽可能向内靠拢;极端场景-reverse)
else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
el.insertBefore(oldEndVnode.el, oldStartVnode.el) // 将 oldEndVnode 移动到 oldStartVnode的前面(把当前节点 移动到 旧列表头指针指向的节点 前面)
oldEndVnode = oldChildren[--oldEndIndex]
newStartVnode = newChildren[++newStartIndex]
}
// 乱序比对
// 根据旧的列表做一个映射关系,拿新的节点去找,找到则移动;找不到则添加;最后删除多余的旧节点
else {
let moveIndex = map[newStartVnode.key]
// 找的到相同key的老节点,并且是相同节点
if (moveIndex !== undefined && isSameVnode(oldChildren[moveIndex], newStartVnode)) {
let moveVnode = oldChildren[moveIndex] // 复用旧的节点
el.insertBefore(moveVnode.el, oldStartVnode.el) // 将 moveVnode 移动到 oldStartVnode的前面(把复用节点 移动到 旧列表头指针指向的节点 前面)
oldChildren[moveIndex] = undefined // 表示这个旧节点已经被移动过了
patchVnode(moveVnode, newStartVnode) // 比对属性和子节点
}
// 找不到相同key的老节点 or 找的到相同key的老节点但tag不相同
else {
el.insertBefore(createElm(newStartVnode), oldStartVnode.el) // 将 创建的节点 移动到 oldStartVnode的前面(把创建的节点 移动到 旧列表头指针指向的节点 前面)
}
newStartVnode = newChildren[++newStartIndex]
}
} // 同序列尾部挂载,向后追加
// a b c d
// a b c d e f
// 同序列头部挂载,向前追加
// a b c d
// e f a b c d
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
let childEl = createElm(newChildren[i])
// 这里可能是向后追加 ,也可能是向前追加
let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null // 获取下一个元素
// el.appendChild(childEl);
el.insertBefore(childEl, anchor) // anchor为null的时候等同于 appendChild
}
} // 同序列尾部卸载,删除尾部多余的旧孩子
// a b c d e f
// a b c d
// 同序列头部卸载,删除头部多余的旧孩子
// e f a b c d
// a b c d
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (oldChildren[i]) {
let childEl = oldChildren[i].el
el.removeChild(childEl)
}
}
}
}

常见问题

为什么不建议key用索引

我们先看一段代码。其效果是:当点击按钮后,会在数组前面追加一项数据

/** template代码 */
<div id="app">
<ul class="ul-wrap">
<li v-for="(item,index) in arr" :key="index">
{{item.name}} <input type="checkbox">
</li>
</ul>
<button @click="append">追加</button>
</div> /** js代码 */
let vm = new Vue({
el: '#app',
data() {
return {
arr: [{ id: 0, name: '柏成0号' },
{ id: 1, name: '柏成1号' },
{ id: 2, name: '柏成2号' }]
}
},
methods: {
append() {
this.arr.unshift({
id: 7,
name: '柏成7号'
});
}
}
})

index作为key

使用index作为key时,运行结果如下:



我们会发现一个神奇的现象,虽然只unshift了一条数据,但是所有的li标签都更新了。并且新增的柏成7号节点还复用了柏成0号节点的checkbox多选框!!!

其原理就是,我们在进行头头比对时,前三项虽然可以匹配到相同节点(标签名和key都相同),但其内容并非一致,所以进行了打补丁更新操作。然后我们又创建一个key为3柏成2号节点插入到列表尾部

id作为key

使用id作为key时,运行结果如下:



这次的diff更新就符合了我们的预期效果,它找到需要更新的最小单位,即只会新增key为3柏成7号节点,最大限度地减少DOM操作

此时我们在进行尾尾比对时,后三项都可以匹配到相同节点(标签名和key都相同),而且会发现无需更新任何内容。然后去创建一个key为7柏成7号节点插入列表头部,严格来说是插入新列表头指针下一个虚拟节点对应的真实元素newChildren[newEndIndex + 1].el前面

参考文章

diff 算法深入一下?

聊聊 Vue 的双端 diff 算法

15张图,20分钟吃透Diff算法核心原理,我说的!!!

第三十篇 - diff 算法

【Vue2.x源码系列08】Diff算法原理的更多相关文章

  1. Vue3源码分析之Diff算法

    Diff 算法源码(结合源码写的简易版本) 备注:文章后面有详细解析,先简单浏览一遍整体代码,更容易阅读 // Vue3 中的 diff 算法 // 模拟节点 const { oldVirtualDo ...

  2. 【Vue2.x源码系列06】计算属性computed原理

    上一章 Vue2异步更新和nextTick原理,我们介绍了 JavaScript 执行机制是什么?nextTick源码是如何实现的?以及Vue是如何异步更新渲染的? 本章目标 计算属性是如何实现的? ...

  3. 【Vue2.x源码系列07】监听器watch原理

    上一章 Vue2计算属性原理,我们介绍了计算属性是如何实现的?计算属性缓存原理?以及洋葱模型是如何应用的? 本章目标 监听器是如何实现的? 监听器选项 - immediate.deep 内部实现 初始 ...

  4. Sentinel-Go 源码系列(三)滑动时间窗口算法的工程实现

    要说现在工程师最重要的能力,我觉得工程能力要排第一. 就算现在大厂面试经常要手撕算法,也是更偏向考查代码工程实现的能力,之前在群里看到这样的图片,就觉得很离谱. 算法与工程实现 在 Sentinel- ...

  5. 大白话Vue源码系列(05):运行时鸟瞰图

    阅读目录 Vue 实例的生命周期 实例创建 响应的数据绑定 挂载到 DOM 节点 结论 研究 runtime 一边 Vue 一边源码 初看 Vue 是 Vue 源码是源码 再看 Vue 不是 Vue ...

  6. 大白话Vue源码系列(03):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  7. 大白话Vue源码系列(04):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  8. Ioc容器beanDefinition-Spring 源码系列(1)

    Ioc容器beanDefinition-Spring 源码系列(1) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Ioc容器 ...

  9. Chromium源码系列一:Chromium简介及源代码获取和编译

    Chromium源码系列一:Chromium简介及源代码获取和编译 Chromium简介 ​ Chromium是一个由Google主导开发的网页浏览器,以BSD许可证等多重自由版权发行并开放源代码.C ...

  10. hbase源码系列(十二)Get、Scan在服务端是如何处理

    hbase源码系列(十二)Get.Scan在服务端是如何处理?   继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Del ...

随机推荐

  1. 详解数仓中sequence的应用场景及优化

    摘要:本文简单介绍sequence的使用场景及如何修改sequence的cache值提高性能. 本文分享自华为云社区<GaussDB(DWS)关于sequence的那些事>,作者:Arro ...

  2. Salesforce Sharing And Visibility 零基础学习(一)基础知识篇

    本篇参考: https://trailhead.salesforce.com/en/users/strailhead/trailmixes/architect-sharing-and-visibili ...

  3. [Volo.Abp升级笔记]使用旧版Api规则替换RESTful Api以兼容老程序

    @ 目录 原理分析 开始改造 更换基类型 重写接口 替换默认规则 在微服务架构中的问题 Volo.Abp 配置应用层自动生成Controller,增删查改服务(CrudAppService)将会以RE ...

  4. 项目优化-CDN缓存

    名次解释 CDN(Content Delivery Network)内容分发网络. CDN出现背景: 客户端从源站点获取数据,当服务端访问流量较为拥挤的时候 可能出现缓慢卡顿的现象,为了解决这个问题, ...

  5. 从0开始学杂项 第三期:隐写分析(2) PNG图片隐写

    Misc 学习(三) - 隐写分析:PNG 图片隐写 在上一期,我主要讲了讲自己对于隐写分析.信息搜集和直接附加的一些浅薄理解,这一期我们继续对隐写分析的学习,开始讲隐写分析最喜欢考的一项--图片隐写 ...

  6. kube-apiserver启动命令参数解释

    在apiserver启动时候会有很多参数来配置启动命令,有些时候不是很明白这些参数具体指的是什么意思. 我的kube-apiserver启动命令参数: cat > /usr/lib/system ...

  7. Bootstrapd导航条使用

    要想在程序中集成Bootstrap,显然要对模板做所有必要的改动.不过,更简单的方法是使用一个名为Flask-Bootstrap 的Flask 扩展,简化集成的过程. 安装:Flask-Bootstr ...

  8. Java线程创建

    程序.进程.线程 程序:指令和数据的有序集合,静态 进程:程序的一次执行过程,动态,系统分配资源的单位 线程:一个进程可以包含多个线程,一个进程至少有一个线程,线程是CPU调度的基本单位 线程创建 三 ...

  9. Carla 自动驾驶仿真平台的安装与配置指南

    简介 Carla 是一款基于 Python 编写和 UE(虚幻引擎)的开源仿真器,用于模拟自动驾驶车辆在不同场景下的行为和决策.它提供了高度可定制和可扩展的驾驶环境,包括城市.高速公路和农村道路等.C ...

  10. 记一次 MySQL 主从同步异常的排查记录,百转千回

    你好,我是悟空. 这是悟空的第 183 篇原创文章 官网:www.passjava.cn 本文主要内容如下: 一.现象 最近项目的测试环境遇到一个主备同步的问题: 备库的同步线程停止了,无法同步主库的 ...