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

news2025/1/14 0:53:38

靶向更新的流程

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

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

  • 编译时阶段找出动态节点,使用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标签中的节点。

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

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

相关文章

vue3 路由跳转 携带参数

实现功能&#xff1a;页面A 跳转到 页面B&#xff0c;携带参数 路由router.ts import { createRouter, createWebHistory } from "vue-router";const routes: RouteRecordRaw[] [{path: "/demo/a",name: "aa",component: () > import(&quo…

RabbitMQ(二)七种工作模式

文章目录 概述:工作模式&#xff08;七种&#xff09;1. "Hello World!"2. Work Queues&#xff08;工作队列模式&#xff09;3. Publish/Subscribe&#xff08;发布订阅模式&#xff09;4. Routing5. Topics6. RPC7. Publisher Confirms 详细1. "Hello World!&…

探秘死锁:原理、发生条件及解决方案

探秘死锁&#xff1a;原理、发生条件及解决方案 死锁是多线程编程中常见的一个问题&#xff0c;它会导致程序停止响应&#xff0c;进而影响系统的稳定性和性能。理解死锁的原理、发生条件以及如何预防和解决死锁是编写健壮并发程序的关键。 1. 死锁的定义 死锁是指两个或多个…

前端 CSS 经典:SVG 描边动画

1. 原理 使用 css 中的 stroke 属性&#xff0c;用来描述描边的样式&#xff0c;其中重要的属性 stroke-dasharray、stroke-dashoffset。理解了这两个属性的原理&#xff0c;才能理解描边动画实现的原理。 stroke-dasharray&#xff1a;将描边线变成虚线、其中实线和虚线部分…

NDIS小端口驱动开发(一)

在四种NDIS相关的驱动中&#xff0c;微型端口驱动(也经常翻译为为小端口驱动)位于驱动栈的底部&#xff0c;一般将它理解为NIC设备的驱动程序&#xff1a; 有几种类型的微型端口驱动程序类型&#xff1a; 无连接微型端口驱动程序用于控制无连接网络媒体 &#xff0c;如以太网的…

代码随想录算法训练营第十四天(py)| 二叉树 | 递归遍历、迭代遍历、统一迭代

1 理论基础 1.1 二叉树的种类 满二叉树 只有度为0和2的节点&#xff0c;且度为0的节点在同一层。 深度为k&#xff0c;有2^k-1个节点 完全二叉树 除了最底层可能没填满&#xff0c;其余每层节点数都达到最大。并且最底层节点全部集中在左边。 二叉搜索树 是一个有数值…

【class14】人工智能初步之语音识别

【class14】 从本节课开始&#xff0c;我们将一起踏入语音识别的世界&#xff0c;学习这些知识点&#xff0c;为自己的视频匹配上字幕&#xff1a;1. 语音识别2. 采样率3. 创建语音识别应用4. 创建语音识别SDK客户端 人类的自然语言可分为两种形态&#xff1a;1.语音&#xff…

Python使用pymysql操作数据库

大家好&#xff0c;当涉及到与数据库进行交互和操作时&#xff0c;Python的pymysql库是一个常用且功能强大的选择。pymysql提供了与MySQL数据库的连接、查询、插入、更新和删除等操作的方法&#xff0c;使得在Python中进行数据库操作变得简单而高效。 1、安装 pymysql 库 在开…

python判断字符串是否为回文串的详细解析与实现

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;回文串的定义与背景 二、判断回文串的基本思路 示例解析 三、代码实…

LabVIEW和ZigBee无线温湿度监测

LabVIEW和ZigBee无线温湿度监测 随着物联网技术的迅速发展&#xff0c;温湿度数据的远程无线监测在农业大棚、仓库和其他需环境控制的场所变得日益重要。开发了一种基于LabVIEW和ZigBee技术的多区域无线温湿度监测系统。系统通过DHT11传感器收集温湿度数据&#xff0c;利用Zig…

【ARK Survival Evolved】方舟:生存进化一键使用服务器开服联机教程

1、进入控制面板 2、第一次购买服务器会安装游戏端&#xff0c;大约5分钟左右&#xff0c;如果长时间处于安装状态请联系客服 3、设置游戏端口 方舟生存进化的设置需要三个端口&#xff0c;它们用于游戏端口&#xff08;必须为首选端口&#xff09;&#xff0c;查询端口&#…

html5 笔记02

目录 01 svg的基本使用 02 svg绘图 03 进程和线程 01 svg的基本使用 svg和canvas的区别: canvas: 1.canvas作为一个容器只有一个dom元素 ,内部元素无法使用dom操作 (canvas不能展开然后选择不到 svg查看元素能选中因为是通过标签控制的) 2.canvas 是配合js完成各种绘制效果 …

【学习笔记】Webpack5(Ⅱ)

Webpack 3、高级篇 3.1、提升开发体验 —— SourceMap 3.2、提升打包速度 3.2.1 HotModuleReplacement 3.2.2 OneOf 3.2.3 Include / Exclude 3.2.4 Cache 3.2.5 Thread 3.3、减少代码体积 …

iMX6ULL 嵌入式linux开发 | 4G无线广播终端实现方案介绍

现有的有线广播&#xff0c;如村上的大喇叭&#xff0c;需要布线&#xff0c;施工麻烦。借助现有的4G网络&#xff0c;传输音频流完全没问题&#xff0c;4G网络结合流媒体技术和MQTT消息传递实现设备间的同步推拉流。这种方案可以避免有线布线的麻烦&#xff0c;同时实现4G无线…

Spark在YARN上运行图解(资源调度+任务调度)及案例

前提&#xff1a;已经安装了spark集群&#xff0c;可参考上篇文章搭建&#xff1a;http://t.csdnimg.cn/UXBOp 一、Spark集群配置YARN 1、增加hadoop 配置文件地址 vim spark-env.sh 增加export HADOOP_CONF_DIR/usr/local/soft/hadoop-3.1.1/etc/hadoop 2、关闭虚拟内存 cd …

DMR对讲机数字协议详解

一、概述 DMR数字对讲机协议是欧洲电信标准协会在2005年4月推出的数字对讲机标准&#xff0c;后来又进行了多次修改。最新版本DMR数字对讲机协议是2007年12月公布的&#xff0c;共有四部分&#xff1a;第一部分为空中接口物理层和数据链路层协议&#xff0c;第二部分为空中接口…

【Unity AR开发插件】四、制作热更数据-AR图片识别场景

专栏 本专栏将介绍如何使用这个支持热更的AR开发插件&#xff0c;快速地开发AR应用。 链接&#xff1a; Unity开发AR系列 插件简介 通过热更技术实现动态地加载AR场景&#xff0c;简化了AR开发流程&#xff0c;让用户可更多地关注Unity场景内容的制作。 “EnvInstaller…”支…

牛客网刷题 | BC93 公务员面试

目前主要分为三个专栏&#xff0c;后续还会添加&#xff1a; 专栏如下&#xff1a; C语言刷题解析 C语言系列文章 我的成长经历 感谢阅读&#xff01; 初来乍到&#xff0c;如有错误请指出&#xff0c;感谢&#xff01; 描述 公务员面试现场打分…

【安装笔记-20240520-Windows-在 QEMU 中尝试运行 OpenWRT】

安装笔记-系列文章目录 安装笔记-20240520-Windows-在 QEMU 中尝试运行 OpenWRT 文章目录 安装笔记-系列文章目录安装笔记-20240520-Windows-在 QEMU 中尝试运行 OpenWRT 前言一、软件介绍名称&#xff1a;OpenWRT主页官方介绍 二、安装步骤测试版本&#xff1a;openwrt-23.05…

C语言在VS中使用scanf报错?

我们在使用VS时&#xff0c;用scanf函数&#xff0c;VS会报以下错误&#xff1a; 以下是解决方法&#xff1a; 来到输出窗口&#xff0c;复制以下语句&#xff1a;_CRT_SECURE_NO_WARNINGS 第一种暂时方法 1.在代码的第一行&#xff0c;写下&#xff1a;#define _CRT_SECURE_…