手写一个虚拟DOM库,彻底让你理解diff算法
所谓虚拟DOM
就是用js
对象来描述真实DOM
,它相对于原生DOM
更加轻量,因为真正的DOM
对象附带有非常多的属性,另外配合虚拟DOM
的diff
算法,能以最少的操作来更新DOM
,除此之外,也能让Vue
和React
之类的框架支持除浏览器之外的其他平台,本文会参考知名的snabbdom库来手写一个简易版的,配合图片示例一步步完成代码,一定让你彻底理解虚拟DOM
的patch
及diff
算法。
创建虚拟DOM对象
虚拟DOM
(下文称VNode
)就是使用js
的普通对象来描述DOM
的类型、属性、子元素等信息,一般通过名为h
的函数来创建,为了纯粹的理解VNode
的patch
过程,我们先不考虑元素的属性、样式、事件等,只考虑节点类型及节点内容,看一下此时的VNode
结构:
{
tag: '',// 元素标签
children: [],// 子元素
text: '',// 子元素是文本节点的话,保存文本
el: null// 对应的真实dom
}
h
函数根据接收的参数返回该对象即可:
export const h = (tag, children) => {
let text = ''
let el
// 子元素是文本节点
if (typeof children === 'string' || typeof children === 'number') {
text = children
children = undefined
} else if (!Array.isArray(children)) {
children = undefined
}
return {
tag, // 元素标签
children, // 子元素
text, // 文本子节点的文本
el// 真实dom
}
}
比如我们要创建一个div
的VNode
可以这样使用:
h('div', '我是文本')
h('div', [h('span')])
详解patch过程
patch
函数是我们的主函数,主要用来进行新旧VNode
的对比,找到差异来更新实际DOM
,它接收两个参数,第一个参数可以是DOM
元素或者是VNode
,表示旧的VNode
,第二参数表示新的VNode
,一般只有第一次调用时才会传DOM
元素,如果第一个参数为DOM
元素的话我们直接忽略它的子元素把它转为一个VNode
:
export const patch = (oldVNode, newVNode) => {
// dom元素
if (!oldVNode.tag) {
let el = oldVNode
el.innerHTML = ''
oldVNode = h(oldVNode.tagName.toLowerCase())
oldVNode.el = el
}
}
接下来新旧两个VNode
就可以进行比较了:
export const patch = (oldNode, newNode) => {
// ...
patchVNode(oldVNode, newVNode)
// 返回新的vnode
return newVNode
}
在patchVNode
方法里我们对新旧VNode
进行比较及更新DOM
。
首先如果两个VNode
的类型不同,那么不用比较,直接使用新的VNode
替换旧的:
const patchVNode = (oldNode, newNode) => {
if (oldVNode === newVNode) {
return
}
// 元素标签相同,进行patch
if (oldVNode.tag === newVNode.tag) {
// ...
} else { // 类型不同那么根据新的VNode创建新的dom节点,然后插入新节点,移除旧节点
let newEl = createEl(newVNode)
let parent = oldVNode.el.parentNode
parent.insertBefore(newEl, oldVNode.el)
parent.removeChild(oldVNode.el)
}
}
createEl
方法用来递归的把VNode
转换成真实的DOM
节点:
const createEl = (vnode) => {
let el = document.createElement(vnode.tag)
vnode.el = el
// 创建子节点
if (vnode.children && vnode.children.length > 0) {
vnode.children.forEach((item) => {
el.appendChild(createEl(item))
})
}
// 创建文本节点
if (vnode.text) {
el.appendChild(document.createTextNode(vnode.text))
}
return el
}
如果类型相同,那么就要根据其子节点的情况来判断进行哪种操作。
如果新节点只有一个文本子节点,那么移除旧节点的所有子节点(如果有的话),创建一个文本子节点:
const patchVNode = (oldVNode, newVNode) => {
// 元素标签相同,进行patch
if (oldVNode.tag === newVNode.tag) {
// 元素类型相同,那么旧元素肯定是进行复用的
let el = newVNode.el = oldVNode.el
// 新节点的子节点是文本节点
if (newVNode.text) {
// 移除旧节点的子节点
if (oldVNode.children) {
oldVNode.children.forEach((item) => {
el.removeChild(item.el)
})
}
// 文本内容不相同则更新文本
if (oldVNode.text !== newVNode.text) {
el.textContent = newVNode.text
}
} else {
// ...
}
} else { // 不同使用newNode替换oldNode
// ...
}
}
如果新节点的子节点非文本节点,那也有几种情况:
1.新节点不存在子节点,而旧节点存在,那么移除旧节点的子节点;
2.新节点不存在子节点,旧节点存在文本节点,那么移除该文本节点;
3.新节点存在子节点,旧节点存在文本节点,那么移除该文本节点,然后插入新节点;
4.新旧节点都有子节点的话那么就需要进入到diff
阶段;
const patchVNode = (oldVNode, newVNode) => {
// 元素标签相同,进行patch
if (oldVNode.tag === newVNode.tag) {
// ...
// 新节点的子节点是文本节点
if (newVNode.text) {
// ...
} else {// 新节点不存在文本节点
// 新旧节点都存在子节点,那么就要进行diff
if (oldVNode.children && newVNode.children) {
diff(el, oldVNode.children, newVNode.children)
} else if (oldVNode.children) {// 新节点不存在子节点,那么移除旧节点的所有子节点
oldVNode.children.forEach((item) => {
el.removeChild(item.el)
})
} else if (newVNode.children) {// 新节点存在子节点
// 旧节点存在文本节点则移除
if (oldVNode.text) {
el.textContent = ''
}
// 添加新节点的子节点
newVNode.children.forEach((item) => {
el.appendChild(createEl(item))
})
} else if (oldVNode.text) {// 新节点啥也没有,旧节点存在文本节点
el.textContent = ''
}
}
} else { // 不同使用newNode替换oldNode
// ...
}
}
如果当新旧节点都存在非文本的子节点的话,那么就要进入到著名的diff
阶段了,diff
算法的目的主要是用来尽可能复用旧的节点,以减小DOM
操作的开销。
图解diff算法
首先最简单的diff
显然是同位置的新旧节点两两比较,但是在WEB
场景下,倒序、排序、换位都是经常有可能发生的,所以同位置比较很多时候都很低效,无法满足这种常见场景,各种所谓的diff
算法就是用来尽量能检查出这些情况,然后进行复用,snabbdom
里的diff
算法是一种双端比较的策略,同时从新旧节点的两端向中间开始比较,每一轮都会进行四次比较,所以需要四个指针,如下图:
即上述四个位置的排列组合:oldStartIdx
与newStartIdx
、oldStartIdx
与newEndIdx
、oldEndIdx
与newStartIdx
、oldEndIdx
与newEndIdx
,每当发现所比较的两个节点可能可以复用的话,那么就对这两个节点进行patch
和相应操作,并更新指针进入下一轮比较,那怎么判断两个节点是否能复用呢?这就需要使用到key
了,因为光看是否是同类型的节点是远远不够的,因为同一个列表基本上类型都是一样的,那就跟从头开始的两两比较没有区别了,先修改一下我们的h
函数:
export const h = (tag, data = {}, children) => {
// ...
let key
// 文本节点
// ...
if (data && data.key) {
key = data.key
}
return {
// ...
key
}
}
现在创建VNode
的时候可以传入key
:
h('div', {key: 1}, '我是文本')
比较的终止条件也很明显,其中一个列表已经比较完了,也就是oldStartIdx>oldEndIdx
或newStartIdx>newEndIdx
,先把算法基本框架写一下:
// 判断两个节点是否可进行复用
const isSameNode = (a, b) => {
return a.key === b.key && a.tag === b.tag
}
// 进行diff
const diff = (el, oldChildren, newChildren) => {
// 位置指针
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
// 节点指针
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isSameNode(oldStartVNode, newStartVNode)) {
} else if (isSameNode(oldStartVNode, newEndVNode)) {
} else if (isSameNode(oldEndVNode, newStartVNode)) {
} else if (isSameNode(oldEndVNode, newEndVNode)) {
}
}
}
新增了四个变量用来保存四个位置的节点,接下来以上图为例来完善代码。
第一轮会发现oldEndVNode
与newEndVNode
是可复用节点,那么对它们进行patch
,因为都在最后的位置,所以不需要移动DOM
节点,更新指针即可:
const diff = (el, oldChildren, newChildren) => {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isSameNode(oldStartVNode, newStartVNode)) {}
else if (isSameNode(oldStartVNode, newEndVNode)) {}
else if (isSameNode(oldEndVNode, newStartVNode)) {}
else if (isSameNode(oldEndVNode, newEndVNode)) {
patchVNode(oldEndVNode, newEndVNode)
// 更新指针
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
}
}
}
此时的位置信息如下:
下一轮会发现oldStartIdx
与newEndIdx
是可复用节点,那么对oldStartVNode
和newEndVNode
两个节点进行patch
,同时该节点在新列表里的位置是当前比较区间的最后一个,所以需要把oldStartIdx
的真实DOM
移动到旧列表当前比较区间的最后,也就是oldEndVNode
之后:
const diff = (el, oldChildren, newChildren) => {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isSameNode(oldStartVNode, newStartVNode)) {}
else if (isSameNode(oldStartVNode, newEndVNode)) {
patchVNode(oldStartVNode, newEndVNode)
// 把节点移动到oldEndVNode之后
el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
}
else if (isSameNode(oldEndVNode, newStartVNode)) {}
else if (isSameNode(oldEndVNode, newEndVNode)) {}
}
}
这轮以后位置如下:
下一轮比较很明显oldStartVNode
与newStartVNode
是可复用节点,那么对它们进行patch
,因为都在第一个位置,所以也不需要移动节点,更新指针即可:
const diff = (el, oldChildren, newChildren) => {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isSameNode(oldStartVNode, newStartVNode)) {
patchVNode(oldStartVNode, newStartVNode)
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
}
else if (isSameNode(oldStartVNode, newEndVNode)) {}
else if (isSameNode(oldEndVNode, newStartVNode)) {}
else if (isSameNode(oldEndVNode, newEndVNode)) {}
}
}
这轮过后位置如下:
再下一轮会发现oldEndVNode
与newStartVNode
是可复用节点,在新的列表里位置变成了当前比较区间的第一个,所以patch
完后需要把节点移动到oldStartVNode
的前面:
const diff = (el, oldChildren, newChildren) => {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isSameNode(oldStartVNode, newStartVNode)) {}
else if (isSameNode(oldStartVNode, newEndVNode)) {}
else if (isSameNode(oldEndVNode, newStartVNode)) {
patchVNode(oldEndVNode, newStartVNode)
// 把oldEndVNode节点移动到oldStartVNode前
el.insertBefore(oldEndVNode.el, oldStartVNode.el)
// 更新指针
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
}
else if (isSameNode(oldEndVNode, newEndVNode)) {}
}
}
这轮后位置如下:
再下一轮会发现四次比较都没有发现可以复用的节点,这咋办呢,因为最终我们需要让旧列表变成新列表,所以当前的newStartVNode
如果在旧列表里没找到可复用的,需要直接创建一个新节点插进去,但是我们一眼就看到了旧节点里有c
节点,只是不在此轮比较的四个位置上,那么我们可以直接在旧的列表里搜索,找到了就进行patch
,并且把该节点移动到当前比较区间的第一个,也就是oldStartIdx
之前,这个位置空下来了就置为null
,后续遍历到就跳过,如果没找到,那么说明这丫节点真的是新增的,直接创建该节点插入到oldStartIdx
之前即可:
// 在列表里找到可以复用的节点
const findSameNode = (list, node) => {
return list.findIndex((item) => {
return item && isSameNode(item, node)
})
}
const diff = (el, oldChildren, newChildren) => {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 某个位置的节点为null跳过此轮比较,只更新指针
if (oldStartVNode === null) {
oldStartVNode = oldChildren[++oldStartIdx]
} else if (oldEndVNode === null) {
oldEndVNode = oldChildren[--oldEndIdx]
} else if (newStartVNode === null) {
newStartVNode = oldChildren[++newStartIdx]
} else if (newEndVNode === null) {
newEndVNode = oldChildren[--newEndIdx]
}
else if (isSameNode(oldStartVNode, newStartVNode)) {}
else if (isSameNode(oldStartVNode, newEndVNode)) {}
else if (isSameNode(oldEndVNode, newStartVNode)) {}
else if (isSameNode(oldEndVNode, newEndVNode)) {}
else {
let findIndex = findSameNode(oldChildren, newStartVNode)
// newStartVNode在旧列表里不存在,那么是新节点,创建并插入之
if (findIndex === -1) {
el.insertBefore(createEl(newStartVNode), oldStartVNode.el)
} else {// 在旧列表里存在,那么进行patch,并且移动到oldStartVNode前
let oldVNode = oldChildren[findIndex]
patchVNode(oldVNode, newStartVNode)
el.insertBefore(oldVNode.el, oldStartVNode.el)
// 原位置空了置为null
oldChildren[findIndex] = null
}
// 更新指针
newStartVNode = newChildren[++newStartIdx]
}
}
}
具体到我们的示例上,在旧的列表里找到了,所以这轮过后位置信息如下:
再下一轮比较和上轮一样,会进入搜索的分支,并且找到了d
,所以也是path
加移动节点,本轮过后如下:
因为newStartIdx
大于newEndIdx
,所以while
循环就结束了,但是我们发现旧的列表里多了g
和h
节点,这两个在新列表里没有,所以需要把它们移除,反过来,如果新的列表里多了旧列表里没有的节点,那么就创建和插入之:
const diff = (el, oldChildren, newChildren) => {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isSameNode(oldStartVNode, newStartVNode)) {}
else if (isSameNode(oldStartVNode, newEndVNode)) {}
else if (isSameNode(oldEndVNode, newStartVNode)) {}
else if (isSameNode(oldEndVNode, newEndVNode)) {}
else {}
}
// 旧列表里存在新列表里没有的节点,需要删除
if (oldStartIdx <= oldEndIdx) {
for(let i = oldStartIdx; i <= oldEndIdx; i++) {
oldChildren[i] && el.removeChild(oldChildren[i].el)
}
} else if (newStartIdx <= newEndIdx) {// 新列表里存在旧列表没有的节点,创建和插入
// 在newEndVNode的下一个节点前插入,如果下一个节点不存在,那么insertBefore方法会执行appendChild的操作
let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
for(let i = newStartIdx; i <= newEndIdx; i++) {
el.insertBefore(createEl(newChildren[i]), before)
}
}
}
以上就是双端diff
的全过程,是不是还挺简单,画个图就十分容易理解了。
属性的更新
其他属性都通过data
参数传入,先修改一下h
函数:
export const h = (tag, data = {}, children) => {
// ...
return {
// ...
data
}
}
类名
类名通过data
选项的class
字段传递,比如:
h('div',{
class: {
btn: true
}
}, '文本')
类名的更新在patchVNode
方法里进行,当两个节点的类型一样,那么更新类名,替换的话就相当于设置类名:
// 更新节点类名
const updateClass = (el, newVNode) => {
el.className = ''
if (newVNode.data && newVNode.data.class) {
let className = ''
Object.keys(newVNode.data.class).forEach((cla) => {
if (newVNode.data.class[cla]) {
className += cla + ' '
}
})
el.className = className
}
}
const patchVNode = (oldVNode, newVNode) => {
// ...
// 元素标签相同,进行patch
if (oldVNode.tag === newVNode.tag) {
let el = newVNode.el = oldVNode.el
// 更新类名
updateClass(el, newVNode)
// ...
} else { // 不同使用newNode替换oldNode
let newEl = createEl(newVNode)
// 更新类名
updateClass(newEl, newVNode)
// ...
}
}
逻辑很简单,直接把旧节点的类名替换成newVNode
的类名。
样式
样式属性使用data
的style
字段传入:
h('div',{
style: {
fontSize: '30px'
}
}, '文本')
更新的时机和类名的位置一致:
// 更新节点样式
const updateStyle = (el, oldVNode, newVNode) => {
let oldStyle = oldVNode.data.style || {}
let newStyle = newVNode.data.style || {}
// 移除旧节点里存在新节点里不存在的样式
Object.keys(oldStyle).forEach((item) => {
if (newStyle[item] === undefined || newStyle[item] === '') {
el.style[item] = ''
}
})
// 添加旧节点不存在的新样式
Object.keys(newStyle).forEach((item) => {
if (oldStyle[item] !== newStyle[item]) {
el.style[item] = newStyle[item]
}
})
}
const patchVNode = (oldVNode, newVNode) => {
// ...
// 元素标签相同,进行patch
if (oldVNode.tag === newVNode.tag) {
let el = newVNode.el = oldVNode.el
// 更新样式
updateStyle(el, oldVNode, newVNode)
// ...
} else {
let newEl = createEl(newVNode)
// 更新样式
updateStyle(el, null, newVNode)
// ...
}
}
其他属性
其他属性保存在data
的attr
字段上,更新方式及位置和样式的完全一致:
// 更新节点属性
const updateAttr = (el, oldVNode, newVNode) => {
let oldAttr = oldVNode && oldVNode.data.attr ? oldVNode.data.attr : {}
let newAttr = newVNode.data.attr || {}
// 移除旧节点里存在新节点里不存在的属性
Object.keys(oldAttr).forEach((item) => {
if (newAttr[item] === undefined || newAttr[item] === '') {
el.removeAttribute(item)
}
})
// 添加旧节点不存在的新属性
Object.keys(newAttr).forEach((item) => {
if (oldAttr[item] !== newAttr[item]) {
el.setAttribute(item, newAttr[item])
}
})
}
const patchVNode = (oldVNode, newVNode) => {
// ...
// 元素标签相同,进行patch
if (oldVNode.tag === newVNode.tag) {
let el = newVNode.el = oldVNode.el
// 更新属性
updateAttr(el, oldVNode, newVNode)
// ...
} else {
let newEl = createEl(newVNode)
// 更新属性
updateAttr(el, null, newVNode)
// ...
}
}
事件
最后来看一下事件的更新,事件与其他属性不同的是如果删除一个节点的话需要把它的事件先全部解绑,否则可能会存在内存泄漏的问题,那么就需要在各个移除节点的时机都先解绑事件:
// 移除某个VNode对应的dom的所有事件
const removeEvent = (oldVNode) => {
if (oldVNode && oldVNode.data && oldVNode.data.event) {
Object.keys(oldVNode.data.event).forEach((item) => {
oldVNode.el.removeEventListener(item, oldVNode.data.event[item])
})
}
}
// 更新节点事件
const updateEvent = (el, oldVNode, newVNode) => {
let oldEvent = oldVNode && oldVNode.data.event ? oldVNode.data.event : {}
let newEvent = newVNode.data.event || {}
// 解绑不再需要的事件
Object.keys(oldEvent).forEach((item) => {
if (newEvent[item] === undefined || oldEvent[item] !== newEvent[item]) {
el.removeEventListener(item, oldEvent[item])
}
})
// 绑定旧节点不存在的新事件
Object.keys(newEvent).forEach((item) => {
if (oldEvent[item] !== newEvent[item]) {
el.addEventListener(item, newEvent[item])
}
})
}
const patchVNode = (oldVNode, newVNode) => {
// ...
// 元素标签相同,进行patch
if (oldVNode.tag === newVNode.tag) {
// 元素类型相同,那么旧元素肯定是进行复用的
let el = newVNode.el = oldVNode.el
// 更新事件
updateEvent(el, oldVNode, newVNode)
// ...
} else {
let newEl = createEl(newVNode)
// 移除旧节点的所有事件
removeEvent(oldNode)
// 更新事件
updateEvent(newEl, null, newVNode)
// ...
}
}
// 其他还有几处需要添加removeEvent(),有兴趣请看源码
以上属性的更新逻辑都比较粗糙,仅用于参考,可以参考snabbdom的源码自行完善。
总结
以上代码实现了一个简单的虚拟DOM
库,详细分解了patch
过程和diff
的过程,如果需要用在非浏览器平台上,只要把DOM
相关的操作抽象成接口,不同平台上使用不同的接口即可,完整代码在https://github.com/wanglin2/VNode-Demo。
手写一个虚拟DOM库,彻底让你理解diff算法的更多相关文章
- 放弃antd table,基于React手写一个虚拟滚动的表格
缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...
- 手撸一个虚拟DOM,不错
大家好,我是半夏,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注 点赞 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师-关注公众号:搞前端的半夏,了解更多前端知 ...
- 如何手写一个js工具库?同时发布到npm上
自从工作以来,写项目的时候经常需要手写一些方法和引入一些js库 JS基础又十分重要,于是就萌生出自己创建一个JS工具库并发布到npm上的想法 于是就创建了一个名为learnjts的项目,在空余时间也写 ...
- webview的简单介绍和手写一个H5套壳的webview
1.webview是什么?作用是什么?和浏览器有什么关系? Webview 是一个基于webkit引擎,可以解析DOM 元素,展示html页面的控件,它和浏览器展示页面的原理是相同的,所以可以把它当做 ...
- 手把手教你手写一个最简单的 Spring Boot Starter
欢迎关注微信公众号:「Java之言」技术文章持续更新,请持续关注...... 第一时间学习最新技术文章 领取最新技术学习资料视频 最新互联网资讯和面试经验 何为 Starter ? 想必大家都使用过 ...
- 『练手』手写一个独立Json算法 JsonHelper
背景: > 一直使用 Newtonsoft.Json.dll 也算挺稳定的. > 但这个框架也挺闹心的: > 1.影响编译失败:https://www.cnblogs.com/zih ...
- 教你如何使用Java手写一个基于链表的队列
在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...
- 【spring】-- 手写一个最简单的IOC框架
1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...
- 只会用就out了,手写一个符合规范的Promise
Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.Prom ...
随机推荐
- 记录,element ui的日期选择器只有第一次回显成功
首先是这个 <el-date-picker v-model="value1" type="daterange" range-separator=" ...
- 论文阅读 Continuous-Time Dynamic Network Embeddings
1 Continuous-Time Dynamic Network Embeddings Abstract 描述一种将时间信息纳入网络嵌入的通用框架,该框架提出了从CTDG中学习时间相关嵌入 Co ...
- ASP.NETCore统一处理404错误都有哪些方式?
当未找到网页并且应用程序返回 404 错误时,ASP.NET Core MVC 仅呈现通用浏览器错误页面,如下图所示 这不是很优雅,是吗? 我们平时看到的404页面一般是这样的 还有这样的 试了下京东 ...
- GO语言学习——Go语言基础之流程控制一
Go语言基础之流程控制 if else(分支结构) package main import "fmt" // if条件判断 func main(){ // age := 19 // ...
- 如何用C/C++实现去除字符串头和尾指定的字符
编程时我们经常需要对字符串进行操作,其中有一项操作就是去除字符串的头(尾)指定的字符,比如空格.通常我们会使用封装好的库函数或者类函数的Trim方法来实现,如果自己动手写一个TrimHead和Trim ...
- AliIAC 智能音频编解码器:在有限带宽条件下带来更高质量的音频通话体验
随着信息技术的发展,人们对实时通信的需求不断增加,并逐渐成为工作生活中不可或缺的一部分.每年海量的音视频通话分钟数对互联网基础设施提出了巨大的挑战.尽管目前全球的互联网用户绝大多数均处于良好的网络状况 ...
- ZABBIX新功能系列1-使用Webhook将告警主动推送至第三方系统
Zabbix5以来的新版本与以前的版本除UI界面变化较大外,在很多功能上也有许多亮点,我这里计划安排1个系列来和大家交流一些新功能的使用,这是第一篇:使用Webhook将告警主动推送至第三方系统. 首 ...
- 更换国内镜像源进行pip安装
Linux中当我们需要安装某个模块时(比如tensorflow2.0.0),常见有三种方法: pip install tensorflow==2.0.0 pip install https://pyp ...
- selenium模块使用详解、打码平台使用、xpath使用、使用selenium爬取京东商品信息、scrapy框架介绍与安装
今日内容概要 selenium的使用 打码平台使用 xpath使用 爬取京东商品信息 scrapy 介绍和安装 内容详细 1.selenium模块的使用 # 之前咱们学requests,可以发送htt ...
- numpy学习Ⅱ
今天有空再把numpy看一下,补充点不会的,再去看matplotlib 回顾之前笔记,发现之前的numpy学习Ⅰ中关于numpy的行.列.维可能表述有点不清晰,这里再叙述一下 import numpy ...