从异步传染浅谈代数效应

news2024/11/6 2:59:37

如果你经常使用并且关注React,你一定会在不少地方见过"代数效应"(algebra effect) 这个抽象概念。可能是翻译和过度学术的缘故,我看了很多文章才大致理解,在这里简单记录一下。

try/catch & try/handle

你一定使用过try...catch 来做异常处理,看下面的例子

function divide(x: number, y: number) {
  try {
    if (0 === y) {
      throw new Error('分母不能为0');
    }
    return x / y;
  } catch (err) {
    alert(err);
  }
}

console.log(divide(100,0))

上面例子中,我们定义了除法运算,当分母为0时会抛出异常,并且中断try块内代码的执行。在catch块中, 我们可以对错误信息进行打印,弹窗提示等等,告诉调用者/用户参数异常。

这看上去很正常,但是如果你现在有个需求,当用户输入分母为0时,自动把分母调整为1,这样就能计算了,此时你会发现,你无法在catch块中调整分母的值。因为在异常抛出,进入catch块后,try内的调用栈就会被销毁,无法重新回到异常抛出位置继续执行。也就是说,当前的javascript规范中,是没有可以处理并且恢复继续执行错误代码的语法的。

看上去很头疼,但是为了解释代数效应这个概念,我们可以假设,在未来的ESXXXX标准中,引入了新的try...handle语法,那么我们可以将代码写成:

function divide(x: number, y: number) {
  try {
    if (0 === y) {
      return x/(perform y)
    }
    return x / y;
  } handle (val) {
    if(0 === val){
      // 恢复try内代码运行
      resume 1
    }
  }
}

console.log(divide(100,0))

在上面代码中,我们引入了新的关键字 "perform" "resume" "try...handle"

在try..handle块中,我们可以使用perform关键字来抛出"可以被处理并且修复的异常(类比throw)

perform后,会进入执行handle块中的代码,可以在其中对错误进行修复,并且通过resume关键字,把修复后的值带回到perform的位置,继续执行。 此时我们就可以完成对异常分母值的替换和修复了。

当然了,再次声明,这个语法是虚构的,也许在未来的ES标准中会加入这个语法,但是当前我们不能这么写。

现在我们可以简单的说一下 “代数效应” 在后面的例子中,你会更充分的体会理解这个抽象的概念。

代数其实可以简单的理解为数学中的代数式/操作符号,比如 x+y x/y 等等,而效应可以理解为 效果。 即 ”新增代数式带来的效果“

什么意思,比如上面的Divide例子,数学中可以把除法运算表达成 x / y 这样的表达式,但是这个表达式是没有处理分母为0异常的功能的。 此时我们新增一个标识符 # , 我们定义:

#: 当分母为0时,会替换成 1 并且进行除法运算

那么我们新增了之后的表达式,就变成了 x # y 此时虽然还是除法运算,但是新增的#带来了新的效果,让除法运算变得更"强大了" 

你也许会问,啊 代数效应就是新增表达式/运算符么 可以这样理解,对于上面的例子,我们新增的 try...handle 就可以理解为 # 我们把分母为0的异常修复过程,通过一个新的运算符的引入进行封装,从而增强了Divide函数的功能,同时在函数的逻辑处理部分,我们也不需要关心这个修复过程如何进行,只需要关注逻辑运算部分,完成了结偶。

你也许会问,你这就是脱了裤子放屁,你直接在参数接受阶段判断一下并且修改不就得了,非得走异常传递这一步么? 你说的对!这个问题确实可以更加简化,但是看下面的例子,如果这个过程掺杂着异步处理,就更能凸显代数效应的作用了!

async/await 如何解决传染问题?

async/await 你肯定用过,声明了async的函数都会被作为"任务"异步处理。但是你有没有想过,这个过程是有传染性的,当外层要使用async函数的结果时,就必须使用await 但是await必须在async函数内,导致外层韩式也必须声明async,作为一个任务,一层一层,导致了async/await传染问题。

如何消除? 我们还是用上面的try...handle 解决

看如下例子,我们写一个函数打印个人信息,其中 姓名 年龄 需要通过远程服务器获取:

/** 模拟异步获取个人信息 */
function fetchRemoteInfo(type: string){
  return new Promise((resolve=>{
    setTimeout(() => {
      if('name' === type){
        resolve('李雷')
      }else if('age' === type){
        resolve(18)
      }
    }, 2000);
  }))
}

async function printPersonInfo(){
  const name = await fetchRemoteInfo('name')
  const age = await fetchRemoteInfo('age')
  console.log(`${name} ${age}岁啦!`)
}

printPersonInfo() // 4s之后,输出 "李雷 18岁啦!"

由于有传染问题,我们不想用async await 把printPersonInfo变成任务,怎么办? 

假设未来的ESXXXX标准引入了try handle语法,那么我们的函数可以写成:

/** 模拟异步获取个人信息 */
function fetchRemoteInfo(type: string){
  return new Promise((resolve=>{
    setTimeout(() => {
      if('name' === type){
        resolve('李雷')
      }else if('age' === type){
        resolve(18)
      }
    }, 2000);
  }))
}

function printPersonInfo(){
  const name = perform 'name'
  const age = perform 'age'
  console.log(`${name} ${age}岁啦!`)
}

try{
  printPersonInfo()
}handle(type){
  fetchRemoteInfo(type).then(val=>{
    /** 恢复到perform运行 */
    resume val
  })
}

上面的例子中,我们在printPersonInfo中,使用perform关键字,此时函数将会被停止(挂起)在handle块中请求数据,并且resume恢复,并且带结果到perform的位置,继续运行,达到了函数停止等待异步结果并且继续运行的目的。

你会发现,由于perfrom关键字的加入,printPersonInfo变得纯粹,没有任何的副作用,达到了逻辑和实现的分离,在编写该函数的过程中,你不需要关心如何请求name age的值,只需要知道,perform会帮你完成这些操作即可!

这就是代数效应,通过引入perfrom关键字,达到了消除异步传染,结偶异步请求的效果!

当然了! 上面都是我们的幻想,因为到目前为止,ES标准还没有加入可以恢复的perform resume关键字!那我们就没办法了吗? 也不是。

我们分析一下perfrom resume 做了什么,归纳如下:

1. 暂停函数运行

2. 返回处理结果并且从暂停位置继续运行

我们可以用当前的try catch完成或者接近实现这个过程吗? 其实可以

首先 try catch可以暂停函数的运行,但是无法在完成处理之后恢复,因为catch之后,调用栈就会被销毁,我们回不去了,但是我们可以近似的实现这个过程。

首先,要暂停恢复的函数是个纯函数,运行多次,不论每次运行到哪一行 结果一定是一样的,那么我们可不可以在获得结果后缓存这个结果,然后重新运行这个函数,当再次执行到这个异步处理时,直接使用缓存内容,不再请求,这样也就达到了恢复的效果。 具体流程如下图:

 

我们可以实现一个 runSyncMockRequest函数,用来提供perform操作

/** 同步请求函数 */
export function runSyncMockRequest(syncFunc: (perform: (type: string) => string) => void) {
  /** 存储异步函数返回结果 */
  const fetchResults: any[] = [];
  /** 当前运行到的函数index */
  let currentRunningFetchIndex = 0;
  /** 实现_fetch函数,fetch为同步函数 */
  const _perform = (type: string) => {
    /** 本函数会多次运行,如果前面的_fetch函数已经有结果,则直接使用缓存,没有再去调用winodw.fetch请求 */
    const fetchResult = fetchResults[currentRunningFetchIndex];
    if (fetchResult) {
      /** 已经有缓存了 直接返回 */
      currentRunningFetchIndex++
      return fetchResult
    } else {
      /** 没有缓存,发请求 */
      /** 为了实现同步运行,这里在发出请求后,需要暂停函数运行,等待返回结果
       *  如何实现停止 并且 等待?
       *  使用抛异常的方式暂停函数运行,抛出当前请求的Promise对象,并且在catch中,设置改Promise对象的then方法
       *  在then方法中,把请求回来的结果/失败原因 设置到缓存列表中,并且重新调用传入的syncFunc, 从头开始执行函数
       *  遇到已经返回的_fetch 直接返回值,遇到没返回的 重复上面过程,指导函数运行结束
       */

      throw fetchRemoteInfo(type).then(val=>{
        fetchResults[currentRunningFetchIndex++] = val
      })
    }
  };

  /** 注意,try catch 一定要拿到_fetch外面,因为你要通过throw终止syncFunc的运行,而不仅仅是_fetch的运行 */
  const runSync = () => {
    try {
      /** 运行传入的同步函数 */
      syncFunc(_perform);
    } catch (pendingPromise) {
      /** 检查,是否为Primise类型 */
      if (pendingPromise instanceof Promise) {
        pendingPromise.then(() => {
          /** 由于上一步的onRejected和onResolve回调都没有显式返回值,所以只会进入当前的onResolved */
          /** 不论上一步成功与否,都重置currentRunningFetchIndex 并且重新执行syncFunc */
          currentRunningFetchIndex = 0;
          runSync();
        });
      }
      return {};
    }
  };

  runSync();
}

runSyncMockReques函数内部实现了一个_perform函数,_perform会作为参数,传入syncFunc内,在syncFunc内,我们调用_perform函数,就可以同步的获取远程数据。

syncFunc内部可能调用一次或多次_perform函数发起请求,_perform可能被调用一次或者多次,runSyncMockRequest内部包含一个结果缓存fetchResults数组用来保存每次_perfrom远程获得的结果,currentRunningFetchIndex来表示当前调用的是第几个_perform函数。

当_perform执行时,如果发现数组内已经有缓存结果了,直接返回缓存内容,如果没有缓存,则发起远程异步请求,并且将promise对象通过throw的方式抛出,此时整个syncFunc函数都会因为抛出异常被停止执行。

在catch内,接收到请求的promise对象后,调用其then方法,设置在promise对象返回后重置currentRunningFetchIndex并且重新调用syncFunc函数,这就是上面说的,由于没有try handle语法,我们不能在catch处理完之后回到异常抛出的位置,那么我们只能将每次_perfrom的结果都记录下来,并且在每次发起请求之后,重新执行整个函数,之前缓存过的_perfrom函数会直接使用缓存,不会发起请求,这样也达到了恢复现场的功能。

需要注意,如果syncFunc不是纯函数,比如包含输出语句,那么会导致多次输出,需要避免!

这样,我们就简单的实现了同步发起请求的功能,perform就是我们新增的操作符,通过参数传入,实现了新的功能(效果),也是代数效应的体现。

我们可以完善一下上面的函数,在真正的生产环境下,可以通过fetch真实发起请求,并且兼容了请求失败的情况,如下:

type SyncFetch = (input: RequestInfo | URL, init?: RequestInit) => any;
type SyncFetchResult = {
  status: 'fulfilled' | 'rejected' | 'pending';
  value?: any;
  error?: any;
};

/** 同步请求函数 */
export function runSyncRequest(syncFunc: (_fetch: SyncFetch) => void) {
  /** 存储异步函数返回结果 */
  const fetchResults: SyncFetchResult[] = [];
  /** 当前运行到的函数index */
  let currentRunningFetchIndex = 0;
  /** 实现_fetch函数,fetch为同步函数 */
  const _fetch: SyncFetch = (...args) => {
    /** 本函数会多次运行,如果前面的_fetch函数已经有结果,则直接使用缓存,没有再去调用winodw.fetch请求 */
    const fetchResult = fetchResults[currentRunningFetchIndex];
    if (fetchResult) {
      /** 已经有缓存了 直接返回 */
      currentRunningFetchIndex++;
      if (fetchResult.status === 'fulfilled') {
        return fetchResult.value;
      } else if (fetchResult.status === 'rejected') {
        throw new Error(fetchResult.error);
      }
    } else {
      /** 没有缓存,发请求 */
      /** 为了实现同步运行,这里在发出请求后,需要暂停函数运行,等待返回结果
       *  如何实现停止 并且 等待?
       *  使用抛异常的方式暂停函数运行,抛出当前请求的Promise对象,并且在catch中,设置改Promise对象的then方法
       *  在then方法中,把请求回来的结果/失败原因 设置到缓存列表中,并且重新调用传入的syncFunc, 从头开始执行函数
       *  遇到已经返回的_fetch 直接返回值,遇到没返回的 重复上面过程,指导函数运行结束
       */

      const fetchResult = {} as SyncFetchResult;
      fetchResults[currentRunningFetchIndex++] = fetchResult;
      throw window
        .fetch(...args)
        .then((res) => res.json())
        .then(
          (value) => {
            /** 处理成功 */
            fetchResult.status = 'fulfilled';
            fetchResult.value = value;
          },
          (reason) => {
            /** 处理失败 */
            fetchResult.status = 'rejected';
            fetchResult.error = reason;
          },
        );
    }
  };

  /** 注意,try catch 一定要拿到_fetch外面,因为你要通过throw终止syncFunc的运行,而不仅仅是_fetch的运行 */
  const runSync = () => {
    try {
      /** 运行传入的同步函数 */
      syncFunc(_fetch);
    } catch (pendingPromise) {
      /** 检查,是否为Primise类型 */
      if (pendingPromise instanceof Promise) {
        pendingPromise.then(() => {
          /** 由于上一步的onRejected和onResolve回调都没有显式返回值,所以只会进入当前的onResolved */
          /** 不论上一步成功与否,都重置currentRunningFetchIndex 并且重新执行syncFunc */
          currentRunningFetchIndex = 0;
          runSync();
        });
      }
      return {};
    }
  };

  runSync();
}

javascript的函数是第一公民,也就是我们可以将函数作为参数传递,这也就让我们可以封装实现很多新的 “运算符” 并且通过参数传入。我们可以将副作用封装,并且通过参数传递进函数,以保证函数运算的纯粹( pure Function )。

代数效应与React

函数式组件是在React未来的趋势。我们知道,React设计初衷,函数式组件是个纯函数,其渲染结果只和其传入的props参数有关。由于原生没有状态,导致单纯的函数组件只能完成无状态的渲染,只能实现简单功能。

引入了状态,或者在函数组件内发起请求,会导致函数组件不纯,所以React引入了hooks。hooks本质也是代数效应的一种体现,通过引入新的操作符/代数,如useState useEffect等等,将副作用从函数内部抽离出去,实现了函数组件可以使用状态,发起请求的效果。同时也保证了函数组件的纯粹。

在函数组件编写的过程中,我们不需要管useState是如何识别组件,如何保存状态,状态保存在哪里了这些问题,只需要简单的使用,设置状态即可。这让函数组件内部,只需要处理业务逻辑,将实现和逻辑完成了结偶和抽离。

Suspense如何实现

我们通常使用React中的Suspense组件进行懒加载和webpack分包处理来优化性能,其使用方法如下:

const LazyComp = lazy(()=>import('../LazyComp'))

function FatherComp(){
  ...
  return <Suspense fallback='loading...'>
    <LazyComp/>
</Suspense>
}

通常,Suspense函数需要配合lazy函数使用,fallback是Suspense的必传参数,当LazyComp组件还没有请求回来之前,Suspense组件会展示fallback内容。

lazy函数需要传一个返回Promise类型的函数进去,通常使用import(path)函数来请求并且返回value为待加载组件的promise。

那么其内部是如何实现的呢?看到promise,你也许会想到上面async的例子,没错,其内部原理本质上就是上面的runSyncRequest

思考一个问题,Suspense必须配合lazy使用吗? 或者说只有lazy才能让Suspense显示fallback吗?

其实不然,我们实现一个非lazy的组件:

let isLoad = false;
function UnLazyComp() {
  if (!isLoad) {
    throw new Promise((resolve) => {
      setTimeout(() => {
        isLoad = true;
        resolve('');
      }, 2000);
    });
  }

  return <h2>UN LAZT COMP</h2>;
}

function FatherComp(){
  ...
  return <Suspense fallback='loading...'>
    <UnLazyComp/>
</Suspense>
}

在这个组件外部设置一个isLoad标记,当组件第一次加载的时候,抛出一个promise,并且在2s之后resolve,并且修改isLoad,达到仅在第一次渲染时抛出promise的效果,看输出结果:

2s之后...

会发现也达到了lazy的效果,看到这里,你肯定也猜到Suspense的原理了。

Suspense是个类组件,其内部实现了componentDidCatch这个勾子,当其子组件抛出异常的时候,可以在这里拿到异常值,我们可以检测,如果err instanceOf Promise 就修改状态值,显示fallback,并且在该promise被决定之后,修改回状态值并且展示内容。

简单的实现了个MySusupense 和 MyLazy 如下:

import React from 'react';

/** 利用代数效应,自己实现一个Suspense */

interface MySuspenseProps {
  fallback: JSX.Element | string;
}

interface MySuspenseStates {
  /** 组件是否加载 */
  loading: boolean;
}

export function myLazy(fetchFunc: () => Promise<any>) {
  let loadedComponent: any = null;
  return () => {
    if (!loadedComponent) {
      throw {
        promise: fetchFunc().then((value) => {
          loadedComponent = value?.default;
        }),
      };
    }
 
    return <>{loadedComponent()}</>;
  };
}

export default class MySuspense extends React.PureComponent<MySuspenseProps, MySuspenseStates> {
  constructor(props: MySuspenseProps) {
    super(props);

    this.state = {
      loading: false,
    };
  }

  componentDidCatch(error: any): void {
    if (error?.promise instanceof Promise) {
      this.setState({
        loading: true,
      });
      error?.promise.then(() => {
        this.setState({
          loading: false,
        });
      });
    }
  }

  render() {
    const { fallback, children } = this.props;
    return <>{this.state.loading ? fallback : children}</>;
  }
}

这里解释一下,为什么myLazy中throw的promise需要用对象包一下,因为React中包含了一些错误检测,如果直接抛出promise对象的话,React会以为你在使用Lazy函数,并且没有通过Suspense直接渲染(因为我们在使用我们自己写的Suspense),React会拦住这个promise,并且重新抛出新的错误信息:

XXX组件 suspended while rendering, but no fallback UI was specified. Add a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.

所以为了规避这个拦截,我们包一层对象,就可以在componentDidCatch中拿到错误信息了!

mylazy中包含了一个loadedComponent作为缓存,类似于上面的fetchResults,当缓存为空,则发起请求,并且throw promise ,此时该函数组件执行被中断,相当于返回了undefined,不会被渲染。

当请求返回,promise被决定后,由于修改了this.state.loading 导致MySusupense及其内部组件被重新渲染,此时 loadedComponent已经被赋值,可以直接返回loadedComponent的内容并且渲染出结果。

通过引入Suspense和lazy,可以让开发者忽略懒加载的过程,也是代数效应的体现。

仔细想想就会发现,React中代数效应无处不在,其目的也是为了分离副作用,让函数组件保持纯净。

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

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

相关文章

通信工程学习:什么是CSMA/CD载波监听多路访问/冲突检测

CSMA/CD&#xff1a;载波监听多路访问/冲突检测 CSMA/CD&#xff08;Carrier Sense Multiple Access/Collision Detect&#xff09;&#xff0c;即载波监听多路访问/冲突检测&#xff0c;是一种用于数据通信的介质访问控制协议&#xff0c;广泛应用于局域网&#xff08;特别是以…

SQL增删查改操作

目录 数据库概述 SQL基础操作 SQL通用语法 数据类型 SQL语句的分类 DDL(数据库定义,表定义,字段定义) 数据库操作 表操作 DDL小结 DML(数据的增删改,数据操作语言) DQL(查询) DQL小节 数据库概述 数据库,顾名思义就是用来存储和管理数据的,我们平时所使用的各大软…

养老院管理系统(含源码+sql+视频导入教程+文档)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 养老院管理系统拥有两种角色&#xff1a;管理员和护工 管理员&#xff1a;用户管理、老人信息管理、事故记录管理、入住费用管理、护工薪资管理、护工请假管理、床位管理、请假管理等 护…

消息中间件:RabbitMQ

消息中间件&#xff1a;RabbitMQ 前言安装Window安装Linux安装 管理页面什么是RabbitMQ&#xff1f;入门基本概念简单队列工作队列&#xff08;Work Queues&#xff09;发布/订阅&#xff08;Publish/Subscribe&#xff09;临时队列 路由&#xff08;Routing&#xff09;主题&a…

NanoDet安装教程

目录 1.安装NanoDet 1.1官网下载NanoDet 1.2 创建虚拟conda环境 1.3安装Pytorch库 2.侦测 3.训练 3.1yml文件修改 3.训练 3.1yml文件修改 轻量级模型 NanoDet-m 目标检测框架&#xff1a; YOLO 、 SSD 、 Fast R-CNN 等模型 &#xff1b;但模型太大&#xff0c;不适合移植到移…

HTTP状态码全解

文章目录 常见状态码1XX Informational&#xff08;请求正在处理&#xff09;2XX Success&#xff08;请求成功&#xff09;3XX Redirection&#xff08;重定向&#xff09;4XX Client Error&#xff08;客户端错误&#xff09;5XX Server Error&#xff08;服务器错误&#xf…

ros2安装完成后重要的一步

安装完成ros2之后&#xff0c;每次打开新的终端都需要 source /opt/ros/humble/setup.bash 为了解决这个为题&#xff0c;我们需要做如下操作,避免每次打开一个新的在终端都要设置。 在文件的最后一行添加 source /opt/ros/foxy/setup.bash

浸没式密封连接器

在当今科技快速发展的背景下&#xff0c;电子设备的整合度与性能需求持续提高&#xff0c;而连接技术作为电子设备间交互的关键&#xff0c;其重要性显而易见。在各式各样的连接技术当中&#xff0c;浸没式密封连接器凭借其独到设计和高超性能&#xff0c;在特定使用环境中显示…

学习经验分享【38】YOLOv11解读——最新YOLO版本

YOLO算法更新速度很快&#xff0c;已经出到V11版本&#xff0c;后续大家有想发论文或者搞项目可更新自己的baseline了。后续将改进YOLOv11算法&#xff0c;有需要的朋友可关注&#xff0c;我会持续进行更新。 YOLO11是Ultralytics YOLO系列实时目标检测器的最新迭代版本&#x…

《Linux从小白到高手》理论篇(七):Linux的时间管理运行级别启动过程原理详解

List item 本篇将介绍Linux的时间管理&运行级别相关知识&#xff0c;并将深入介绍Linux的启动过程及原理。 Linux的时间管理 Linux 时钟分为系统时钟&#xff08;System Clock&#xff09;和硬件&#xff08;Real Time Clock&#xff0c;简称 RTC&#xff09;时钟。系统时…

STM32CubeMX创建STM32H743工程

1、下载安装STM32CubeMX STM32CubeMX是STM32Cube工具家族中的一员&#xff0c;从MCU/MPU选型&#xff0c;引脚配置&#xff0c;系统时钟以及外设时钟设置&#xff0c;到外设参数配置&#xff0c;中间件参数配置&#xff0c;它给STM32开发者们提供了一种简单&#xff0c;方便&a…

华为OD机试 - 积木最远距离(Python/JS/C/C++ 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试真题&#xff08;Python/JS/C/C&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加入华为OD刷题交流群&#xff0c;…

[Everything] 文件搜索工具的下载及详细安装使用过程(附有下载文件)

快速搜索文件名及其所在路径 下载链接在文末 下载压缩包后解压 &#xff01;&#xff01;安装路径不要有中文 解压后得到文件 双击exe文件得到 选择简体中文&#xff0c;点击OK 点击“我接受” 更改安装目录&#xff0c;最好不要放在C盘&#xff0c;点击下一步 点击下一步 点…

使用SNAP工具处理Sentinel-1数据应注意磁盘和内存问题

近期使用SNAP处理数据比较多&#xff0c;有一些心得给大家分享一下&#xff01;在预处理Sentinel-1数据 的过程中出错基本上是有3种情况&#xff1a; 磁盘被写满 由于JAVA优化一般是通过空间换效率的方式。所以SNAP为了提高效率&#xff0c;本版本升级增加了数据本地缓存的比重…

zabbix7.0监控linux主机案例详解

前言 服务端配置 链接: rocky9.2部署zabbix服务端的详细过程 环境 主机ip应用zabbix-server192.168.10.11zabbix本体zabbix-client192.168.10.12zabbix-agent zabbix-server(服务端已配置) 具体实现过程 zabbix-client配置 安装zabbix-agent 添加扩展包 dnf -y instal…

nlp任务之预测中间词-huggingface

目录 1.加载编码器 1.1编码试算 2.加载数据集 3.数据集处理 3.1 map映射&#xff1a;只对数据集中的sentence数据进行编码 3.2用filter()过滤 单词太少的句子过滤掉 3.3截断句子 4.创建数据加载器Dataloader 5. 下游任务模型 6.测试预测代码 7.训练代码 8.保…

MKV转MP4丨FFmpeg的简单命令使用——视频格式转换

MKV是一种视频封装格式&#xff0c;很好用&#xff0c;也是OBS的默认推荐录制格式&#xff0c;因为不会突然断电关机而导致整个视频录制文件丢失。 但是MKV无法直接导入PR中剪辑&#xff0c;最直接的方法是将MKV转换为MP4格式&#xff0c;最方便且安全无损的转换方法便是用FFmp…

AD软件的分屏显示功能

1.鼠标右键点击上面的窗格&#xff0c;选择“垂直分布”&#xff0c;即可以将AD软件分屏&#xff0c;左边选择原理图&#xff0c;右边选择PCB即可以方便去设计PCB的布局。实现原理图和pcb文件的同时查看。 还可以建立起2个图之间的联动关系。 比如我们在电路图里面选择stm32 m…

Threejs中使用A*算法寻路导航

<!DOCTYPE html> <html><head><title>Threejs中使用A*算法寻路导航&#xff0c;Threejs室内室外地图导航</title><script type"text/javascript" src"libs/three.js"></script><script type"text/javas…