Vue渲染器(三):简单diff算法

news2024/11/23 3:56:13

渲染器(三):简单diff算法

我们将介绍渲染器的核心Diff算法。简单来说就是当新旧vnode的子节点都是一组节点时,为了以最小的性能开销完成更新操作,需要比较两组子节点。

1.减少DOM操作的性能开销:

核心Diff算法只关心新旧虚拟节点都存在一组子节点的清空。前面我们针对这个,采用了简单粗暴的方式完成,即卸载全部旧子节点,再挂载全部新子节点。这么做确实可以完成更新,但是没有实现DOM复用,所以会产生较大的性能开销。

以下面新旧vnode为例:

// 旧 vnode 
const oldVNode = {
    type: 'div',
    children: [
        { type: 'p', children: '1' },
        { type: 'p', children: '2' },
        { type: 'p', children: '3' }
    ]
}

// 新 vnode 
const newVNode = {
    type: 'div',
    children: [
        { type: 'p', children: '4' },
        { type: 'p', children: '5' },
        { type: 'p', children: '6' }
    ]
}

按照之前的做法,当更新子节点时,需要执行6次DOM操作:

  • 卸载所有旧子节点,3次DOM删除;
  • 挂载所有新子节点,3次DOM添加。

但是观察上面新旧vnode的子节点,可以发现:

  • 更新前后的所有子节点都是p标签,即标签元素不变;
  • 只有p标签的子节点(文本节点)会发生变化。

所以最理想的更新方式:直接更新这个p标签的文本节点的内容,这样只需要3次DOM操作。

在动手完善代码之前,来思考一个问题:新旧两组子节点的数量未必相同。

  • 当新vnode < 旧vnode时,多余旧vnode删除。
  • 当新vnode > 旧vnode时,挂载新增节点。
  • 在进行新旧vnode更新时,应该遍历其中长度较短的那一组。这样才能够尽可能多调用patch函数进行更新。
function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
        if (Array.isArray(n1.children)) {
            n1.children.forEach((c) => unmount(c));
        }
        setElementText(container, n2.children);
    } else if (Array.isArray(n2.children)) {
        // 重新实现两组子节点的更新方式,新旧children
        const oldChildren = n1.children;
        const newChildren = n2.children;
        const oldLen = oldChildren.length;
        const newLen = newChildren.length;
        // 两组子节点的公共长度,取两者中较短的一组
        const commonLength = Math.min(oldLen, newLen);
        // 遍历旧的children
        for (let i = 0; i < commonLength; i++) {
            patch(oldChildren[i], newChildren[i], container);
        }
        // 如果newLen > oldLen , 说明有新子节点需要挂载
        if (newLen > oldLen) {
            for (let i = commonLength; i < newLen; i++) {
                patch(null, newChildren[i], container);
            }
        } else if (newLen < oldLen) {
            // 有旧子节点需要卸载
            for (let i = commonLength; i < oldLen; i++) {
                unmount(oldChildren[i]);
            }
        }
    } else {
        if (Array.isArray(n1.children)) {
            n1.children.forEach(c => unmount(c));
        } else if (typeof n1.children === 'string') {
            setElementText(container, '');
        }
    }
}

这样便实现了无论新旧两组子节点的数量关系如何,渲染器都能够正确地挂载或卸载它们。

2.DOM复用与key的作用:

在前面,通过减少DOM操作的次数来提升更新性能,但是还有可优化空间。假设新旧两组子节点的内容如下:

// oldChildren 
[
    { type: 'p' },
    { type: 'div' },
    { type: 'span' }
]
// newChildren 
[
    { type: 'span' },
    { type: 'p' },
    { type: 'div' }
]

如果使用上一节介绍的算法来完成上述两组子节点的更新,需要移动6次DOM操作。

但是可以明显看到,上面的新旧vnode只是顺序不同,最优解:通过DOM的移动来完成子节点的更新,这更节约性能。那该如何移动呢?

得保证一个前提:新旧vnode确实存在可复用的节点。那如何确定新vnode有没有出现在旧vnode中呢?即如何保证上面例子中 新vnode中的第一个子节点 { type: 'span' }与旧vnode中的第三个子节点相同?

一种解决方案是通过vnode.type的值是否相同来判断,但这种方式不可靠,举例:

// oldChildren 
[
    { type: 'p', children: '1' },
    { type: 'p', children: '2' },
    { type: 'p', children: '3' }
]

// newChildren 
[
    { type: 'p', children: '3' },
    { type: 'p', children: '1' },
    { type: 'p', children: '2' }
]

上面案例可以通过移动DOM的方式来完成更新。但是所有节点的 vnode.type属性值都相同,这导致无法确定新旧vnode中节点的对应关系,也就不知道该进行怎样的DOM移动才能更新。

这时,key的作用旧体现出来了。

  • key:vnode的唯一标识。
  • 当两个vnode的 type 和 key都相同,才认为它们是相同的,就可以进行DOM复用了。

如下代码:

// oldChildren 
[
    { type: 'p', children: '1', key: 1 },
    { type: 'p', children: '2', key: 2 },
    { type: 'p', children: '3', key: 3 }
]

// newChildren 
[
    { type: 'p', children: '3', key: 3 },
    { type: 'p', children: '1', key: 1 },
    { type: 'p', children: '2', key: 2 }
]

下图展示了有key和无key时新旧两组子节点的映射情况:

![[)(]](https://img-blog.csdnimg.cn/966f016f895144e185aa59ec75313261.png)

这样,有key了就知道该如何进行相应的DOM移动操作了。

注意一点:DOM可复用并不意味着不需要更新,如:

const oldVnode = { type: 'p', key: 1, children: 'text1' }
const newVnode = { type: 'p', key: 1, children: 'text2' }

patchChildren 函数的修改:

function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
        if (Array.isArray(n1.children)) {
            n1.children.forEach((c) => unmount(c));
        }
        setElementText(container, n2.children);
    } else if (Array.isArray(n2.children)) {
        // 重新实现两组子节点的更新方式,新旧children
        const oldChildren = n1.children;
        const newChildren = n2.children;
        // 遍历新vnode
        for (let i = 0; i < newChildren.length; i++) {
            const newVnode = newChildren[i];
            for (let j = 0; j < oldChildren.length; j++) {
                const oldVnode = oldChildren[j];
                // 如果找到了具有相同key值的两个节点,说明可以复用
                // 但仍需调用patch函数更新
                if (newVnode.key === oldVnode.key) {
                    patch(oldVnode, newVnode, container);
                    break; // 这里需要break
                }
            }
        }
    } else {
        if (Array.isArray(n1.children)) {
            n1.children.forEach(c => unmount(c));
        } else if (typeof n1.children === 'string') {
            setElementText(container, '');
        }
    }
}

在上面代码中,重新实现了新旧vnode的更新逻辑。外层循环遍历新vnode,内层循环逐个对比新旧vnode的key值,试图在旧vnode中找到可复用的节点。一旦找到则调用patch函数进行打补丁。经过这一步操作之后,就能够保证所有可复用的节点本身都已经更新完毕了。接下来就需要通过移动节点来完成真实DOM顺序的更新。

3.找到需要移动的元素:

在前面,我们已经通过key找到可复用的节点了。接下来要思考的是如何判断一个节点是否需要移动,以及如何移动?

先来解决第一个问题,反过来想,那什么时候节点不需要移动?当新旧vnode的节点顺序不变时,就不需要额外的移动操作,如下图:

![[(]](https://img-blog.csdnimg.cn/8d341229f9c245f89348631709a05211.png)

来看更新算法:

  • 取新vnode中的第一个节点p-1,key为1,尝试在旧vnode中寻找具有相同key值的可复用节点,找到了,并且该节点在旧vnode中的索引为0。
  • 后两步一样。

在这个过程中,每一次寻找可复用vnode时,都会记录该可复用节点在旧vnode中的位置索引。如果把这些位置索引值按照先后顺序排列,就能得到一个序列:0、1、2。这是一个递增的序列,在这种情况下不需要移动任何节点。

再看另外一个例子:

执行更新算法:

  • 第一步:取新vnode中的第一个节点p-3,key为3。在旧vnode中找到具有相同key的可复用节点,找到了,并且该节点在旧vnode中的索引为2。
  • 第二步:同上寻找方式,该节点在旧vnode中的索引为0。
  • 第三步:该节点在旧vnode中的索引为1。

这时发现,索引递增的顺序被打破了(2、0、1)。这时就需要进行移动DOM了。这就是Diff算法在执行更新的过程中,判断节点是否需要移动的方式。

其实可以将节点p-3在旧vnode中的索引定义为:在旧vnode寻找具有相同key值节点的过程中,遇到的最大索引值。如果在后续寻找过程中,存在索引值比当前遇到的最大索引值还要小的节点,则意味着该节点需要移动。

lastIndex变量存储整个寻找过程中遇到的最大索引值,如下代码:

function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
        if (Array.isArray(n1.children)) {
            n1.children.forEach((c) => unmount(c));
        }
        setElementText(container, n2.children);
    } else if (Array.isArray(n2.children)) {
        const oldChildren = n1.children;
        const newChildren = n2.children;
        let lastIndex = 0;
        for (let i = 0; i < newChildren.length; i++) {
            const newVnode = newChildren[i];
            for (let j = 0; j < oldChildren.length; j++) {
                const oldVnode = oldChildren[j];
                if (newVnode.key === oldVnode.key) {
                    patch(oldVnode, newVnode, container);
                    if (j < lastIndex) {
                        // 如果当前找到的节点在旧vnode中的索引小于最大索引值,
                        // 说明该节点对应的真实DOM需要移动
                    } else {
                        // 更新 lastIndex的值
                        lastIndex = j;
                    }
                    break; // 这里需要break
                }
            }
        }
    } else {
        if (Array.isArray(n1.children)) {
            n1.children.forEach(c => unmount(c));
        } else if (typeof n1.children === 'string') {
            setElementText(container, '');
        }
    }
}

如上代码,变量lastIndex始终存储着当前遇到的最大索引值。

现在,已经找到了需要移动的节点,下面旧开始讨论如何移动节点从而完成节点顺序的更新。

4.如何移动元素:

在前面讨论了如何判断节点是否需要移动。移动节点:移动一个虚拟节点所对应的真实DOM节点,并不是移动虚拟节点本身。

既然移动的是真实DOM节点,就得取到对它的引用才行。在代码中,我们可以通过旧vnode中的vnode.el属性取得它对应的真实DOM节点。

回顾一下之前的patchElement 函数:

function patchElement(n1, n2) {
    // 新vnode也引用了真实DOM元素
    const el = n2.el = n1.el;
}

这样无论是新vnode还是旧vnode,都存在对真实DOM的引用。在此基础上,就可以进行DOM移动操作了。

为了解释具体应该怎样移动DOM节点,采用上一节的更新案例:

在这里插入图片描述

更新步骤如下:

  • 第一步:取新vnode中的第一个节点p-3,key为3,在旧vnode中找到具有相同key值的可复用节点,此时lastIndex 值为0,旧vnode索引为2,2>0,所以节点p-3对应的真实DOM不需要移动。然后更新 lastIndex为2。

  • 第二步:新vnode第二个节点p-1,key为1,在旧vnode中找到具有相同key值的可复用节点,此时lastIndex 值为2,旧vnode索引为0,0<2,所以节点p-1对应的真实DOM需要移动。

    • 那么该如何移动呢?节点p-1对应的真实DOM需要移动,并且我们知道 新vnode的顺序其实就是更新后真实DOM节点应有的顺序。所以把节点p-1所对应的真实DOM移动到节点p-3所对应的真实DOM后面。
    • 具体如下图:在这里插入图片描述
  • 第三步与第二步类似,就不进行分析了。

开始着手实现代码:

function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
        if (Array.isArray(n1.children)) {
            n1.children.forEach((c) => unmount(c));
        }
        setElementText(container, n2.children);
    } else if (Array.isArray(n2.children)) {
        const oldChildren = n1.children;
        const newChildren = n2.children;

        let lastIndex = 0;
        for (let i = 0; i < newChildren.length; i++) {
            const newVnode = newChildren[i];
            for (let j = 0; j < oldChildren.length; j++) {
                const oldVnode = oldChildren[j];
                if (newVnode.key === oldVnode.key) {
                    patch(oldVnode, newVnode, container);
                    if (j < lastIndex) {
                        // 代码运行到这里,说明newVNode对应的真实DOM需要移动
                        // 先获取newVnode的前一个vnode,即 prevVnode
                        const prevVnode = newChildren[i - 1];
                        // 如果prevVnode不存在,则说明当前newVnode是第一个节点,不需要移动
                        if (prevVnode) {
                            // 将newVnode对应的真实DOM移动到 prevVnode所对应的真实DOM后面
                            // 所以需要获取prevVnode所对应真实DOM的下一个兄弟节点,将其作为锚点
                            const anchor = prevVnode.el.nextSibling;
                            // 调用insert将newVnode对应的真实DOM插入到锚点前面
                            insert(newVnode.el, container, anchor);
                        }
                    } else {
                        lastIndex = j;
                    }
                    break;
                }
            }
        }
    } else {
        if (Array.isArray(n1.children)) {
            n1.children.forEach(c => unmount(c));
        } else if (typeof n1.children === 'string') {
            setElementText(container, '');
        }
    }
}

在上面代码中,如果条件 j<lastIndex成立,则说明当前 newVnode对应的真实DOM需要移动。获取其前一个虚拟节点,然后调用insert函数完成节点的移动即可,其中insert函数依赖浏览器原生API,故需要抽离:

const renderer = createRenderer({
    createElement(tag) {
        return document.createElement(tag);
    },
    setElementText(el, text) {
        el.textContent = text;
    },
    insert(el, parent, anchor = null) {
        parent.insertBefore(el, anchor);
    },
    patchProps(el, key, prevValue, nextValue) {
        if (/^on/.test(key)) {
            let invokers = el._vei || (el._vei = {});
            let invoker = invokers[key];
            const name = key.slice(2).toLowerCase();
            if (nextValue) {
                if (!invoker) {
                    invoker = el._vei[key] = (e) => {
                        // e.timeStamp:事件发生的时间
                        // 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数
                        if (e.timeStamp < invoker.attached) return;
                        if (Array.isArray(invoker.value)) {
                            invoker.value.forEach(fn => fn(e));
                        } else {
                            invoker.value(e);
                        }
                    }
                    invoker.value = nextValue;
                    // 添加 invoker.attached属性,存储事件处理函数被绑定的时间
                    invoker.attached = performance.now();
                    el.addEventListener(name, invoker);
                } else {
                    invoker.value = nextValue;
                }
            } else if (invoker) {
                el.removeEventListener(name, invoker);
            }
        } else if (key === 'class') {
            el.className = nextValue || ''
        } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key];
            if (type === 'boolean' && nextValue === '') {
                el[key] = true;
            } else {
                el[key] = nextValue;
            }
        }
        else {
            el.setAttribute(key, nextValue);
        }
    },
    createText(text) {
        return document.createTextNode(text);
    },
    setText(el, text) {
        el.nodeValue = text;
    },
    insert(el, parent, anchor = null) {
        parent.insertBefore(el, anchor);
    }
})

5.添加新元素:

这节讨论添加新节点的情况,如图:
在这里插入图片描述

观察可知,在新vnode中,多出一个p-4,key值为4,该节点在旧vnode中不存在,因此将其视为新增节点。

对于新增节点,在更新时要将它挂载,这主要分为两步:

  • 想办法找到新增节点;
  • 将新增节点挂载到正确位置。

先来看一下如何找到新增节点,根据前面实现的简单diff算法来模拟下图中的例子:
在这里插入图片描述

此时的更新逻辑:前两步解释和前面一样。当第三步时,新vnode中的第三个节点为p-4,它的key值为4,在旧vnode中寻找可复用的节点,发现找不到,因此渲染器会把节点p-4看作新增节点并挂载它。那么应该挂载到哪里呢?

观察p-4在新vnode中的位置,它在p-1节点的后面,所以应该把它挂载到节点p-4所对应的真实DOM后面。第

四步移动操作完后真实DOM顺序是:p-3、p-1、p-4、p-2。

在这里插入图片描述

代码实现:

function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
        if (Array.isArray(n1.children)) {
            n1.children.forEach((c) => unmount(c));
        }
        setElementText(container, n2.children);
    } else if (Array.isArray(n2.children)) {
        const oldChildren = n1.children;
        const newChildren = n2.children;

        let lastIndex = 0;
        for (let i = 0; i < newChildren.length; i++) {
            const newVnode = newChildren[i];
            // 在第一层循环中定义find,代表是否在旧vnode中找到可复用节点
            // 初始值为false,则代表没找到
            let find = false;
            for (let j = 0; j < oldChildren.length; j++) {
                const oldVnode = oldChildren[j];
                if (newVnode.key === oldVnode.key) {
                    // 一旦找到可复用的节点,将变量find的值设为true
                    find = true;
                    patch(oldVnode, newVnode, container);
                    if (j < lastIndex) {
                        const prevVnode = newChildren[i - 1];
                        if (prevVnode) {
                            const anchor = prevVnode.el.nextSibling;
                            insert(newVnode.el, container, anchor);
                        }
                    } else {
                        lastIndex = j;
                    }
                    break;
                }
            }
            // 如果代码运行到这里,find仍然为false,
            // 说明新vnode没有在旧vnode中找到可复用的节点,此时说明新vnode是新增节点,执行挂载
            if (!find) {
                // 为了将节点挂载到正确位置,需要先获取锚点元素
                // 获取当前新vnode的前一个vnode节点
                const prevVnode = newChildren[i - 1];
                let anchor = null;
                if (prevVnode) {
                    // 如果有前一个vnode节点,则使用它的下一个兄弟节点作为锚点元素
                    anchor = prevVnode.el.nextSibling;
                } else {
                    // 如果没有前一个vnode节点,说明即将挂载的新vnode是第一个子节点
                    // 这时使用容器元素的 findChild 作为锚点
                    anchor = container.firstChild;
                }
                // 挂载新vnode
                patch(null, newVnode, container, anchor);
            }
        }
    } else {
        if (Array.isArray(n1.children)) {
            n1.children.forEach(c => unmount(c));
        } else if (typeof n1.children === 'string') {
            setElementText(container, '');
        }
    }
}

上面代码的解释都写在注释里了,并且由于目前实现的patch函数不支持传递第四个参数,修改patch函数:

    function patch(n1, n2, container, anchor) {
        if (n1 && n1.type !== n2.type) {
            unmount(n1);
            n1 = null;
        }
        const { type } = n2;
        if (typeof type === 'string') {
            if (!n1) {
                // 挂载时将锚点元素作为第三个参数传递给 mountElement
                mountElement(n2, container, anchor);
            } else {
                patchElement(n1, n2);
            }
        } else if (typeof type === 'Text') {
            if (!n1) {
                const el = n2.el = createTextNode(n2.children);
                insert(el, container);
            } else {
                const el = n2.el = n1.el;
                if (n2.children !== n1.children) {
                    setText(el, n2.children);
                }
            }
        } else if (type === 'Fragment') {
            if (!n1) {
                n2.children.forEach(c => patch(null, c, container));
            } else {
                patchChildren(n1, n2, container);
            }
        }
    }
// 它需要增加第三个参数,即锚点元素
function mountElement(vnode, container, anchor) {
	// 省略部分代码
	// 插入节点时,将锚点元素传递给insert函数
	insert(el, container, anchor);
}

6.移除不存在的元素:

在更新子节点时,不仅会遇到新增元素,还有可能遇到元素被删除的情况,如图:

在新vnode中,节点p-2不存在,这说明该节点被删除了。那么渲染器该如何找到那些需要删除的节点并正确地删除呢?

先看如何找到需要删除的节点,模拟执行更新的过程:

  • 第一步:跟前面一样。
  • 第二步:新vnode中的第二个节点p-1,在旧vnode中找到对应的,进行移动。

此时真实DOM的状态如图所示:
在这里插入图片描述

至此更新便结束了,但是p-2对应的真实DOM仍然存在,所以需要增加额外的逻辑来删除遗留节点。

思路很简单,当更新结束后,再遍历一遍旧vnode,然后去新vnode中寻找具有相同key的节点,找不到则删除该节点。如下代码:

function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
        if (Array.isArray(n1.children)) {
            n1.children.forEach((c) => unmount(c));
        }
        setElementText(container, n2.children);
    } else if (Array.isArray(n2.children)) {
        const oldChildren = n1.children;
        const newChildren = n2.children;

        let lastIndex = 0;
        for (let i = 0; i < newChildren.length; i++) {
            const newVnode = newChildren[i];
            let find = false;
            for (let j = 0; j < oldChildren.length; j++) {
                const oldVnode = oldChildren[j];
                if (newVnode.key === oldVnode.key) {
                    // 一旦找到可复用的节点,将变量find的值设为true
                    find = true;
                    patch(oldVnode, newVnode, container);
                    if (j < lastIndex) {
                        const prevVnode = newChildren[i - 1];
                        if (prevVnode) {
                            const anchor = prevVnode.el.nextSibling;
                            insert(newVnode.el, container, anchor);
                        }
                    } else {
                        lastIndex = j;
                    }
                    break;
                }
            }
            if (!find) {
                const prevVnode = newChildren[i - 1];
                let anchor = null;
                if (prevVnode) {
                    anchor = prevVnode.el.nextSibling;
                } else {
                    anchor = container.firstChild;
                }
                // 挂载新vnode
                patch(null, newVnode, container, anchor);
            }
        }
        // 上一步的更新操作完成后,遍历旧vnode
        for (let i = 0; i < oldChildren.length; i++) {
            const oldVnode = oldChildren[i];
            // 拿旧node去信vnode中寻找有相同key值的节点
            const has = newChildren.find(
                vnode => vnode.key === oldVnode.key
            )
            if (!has) {
                // 如果没有中找到则删除该节点
                unmount(oldVnode);
            }
        }
    } else {
        if (Array.isArray(n1.children)) {
            n1.children.forEach(c => unmount(c));
        } else if (typeof n1.children === 'string') {
            setElementText(container, '');
        }
    }
}

如以上代码及注释所示。

7.总结:

在这一章中,我们讨论了Diff算法作用,主要是用来计算新旧vnode的差异,并尝试最大程度复用DOM元素。并解释了为什么需要Diff算法,以及DOM复用带来的好处,以及key的作用。

并通过几个例子讲解了渲染器是如何移动、添加、删除vnode所对应的DOM元素的。

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

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

相关文章

第46篇 进阶(六) 国际化

导语 在第2篇中讲述如何显示中文时&#xff0c;曾提到使用QTextCodec和tr()的方式直接显示中文&#xff0c;其实这只是一种临时的方法&#xff0c;方便我们快速完成程序&#xff0c;显示效果。当真正要发布一个程序时&#xff0c;最好的方式是在程序中使用英文字符串&#xff…

四、函数基础、函数种类、形实参和映射关系

四、函数基础、函数种类、形实参和映射关系 编程基本原则&#xff1a;高内聚、低耦合。 我总结&#xff1a; 模块自身&#xff0c;要求高内聚&#xff1a;模块中代码相关性变强&#xff0c;代码紧密联系程度变高&#xff0c;希望它能独立完成一个功能。模块之间&#xff0c;…

【20221213】【每日一题】零钱兑换II

给你一个整数数组 coins 表示不同面额的硬币&#xff0c;另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额&#xff0c;返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。 …

新课程导学杂志新课程导学杂志社新课程导学编辑部2022年第30期目录

前沿《新课程导学》投稿&#xff1a;cn7kantougao163.com 贵州四条线路入选“稻花香里说丰年”全国乡村旅游精品线路 邓小青; 1 基地 梯云村落&#xff0c;晒秋人家——用绚烂色调谱写秋意之诗 邓小青; 2-9 分享 走进“那”文化回归稻香梦聆听丰收声——2022年顶蛳…

Redis 7 新特性之 自定义Functions

Redis 7 新特性之 自定义Functions Redis Functions&#xff08;函数&#xff09;是用于管理服务端执行代码的API。在Redis 7中出现&#xff0c;旨在取代之前版本的EVAL函数&#xff0c;是Redis 7新特性之一。 Eval 脚本的缺点 Redis 7之前的版本通过Eval执行脚本&#xff0…

流媒体协议介绍(RTP/RTCP/RTSP/RTMP/MMS/HLS/HTTP/ HTTP-FLV(HDL) /SDP)

流媒体协议介绍&#xff08;RTP/RTCP/RTSP/RTMP/MMS/HLS/HTTP/ HTTP-FLV(HDL) /SDP&#xff09; 一、RTP&#xff1a;实时传输协议&#xff08;Real-time Transport Protocol&#xff09; RTP是一种基于包的传输协议&#xff0c;它用来传输实时数据。在网络上传输数据包的延迟…

艾美捷CD8α体内抗体参数说明化学性质

CD8a&#xff08;Ly 2.2&#xff09;存在于大多数胸腺细胞和包括大多数T抑制/细胞毒性细胞的成熟T淋巴细胞亚群的表面。CD8通过与T细胞受体复合物和蛋白酪氨酸激酶lck的结合参与T细胞活化。 艾美捷CD8α体内抗体基本参数&#xff1a; 中文名称&#xff1a;抗小鼠CD8a体内抗体-…

Kafka极客 - 15 重设消费者位移 Offset

文章目录1. 为什么要重设消费者组位移&#xff1f;2. 重设位移策略3. 消费者 API 方式设置4. 命令行方式设置1. 为什么要重设消费者组位移&#xff1f; 我们知道&#xff0c;Kafka 和传统的消息引擎在设计上是有很大区别的&#xff0c;其中一个比较显著的区别就是&#xff0c;…

怎么看xray发了那些数据包

怎么看xray发了那些数据包。版本说明&#xff1a;Xray 下载地址&#xff1a;https://github.com/chaitin/xray/releases 使用环境&#xff1a;windows、linux、macos皆可 工具说明&#xff1a;Xray扫描器是一款功能强大的安全评估工具。支持主动、被动多种扫描方式&#xff…

UNIAPP实战项目笔记51 登录用户名和密码输入框的数据验证功能

UNIAPP实战项目笔记51 登录账号用户名和密码输入框的数据验证功能 实际案例图片 账号验证 密码验证 登录成功跳转 显示登录和注册页面布局 账号密码的验证功能和登录验证提交 具体内容图片自己替换哈&#xff0c;随便找了个图片的做示例 具体位置见目录结构 完善布局页面和样式…

基于PHP和MySQL的新闻发布系统

关于世界杯⚽️ 国际足联世界杯&#xff08;FIFA World Cup&#xff09;&#xff0c;简称“世界杯”&#xff0c;是由全世界国家级别球队参与&#xff0c;象征足球界最高荣誉&#xff0c;并具有最大知名度和影响力的足球赛事。世界杯全球电视转播观众超过35亿 。世界杯每四年举…

【设计模式】简单工厂模式描述总结

简单工厂模式 定义&#xff1a;定义一个创建对象的接口&#xff0c;让子类决定实例化哪一个类。 类型&#xff1a;创建型模式 介绍&#xff1a; 在简单工厂模式中定义一个抽象产品类&#xff0c;抽象产品类声明公共的特性及属性&#xff0c;具体产品类继承抽象产品类后去实…

Educational Codeforces Round 121 (Rated for Div. 2) C. Monsters And Spells

翻译&#xff1a; Monocarp又在玩电脑游戏了。他是个巫师学徒&#xff0c;只会一个咒语。幸运的是&#xff0c;这个法术可以伤害怪物。 他目前所在的关卡包含&#x1d45b;个怪物。他们中的&#x1d456;-th在关卡开始后&#x1d458;&#x1d456;秒出现&#xff0c;并拥有ℎ…

Java石头剪刀布

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;JAVA开发者…

【iMessage苹果源码家庭推】tils扩大软件安装大概释放事变是由程序员筑造的,很轻易发生MemoryLeak控制

推荐内容IMESSGAE相关 作者推荐内容iMessage苹果推软件 *** 点击即可查看作者要求内容信息作者推荐内容1.家庭推内容 *** 点击即可查看作者要求内容信息作者推荐内容2.相册推 *** 点击即可查看作者要求内容信息作者推荐内容3.日历推 *** 点击即可查看作者要求内容信息作者推荐…

[附源码]计算机毕业设计的小区宠物管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

小侃设计模式(十七)-中介者模式

1.概述 中介者模式&#xff08;Mediator Pattern&#xff09;是用来降低多个对象和类之间的通信复杂性&#xff0c;这种模式提供了一个中介类&#xff0c;来封装一组对象之间的交互&#xff0c;它将对象之间的交互委派给中介对象交互&#xff0c;避免了对象之间的直接交互。中…

Vue2基础总结

知识点学了太多还是需要总结复习&#xff0c;否则后面会因为零碎的知识点而感到繁杂&#xff0c;那么今天我来总结一下vue相关的知识点&#xff0c;新学习vue的朋友也可以把这当做一个细致总结&#xff1a; 1.Vue是什么&#xff08;重点&#xff09;&#xff1a; 对于Vue的总…

创建 Vue3.0 工程

1.使用 vue-cli 创建 官方文档 : https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create // 查看vue/cli版本&#xff0c;确保vue/cli版本在4.5.以上 vue --version vue -V// 安装或者升级你的vue/cli、 覆盖安装最新版本; npm install -g vue/cli//1.创建…

C++初阶 stack和queue的模拟实现

作者&#xff1a;小萌新 专栏&#xff1a;C初阶 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;模拟实现STL库中的stack和queue 考试周结束咯 狠狠的学&#xff01; stack和queue的模拟实现容器适配器Stack模拟实现接口函数一览代码…