vue3的节点靶向更新知识分享

靶向更新的流程

先来看看我画的整个靶向更新的流程,如下图:
在这里插入图片描述

整个流程主要分为两个大阶段:编译时和运行时。

  • 编译时阶段找出动态节点,使用patchFlag属性将其标记为动态节点。

  • 运行时阶段分为两块:执行render函数阶段和更新视图阶段。

  • 执行render函数阶段会找出所有被标记的动态节点,将其塞到block节点的dynamicChildren属性数组中。

  • 更新视图阶段会从block节点的dynamicChildren属性数组中拿到所有的动态节点,然后遍历这个数组将里面的动态节点进行靶向更新。

一个简单的demo

我们通过debug一个demo,来搞清楚vue3是如何找出动态节点以及响应式变量修改后如何靶向更新的,demo代码如下:

<template>
  <div>
    <h1>title</h1>
    <p>{{ msg }}</p>
    <button @click="handleChange">change msg</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const msg = ref("hello");

function handleChange() {
  msg.value = "world";
}
</script>

p标签绑定了响应式变量msg,点击button按钮时会将msg变量的值从hello更新为world。 由于p标签使用了msg响应式变量,所以在编译时就会找出p标签。并且将其标记为动态节点,而这里的h1标签由于没有使用响应式变量,所以不会被标记为动态节点。

在运行时阶段点击button按钮修改msg变量的值,由于我们在编译阶段已经将p标签标记为了动态节点,所以此时只需要将标记的p标签动态节点中的文本更新为最新的值即可,省去了传统 patch 函数中的比较新旧虚拟DOM的步骤。

编译阶段

编译阶段对vue内置的指令、模版语法是在transform函数中处理的。在transform函数中实际干活的是一堆转换函数,每种转换函数都有不同的作用。比如v-for标签就是由transformFor转换函数处理的,而将节点标记为动态节点就是在transformElement转换函数中处理的。

首先我们需要启动一个debug终端,才可以在node端打断点。这里以vscode举例,首先我们需要打开终端,然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。

在这里插入图片描述
然后给transformElement函数打个断点,transformElement函数在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。

transformElement转换函数

接着在debug终端中执行yarn dev(这里是以vite举例)。在浏览器中访问 http://localhost:5173/,此时断点就会走到transformElement函数中了。我们看到transformElement函数中的代码是下面这样的:

const transformElement = (node, context) => {
  return function postTransformElement() {
    // ...
  }
}

从上面可以看到transformElement函数中没有做任何事情,直接返回了一个名为postTransformElement的回调函数,我们接着给这个回调函数打上断点,将transformElement函数的断点给移除了。

每处理一个node节点都会走进一次postTransformElement函数这个断点,将断点放了,直到断点走进处理到使用响应式变量的p标签node节点时。在我们这个场景中简化后的postTransformElement函数代码如下:

const transformElement = (node, context) => {
  return function postTransformElement() {
    // 第一部分
    let vnodePatchFlag;
    let patchFlag = 0;
    const child = node.children[0];
    const type = child.type;

    // 第二部分
    const hasDynamicTextChild =
      type === NodeTypes.INTERPOLATION ||
      type === NodeTypes.COMPOUND_EXPRESSION;
    if (
      hasDynamicTextChild &&
      getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
    ) {
      patchFlag |= PatchFlags.TEXT;
    }
    if (patchFlag !== 0) {
      vnodePatchFlag = String(patchFlag)
    }

    // 第三部分
    node.codegenNode = createVNodeCall(
      vnodePatchFlag
      // ...省略
    );
  };
};

从上面可以看到简化后的postTransformElement函数主要分为三部分,其实很简单。

第一部分

第一部分很简单定义了vnodePatchFlagpatchFlag这两个变量,patchFlag变量的作用是标记节点是否为动态节点,vnodePatchFlag变量除了标记节点为动态节点之外还保存了一些额外的动态节点信息。child变量中存的是当前节点的子节点,type变量中存的是当前子节点的节点类型。

第二部分
const hasDynamicTextChild =
  type === NodeTypes.INTERPOLATION ||
  type === NodeTypes.COMPOUND_EXPRESSION;

我们接着来看第二部分,其中的hasDynamicTextChild变量表示当前子节点是否为动态文本子节点,很明显我们这里的p标签使用了响应式变量msg,其文本子节点当然是动态的,所以hasDynamicTextChild变量的值为true。

接着我们来看第二部分的这段if语句:

if (
  hasDynamicTextChild &&
  getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
) {
  patchFlag |= PatchFlags.TEXT;
}

我们先来看这段if语句的条件,如果hasDynamicTextChild为true表示当前子节点是动态文本子节点。getConstantType函数是判断动态文本节点涉及到的变量是不是不会改变的常量,为什么判断了hasDynamicTextChild还要判断getConstantType呢?

答案是如果我们给p标签绑定一个不会改变的常量,因为确实绑定了变量,hasDynamicTextChild的值还是为true。但是由于我们绑定的是不会改变的常量,所以p标签中的文本节点永远都不会改变。比如下面这个demo:

<template>
  <div>
    <p>{{ count }}</p>
  </div>
</template>

<script setup lang="ts">
const count = 10;
</script>

我们接着来看if语句里面的内容patchFlag |= PatchFlags.TEXT,如果if的判断结果为true,那么就使用“按位或”的运算符。由于此时的patchFlag变量的值为0,所以经过“按位或”的运算符计算下来patchFlag变量的值变成了PatchFlags.TEXT变量的值。我们先来看看PatchFlags中有哪些值:

enum PatchFlags {
  TEXT = 1,         // 二进制值为 1
  CLASS = 1 << 1,   // 二进制值为 10
  STYLE = 1 << 2,   // 二进制值为 100
  // ...等等等
}

这里涉及到了位运算 <<,他的意思是向左移多少位。比如TEXT表示向左移0位,二进制表示为1。CLASS表示为左移一位,二进制表示为10。STYLE表示为左移两位,二进制表示为100。

现在你明白了为什么给patchFlag赋值要使用“按位或”的运算符了吧,假如当前p标签除了有动态的文本节点,还有动态的class。那么patchFlag就会进行两次赋值,分别是:patchFlag |= PatchFlags.TEXTpatchFlag |= PatchFlags.CLASS。经过两次“按位或”的运算符进行计算后,patchFlag的二进制值就是11,二进制值信息中包含动态文本节点和动态class,从右边数的第一位1表示动态文本节点,从右边数的第二位1表示动态class。如下图:

在这里插入图片描述
这样设计其实很精妙,后面拿到动态节点进行更新时,只需要将动态节点的patchFlagPatchFlags中的枚举进行&"按位与"运算就可以知道当前节点是否是动态文本节点、动态class的节点。上面之所以没有涉及到PatchFlags.CLASS相关的代码,是因为当前例子中不存在动态class,所以我省略了。

我们接着来看第二部分的第二个if语句,如下:

if (patchFlag !== 0) {
  vnodePatchFlag = String(patchFlag)
}

这段代码很简单,如果patchFlag !== 0表示当前节点是动态节点。然后将patchFlag转换为字符串赋值给vnodePatchFlag变量,在dev环境中vnodePatchFlag字符串中还包含节点是哪种动态类型的信息。如下图:

在这里插入图片描述

第三部分

我们接着将断点走到第三部分,这一块也很简单。将createVNodeCall方法的返回值赋值给codegenNode属性,codegenNode属性中存的就是节点经过transform转换函数处理后的信息。

node.codegenNode = createVNodeCall(
  vnodePatchFlag
  // ...省略
);

我们将断点走到执行完createVNodeCall函数后,看看当前的p标签节点是什么样的。如下图:

在这里插入图片描述
从上图中可以看到此时的p标签的node节点中有了一个patchFlag属性,经过编译处理后p标签已经被标记成了动态节点。

执行render函数阶段

经过编译阶段的处理p标签已经被标记成了动态节点,并且生成了render函数。此时编译阶段的任务已经完了,该到浏览器中执行的运行时阶段了。首先我们要在浏览器中找到编译后的js文件。

其实很简单直接在network上面找到你的那个vue文件就行了,比如我这里的文件是index.vue,那我只需要在network上面找叫index.vue的文件就行了。但是需要注意一下network上面有两个index.vue的js请求,分别是template模块+script模块编译后的js文件,和style模块编译后的js文件。

那怎么区分这两个index.vue文件呢?很简单,通过query就可以区分。由style模块编译后的js文件的URL中有type=style的query,如下图所示:

在这里插入图片描述
接下来我们来看看编译后的index.vue,简化的代码如下:

import {
  createElementBlock as _createElementBlock,
  createElementVNode as _createElementVNode,
  defineComponent as _defineComponent,
  openBlock as _openBlock,
  toDisplayString as _toDisplayString,
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";

const _sfc_main = _defineComponent({
  __name: "index",
  setup(__props, { expose: __expose }) {
    // ...省略
  },
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", null, [
      _createElementVNode("h1", null, "title", -1),
      _createElementVNode(
        "p",
        null,
        _toDisplayString($setup.msg),
        1
        /* TEXT */
      ),
      _createElementVNode(
        "button",
        { onClick: $setup.handleChange },
        "change msg"
      ),
    ])
  );
}
_sfc_main.render = _sfc_render;
export default _sfc_main;

从上面的代码可以看到经过编译后生成了一个render函数,执行这个render函数就会生成虚拟DOM。仔细来看这个render函数的返回值结构,这里使用return返回了一个括号。在括号中有两项,分别是openBlock函数的返回值和createElementBlock函数的返回值。那么这里的return返回的到底是什么呢?

答案是会先执行openBlock函数,然后将createElementBlock函数执行后的值返回。

现在我们思考一个问题,在编译阶段我们只是将p标签标记成了动态节点,如果还有其他标签也是动态节点那么也会将其标记成动态节点。这些动态节点的标记还是在DOM树中的每个标签中,如果响应式变量的值改变,那么岂不还是需要去遍历DOM树?

答案是在执行render函数生成虚拟DOM的时候会生成一个block节点作为根节点,并且将这些标记的动态节点收集起来塞到block根节点的dynamicChildren属性数组中。在dynamicChildren属性数组中存的是平铺的DOM树中的所有动态节点,和动态节点在DOM树中的位置无关。

那么根block节点又是怎么收集到所有的动态子节点的呢?

我们先来搞清楚render函数中的那一堆嵌套函数的执行顺序,我们前面已经讲过了首先会执行返回的括号中的第一项openBlock函数,然后再执行括号中的第二项createElementBlock函数。createElementBlock函数是一个层层嵌套的结构,执行顺序是内层先执行,外层再执行。所以接下来会先执行里层createElementVNode生成h1标签的虚拟DOM,然后执行createElementVNode生成p标签的虚拟DOM,最后执行createElementVNode生成button标签的虚拟DOM。内层的函数执行完了后再去执行外层的createElementBlock生成div标签的虚拟DOM。如下图:

在这里插入图片描述
从上图中可以看到render函数中主要就执行了这三个函数:

  • openBlock函数
  • createElementVNode函数
  • createElementBlock函数

openBlock函数

我们先来看最先执行的openBlock函数,在我们这个场景中简化后的代码如下:

let currentBlock;

function openBlock() {
  currentBlock = [];
}

首先会定义一个全局变量currentBlock,里面会存DOM树中的所有的动态节点。在openBlock函数中会将其初始化为一个空数组,所以openBlock函数需要第一个执行。

createElementVNode函数

我们接着来看createElementVNode函数,在我们这个场景中简化后的代码如下:

export { createBaseVNode as createElementVNode };

function createBaseVNode() {
  const vnode = {
    // ...省略
  };
  if (vnode.patchFlag > 0) {
    currentBlock.push(vnode);
  }
  return vnode;
}

createElementVNode函数在内部其实叫createBaseVNode函数,从上面的代码中可以看到他除了会生成虚拟DOM之外,还会去判断当前节点是否为动态节点。如果是动态节点,那么就将其push到全局的currentBlock数组中。比如我们这里的p标签绑定了msg变量,当执行createElementVNode函数生成p标签的虚拟DOM时就会将p标签的node节点收集起来push到currentBlock数组中。

createElementBlock函数

我们来看最后执行的createElementBlock函数,在我们这个场景中简化后的代码如下:

function createElementBlock() {
  return setupBlock(
    createBaseVNode()
    // ...省略
  );
}

createElementBlock函数会先执行createBaseVNode也就是上一步说的createElementVNode函数生成最外层div标签对应的虚拟DOM。由于外层div标签没有被标记为动态节点,所以执行createElementVNode函数也就只生成div标签的虚拟DOM。

然后将div标签的虚拟DOM作为参数去执行setupBlock函数,setupBlock函数的代码如下:

function setupBlock(vnode) {
  vnode.dynamicChildren = currentBlock;
  return vnode;
}

此时子节点生成虚拟DOM的createElementVNode函数全部都已经执行完了,这个div标签也就是我们的根节点,

我们前面讲过了执行顺序是内层先执行,外层再执行,所以执行到最外层的div标签时,子节点已经全部都执行完成了。此时currentBlock数组中已经存了所有的动态子节点,将currentBlock数组赋值给根block节点(这里是div节点)的dynamicChildren属性。

现在你知道我们前面提的那个问题,根block节点是怎么收集到所有的动态子节点的呢?

后续更新视图执行patch函数时只需要拿到根节点的dynamicChildren属性,就可以拿到DOM树中的所有动态子节点。

更新视图阶段

当响应式变量改变后,对应的视图就需要更新。对应我们这个场景中就是,点击button按钮后,p标签中的内容从原来的hello,更新为world。

按照传统的patch函数此时需要去遍历比较老的虚拟DOM和新的虚拟DOM,然后找出来p标签是需要修改的node节点,然后将其文本节点更新为最新值"world"。

但是我们在上一步生成虚拟DOM阶段已经将DOM树中所有的动态节点收集起来,存在了根block节点的dynamicChildren属性中。我们接着来看在新的patch函数中是如何读取dynamicChildren属性,以及如何将p标签的文本节点更新为最新值"world"。

处理div根节点

在source面板中找到vue源码中的patch函数,给patch函数打上断点。然后点击button按钮修改msg变量的值,导致render函数重新执行,接着会走进了patch函数进行视图更新。此时代码已经走到了patch函数的断点,在我们这个场景中简化后的patch函数代码如下:

const patch = (n1, n2) => {
  processElement(n1, n2);
};

从上面可以看到简化后的patch函数中实际是调用了processElement函数,接着将断点走进processElement函数,在我们这个场景中简化后的processElement函数代码如下:

const processElement = (n1, n2) => {
  patchElement(n1, n2);
};

从上面可以看到在processElement函数中依然不是具体实现视图更新的地方,在里面调用了patchElement函数。接着将断点走进patchElement函数,在我们这个场景中简化后的patchElement函数代码如下:

const patchElement = (n1, n2) => {
  const el = (n2.el = n1.el);
  let { patchFlag, dynamicChildren } = n2;
  patchFlag = n1.patchFlag;

  if (dynamicChildren) {
    patchBlockChildren(n1.dynamicChildren, dynamicChildren);
  }

  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.CLASS) {
      // 处理动态class
    }
    if (patchFlag & PatchFlags.STYLE) {
      // 处理动态style
    }
    if (patchFlag & PatchFlags.TEXT) {
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children);
      }
    }
  }
};

从上面可以看到patchElement函数是实际干活的地方了,我们在控制台中来看看接收n1、n2这两个参数是什么样的。

先来看看n1旧虚拟DOM ,如下图:

在这里插入图片描述

从上面可以看到此时的n1为根block节点,此时p标签中的文本还是更新前的文本"hello",dynamicChildren属性为收集到的所有动态子节点。

接着来看n2新虚拟DOM,如下图:

在这里插入图片描述

从上面可以看到新虚拟DOM中p标签中的文本节点已经是更新后的文本"world"了。

我们接着来看patchElement函数中的代码,第一次处理div根节点时patchElement函数中只会执行部分代码。后面处理p标签时还会走进patchElement函数才会执行剩下的代码,当前执行的代码如下:

const patchElement = (n1, n2) => {
  let { patchFlag, dynamicChildren } = n2;
  if (dynamicChildren) {
    patchBlockChildren(n1.dynamicChildren, dynamicChildren);
  }
};

从根block节点(也就是n2新虚拟DOM)中拿到dynamicChildren。这个dynamicChildren数组我们前面讲过了,里面存的是DOM树中所有的动态节点。然后调用patchBlockChildren函数去处理所有的动态节点,我们将断点走进patchBlockChildren函数中,在我们这个场景中简化后的patchBlockChildren函数代码如下:

const patchBlockChildren = (oldChildren, newChildren) => {
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i];
    const newVNode = newChildren[i];
    patch(oldVNode, newVNode);
  }
};

patchBlockChildren函数中会去遍历所有的动态子节点,在我们这个场景中,oldVNode也就是旧的p标签的node节点,newVNode是新的p标签的node节点。然后再去调用patch函数将这个p标签动态节点更新为最新的文本节点。

如果按照vue2传统的patch函数的流程,应该是进行遍历旧的n1虚拟DOM和新的n2虚拟DOM。然后才能找出p标签是需要更新的节点,接着执行上面的patch(oldVNode, newVNode)将p标签更新为最新的文本节点。

而在vue3中由于我们在编译阶段就找出来p标签是动态节点,然后将其收集到根block节点的dynamicChildren属性中。在更新阶段执行patch函数时,就省去了遍历比较新旧虚拟DOM的过程,直接从dynamicChildren属性中就可以将p标签取出来将其更新为最新的文本节点。

处理p标签节点

我们接着来看此时执行patch(oldVNode, newVNode)是如何处理p标签的。前面已经讲过了patch函数进行层层调用后实际干活的是patchElement函数,将断点走进patchElement函数。再来回忆一下前面讲的patchElement函数代码:

const patchElement = (n1, n2) => {
  const el = (n2.el = n1.el);
  let { patchFlag, dynamicChildren } = n2;
  patchFlag = n1.patchFlag;

  if (dynamicChildren) {
    patchBlockChildren(n1.dynamicChildren, dynamicChildren);
  }
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.CLASS) {
      // 处理动态class
    }
    if (patchFlag & PatchFlags.STYLE) {
      // 处理动态style
    }
    if (patchFlag & PatchFlags.TEXT) {
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children);
      }
    }
  }
};

此时的n1就是p标签旧的虚拟DOM节点,n2就是p标签新的虚拟DOM节点。我们在编译时通过给p标签添加patchFlag属性将其标记为动态节点,并没有给p标签赋值dynamicChildren属性。所以此时不会像处理block根节点一样去执行patchBlockChildren函数了,而是会走后面的逻辑。

还记得我们前面讲的是如何给p标签设置patchFlag属性吗?

定义了一个PatchFlags枚举:

enum PatchFlags {
  TEXT = 1,         // 二进制值为 1
  CLASS = 1 << 1,   // 二进制值为 10
  STYLE = 1 << 2,   // 二进制值为 100
  // ...等等等
}

由于一个节点可能同时是:动态文本节点、动态class节点、动态style节点。所以patchFlag中需要包含这些信息。

如果是动态文本节点,那就执行“按位或”运算符:patchFlag |= PatchFlags.TEXT。执行后patchFlag的二进制值为1

如果也是动态class节点,在前一步的执行结果基础上再次执行“按位或”运算符:patchFlag |= PatchFlags.CLASS。执行后patchFlag的二进制值为11

如果也是动态style节点,同样在前一步的执行结果基础上再次执行“按位或”运算符:patchFlag |= PatchFlags.STYLE。执行后patchFlag的二进制值为111

我们前面给p标签标记为动态节点时给c。在patchElement函数中使用patchFlag属性进行"按位与"运算,判断当前节点是否是动态文本节点、动态class节点、动态style节点。

patchFlag的值是1,转换为两位的二进制后是01。PatchFlags.CLASS1 << 1,转换为二进制值为10。01和10进行&(按位与)操作,计算下来的值为00。所以patchFlag & PatchFlags.CLASS转换为布尔值后为false,说明当前p标签不是动态class标签。如下图:

在这里插入图片描述
同理将patchFlag转换为三位的二进制后是001。PatchFlags.STYLE1 << 2,转换为二进制值为100。001和100进行&(按位与)操作,计算下来的值为000。所以patchFlag & PatchFlags.CLASS转换为布尔值后为false,说明当前p标签不是动态style标签。如下图:

在这里插入图片描述
同理将patchFlag转换为一位的二进制后还是1。PatchFlags.TEXT为1,转换为二进制值还是1。1和1进行&(按位与)操作,计算下来的值为1。所以patchFlag & PatchFlags.TEXT转换为布尔值后为true,说明当前p标签是动态文本标签。如下图:

在这里插入图片描述

判断到当前节点是动态文本节点,然后使用n1.children !== n2.children判断新旧文本是否相等。如果不相等就传入eln2.children执行hostSetElementText函数,其中的el为当前p标签,n2.children为新的文本。我们来看看hostSetElementText函数的代码,如下:

function setElementText(el, text) {
  el.textContent = text;
}

setElementText函数中的textContent属性你可能用的比较少,他的作用和innerText差不多。给textContent属性赋值就是设置元素的文字内容,在这里就是将p标签的文本设置为最新值"world"。

至此也就实现了当响应式变量msg修改后,靶向更新p标签中的节点。

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

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

相关文章

C语言实现Hash Map(2):Map代码实现详解

在上一节C语言实现Hash Map(1)&#xff1a;Map基础知识入门中&#xff0c;我们介绍了Map的基础概念和在C中的用法。但我写这两篇文章的目的是&#xff0c;能够在C语言中实现这样的一个数据结构&#xff0c;毕竟有时我们的项目中可能会用到Map&#xff0c;但是C语言库中并没有提…

springboot vue 开源 会员收银系统 (2) 搭建基础框架

前言 完整版演示 前面我们对会员系统https://blog.csdn.net/qq_35238367/article/details/126174288进行了分析 确定了技术选型 和基本的模块 下面我们将从 springboot脚手架开发一套收银系统 使用脚手架的好处 不用编写基础的rabc权限系统将工作量回归业务本身生成代码 便于…

【通义千问—Qwen-Agent系列2】案例分析(图像理解图文生成Agent||多模态助手|| 基于ReAct范式的数据分析Agent)

目录 前言一、快速开始1-1、介绍1-2、安装1-3、开发你自己的Agent 二、基于Qwen-Agent的案例分析2-0、环境安装2-1、图像理解&文本生成Agent2-2、 基于ReAct范式的数据分析Agent2-3、 多模态助手 附录1、agent源码2、router源码 总结 前言 Qwen-Agent是一个开发框架。开发…

【LeetCode】【209】长度最小的子数组(1488字)

文章目录 [toc]题目描述样例输入输出与解释样例1样例2样例3 提示进阶Python实现前缀和二分查找滑动窗口 个人主页&#xff1a;丷从心 系列专栏&#xff1a;LeetCode 刷题指南&#xff1a;LeetCode刷题指南 题目描述 给定一个含有n个正整数的数组和一个正整数target找出该数组…

Java进阶学习笔记3——static修饰成员方法

成员方法的分类&#xff1a; 类方法&#xff1a;有static修饰的成员方法&#xff0c;属于类&#xff1a; 成员方法&#xff1a;无static修饰的成员方法&#xff0c;属于对象。 Student类&#xff1a; package cn.ensource.d2_staticmethod;public class Student {double scor…

SpringMVC流程

1、SpringMVC常用组件&#xff1a; DispatcherServlet&#xff08;请求分发器&#xff09;&#xff1a;Spring MVC的核心组件之一&#xff0c;负责处理全局配置和将用户请求分发给其他组件进行处理。Controller&#xff08;处理器&#xff09;&#xff1a; 实际处理业务逻辑的…

springmvc中HandlerMapping是干什么用的

HandlerMapping处理器映射器 作用是根据request找到相应的处理器Handler和Interceptors&#xff0c;然后封装成HandlerExecutionChain对象返回 HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; 实现类 HandlerMapping帮助DispatcherServlet进…

Oblivion Desktop:一款强大的网络工具介绍

一款优秀的开源网络工具。 文章目录 Oblivion Desktop: 安全与隐私的网络工具软件背景开发背景 使用方法安装日常使用高级功能 总结 Oblivion Desktop: 安全与隐私的网络工具 软件背景 Oblivion Desktop 是一个由 BePass 团队开发的开源桌面应用&#xff0c;旨在为用户提供更…

喜报 | 江苏刺掌信息科技有限公司获选市企业发展服务中心优质合作伙伴

喜报 江苏刺章信息成功入选 镇江市企业发展服务中心 “优质合作伙伴” 为进一步完善镇江市公共服务体系建设&#xff0c;提升服务范围和能力&#xff0c;更好地为企业提供专业、高效、安全的服务&#xff0c;镇江市企业发展服务中心启动了优质合作伙伴的征选工作&#xff0c;通…

win10右键没有默认打开方式的选项的处理方法

问题描述 搞了几个PDF书籍学习一下&#xff0c;不过我不想用默认的WPS打开&#xff0c;因为WPS太恶心人了&#xff0c;占用资源又高。我下载了个Sumatra PDF&#xff0c;这时候我像更改pdf文件默认的打开程序&#xff0c;发现右击没有这个选项。 问题解决 右击文件–属性–…

Linux——进程与线程

进程与线程 前言一、Linux线程概念线程的优点线程的缺点线程异常线程用途 二、Linux进程VS线程进程和线程 三、Linux线程控制创建线程线程ID及进程地址空间布局线程终止线程等待分离线程 四、习题巩固请简述什么是LWP请简述LWP与pthread_create创建的线程之间的关系简述轻量级进…

JAVA云HIS医院系统源码 HIS源码:云HIS系统与SaaS的关系

云HIS系统与SaaS的关系 云HIS系统是一种基于云计算技术的医院信息系统&#xff0c;它采用B/S架构&#xff0c;通过云端SaaS服务的方式提供。用户可以通过浏览器访问云HIS系统&#xff0c;无需关注系统的部署、维护、升级等问题。云HIS系统通常具有模板化、配置化、智能化等特点…

Android 共享内存

Parcelable 和 Serializable 区别 Serializable IO完成&#xff08;通过磁盘文件读写&#xff09; Parcelable C 对象指针 来实现共享内存 import android.os.Parcel; import androidx.annotation.NonNull;public class ApiResponseBean extends Throwable implements Parce…

吴恩达2022机器学习专项课程C2W2实验:Relu激活函数

目录 代码修改1.Activation2.Dense3.代码顺序 新的内容1.总结上节课内容2.展示ReLU激活函数的好处3.结论 代码案例一代码案例二1.构建数据集2.构建模型 2D1.构建数据集2.模型预测3.扩展 代码修改 1.Activation &#xff08;1&#xff09;需要添加代码from tensorflow.keras i…

深度学习模型keras第二十四讲:KerasNPL概述

1、KerasNPL简介 KerasNLP是一个与TensorFlow深度集成的库&#xff0c;旨在简化NLP&#xff08;自然语言处理&#xff09;任务的建模过程。它提供了一系列高级API&#xff0c;用于预处理文本数据、构建序列模型和执行常见的NLP任务&#xff0c;如情感分析、命名实体识别和机器…

深入解析力扣162题:寻找峰值(线性扫描与二分查找详解)

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

php基础笔记

开端&#xff1a; PHP 脚本可以放在文本的任意位置 PHP 脚本以 开始&#xff0c;以 ?>** 结束&#xff1a; PHP 文件的默认文件扩展名是 ".php" 标签替换 <? echo 123;?> //short_open_tagson 默认开启 <?(表达式)?> 等价于 <?php echo …

virtual box ubuntu20 全屏展示

virtual box 虚拟机 ubuntu20 系统 全屏展示 ubuntu20.04 视图-自动调整窗口大小 视图-自动调整显示尺寸 系统黑屏解决 ##设备-安装增强功能 ##进入终端 ##终端打不开&#xff0c;解决方案-传送门ubuntu Open in Terminal打不开终端解决方案-CSDN博客 ##点击cd盘按钮进入文…

保存商品信息功能(VO)

文章目录 1.分析前端保存商品发布信息的json数据1.分析commoditylaunch.vue的submitSkus1.将后面的都注销&#xff0c;只保留查看数据的部分2.填写基本信息3.保存信息&#xff0c;得到json4.使用工具格式化一下 2.使用工具将json转为model3.根据业务修改vo&#xff0c;放到vo包…

力扣hot100学习记录(七)

240. 搜索二维矩阵 II 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 题意 在二维矩阵中搜索是否存在一个目标值&#xff0c;该矩阵每一行每一列都是升序…