【React源码 - Fiber架构之Reconciler】

前言

React16架构可以分为三层也是最核心的三个功能分别是:

  • Scheduler(调度器)—调度任务的优先级,高优任务优先进入Reconciler(16新增)
  • Reconciler(协调器)—负责找出变化的组件
  • Renderer(渲染器)— 负责将变化的组件渲染到页面上

本文主要是分析一下Reconciler协调器的理念以及流程。简单就是一句话:Reconciler协调器中主要功能就是使用循环实现可中断递归,并进行Fiber节点的对比,然后打上标记,然后通知Renderer更新

背景

在React中可以通过this.setState、useState、this.forceUpdate、ReactDOM.render等API触发更新,在Reconciler中,mount的组件会调用mountComponent ,update的组件会调用updateComponent 。这两个方法都会递归更新子组件。15版本的时候,是使用递归的方式来遍历Diff对比虚拟DOM的差异然后通知Renderer来进行渲染的,主要是如下功能:

  1. 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
  2. 将虚拟DOM和上次更新时的虚拟DOM对比,找出本次更新中变化的虚拟DOM
  3. 通知Renderer将变化的虚拟DOM渲染到页面上

由于递归一旦开始就不可中断,如果diff的层级较多的时候,就会出现递归时间超过16ms,导致页面卡顿。以及在ReReconciler和Renderer是交替工作的。如下图:当第一个li在页面上已经变化后,第二个li再进入Reconciler。
在这里插入图片描述由于整个过程都是同步的,所以在用户看来所有DOM是同时更新的
为了解决这些问题,React决定重写这块架构(Fiber架构),用异步可中断的更新来代替同步更新。使用循环的方式来代替递归实现可中断,从代码可以看出,每次循环都会调用shouldYield判断当前是否有剩余时间。

/** @noinline */
function  workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

而且Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记(使用二进制),类似这样:

export const Placement = /* 插入      */ 0b0000000000010;                 
export const Update = /*    更新      */ 0b0000000000100;
export const PlacementAndUpdate = /*插入并更新*/ 0b0000000000110;
export const Deletion = /* 删除       */ 0b0000000001000;

整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。所以即使Scheduler和Reconciler是异步可中断的,但是用户也不会看到未完全更新数据,因为当处理完了之后,统一通知Renderer更新,而Renderer是同步不可中断更新的。
在这里插入图片描述

接下来解释一下待会儿会提到的一些术语:

  • Reconciler工作的阶段被称为render阶段。因为在该阶段会调用组件的render方法。
  • Renderer工作的阶段被称为commit阶段。就像你完成一个需求的编码后执行git commit提交代码。commit阶段会把render阶段提交的信息渲染在页面上。
  • render与commit阶段统称为work,即React在工作中。相对应的,如果任务正在Scheduler内调度,就不属于work。

双缓存树

都知道React在Fiber架构中使用了双缓存,所以开始之前我们先要了解一下什么是双缓存树。

当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。这种在内存中构建并直接替换的技术叫做双缓存 。

简单来说,双缓存树就是使用两个树来避免更新时的闪烁问题的解决策略,一棵Current Tree(视图中的),一个WorkInProgress Tree(内存中的),这两颗树是可以通过修改alternate指向来互相转化的,当需要更新时,直接用缓存树替换视图树。React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

双缓存Fiber树

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过使current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。

接下来我们以具体例子讲解mount时、update时的构建/替换流程。

Mount时

以下面代码为例:

function  App() {
  const [num, add] = useState(0);
  return (
    <p onClick={() => add(num + 1)}>{num}</p>
  )
}

ReactDOM.render(<App/>, document.getElementById('root'));

mount阶段

首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点。(之所以要区分fiberRootNode与rootFiber,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode。)

ReactDOM.render(<A/>, dom) // rootFiberA
ReactDOM.render(<B/>, dom) // rootFiberB
ReactDOM.render(<C/>, dom) // rootFiberC

fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。
在这里插入图片描述

fiberRootNode.current = rootFiber;

由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)。

render阶段

接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。(下图中右侧为内存中构建的树,左侧为页面显示的树)。在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。
在这里插入图片描述
commit阶段

render阶段结束之后,以及有两颗树了,一个Current Fiber Tree在视图显示,一直WorkInProgress Fiber Tree在内存中构建然后,然后在commit阶段修改fiberRootNode的current指针指向workInProgress Fiber树使其变为current Fiber 树,试图更新。
在这里插入图片描述
update阶段

当节点发生更新,就会进入update阶段,再一次进入render阶段,和上面一样根据JSX生成FIber节点,进而构建FIber树(可复用Current FIber Tree的节点)
在这里插入图片描述
workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。
在这里插入图片描述

Reconciler协调器

Reconciler是独立React之外的包,支持其他包引用,包名:react-reconciler,主要功能如下:

  • 输入: 暴露api函数(如: scheduleUpdateOnFiber), 供给其他包(如react包)调用.
  • 注册调度任务: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调.
  • 执行任务回调: 在内存中构造出fiber树, 同时与与渲染器(react-dom)交互, 在- 内存中创建出与fiber对应的DOM节点.
  • 输出: 与Renderer渲染器(react-dom)交互, 渲染DOM节点.

以上功能源码都集中在ReactFiberWorkLoop.js中. 现在将这些功能(从输入到输出)串联起来, 用下图表示:
在这里插入图片描述

  • workInProgress: 代表当前已创建的workInProgress fiber。
  • performUnitOfWork: 创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。

我们知道Fiber Reconciler是从Stack Reconciler重构而来,通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”。

“递”阶段

首先从rootFiber开始向下深度优先遍历(DFS)。为遍历到的每个Fiber节点调用beginWork方法 。该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。当遍历到叶子节点(即没有子组件的组件)时就会进入下面的“归”阶段。

“归”阶段

在“归”阶段会调用completeWork 处理Fiber节点。当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。然后进入commit阶段(Renderer渲染器)

以为下面代码为例

function  App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));

其对应的FIber树为:
在这里插入图片描述
render阶段会依次执行:

1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

之所以没有 “KaSong” Fiber 的 beginWork/completeWork,是因为作为一种性能优化手段,针对只有单一文本子节点的Fiber,React会特殊处理。

这里可以看出,整个递归过程中,主要就是调用beginWork、completeWork函数,下面主要分析这两个函数的作用。

BeginWork

beginWork源码
beginWork函数主要是传入一个current fiber节点来生成子fiber节点的过程,从代码来看,接受三个参数:

  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  • workInProgress:当前组件对应的Fiber节点
  • renderLanes:优先级相关,Scheduler调度器中设置
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...省略函数体
}

从双缓存机制一节我们知道,除rootFiber以外, 组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null。所以我们可以根据current来判断当前是mount还是update。所以可以从这两个方面来分析:

  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。
  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点
function  beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    // ...省略

    // 复用current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    // ...省略其他类型
  }
}

update时
我们可以看到,满足如下情况时didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)

  • oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变
  • !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够
if (current !== null) {
    const oldProps = current.memoizedProps;   
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        // 省略处理
      }
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
  }

mount时
当不满足条件时就需要根据tag来新建子Fiber。Tag枚举

// mount时:根据tag不同,创建不同的Fiber节点
switch (workInProgress.tag) {
  case IndeterminateComponent: 
    // ...省略
  case LazyComponent: 
    // ...省略
  case FunctionComponent: 
    // ...省略
  case ClassComponent: 
    // ...省略
  case HostRoot:
    // ...省略
  case HostComponent:
    // ...省略
  case HostText:
    // ...省略
  // ...省略其他类型
}

对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren 方法

reconcileChildren
reconcileChildren函数主要做了一下事情:

  • 对于mount的组件,他会创建新的子Fiber节点
  • 对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

可以看出,也是通过current来区分是mount还是update,但是不管那个阶段最后都会生成一个子fiber节点并赋值给workInProgress.child

值得一提的是,mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。mount阶段只有rootfiber节点有effectTag属性,调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下

effectTag
我们知道Reconciler主要就是对比构建Fiber树,并标记节点需要进行的操作,就是通过effectTag属性以二进制来保存需要做什么操作

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;     
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;

beginWork流程图
在这里插入图片描述

CompleteWork

类似beginWork,completeWork也是针对不同fiber.tag调用不同的处理逻辑

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

其中需要注意注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点)。它和beginWork一样,我们根据current === null ?判断是mount还是update。同时针对HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)

case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer(); 
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    // update的情况
    // ...省略
  } else {
    // mount的情况
    // ...省略
  }
  return null;
}

mount时
mount时的主要逻辑包括三个:

  • 为Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点中
  • 与update逻辑中的updateHostComponent类似的处理props的过程
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;

// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}

update时
当节点进行update的时候,fiber节点已经存在对应的Dom节点了,所以主要处理的是props和一些回调

  • onClick、onChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop
updateHostComponent = function(                
    current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container,
  ) {
    const oldProps = current.memoizedProps;
    if (oldProps === newProps) {
      return;
    }
    const instance: Instance = workInProgress.stateNode;
    const currentHostContext = getHostContext();
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );
    workInProgress.updateQueue = (updatePayload: any);
    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

被处理完的props会保存在workInProgress.updateQueue(以key,value形式的数组,奇数下标为key,偶数下标为value),在commit阶段进行更新。

effectList
在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中,最后通过“归”阶段到rootfiber节点,形成以rootfiber为起点的单向链表。
在这里插入图片描述
completeWork流程图
在这里插入图片描述

至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。然后在commit阶段,会遍历整个effectList来更新对应的dom节点(fiber.stateNode保存的fiber对应的dom节点)

commitRoot(root);

参考文档

React技术揭秘
图解React

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

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

相关文章

Android readelf 工具查找函数符号

ELF&#xff08;Executable and Linkable Format&#xff09;是一种执行文件和可链接文件的格式。它是一种通用的二进制文件格式&#xff0c;用于在各种操作系统中存储可执行程序、共享库和内核模块。 Android 开发当中的 so 库本质上就是一种特殊类型的 ELF 文件&#xff0c;…

crash分析ramdump

我们需要在PC机上运行crash,从源码来编译, 可以从Redhat社区下载源码。 解压后&#xff0c;进入目录后使用一下命令&#xff1a; make targetarm64 make install这样就安装好了crash了。 crash介绍 crash主要是用来离线分析linux内核内存转存文件&#xff0c;它整合了gdb…

计算机基础面试题 |17.精选计算机基础面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

e2studio开发三轴加速度计LIS2DW12(1)----轮询获取加速度数据

e2studio开发三轴加速度计LIS2DW12.1--轮询获取加速度数据 概述视频教学样品申请源码下载通信模式管脚定义IIC通信模式速率新建工程工程模板保存工程路径芯片配置工程模板选择时钟设置UART配置UART属性配置设置e2studio堆栈e2studio的重定向printf设置R_SCI_UART_Open()函数原…

大数据Doris(五十二):SQL函数之数学函数

文章目录 SQL函数之数学函数 一、abs(double a)

基于多反应堆的高并发服务器【C/C++/Reactor】(中)HttpRequest模块 解析http请求协议

一、HTTP响应报文格式 HTTP/1.1 200 OK Bdpagetype: 1 Bdqid: 0xf3c9743300024ee4 Cache-Control: private Connection: keep-alive Content-Encoding: gzip Content-Type: text/html;charsetutf-8 Date: Fri, 26 Feb 2021 08:44:35 GMT Expires: Fri, 26 Feb 2021 08:44:35 GM…

系列三十五、获取Excel中的总记录数

一、获取Excel中的总记录数 1.1、概述 使用EasyExcel开发进行文件上传时&#xff0c;通常会碰到一个问题&#xff0c;那就是Excel中的记录数太多&#xff0c;使用传统的方案进行文件上传&#xff0c;很容易就超时了&#xff0c;这时可以通过对用户上传的Excel中的数量进行限制…

闰年问题-第11届蓝桥杯选拔赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第25讲。 闰年问题&#xf…

swaggerUI不好用,试试这个openapiUI?

1.背景 由于长期使用 swaggerUI 工具&#xff0c;它的轻量风格个人觉得还是不错的&#xff0c;但是它的整体使用体验确实不好&#xff0c;用过的可能都有体会&#xff0c;这里就不一一列举了&#xff08;由于语言表达能力有限&#xff0c;手动&#x1f436;保命&#xff0c;毕…

1.7数算PPT选择汇总,PTA选择汇总,计算后缀表达式,中缀转后缀、前缀、快速排序

PTA选择汇总 在第一个位置后插入&#xff0c;注意是在后面插入&#xff0c;而不是前面&#xff1b;要移动49&#xff0c;为50-I&#xff0c;第25个的话&#xff0c;移25个 如果是插在前面&#xff0c;就移动50&#xff0c;N-I1&#xff0c;注意是插在前面还是后面 删第一个&a…

JS-基础语法(一)

JavaScript简单介绍 变量 常量 数据类型 类型转换 案例 1.JavaScript简单介绍 JavaScript 是什么&#xff1f; 是一种运行在客户端&#xff08;浏览器&#xff09;的编程语言&#xff0c;可以实现人机交互效果。 JS的作用 JavaScript的组成 JSECMAScript( 基础语法 )…

变换器电感饱和以及电流变大电感变小原因分析

电感电流变大电感值变小 在一个DC-DC电源转换器中&#xff0c;电感器的电流与其电感量是有关系的。当电感器的电流增大时&#xff0c;其电感量通常会变小。 电感器的电感量&#xff08;L&#xff09;是指在单位电流变化率下&#xff0c;电感器两端的电压变化的比例。根据电感…

【JAVA GUI+MYSQL]社团信息管理系统

本社团信息管理系统主要实现登录注册、管理员信息管理、社团用户信息管理、用户申请信息管理功能模块。 目录 &#xff11;&#xff0e;系统主要功能介绍 &#xff12;&#xff0e; 数据库概念模型设计 3.具体功能模块的实现 3.1模型类 3.1.1Student.java 3.1.2User .j…

高通平台开发系列讲解(USB篇)Ubuntu 下如何使用模块

文章目录 一、查看VID、PID二、adb添加2.1、在udev下添加模块的VID2.2、重启adb服务三、虚拟串口添加(AT、Diag)沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要图解高通平台上位机使用方法 一、查看VID、PID 在ubuntu下使用模块进行AT指令发送,Diag等串…

【QML COOK】- 003-处理鼠标事件

1. 编辑main.qml import QtQuickWindow {width: 800height: 800visible: truetitle: qsTr("Hello World")Image {id: backgroudanchors.fill: parentsource: "qrc:/Resources/Images/arrow.png"}MouseArea {anchors.fill: parentonClicked: backgroud.rot…

docker run 命令详解

一、前言 Docker容器是一个开源的应用容器引擎&#xff0c;让开发者可以以统一的方式打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何安装了Docker引擎的服务器上&#xff08;包括流行的Linux机器、Windows机器&#xff09;&#xff0c;也可以实现虚拟…

【hcie-cloud】【20】容器详解【容器介绍,容器工作机制、容器常用命令说明】【上】

文章目录 前言容器是什么虚拟化技术的四个特点容器也是一种虚拟化技术容器是怎么实现虚拟化的&#xff1f;容器对比虚拟机有哪些优势&#xff1f;容器对比虚拟机有哪些不足&#xff1f;容器不仅是一种虚拟化技术&#xff0c;更重要的是一种应用打包机制容器提供的是PaaS服务常见…

SSH远程访问出现Permission denied(password)解决方法

首先&#xff0c;这个不是密码输错了的问题&#xff1b; 1、在主机先ping一下服务器 ping XXX.XXX.XX.XXX (服务器ip地址) 如果pin成功了&#xff0c;说明可以进行连接 查看服务器的ip ifconfig2、主机连接服务器 &#xff08;服务器的ip&#xff09; ssh testXXX.XXX.XX.…

Java中SpringBoot组件集成接入【MQTT中间件】

Java中SpringBoot组件集成接入【MQTT中间件】 1.MQTT介绍2.搭建MQTT服务器1.Windows2.Ubuntu3.Docker4.其他方式3.mqtt可视化客户端MQTTX及快速使用教程4.SpringBoot接入MQTT1、maven依赖2、MQTT配置3、MQTT组件具体代码1.定义通道名字2.消息发布器3.MQTT配置、生产者、消费者4…

线扫相机品牌汇总(国外+国内)

线扫相机品牌汇总(国外+国内) 行者 ​ 热爱生活 22 人赞同了该文章 线扫相机也叫做线阵相机,和面阵相机一样,都是重要的工业相机。 线扫相机正如其名字那样,拍照时像扫描一样,相机和被拍照物体有相对匀速运动。 Perhaps the most common example of line scan imagin…