React 之 Suspense

Suspense

Suspense 组件我们并不陌生,中文名可以理解为暂停or悬停  , 在 React16 中我们通常在路由懒加载中配合 Lazy 组件一起使用 ,当然这也是官方早起版本推荐的唯一用法。

那它暂停了什么? 进行异步网络请求,然后再拿到请求后的数据进行渲染是很常见的需求,但这不可避免的需要先渲染一次没有数据的页面,数据返回后再去重新渲染。so , 我们想要暂停的就是第一次的无数据渲染。

通常我们在没有使用Suspense 时一般采用下面这种写法, 通过一个isLoading状态来显示加载中或数据。这样代码是不会有任何问题,但我们需要手动去维护一个isLoading 状态的值。

const [data, isLoading] = fetchData("/api");
if (isLoading) {return <Spinner />;
}
return <MyComponent data={data} />;

当我们使用Suspense 后,使用方法会变为如下, 我们只需将进行异步数据获取的组件进行包裹,并将加载中组件通过fallback传入

return (<Suspense fallback={<Spinner />}><MyComponent /></Suspense>
);

那 React 是如何知道该显示MyComponent还是Spinner的?

答案就在于MyComponent内部进行fetch远程数据时做了一些手脚。

export const App = () => {return (<div><Suspense fallback={<Spining />}><MyComponent /></Suspense></div>);
};function Spining() {return <p>loading...</p>;
}let data = null;function MyComponent() {if (!data) {throw new Promise((resolve) => {setTimeout(() => {data = 'kunkun';resolve(true);}, 2000);});}return (<p>My Component, data is {data}</p>);
}

Suspense是根据捕获子组件内的异常来实现决定展示哪个组件的。这有点类似于ErrorBoundary ,不过ErrorBoundary是捕获 Error 时就展示回退组件,而Suspense 捕获到的 Error 需要是一个Promise对象(并非必须是 Promise 类型,thenable 的都可以)。

我们知道 Promise 有三个状态,pendingfullfilledrejected ,当我们进行远程数据获取时,会创建一个Promise,我们需要直接将这个Promise 作为Error进行抛出,由 Suspense 进行捕获,捕获后对该thenable对象的then方法进行回调注册thenable.then(retry) , 而 retry 方法就会开始一个调度任务进行更新,后面会详细讲。

file

知道了大致原理,这时还需要对我们的fetcher进行一层包裹才能实际运用。

// MyComponent.tsx
const getList = wrapPromise(fetcher('http://api/getList'));export function MyComponent() {const data = getList.read();return (<ul>{data?.map((item) => (<li>{item.name}</li>))}</ul>);
}function fetcher(url) {return new Promise((resove, reject) => {setTimeout(() => {resove([{ name: 'This is Item1' }, { name: 'This is Item2' }]);}, 1000);});
}// Promise包裹函数,用来满足Suspense的要求,在初始化时默认就会throw出去
function wrapPromise(promise) {let status = 'pending';let response;const suspend = promise.then((res) => {status = 'success';response = res;},(err) => {status = 'error';response = err;});const read = () => {switch (status) {case 'pending':throw suspend;default:return response;}};return { read };

从上述代码我们可以注意到,通过const data = getList.read() 这种同步的方式我们就能拿到数据了。 注意: 上面这种写法并非一种范式,目前官方也没有给出推荐的写法
为了与Suspense配合,则我们的请求可能会变得很不优雅 ,官方推荐是直接让我们使用第三方框架提供的能力使用Suspense请求数据,如 useSWR 等
下面时useSWR的示例,简明了很多,并且对于Profile组件,数据获取的写法可以看成是同步的了。

import { Suspense } from 'react'
import useSWR from 'swr'function Profile () {const { data } = useSWR('/api/user', fetcher, { suspense: true })return <div>hello, {data.name}</div>
}function App () {return (<Suspense fallback={<div>loading...</div>}><Profile/></Suspense>)
}

Suspense的另一种用法就是与懒加载lazy组件配合使用,在完成加载前展示Loading

<Suspense fallback={<GlobalLoading />}>{lazy(() => import('xxx/xxx.tsx'))}
</Suspense>

由此得出,通过lazy返回的组件也应该包裹一层类似如上的 Promise,我们看看 lazy 内部是如何实现的。
其中ctor就是我们传入的() => import('xxx/xxx.tsx'), 执行lazy也只是帮我们封装了层数据结构。

export function lazy<T>(ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {const payload: Payload<T> = {// We use these fields to store the result._status: Uninitialized,_result: ctor,};const lazyType: LazyComponent<T, Payload<T>> = {$$typeof: REACT_LAZY_TYPE,_payload: payload,_init: lazyInitializer,};return lazyType;
}

React 会在Reconciler过程中去实际执行,在协调的render阶段beginWork中可以看到对lazy单独处理的逻辑。

function mountLazyComponent(_current,workInProgress,elementType,renderLanes,
) {const props = workInProgress.pendingProps;const lazyComponent: LazyComponentType<any, any> = elementType;const payload = lazyComponent._payload;const init = lazyComponent._init;// 在此处初始化lazylet Component = init(payload);// 下略
}

那我们再来看看init干了啥,也就是封装前的lazyInitializer方法,整体跟我们之前实现的 fetch 封装是一样的。

function lazyInitializer<T>(payload: Payload<T>): T {if (payload._status === Uninitialized) {const ctor = payload._result;// 这时候开始进行远程模块的导入const thenable = ctor();thenable.then(moduleObject => {if (payload._status === Pending || payload._status === Uninitialized) {// Transition to the next state.const resolved: ResolvedPayload<T> = (payload: any);resolved._status = Resolved;resolved._result = moduleObject;}},error => {if (payload._status === Pending || payload._status === Uninitialized) {// Transition to the next state.const rejected: RejectedPayload = (payload: any);rejected._status = Rejected;rejected._result = error;}},);}if (payload._status === Resolved) {const moduleObject = payload._result;}return moduleObject.default;} else {// 第一次执行肯定会先抛出异常throw payload._result;}
}

Suspense 底层是如何实现的?

其底层细节非常之多,在开始之前,我们先回顾下 React 的大致架构

Scheduler: 用于调度任务,我们每次setState可以看成是往其中塞入一个Task,由Scheduler内部的优先级策略进行判断何时调度运行该Task

Reconciler: 协调器,进行 diff 算法,构建 fiber 树

Renderer: 渲染器,将 fiber 渲染成 dom 节点

Fiber 树的结构, 在 reconciler 阶段,采用深度优先的方式进行遍历,往下递即调用beginWork的过程,往上回溯即调用ComplteWork的过程
 

file


我们先直接进入Reconciler 中分析下Suspensefiber节点是如何被创建的

function beginWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,
): Fiber | null {switch (workInProgress.tag) {case HostText:return updateHostText(current, workInProgress);case SuspenseComponent:return updateSuspenseComponent(current, workInProgress, renderLanes);// 省略其他类型}
}
  • beginWork中会根据**不同的组件类型**执行不同的创建方法, 而Suspense 对应的会进入到updateSuspenseComponent
function updateSuspenseComponent(current, workInProgress, renderLanes) {const nextProps = workInProgress.pendingProps;let showFallback = false;// 标识该Suspense是否已经捕获过子组件的异常了const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;if (didSuspend) {showFallback = true;workInProgress.flags &= ~DidCapture;} // 第一次组件加载if (current === null) {const nextPrimaryChildren = nextProps.children;const nextFallbackChildren = nextProps.fallback;// 第一次默认不展示fallback,因为要先走到children后才会产生异常if (showFallback) {const fallbackFragment = mountSuspenseFallbackChildren(workInProgress,nextPrimaryChildren,nextFallbackChildren,renderLanes,);const primaryChildFragment: Fiber = (workInProgress.child: any);primaryChildFragment.memoizedState = mountSuspenseOffscreenState(renderLanes,);return fallbackFragment;} else {return mountSuspensePrimaryChildren(workInProgress,nextPrimaryChildren,renderLanes,);}} else {// 如果是更新,操作差不多,此处略}
}
  • 第一次updateSuspenseComponent 时 ,我们会把mountSuspensePrimaryChildren 的结果作为下一个需要创建的fiber , 因为需要先去触发异常。
  • 实际上mountSuspensePrimaryChildren  会为我们的PrimaryChildren 在包上一层OffscreenFiber 。
function mountSuspensePrimaryChildren(workInProgress,primaryChildren,renderLanes,
) {const mode = workInProgress.mode;const primaryChildProps: OffscreenProps = {mode: 'visible',children: primaryChildren,};const primaryChildFragment = mountWorkInProgressOffscreenFiber(primaryChildProps,mode,renderLanes,);primaryChildFragment.return = workInProgress;workInProgress.child = primaryChildFragment;return primaryChildFragment;
}

什么是OffscreenFiber/Component  ?
通过其需要的 mode 参数值,我们可以大胆的猜测,应该是一个能控制是否显示子组件的组件,如果hidden,则会通过 CSS 样式隐藏子元素。
 

file


在这之后的 Fiber 树结构
 

file


当我们向下执行到MyComponent 时,由于抛出了错误,当前的reconciler阶段会被暂停
让我们再回到 Reconciler 阶段的起始点可以看到有Catch语句。

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {// 省略..do {try {workLoopConcurrent();break;} catch (thrownValue) {handleError(root, thrownValue);}} while (true);// 省略..
}performConcurrentWorkOnRoot(root, didTimeout) {// 省略..let exitStatus = shouldTimeSlice? renderRootConcurrent(root, lanes): renderRootSync(root, lanes);// 省略..
}

我们再看看错误处理函数handleError中做了些什么

function handleError(root, thrownValue): void {// 这时的workInProgress指向MyComponentlet erroredWork = workInProgress;try {throwException(root,erroredWork.return,erroredWork,thrownValue,workInProgressRootRenderLanes,);completeUnitOfWork(erroredWork);
}function throwException(root: FiberRoot, returnFiber: Fiber, sourceFiber: Fiber, value: mixed, rootRenderLanes: Lanes) 
{// 给MyComponent打上未完成标识sourceFiber.flags |= Incomplete;if (value !== null &&typeof value === 'object' &&typeof value.then === 'function') {// wakeable就是我们抛出的Promiseconst wakeable: Wakeable = (value: any);// 向上找到第一个Suspense边界const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);if (suspenseBoundary !== null) {// 打上标识suspenseBoundary.flags &= ~ForceClientRender;suspenseBoundary.flags |= ShouldCapture;// 注册监听器attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes);return;}
}

主要做了三件事

  • 给抛出错误的组件打上Incomplete标识
  • 如果捕获的错误是 thenable 类型,则认定为是 Suspense 的子组件,向上找到最接近的一个Suspense 边界,并打上ShouldCapture 标识
  • 执行attachRetryListener 对 Promise 错误监听,当状态改变后开启一个调度任务重新渲染 Suspense

在错误处理的事情做完后,就不应该再往下递了,开始调用completeUnitOfWork往上归, 这时由于我们给 MyComponent 组件打上了Incomplete 标识,这个标识表示由于异常等原因渲染被搁置,那我们是不是就要开始往上找能够处理这个异常的组件?

我们再看看completeUnitOfWork 干了啥

function completeUnitOfWork(unitOfWork: Fiber): void {// 大致逻辑let completedWork = unitOfWork;if ((completedWork.flags & Incomplete) !== NoFlags) {const next = unwindWork(current, completedWork, subtreeRenderLanes);if (next) {workInProgress = next;return}// 给父节点打上Incomplete标记if (returnFiber !== null) {returnFiber.flags |= Incomplete;returnFiber.subtreeFlags = NoFlags;returnFiber.deletions = null;}}
}

可以看到最终打上Incomplete 标识的组件都会进入unwindWork流程 , 并一直将祖先节点打上Incomplete 标识,直到unwindWork 中找到一个能处理异常的边界组件,也就ClassComponentSuspenseComponent , 会去掉ShouldCapture标识,加上DidCapture标识

这时,对于Suspense来说需要的DidCapture已经拿到了,下面就是重新从Suspense 开始走一遍beginWork流程

再次回到 Suspense 组件, 这时由于有了DidCapture 标识,则展示fallback
对于fallback组件的fiber节点是通过mountSuspenseFallbackChildren 生成的

function mountSuspenseFallbackChildren(workInProgress,primaryChildren,fallbackChildren,renderLanes,
) {const primaryChildProps: OffscreenProps = {mode: 'hidden',children: primaryChildren,};let primaryChildFragment = mountWorkInProgressOffscreenFiber(primaryChildProps,mode,NoLanes,);let fallbackChildFragment = createFiberFromFragment(fallbackChildren,mode,renderLanes,null,);primaryChildFragment.return = workInProgress;fallbackChildFragment.return = workInProgress;primaryChildFragment.sibling = fallbackChildFragment;workInProgress.child = primaryChildFragment;return fallbackChildFragment;
}

它主要做了三件事

  • PrimaryChild 即Offscreen组件通过css隐藏
  • fallback组件又包了层Fragment 返回
  • fallbackChild 作为sibling链接至PrimaryChild

file


到这时渲染 fallback 的 fiber 树已经基本构建完了,之后进入commit阶段从根节点rootFiber开始深度遍历该fiber树 进行 render。

等待一段时间后,primary组件数据返回,我们之前在handleError中添加的监听器attachRetryListener 被触发,开始新的一轮任务调度。注:源码中调度回调实际在 Commit 阶段才添加的。

这时由于Suspense 节点已经存在,则走的是updateSuspensePrimaryChildren 中的逻辑,与之前首次加载时 monutSuspensePrimaryChildren不同的是多了删除的操作, 在 commit 阶段时则会删除fallback 组件, 展示primary组件。

if (currentFallbackChildFragment !== null) {// Delete the fallback child fragmentconst deletions = workInProgress.deletions;if (deletions === null) {workInProgress.deletions = [currentFallbackChildFragment];workInProgress.flags |= ChildDeletion;} else {deletions.push(currentFallbackChildFragment);}}

至此,Suspense 的一生我们粗略的过完了,在源码中对 Suspense 的处理非常多,涉及到优先级相关的本篇都略过。
Suspense 中使用了Offscreen组件来渲染子组件,这个组件的特性是能根据传入 mode 来控制子组件样式的显隐,这有一个好处,就是能保存组件的状态,有些许类似于 Vue 的keep-alive 。其次,它拥有着最低的调度优先级,比空闲时优先级还要低,这也意味着当 mode 切换时,它会被任何其他调度任务插队打断掉。

file

useTransition

useTransition 可以让我们在不阻塞 UI 渲染的情况下更新状态。useTransition 和 startTransition 允许将某些更新标记为低优先级更新。默认情况下,其他更新被视为紧急更新。React 将允许更紧急的更新(例如更新文本输入)来中断不太紧急的更新(例如展示搜索结果列表)。
其核心原理其实就是将startTransition 内调用的状态变更方法都标识为低优先级的lane

const [isPending, startTransition] = useTransition()startTransition(() => {setData(xxx)
})

一个输入框的例子

function Demo() {const [value, setValue] = useState();const [isPending, startTransition] = useTransition();return (<div><h1>useTramsotopm Demo</h1><inputonChange={(e) => {startTransition(() => {setValue(e.target.value);});}}/><hr />{isPending ? <p>加载中。。</p> : <List value={value} />}</div>);
}function List({ value }) {const items = new Array(5000).fill(1).map((_, index) => {return (<li><ListItem index={index} value={value} /></li>);});return <ul>{items}</ul>;
}function ListItem({ index, value }) {return (<div><span>index: </span><span>{index}</span><span>value: </span><span>{value}</span></div>);
}

当我每次进行输入时,会触发 List 进行大量更新,但由于我使用了startTransition  对List的更新进行延后 ,所以Input输入框不会出现明显卡顿现象

file

由于更新被滞后了,所以我们怎么知道当前有没有被更新呢?
这时候第一个返回参数isPending 就是用来告诉我们当前是否还在等待中。
但我们可以看到,input组件目前是非受控组件 ,如果改为受控组件 ,即使使用了startTransition 一样会出现卡顿,因为 input 响应输入事件进行状态更新应该是要同步的。
所以这时候下面介绍的useDeferredValue 作用就来了。

useDeferredValue

useDeferredValue 可让您推迟更新部分 UI, 它与useTransition 做的事差不多,不过useTransition 是在状态更新层,推迟状态更新来实现非阻塞,而useDeferredValue 则是在状态已经更新后,先使用状态更新前的值进行渲染,来延迟因状态变化而导致的组件重新渲染。

它的基本用法

function Page() {const [value, setValue] = useState('');const deferredValue = useDeferredValue(setValue);
}

我们再用useDeferredValue 去实现上面输入框的例子

function Demo() {const [value, setValue] = useState('');const deferredValue = useDeferredValue(value);return (<div><h1>useDeferedValue Demo</h1><inputvalue={value}onChange={(e) => {setValue(e.target.value)}}/><hr /><List value={deferredValue} /></div>);
}

我们将input作为受控组件 ,对于会因输入框值而造成大量渲染List,我们使用deferredValue 。

其变化过程如下

  1. 当输入变化时,deferredValue 首先会是变化前的旧值进行重新渲染,由于值没有变,所以 List 没有重新渲染,也就没有出现阻塞情况,这时,input 的值能够实时响应到页面上。
  2. 在这次旧值渲染完成后,deferredValue 变更为新的值,React 会在后台开始对新值进行重新渲染,List 组件开始 rerender,且此次 rerender 会被标识为低优先级渲染,能够被中断
  3. 如果此时又有输入框输入,则中断此次后台的重新渲染,重新走1,2的流程

我们可以打印下deferredValue  的值看下
初始情况输入框为1,打印了两次1

file

输入2时,再次打印了两次1,随后打印了两次2

file

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

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

相关文章

瑞_23种设计模式_解释器模式

文章目录 1 解释器模式&#xff08;Interpreter Pattern&#xff09;1.1 介绍1.2 概述1.2.1 文法&#xff08;语法&#xff09;规则1.2.2 抽象语法树 1.3 解释器模式的结构1.4 解释器模式的优缺点1.5 解释器模式的使用场景 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代…

STM32 DMA直接存储器存取

单片机学习&#xff01; 目录 文章目录 前言 一、DMA简介 1.1 DMA是什么 1.2 DMA作用 1.3 DMA通道 1.4 软硬件触发 1.5 芯片资源 二、存储器映像 2.1 存储器 2.2 STM32存储器 三、DMA框图 3.1 内核与存储器 3.2 寄存器 3.3 DMA数据转运 3.4 DMA总线作用 3.5 DMA请求 3.6 DMA结构…

上位机图像处理和嵌入式模块部署(树莓派4b读写json数据)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们说过&#xff0c;ini文件是用来进行配置的&#xff0c;数据库是用来进行数据存储的。那json是用来做什么的呢&#xff0c;json一般是用来做…

【JavaEE】线程的概念

文章目录 1、什么是线程2、进程和线程的区别3、多线程的概述4、在Java中实现多线程的方法1.继承Thread类2.实现Runnable接口3.使用匿名内部类来继承Thread类&#xff0c;实现run方法4.使用匿名内部类来实现Runnable接口&#xff0c;实现run方法5.使用 lambda表达式 1、什么是线…

【R语言数据分析】数据类型与数据结构

目录 对数据框的基本操作 创建矩阵 列表 字符串 日期变量与时间变量 缺失值NA 缺失值NA的处理 重新编码 R的数据类型有数值型num&#xff0c;字符型chr&#xff0c;逻辑型logi等等。 R最常处理的数据结构是&#xff1a;向量&#xff0c;数据框&#xff0c;矩阵&#x…

JAVA第二周学习笔记

文章目录 JAVA第二周学习笔记IDEA方法格式带参数及返回值的方法方法的重载方法的内存 二维数组静态初始化动态初始化 面向对象类和对象如何定义类如何得到对象注意 封装封装的优点private关键字成员变量和局部变量 this关键字构造方法作用类型特点执行时机定义重载 标准javabea…

neo4j 的插入速度为什么越来越慢,可能是使用了过多图谱查询操作

文章目录 背景描述分析解决代码参考neo4j 工具类Neo4jDriver知识图谱构建效果GuihuaNeo4jClass 背景描述 使用 tqdm 显示&#xff0c;处理的速度&#xff1b; 笔者使用 py2neo库&#xff0c;调用 neo4j 的API 完成节点插入&#xff1b; 有80万条数据需要插入到neo4j图数据中&am…

FANUC机器人SOCKET断开KAREL程序编写

一、添加一个.KL文件创建编辑断开指令 添加一个KL文件用来创建karel程序中socket断开指令 二、断开连接程序karel代码 PROGRAM SOC_DIS %COMMENT SOCKET断开 %INCLUDE klevccdf VAR str_input,str_val : STRING[20] status,data_type,int_val : INTEGER rel_val : REALBEGING…

【氮化镓】GaN器件在航天器高可靠正向转换器中应用

文章是发表在《IEEE Journal of Emerging and Selected Topics in Power Electronics》2022年10月第10卷第5期上的一篇关于GaN(氮化镓)器件在航天器高可靠性正向转换器中应用的研究。文章的作者是匹兹堡大学电气与计算机工程系的Aidan Phillips, Thomas Cook和Brandon M. Gra…

Android AOSP探索之Ubantu下Toolbox的安装

文章目录 概述安装Toolbox解决运行的问题 概述 由于最近需要进军android的framework,所以需要工具的支持&#xff0c;之前听说江湖上都流传source insight,我去弄了一个破解版&#xff0c;功能确实强大&#xff0c;但是作为多年android开发的我习惯使用android studio。虽然使…

linux 光驱(光盘)安装

文章目录 自带 YUM 库创建 repo创建文件夹挂载光驱开机自启动挂载安装软件YUM 安装RPM 安装 自带 YUM 库 ls /etc/yum.repos.d创建 repo vim /etc/yum.repo.d/demo.repo // 编写 repo 相关配置 [demo] namedemo baseurlfile:///mnt/cdrom gpkcheck0创建文件夹挂载光驱 /dev/…

【沉淀之华】从0到1实现用户推荐 - 实时特征系统构建,包含特征计算,特征存储,特征查询,特征补偿超详细思路分享

文章目录 背景介绍设计初衷基本概念 技术架构"四高"特征存储特征计算特征查询特征补偿 技术难点Q&A彩蛋 背景介绍 设计初衷 作为用户推荐系统的支撑系统之一&#xff1a;用户实时特征系统有着举足轻重的重要&#xff0c;甚至说它是一起推荐行为触发的必要条件。…

c#word文档:3.向Word文档中插入表格/4.读取Word文档中表格

--向Word文档中插入表格-- &#xff08;1&#xff09;在OfficeOperator项目的WordOperator类中定义向Word文档插入换页的函数NewPage &#xff08;2&#xff09;在WordOperator类中定义向Word文档插入表格的函数InsertTable using Microsoft.Office.Interop.Word;// 引入Mic…

探索APP内测分发的全过程(APP开发)

什么是APP内测分发探索APP内测分发的全过程&#xff1f; APP内测分发是在应用程序开发过程中探索APP内测分发的全过程&#xff0c;开发者将应用程序的测试版或预发布版分发给特定用户进行测试、反馈和评估的一种方式。这是一个非常重要的环节&#xff0c;可以有效地提高应用的…

详解SDRAM基本原理以及FPGA实现读写控制

文章目录 一、SDRAM简介二、SDRAM存取结构以及原理2.1 BANK以及存储单元结构2.2 功能框图2.3 SDRAM速度等级以及容量计算 三、SDRAM操作命令3.1 禁止命令&#xff1a; 4b1xxx3.2 空操作命令&#xff1a;4b01113.3 激活命令&#xff1a;4b00113.4 读命令&#xff1a;4b01013.5 写…

mac如何打开exe文件?如何mac运行exe文件 如何在Mac上打开/修复/恢复DMG文件

在macOS系统中&#xff0c;无法直接运行Windows系统中的.exe文件&#xff0c;因为macOS和Windows使用的是不同的操作系统。然而&#xff0c;有时我们仍然需要运行.exe文件&#xff0c;比如某些软件只有Windows版本&#xff0c;或者我们需要在macOS系统中运行Windows程序。 虽然…

如何安全的使用密码登录账号(在不知道密码的情况下)

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 1、打开工具&#xff0c;进入账号密码模块&#xff0c;如图 2、看到鼠标移动到密码那一栏有提示&#xff0c;按住Ctrl或者Alt点击或者双击就能复制内容&…

正版Office-Word使用时却提示无网络连接请检查你的网络设置 然后重试

这是购买电脑时自带的已经安装好的word。看纸箱外壳有office标记&#xff0c;但是好像没有印系列号。 某天要使用。提示&#xff1a;无网络连接请检查你的网络设置。 经过网上高手的提示&#xff1a; 说要勾选勾选ssl3.0、TLS1.0、1.1、1.2。 我的截图 我电脑进去就缺1.2. …

PCIe总线-MPS MRRS RCB参数介绍(四)

1.概述 PCIe总线的存储器写请求、存储器读完成等TLP中含有数据负载&#xff0c;即Data Payload。Data Payload的长度和MPS&#xff08;Max Payload Size&#xff09;、MRRS&#xff08;Max Read Request Size&#xff09;和RCB&#xff08;Read Completion Boundary&#xff0…

C++ 抽象机制

抽象机制 1. 虚函数 使用关键字virtual 声明的函数&#xff0c;意思是可能随后在其派生类中重新定义。 纯虚函数 在声明的末尾使用0 的函数&#xff0c;说明是纯虚函数。 抽象类 含有纯虚函数多的类称为抽象类(abstract class). 多态类型 如果一个类负责为其他一些类提供接…