【React 源码】(八)fiber 树构造(基础准备)

fiber 树构造(基础准备)

在 React 运行时中, fiber树构造位于react-reconciler包.

在正式解读fiber树构造之前, 再次回顾一下reconciler 运作流程的 4 个阶段:

  1. 输入阶段: 衔接react-dom包, 承接fiber更新请求(可以参考React 应用的启动过程).
  2. 注册调度任务: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调(可以参考React 调度原理(scheduler)).
  3. 执行任务回调: 在内存中构造出fiber树DOM对象, 也是fiber 树构造的重点内容.
  4. 输出: 与渲染器(react-dom)交互, 渲染DOM节点.

fiber树构造处于上述第 3 个阶段, 可以通过不同的视角来理解fiber树构造React运行时中所处的位置:

  • scheduler调度中心的角度来看, 它是任务队列taskQueue中的一个具体的任务回调(task.callback).
  • React 工作循环的角度来看, 它属于fiber树构造循环.

由于fiber 树构造源码量比较大, 本系列根据React运行的内存状态, 分为 2 种情况来说明:

  1. 初次创建: 在React应用首次启动时, 界面还没有渲染, 此时并不会进入对比过程, 相当于直接构造一棵全新的树.
  2. 对比更新: React应用启动后, 界面已经渲染. 如果再次发生更新, 创建新fiber之前需要和旧fiber进行对比. 最后构造的 fiber 树有可能是全新的, 也可能是部分更新的.

无论是初次创建还是对比更新, 基础概念都是通用的, 本节将介绍这些基础知识, 为正式进入fiber树构造做准备.

ReactElement, Fiber, DOM 三者的关系

React 应用中的高频对象一文中, 已经介绍了ReactElementFiber对象的数据结构. 这里我们梳理出ReactElement, Fiber, DOM这 3 种对象的关系

  1. ReactElement 对象(type 定义在shared 包中)

    • 所有采用jsx语法书写的节点, 都会被编译器转换, 最终会以React.createElement(...)的方式, 创建出来一个与之对应的ReactElement对象
  2. fiber 对象(type 类型的定义在ReactInternalTypes.js中)

    • fiber对象是通过ReactElement对象进行创建的, 多个fiber对象构成了一棵fiber树fiber树是构造DOM树的数据模型, fiber树的任何改动, 最后都体现到DOM树.
  3. DOM 对象: 文档对象模型

    • DOM将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合, 也就是常说的DOM树.
    • JavaScript可以访问和操作存储在 DOM 中的内容, 也就是操作DOM对象, 进而触发 UI 渲染.

它们之间的关系反映了我们书写的 JSX 代码到 DOM 节点的转换过程:

注意:

  • 开发人员能够控制的是JSX, 也就是ReactElement对象.
  • fiber树是通过ReactElement生成的, 如果脱离了ReactElement,fiber树也无从谈起. 所以是ReactElement树(不是严格的树结构, 为了方便也称为树)驱动fiber树.
  • fiber树DOM树的数据模型, fiber树驱动DOM树

开发人员通过编程只能控制ReactElement树的结构, ReactElement树驱动fiber树fiber树再驱动DOM树, 最后展现到页面上. 所以fiber树的构造过程, 实际上就是ReactElement对象到fiber对象的转换过程.

全局变量

React 工作循环的角度来看, 整个构造过程被包裹在fiber树构造循环中(对应源码位于ReactFiberWorkLoop.js).

React运行时, ReactFiberWorkLoop.js闭包中的全局变量会随着fiber树构造循环的进行而变化, 现在查看其中重要的全局变量(源码链接):


// 当前React的执行栈(执行上下文)

let executionContext: ExecutionContext = NoContext;

// 当前root节点

let workInProgressRoot: FiberRoot | null = null;

// 正在处理中的fiber节点

let workInProgress: Fiber | null = null;

// 正在渲染的车道(复数)

let workInProgressRootRenderLanes: Lanes = NoLanes;

// 包含所有子节点的优先级, 是workInProgressRootRenderLanes的超集

// 大多数情况下: 在工作循环整体层面会使用workInProgressRootRenderLanes, 在begin/complete阶段层面会使用 subtreeRenderLanes

let subtreeRenderLanes: Lanes = NoLanes;

// 一个栈结构: 专门存储当前节点的 subtreeRenderLanes

const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);

// fiber构造完后, root节点的状态: completed, errored, suspended等

let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;

// 重大错误

let workInProgressRootFatalError: mixed = null;

// 整个render期间所使用到的所有lanes

let workInProgressRootIncludedLanes: Lanes = NoLanes;

// 在render期间被跳过(由于优先级不够)的lanes: 只包括未处理的updates, 不包括被复用的fiber节点

let workInProgressRootSkippedLanes: Lanes = NoLanes;

// 在render期间被修改过的lanes

let workInProgressRootUpdatedLanes: Lanes = NoLanes;

// 防止无限循环和嵌套更新

const NESTED_UPDATE_LIMIT = 50;

let nestedUpdateCount: number = 0;

let rootWithNestedUpdates: FiberRoot | null = null;

const NESTED_PASSIVE_UPDATE_LIMIT = 50;

let nestedPassiveUpdateCount: number = 0;

// 发起更新的时间

let currentEventTime: number = NoTimestamp;

let currentEventWipLanes: Lanes = NoLanes;

let currentEventPendingLanes: Lanes = NoLanes;

在源码中, 大部分变量都带有英文注释(读者可自行查阅), 此处只列举了fiber树构造循环中最核心的变量

执行上下文

在全局变量中有executionContext, 代表渲染期间执行栈(或叫做执行上下文), 它也是一个二进制表示的变量, 通过位运算进行操作(参考React 算法之位运算). 在源码中一共定义了 8 种执行栈:

type ExecutionContext = number;

export const NoContext = /* */ 0b0000000;

const BatchedContext = /* */ 0b0000001;

const EventContext = /* */ 0b0000010;

const DiscreteEventContext = /* */ 0b0000100;

const LegacyUnbatchedContext = /* */ 0b0001000;

const RenderContext = /* */ 0b0010000;

const CommitContext = /* */ 0b0100000;

上文回顾了reconciler 运作流程的 4 个阶段, 这 4 个阶段只是一个整体划分. 如果具体到每一次更新, 是有差异的. 比如说: Legacy模式下的首次更新, 不会经过调度中心(第 2 阶段),而是直接进入fiber树构造(第 3 阶段).

事实上正是executionContext在操控reconciler 运作流程(源码体现在scheduleUpdateOnFiber 函数).

export function scheduleUpdateOnFiber(

fiber: Fiber,

lane: Lane,

eventTime: number,

) {

if (lane === SyncLane) {

// legacy或blocking模式

if (

(executionContext & LegacyUnbatchedContext) !== NoContext &&

(executionContext & (RenderContext | CommitContext)) === NoContext

) {

performSyncWorkOnRoot(root);

} else {

// 后续的更新

// 进入第2阶段, 注册调度任务

ensureRootIsScheduled(root, eventTime);

if (executionContext === NoContext) {

// 如果执行上下文为空, 会取消调度任务, 手动执行回调

// 进入第3阶段, 进行fiber树构造

flushSyncCallbackQueue();

}

}

} else {

// concurrent模式

// 无论是否初次更新, 都正常进入第2阶段, 注册调度任务

ensureRootIsScheduled(root, eventTime);

}

}

在 render 过程中, 每一个阶段都会改变executionContext(render 之前, 会设置executionContext |= RenderContext; commit 之前, 会设置executionContext |= CommitContext), 假设在render过程中再次发起更新(如在UNSAFE_componentWillReceiveProps生命周期中调用setState)则可通过executionContext来判断当前的render状态.

双缓冲技术(double buffering)

在全局变量中有workInProgress, 还有不少以workInProgress来命名的变量. workInProgress的应用实际上就是React的双缓冲技术(double buffering).

在上文我们梳理了ReactElement, Fiber, DOM三者的关系fiber树的构造过程, 就是把ReactElement转换成fiber树的过程. 在这个过程中, 内存里会同时存在 2 棵fiber树:

  • 其一: 代表当前界面的fiber树(已经被展示出来, 挂载到fiberRoot.current上). 如果是初次构造(初始化渲染), 页面还没有渲染, 此时界面对应的 fiber 树为空(fiberRoot.current = null).
  • 其二: 正在构造的fiber树(即将展示出来, 挂载到HostRootFiber.alternate上, 正在构造的节点称为workInProgress). 当构造完成之后, 重新渲染页面, 最后切换fiberRoot.current = workInProgress, 使得fiberRoot.current重新指向代表当前界面的fiber树.

此处涉及到 2 个全局对象fiberRootHostRootFiber, 在React 应用的启动过程中有详细的说明.

用图来表述double buffering的概念如下:

  1. 构造过程中, fiberRoot.current指向当前界面对应的fiber树.

  1. 构造完成并渲染, 切换fiberRoot.current指针, 使其继续指向当前界面对应的fiber树(原来代表界面的 fiber 树, 变成了内存中).

优先级 {#lanes}

在全局变量中有不少变量都以 Lanes 命名(如workInProgressRootRenderLanes,subtreeRenderLanes其作用见上文注释), 它们都与优先级相关.

在前文React 中的优先级管理中, 我们介绍了React中有 3 套优先级体系, 并了解了它们之间的关联. 现在fiber树构造过程中, 将要深入分析车道模型Lane的具体应用.

在整个react-reconciler包中, Lane的应用可以分为 3 个方面:

update优先级(update.lane) {#update-lane}

React 应用中的高频对象一文中, 介绍过update对象, 它是一个环形链表. 对于单个update对象来讲, update.lane代表它的优先级, 称之为update优先级.

观察其构造函数(源码链接),其优先级是由外界传入.

 
​
export function createUpdate(eventTime: number, lane: Lane): Update<*> {

const update: Update<*> = {

eventTime,

lane,

tag: UpdateState,

payload: null,

callback: null,

next: null,

};

return update;

}

在React体系中, 有 2 种情况会创建update对象:

应用初始化: 在react-reconciler包中的updateContainer函数中(源码)
















发起组件更新: 假设在 class 组件中调用setState(源码)

const classComponentUpdater = {

isMounted,

enqueueSetState(inst, payload, callback) {

const fiber = getInstance(inst);

const eventTime = requestEventTime(); // 根据当前时间, 创建一个update优先级

const lane = requestUpdateLane(fiber); // lane被用于创建update对象

const update = createUpdate(eventTime, lane);

update.payload = payload;

enqueueUpdate(fiber, update);

scheduleUpdateOnFiber(fiber, lane, eventTime);

},

};

​

可以看到, 无论是应用初始化或者发起组件更新, 创建update.lane的逻辑都是一样的, 都是根据当前时间, 创建一个 update 优先级.

requestUpdateLane:

 
export function requestUpdateLane(fiber: Fiber): Lane {

// Special cases

const mode = fiber.mode;

if ((mode & BlockingMode) === NoMode) {

// legacy 模式

return (SyncLane: Lane);

} else if ((mode & ConcurrentMode) === NoMode) {

// blocking模式

return getCurrentPriorityLevel() === ImmediateSchedulerPriority

? (SyncLane: Lane)

: (SyncBatchedLane: Lane);

}

// concurrent模式

if (currentEventWipLanes === NoLanes) {

currentEventWipLanes = workInProgressRootIncludedLanes;

}

const isTransition = requestCurrentTransition() !== NoTransition;

if (isTransition) {

// 特殊情况, 处于suspense过程中

if (currentEventPendingLanes !== NoLanes) {

currentEventPendingLanes =

mostRecentlyUpdatedRoot !== null

? mostRecentlyUpdatedRoot.pendingLanes

: NoLanes;

}

return findTransitionLane(currentEventWipLanes, currentEventPendingLanes);

}

// 正常情况, 获取调度优先级

const schedulerPriority = getCurrentPriorityLevel();

let lane;

if (

(executionContext & DiscreteEventContext) !== NoContext &&

schedulerPriority === UserBlockingSchedulerPriority

) {

// executionContext 存在输入事件. 且调度优先级是用户阻塞性质

lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);

} else {

// 调度优先级转换为车道模型

const schedulerLanePriority = schedulerPriorityToLanePriority(

schedulerPriority,

);

lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);

}

return lane;

}

可以看到requestUpdateLane的作用是返回一个合适的 update 优先级.

  1. legacy 模式: 返回SyncLane
  2. blocking 模式: 返回SyncLane
  3. concurrent 模式:
    • 正常情况下, 根据当前的调度优先级来生成一个lane.
    • 特殊情况下(处于 suspense 过程中), 会优先选择TransitionLanes通道中的空闲通道(如果所有TransitionLanes通道都被占用, 就取最高优先级. 源码).

最后通过scheduleUpdateOnFiber(current, lane, eventTime);函数, 把update.lane正式带入到了输入阶段.

scheduleUpdateOnFiber输入阶段的必经函数, 在本系列的文章中已经多次提到, 此处以update.lane的视角分析:

 
export function scheduleUpdateOnFiber(

fiber: Fiber,

lane: Lane,

eventTime: number,

) {

if (lane === SyncLane) {

// legacy或blocking模式

if (

(executionContext & LegacyUnbatchedContext) !== NoContext &&

(executionContext & (RenderContext | CommitContext)) === NoContext

) {

performSyncWorkOnRoot(root);

} else {

ensureRootIsScheduled(root, eventTime); // 注册回调任务

if (executionContext === NoContext) {

flushSyncCallbackQueue(); // 取消schedule调度 ,主动刷新回调队列,

}

}

} else {

// concurrent模式

ensureRootIsScheduled(root, eventTime);

}

}

lane === SyncLane也就是 legacy 或 blocking 模式中, 注册完回调任务之后(ensureRootIsScheduled(root, eventTime)), 如果执行上下文为空, 会取消 schedule 调度, 主动刷新回调队列flushSyncCallbackQueue().

这里包含了一个热点问题(setState到底是同步还是异步)的标准答案:

  • 如果逻辑进入flushSyncCallbackQueue(executionContext === NoContext), 则会主动取消调度, 并刷新回调, 立即进入fiber树构造过程. 当执行setState下一行代码时, fiber树已经重新渲染了, 故setState体现为同步.
  • 正常情况下, 不会取消schedule调度. 由于schedule调度是通过MessageChannel触发(宏任务), 故体现为异步.

渲染优先级(renderLanes)

这是一个全局概念, 每一次render之前, 首先要确定本次render的优先级. 具体对应到源码如下:

 
​
// ...省略无关代码

function performSyncWorkOnRoot(root) {

let lanes;

let exitStatus;

// 获取本次`render`的优先级

lanes = getNextLanes(root, lanes);

exitStatus = renderRootSync(root, lanes);

}

// ...省略无关代码

function performConcurrentWorkOnRoot(root) {

// 获取本次`render`的优先级

let lanes = getNextLanes(

root,

root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,

);

if (lanes === NoLanes) {

return null;

}

let exitStatus = renderRootConcurrent(root, lanes);

}

可以看到, 无论是Legacy还是Concurrent模式, 在正式render之前, 都会调用getNextLanes获取一个优先级(源码链接).


// ...省略部分代码

export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {

// 1. check是否有等待中的lanes

const pendingLanes = root.pendingLanes;

if (pendingLanes === NoLanes) {

return_highestLanePriority = NoLanePriority;

return NoLanes;

}

let nextLanes = NoLanes;

let nextLanePriority = NoLanePriority;

const expiredLanes = root.expiredLanes;

const suspendedLanes = root.suspendedLanes;

const pingedLanes = root.pingedLanes;

// 2. check是否有已过期的lanes

if (expiredLanes !== NoLanes) {

nextLanes = expiredLanes;

nextLanePriority = return_highestLanePriority = SyncLanePriority;

} else {

const nonIdlePendingLanes = pendingLanes & NonIdleLanes;

if (nonIdlePendingLanes !== NoLanes) {

// 非Idle任务 ...

} else {

// Idle任务 ...

}

}

if (nextLanes === NoLanes) {

return NoLanes;

}

return nextLanes;

}

​

getNextLanes会根据fiberRoot对象上的属性(expiredLanessuspendedLanespingedLanes等), 确定出当前最紧急的lanes.

此处返回的lanes会作为全局渲染的优先级, 用于fiber树构造过程中. 针对fiber对象update对象, 只要它们的优先级(如: fiber.lanesupdate.lane)比渲染优先级低, 都将会被忽略.

fiber优先级(fiber.lanes)

React 应用中的高频对象一文中, 介绍过fiber对象的数据结构. 其中有 2 个属性与优先级相关:

  1. fiber.lanes: 代表本节点的优先级
  2. fiber.childLanes: 代表子节点的优先级 从FiberNode的构造函数中可以看出, fiber.lanesfiber.childLanes的初始值都为NoLanes, 在fiber树构造过程中, 使用全局的渲染优先级(renderLanes)和fiber.lanes判断fiber节点是否更新(源码地址).
    • 如果全局的渲染优先级renderLanes不包括fiber.lanes, 证明该fiber节点没有更新, 可以复用.
    • 如果不能复用, 进入创建阶段.
 
function beginWork(

current: Fiber | null,

workInProgress: Fiber,

renderLanes: Lanes,

): Fiber | null {

const updateLanes = workInProgress.lanes;

if (current !== null) {

const oldProps = current.memoizedProps;

const newProps = workInProgress.pendingProps;

if (

oldProps !== newProps ||

hasLegacyContextChanged() ||

// Force a re-render if the implementation changed due to hot reload:

(__DEV__ ? workInProgress.type !== current.type : false)

) {

didReceiveUpdate = true;

} else if (!includesSomeLane(renderLanes, updateLanes)) {

didReceiveUpdate = false;

// 本`fiber`节点的没有更新, 可以复用, 进入bailout逻辑

return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);

}

}

// 不能复用, 创建新的fiber节点

workInProgress.lanes = NoLanes; // 重置优先级为 NoLanes

switch (workInProgress.tag) {

case ClassComponent: {

const Component = workInProgress.type;

const unresolvedProps = workInProgress.pendingProps;

const resolvedProps =

workInProgress.elementType === Component

? unresolvedProps

: resolveDefaultProps(Component, unresolvedProps);

return updateClassComponent(

current,

workInProgress,

Component,

resolvedProps,

// 正常情况下渲染优先级会被用于fiber树的构造过程

renderLanes,

);

}

}

}

栈帧管理

React源码中, 每一次执行fiber树构造(也就是调用performSyncWorkOnRoot或者performConcurrentWorkOnRoot函数)的过程, 都需要一些全局变量来保存状态. 在上文中已经介绍最核心的全局变量.

如果从单个变量来看, 它们就是一个个的全局变量. 如果将这些全局变量组合起来, 它们代表了当前fiber树构造的活动记录. 通过这一组全局变量, 可以还原fiber树构造过程(比如时间切片的实现过程(参考React 调度原理), fiber树构造过程被打断之后需要还原进度, 全靠这一组全局变量). 所以每次fiber树构造是一个独立的过程, 需要独立的一组全局变量, 在React内部把这一个独立的过程封装为一个栈帧stack(简单来说就是每次构造都需要独立的空间. 对于栈帧的深入理解, 请读者自行参考其他资料).

所以在进行fiber树构造之前, 如果不需要恢复上一次构造进度, 都会刷新栈帧(源码在prepareFreshStack 函数)

 
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {

const prevExecutionContext = executionContext;

executionContext |= RenderContext;

const prevDispatcher = pushDispatcher();

// 如果fiberRoot变动, 或者update.lane变动, 都会刷新栈帧, 丢弃上一次渲染进度

if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {

resetRenderTimer();

// 刷新栈帧

prepareFreshStack(root, lanes);

startWorkOnPendingInteractions(root, lanes);

}

}

/**

刷新栈帧: 重置 FiberRoot上的全局属性 和 `fiber树构造`循环过程中的全局变量

*/

function prepareFreshStack(root: FiberRoot, lanes: Lanes) {

// 重置FiberRoot对象上的属性

root.finishedWork = null;

root.finishedLanes = NoLanes;

const timeoutHandle = root.timeoutHandle;

if (timeoutHandle !== noTimeout) {

root.timeoutHandle = noTimeout;

cancelTimeout(timeoutHandle);

}

if (workInProgress !== null) {

let interruptedWork = workInProgress.return;

while (interruptedWork !== null) {

unwindInterruptedWork(interruptedWork);

interruptedWork = interruptedWork.return;

}

}

// 重置全局变量

workInProgressRoot = root;

workInProgress = createWorkInProgress(root.current, null); // 给HostRootFiber对象创建一个alternate, 并将其设置成全局 workInProgress

workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;

workInProgressRootExitStatus = RootIncomplete;

workInProgressRootFatalError = null;

workInProgressRootSkippedLanes = NoLanes;

workInProgressRootUpdatedLanes = NoLanes;

workInProgressRootPingedLanes = NoLanes;

}

注意其中的createWorkInProgress(root.current, null), 其参数root.currentHostRootFiber, 作用是给HostRootFiber创建一个alternate副本.workInProgress指针指向这个副本(即workInProgress = HostRootFiber.alternate), 在上文double buffering中分析过, HostRootFiber.alternate正在构造的fiber树的根节点.

总结

本节是fiber树构造的准备篇, 首先在宏观上从不同的视角(任务调度循环fiber树构造循环)介绍了fiber树构造React体系中所处的位置, 然后深入react-reconciler包分析fiber树构造过程中需要使用到的全局变量, 并解读了双缓冲技术优先级(车道模型)的使用, 最后解释栈帧管理的实现细节. 有了这些基础知识, fiber树构造的具体实现过程会更加简单清晰.

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340