Flutter视图原理之三棵树的建立过程

目录

    • 三棵树的关系
    • 树的构建过程
      • 1.updateChild函数(element的复用)
      • 2.inflateWidget函数
      • 3.mount函数
        • 3.1 componentElement的实现
        • 3.2 RenderObjectElement的实现
          • 3.2.1 attachRenderObject函数
      • 4.performRebuild函数
    • 总结三棵树创建流程

三棵树的关系

Flutter 中存在 Widget 、 Element 、RenderObject 三棵树,其中 Widget与 Element 是一对多的关系 ,Element 与 RenderObject 是一一对应的关系。

Element 中持有Widget 和 RenderObject , 而 Element 与 RenderObject 是一一对应的关系(除去 Element 不存在 RenderObject 的情况,如 ComponentElement是不具备 RenderObject),当 RenderObject 的 isRepaintBoundary 为 true 时,那么个区域形成一个 Layer,所以不是每个 RenderObject 都具有 Layer 的,因为这受 isRepaintBoundary 的影响。

Flutter 中 Widget 不可变,每次保持在一帧,如果发生改变是通过 State 实现跨帧状态保存,而真实完成布局和绘制数组的是 RenderObject , Element 充当两者的桥梁, State 就是保存在 Element 中。

一个可能的三棵树实例如下:
在这里插入图片描述

先看下widget继承关系:
在这里插入图片描述

对应的element继承关系:

在这里插入图片描述

通过上图可以看出,每个widget对应一个element类型,但是只有renderObjectElement类持有renderObject,只有renderObjectWidget才能创建render对象。因此flutter视图的三棵树结构,widget和element是一一对应的,但是renderObjectElement才对应一个render节点。

树的构建过程

flutter视图的入口是:

    runApp(const MyApp());class MyApp extends StatelessWidget {const MyApp({super.key});Widget build(BuildContext context) {return MaterialApp(/....省略);}}

跟踪进入runApp函数:

void runApp(Widget app) {//1 final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();assert(binding.debugCheckZone('runApp'));//2binding..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))//3..scheduleWarmUpFrame();
}
  1. 第一步首先确认engine绑定flutter framework,是个阻塞过程
  2. scheduleAttachRootWidget 将app组件添加到根视图树上
  3. 向native平台层请求绘制一帧的信号

先看第二步:

  void attachRootWidget(Widget rootWidget) {final bool isBootstrapFrame = rootElement == null;_readyToProduceFrames = true;_rootElement = RenderObjectToWidgetAdapter<RenderBox>(container: renderView,debugShortDescription: '[root]',child: rootWidget,).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);if (isBootstrapFrame) {SchedulerBinding.instance.ensureVisualUpdate();}}

RenderObjectToWidgetAdapter有两个成员,child:孩子widget,container:提供render功能容器,将rootWidget作为的child元素,renderView作为container。重点是attachToRenderTree函数,

  RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {if (element == null) {//。。。省略owner.buildScope(element!, () {element!.mount(null, null);});} else {//。。。省略}return element!;}

第一次element为空,那么进入第一个判断,首先调用buildScope,内部会回调element!.mount方法,

void buildScope(Element context, [ VoidCallback? callback ]) {if (callback == null && _dirtyElements.isEmpty) {return;}try {_scheduledFlushDirtyElements = true;callback();_dirtyElements.sort(Element._sort);_dirtyElementsNeedsResorting = false;int dirtyCount = _dirtyElements.length;int index = 0;while (index < dirtyCount) {final Element element = _dirtyElements[index];element.rebuild();index += 1;//。。。省略}return true;}());}}

首先回调callback,接着对脏元素集合调用rebuild方法。第一次肯定列表是空的,那么应该执行callback方法,也就进入了element.mount方法中,

  void mount(Element? parent, Object? newSlot) {assert(parent == null);super.mount(parent, newSlot);_rebuild();assert(_child != null);}

mount方法也是个关键函数,函数的注释:

将此元素添加到给定父级的给定插槽中的树中。
当新创建的元素添加到
树是第一次。使用此方法初始化状态
取决于有父母。独立于父级的状态可以
更容易在构造函数中初始化。
此方法将元素从“初始”生命周期状态转换为
“活动”生命周期状态。
重写此方法的子类可能也希望重写
[update], [visitChildren], [RenderObjectElement.insertRenderObjectChild],
[RenderObjectElement.moveRenderObjectChild],以及
[RenderObjectElement.removeRenderObjectChild]。
此方法的实现应从调用继承的
方法,如 ‘super.mount(parent, newSlot)’。

注:其中newSlot参数,是parent renderObject的插槽位置对象,parent的子renderObject组成一个顺序列表或者是单个节点,插槽的结构是列表中的索引位置和前一个插槽元素的对象,parent将插槽传递给child保存起来,child可以确认自己在parent的布局位置。

首先调用_rebuild()方法,

RenderObjectToWidgetElement类


void _rebuild() {_child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot);
}

Element类

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {//。。。省略final Element newChild;if (child != null) {bool hasSameSuperclass = true;// When the type of a widget is changed between Stateful and Stateless via// hot reload, the element tree will end up in a partially invalid state.// That is, if the widget was a StatefulWidget and is now a StatelessWidget,// then the element tree currently contains a StatefulElement that is incorrectly// referencing a StatelessWidget (and likewise with StatelessElement).//// To avoid crashing due to type errors, we need to gently guide the invalid// element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition// returns false which prevents us from trying to update the existing element// incorrectly.//// For the case where the widget becomes Stateful, we also need to avoid// accessing `StatelessElement.widget` as the cast on the getter will// cause a type error to be thrown. Here we avoid that by short-circuiting// the `Widget.canUpdate` check once `hasSameSuperclass` is false.//1bool hasSameSuperclass = true;final int oldElementClass = Element._debugConcreteSubtype(child);final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);hasSameSuperclass = oldElementClass == newWidgetClass;if (hasSameSuperclass && child.widget == newWidget) {// We don't insert a timeline event here, because otherwise it's// confusing that widgets that "don't update" (because they didn't// change) get "charged" on the timeline.if (child.slot != newSlot) {updateSlotForChild(child, newSlot);}newChild = child;} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {if (child.slot != newSlot) {updateSlotForChild(child, newSlot);}child.update(newWidget);newChild = child;} else {deactivateChild(child);// The [debugProfileBuildsEnabled] code for this branch is inside// [inflateWidget], since some [Element]s call [inflateWidget] directly// instead of going through [updateChild].newChild = inflateWidget(newWidget, newSlot);}} else {// The [debugProfileBuildsEnabled] code for this branch is inside// [inflateWidget], since some [Element]s call [inflateWidget] directly// instead of going through [updateChild].newChild = inflateWidget(newWidget, newSlot);}return newChild;}

1.updateChild函数(element的复用)

更新element对象分为下面几种情况:

  • child不为空
    element的创建,首先判断旧的child element元素是否和新的widget元素class类型匹配,对应匹配关系如下:
elementwidgethasSameSuperclass
StatefulElementStatefulWidgetY
StatelessElementStatelessWidgetY
  1. 如果匹配hasSameSuperclass,并且element.widget和新传递进来的newWidget对象相同,那么说明widget是复用的(我们知道widget是不可变的,每次都要新建widget,所以在使用const定义的widget的情况下,widget使用的常量对象,符合这个判断),对child做slot更新,newChild更新为child。
  2. 如果匹配hasSameSuperclass,并且widget.canUpdate判断成立,这个判断判断element对应的widget和newWidget对应的class类型和key是否相同(这种情况下,如果给widget定义了globalKey,并且参数使用const定义的话,那么判断是可以成立的),对child做slot更新,调用child.update(newWidget),newChild更新为child。
    2.1 child.update方法,首先更新element的widget,然后根据子类的覆写实现,statelessElement的实现是直接调用element的rebuild方法,statefullElement实现是更新state的widget并且回调didUpdateWidget钩子函数,然后调用rebuild方法
  static bool canUpdate(Widget oldWidget, Widget newWidget) {return oldWidget.runtimeType == newWidget.runtimeType&& oldWidget.key == newWidget.key;}
  1. 如果旧的element无法更新的话,需要解除绑定关系,将旧的element回收,用于后面的视图build复用;然后inflateWidget根据newWidget新建一个element。
  void deactivateChild(Element child) {assert(child._parent == this);child._parent = null;child.detachRenderObject();owner!._inactiveElements.add(child); // this eventually calls child.deactivate()}
  • child为空
  1. inflateWidget根据newWidget新建一个element

2.inflateWidget函数

Element inflateWidget(Widget newWidget, Object? newSlot) {//。。。s省略try {final Key? key = newWidget.key;if (key is GlobalKey) {final Element? newChild = _retakeInactiveElement(key, newWidget);if (newChild != null) {newChild._parent == nullnewChild._activateWithParent(this, newSlot);final Element? updatedChild = updateChild(newChild, newWidget, newSlot);return updatedChild!;}}final Element newChild = newWidget.createElement();newChild.mount(this, newSlot);return newChild;}}
  1. 首先判断newWidget是否拥有globalKey,如果有的话尝试从复用池中获取拥有相同key的element元素,然后对该元素进行updateChild操作,这样又回到上面的起点了,这样看起来element更新是深度递归的。
  2. 如果没有key,那么直接创建一个新的element,然后element进行mount挂载操作,也就是将element加入到这个element树中。

3.mount函数

重点看看mount函数:

  void mount(Element? parent, Object? newSlot) {assert(_lifecycleState == _ElementLifecycle.initial);assert(_parent == null);assert(parent == null || parent._lifecycleState == _ElementLifecycle.active);assert(slot == null);_parent = parent;_slot = newSlot;_lifecycleState = _ElementLifecycle.active;_depth = _parent != null ? _parent!.depth + 1 : 1;if (parent != null) {// Only assign ownership if the parent is non-null. If parent is null// (the root node), the owner should have already been assigned.// See RootRenderObjectElement.assignOwner()._owner = parent.owner;}assert(owner != null);final Key? key = widget.key;if (key is GlobalKey) {owner!._registerGlobalKey(key, this);}_updateInheritance();attachNotificationTree();}
  1. 首先对该widget进行参数判断,因为是新的widget,所以依赖关系都是空的,这里需要进行parent关联操作,生命周期设置,树的深度设置,owner是和parent使用的同一个,注册全局key。
  2. _updateInheritance,保存来自parent的_inheritedElements对象,这个对象集合里面包含着这棵树所有的inheritedElement(如果自己也是inheritedElement,也要将自己加入集合),这个类型的element具有从上下文继承数据的功能,可以根据类型读取数据,对应类型数据改变,可以通知所有依赖这个数据的inheritedElement去更新视图。
  3. attachNotificationTree,保存parent的_notificationTree集合,如果自己是NotifiableElementMixin类型,会将自己加入到parent._notificationTree集合中,这个集合是接受通知集合,一般的widget也不需要这个功能(不去深究)。

注: 从上面代码分析可知,mount是element从initial -> active的时间点。

mount是element基本接口,子类会对其进行复写,并且super调用

3.1 componentElement的实现

componentElement类,是负责组合子element的作用的,相当于Android View视图中的viewGroup,但是它也没有绘制功能,仅仅是负责排列组合子element。

component Element的复写:

  void mount(Element? parent, Object? newSlot) {super.mount(parent, newSlot);firstBuild();}void _firstBuild() {rebuild(); // This eventually calls performRebuild.}void rebuild({bool force = false}) {//。。。省略if (_lifecycleState != _ElementLifecycle.active || (!_dirty && !force)) {return;}//。。。省略try {performRebuild();}}

会进行一次rebuild操作,内部直接调用的performRebuild()

3.2 RenderObjectElement的实现

RenderObjectElement类,是起绘制作用的element,相当于Android View视图中的view,视图的正真的绘制操作都在里面实现。

RenderObjectElement复写:

  void mount(Element? parent, Object? newSlot) {super.mount(parent, newSlot);_renderObject = (widget as RenderObjectWidget).createRenderObject(this);attachRenderObject(newSlot);super.performRebuild(); // clears the "dirty" flag}
  • 由RenderObjectWidget创建RenderObject,这个RenderObject是正真实现绘制功能的类。
  • attachRenderObject函数,主要是将parent.newSlot传递给自己,然后和parent建立关联,这个关联主要是renderTree的关联。
3.2.1 attachRenderObject函数
  void attachRenderObject(Object? newSlot) {assert(_ancestorRenderObjectElement == null);_slot = newSlot;_ancestorRenderObjectElement = _findAncestorRenderObjectElement();_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();if (parentDataElement != null) {_updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);}}
  • _ancestorRenderObjectElement
  RenderObjectElement? _findAncestorRenderObjectElement() {Element? ancestor = _parent;while (ancestor != null && ancestor is! RenderObjectElement) {ancestor = ancestor._parent;}return ancestor as RenderObjectElement?;}

向上递归获取第一个RenderObjectElement类型的祖先,然后将当前子renderObject插入到这个祖先RenderObjectElement的孩子中或者孩子队列中。

可以看出也不是所有的element都是RenderObjectElement类型的,componentElement以及它的子类就不是,那么就会被跳过继续向上递归。
进而也就有了文章开头的那三棵树的关系,widget和element是一对一关系的,build过程中,element创建一定需要widget去配置或者更新,renderObject的创建只有RenderObjectWidget才有这个接口功能,因此像componentElement类型的就没有renderObject。

SingleChildRenderObjectElement类的实现,这个类只包含一个单独的renderObject,直接赋值子child,:

  void insertRenderObjectChild(RenderObject child, Object? slot) {final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;assert(slot == null);assert(renderObject.debugValidateChild(child));renderObject.child = child;assert(renderObject == this.renderObject);}

MultiChildRenderObjectElement类的实现,这个类包含多个renderObject,按照自己定义的排列规则排列子renderObject,将child插入到子child队列中对应的位置,上面说了slot已经将child的位置定下来了,可以根据slot的位置,将child插入到指定位置。

  void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;assert(renderObject.debugValidateChild(child));renderObject.insert(child, after: slot.value?.renderObject);assert(renderObject == this.renderObject);}void insert(ChildType child, { ChildType? after }) {adoptChild(child);_insertIntoChildList(child, after: after);}void adoptChild(RenderObject child) {setupParentData(child);markNeedsLayout();markNeedsCompositingBitsUpdate();markNeedsSemanticsUpdate();child._parent = this;if (attached) {child.attach(_owner!);}redepthChild(child);}

adoptChild函数所作的事情是,绑定parentData数据,child根据parent的布局数据layout的时候有用,接下来就是标记parent需要重新layout,child和parent进行关联。


void _insertIntoChildList(ChildType child, { ChildType? after }) {final ParentDataType childParentData = child.parentData! as ParentDataType;_childCount += 1;assert(_childCount > 0);if (after == null) {// insert at the start (_firstChild)childParentData.nextSibling = _firstChild;if (_firstChild != null) {final ParentDataType firstChildParentData = _firstChild!.parentData! as ParentDataType;firstChildParentData.previousSibling = child;}_firstChild = child;_lastChild ??= child;} else {final ParentDataType afterParentData = after.parentData! as ParentDataType;if (afterParentData.nextSibling == null) {// insert at the end (_lastChild); we'll end up with two or more childrenassert(after == _lastChild);childParentData.previousSibling = after;afterParentData.nextSibling = child;_lastChild = child;} else {// insert in the middle; we'll end up with three or more children// set up links from child to siblingschildParentData.nextSibling = afterParentData.nextSibling;childParentData.previousSibling = after;// set up links from siblings to childfinal ParentDataType childPreviousSiblingParentData = childParentData.previousSibling!.parentData! as ParentDataType;final ParentDataType childNextSiblingParentData = childParentData.nextSibling!.parentData! as ParentDataType;childPreviousSiblingParentData.nextSibling = child;childNextSiblingParentData.previousSibling = child;assert(afterParentData.nextSibling == child);}}}

插入操作,以下几种情况:

  1. 前驱为空,那么当前插入的child是队列的第一个元素,直接插入;
  2. 前驱不为空,如果前驱的next指针为空,那么前驱是尾部元素,child插入到尾部;
  3. 前驱不为空,并且前驱在队列中间,那么将child插入到前驱的后面。
  • parentDataElement
  ParentDataElement<ParentData>? _findAncestorParentDataElement() {Element? ancestor = _parent;ParentDataElement<ParentData>? result;while (ancestor != null && ancestor is! RenderObjectElement) {if (ancestor is ParentDataElement<ParentData>) {result = ancestor;break;}ancestor = ancestor._parent;}}

向上递归获取第一个parentDataElement类型的祖先,然后将当前子renderObject传递给祖先,祖先会验证自己的data和子child的data的数据是否一致,不一致会标记脏,下一帧会重新layout,否则不管。

  void _updateParentData(ParentDataWidget<ParentData> parentDataWidget) {if (applyParentData) {parentDataWidget.applyParentData(renderObject); }}void applyParentData(RenderObject renderObject) {//。。。s省略if (needsLayout) {markNeedsLayout();}}

其中实现了ParentDataWidget的子类有:
在这里插入图片描述
这些布局也说明了一个问题,子child的布局属性变化,会导致parent布局重新layout,这样好像会影响性能。

4.performRebuild函数

performRebuild也是element的基本接口,

  void performRebuild() {_dirty = false;}

不同的子类也会有不同的实现,component Element的复写:

  void performRebuild() {Widget? built;try {built = build();}try {_child = updateChild(_child, built, slot);}}Widget build();
  1. 调用build方法,构建出本element所需要的widget组件;widget的build函数由子类实现提供,componentElement的子类主要有StatelessElement,StatefulElement,ProxyElement三个,
    1.1 StatelessElement使用child的widget来构建,Widget build() => (widget as StatelessWidget).build(this);
    1.2 ProxyElement直接使用child Widget build() => (widget as ProxyWidget).child;
    1.3 StatefulElement使用state来创建 Widget build() => state.build(this);

  2. 调用updateChild,用新构建出的widget去更新旧的child element元素,这个updateChild方法上面已经讲过,可以知道,如果element的子element还有child的话,一直递归调用updateChild函数,可以推测出,element树的构建也是深度递归进行的

总结三棵树创建流程

上面梳理了整个树创建的过程,调用链:updateChild -> inflateWidget -> mount -> performRebuild -> (child -> updateChild递归调用)

假如有以下widget树:

return Container(decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(8.0.r)),border: Border.all(color: CtrColor.lineRegular, width: 1),),child: const Row(children: [Image(image: AssetImage("static/images/ic_net_error.png")),Text("data")],));

可以画出整个树创建的流程图:
在这里插入图片描述

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

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

相关文章

简单的elementui倒计时按钮事件

html部分 <el-button :disabled"disableButton" style"width:35%; float: right;" click"startCountdown"><span>{{ buttonText }}</span></el-button> 初始化: disableButton: false,buttonText: 获取验证码,countdow…

WebSocket学习笔记

一篇文章理解WebSocket原理 1.HTTP协议(半双工通信)&#xff1a; HTTP是客户端向服务器发起请求&#xff0c;服务器返回响应给客户端的一种模式。 特点&#xff1a; 1.只能是客户端向服务器发起请求&#xff0c;是单向的。 2.服务器不能主动发送数据给客户端。 半双工通信…

react+ts手写cron表达式转换组件

前言 最近在写的一个分布式调度系统&#xff0c;后端同学需要让我传入cron表达式&#xff0c;给调度接口传参。我去了学习了解了cron表达式的用法&#xff0c;发现有3个通用的表达式刚好符合我们的需求&#xff1a; 需求 每天 xx 的时间&#xff1a; 0 11 20 * * ? 上面是…

Java开发树结构数据封装!

目录 源数据如下controller接口&#xff1a;service层封装:Dao接口&#xff1a;Dao层Mapper:映射实体类&#xff1a; 源数据如下 controller接口&#xff1a; RequestMapping("/UserTreeInfo")public RespBody getUserTreeInfo(Long userId) {List<MenuTreeVo>…

一文学会使用WebRTC API

WebRTC&#xff08;Web Real-Time Communication&#xff09;是一项开放标准和技术集合&#xff0c;由 W3C 和 IETF 等组织共同推动和维护&#xff0c;旨在通过Web浏览器实现实时通信和媒体流传输。WebRTC于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的…

JVS-rules中的基础与复合变量:规则引擎的心脏

JVS-rules中的“变量”概念与编程语言中的变量类似&#xff0c;但它们通常在规则系统中处理条件判断、业务结果复制场景&#xff0c;如下所示&#xff1a; 条件判断&#xff1a;在规则引擎中&#xff0c;规则通常由两个部分组成&#xff1a;条件和分支。变量用于描述条件部分中…

Arduino驱动ICG-20660L传感器(惯性测量传感器篇)

目录 1、传感器特性 2、硬件原理图 3、控制器和传感器连线图 4、驱动程序

职场题:有一件特别紧急的事,群众要办理,且联系不上领导,你怎么办?(2)

接1所写 如果无法联系上领导且有一项特别紧急的事情需要办理&#xff0c;以下是进一步的建议&#xff1a; 11. 尝试其他沟通渠道&#xff1a;除了直接联系领导外&#xff0c;尝试通过其他沟通渠道与领导取得联系。这可能包括电子邮件、即时通讯工具或其他内部通信平台。确保详…

逐字稿 | 2 MoCo 论文逐段精读【论文精读】

bryanyzhu的个人空间-bryanyzhu个人主页-哔哩哔哩视频 评价 今天我们一起来读一下 MOCO 这篇论文。 MOCO 是 CVPR 2020 的最佳论文提名&#xff0c;算是视觉领域里使用对比学习的一个里程碑式的工作。而对比学习作为从 19 年开始一直到现在视觉领域乃至整个机器学习领域里最炙…

深度学习零基础教程

代码运行软件安装&#xff1a; anaconda:一个管理环境的软件–>https://blog.csdn.net/scorn_/article/details/106591160&#xff08;可选装&#xff09; pycharm&#xff1a;一个深度学习运行环境–>https://blog.csdn.net/scorn_/article/details/106591160&#xf…

PAM从入门到精通(七)

接前一篇文章&#xff1a;PAM从入门到精通&#xff08;六&#xff09; 本文参考&#xff1a; 《The Linux-PAM Application Developers Guide》 先再来重温一下PAM系统架构&#xff1a; 更加形象的形式&#xff1a; 五、主要函数详解 5. pam_strerror 概述&#xff1a; 描述…

在vue使用class选择器和下标更改点击列表样式

如果您正在使用Vue 3的<script setup>语法&#xff0c;可以按照以下步骤在Vue中使用class和下标来更改点击项的样式&#xff1a; 首先&#xff0c;在<script setup>部分导入所需的响应式API和定义需要使用的变量。 <script setup> import { ref } from vue…

千兆光模块和万兆光模块的区别?

在网络通信领域&#xff0c;千兆光模块和万兆光模块是最为常见且广泛应用的两种光模块。不同之处在于传输速率、封装、传输距离、功耗、发射光功率、接收光功率和应用场景等。 千兆光模块的传输速率为1 Gbps&#xff0c;万兆光模块的传输速率为10 Gbps&#xff0c;这意味着万…

vue-cli脚手架创建项目时报错Error: command failed: npm install --loglevel error

项目背景 环境&#xff1a;vue-cli 5.x 在工程文件中&#xff0c;后端模块wms已经创建完成&#xff0c;现在想新建一个名为vue-web的前端模块 执行命令vue create vue-web时&#xff0c; 报错Error: command failed: npm install --loglevel error 问题分析及解决 排查过程…

ScyllaDB获4300万美元融资,NoSQL数据库市场再掀热潮!

ScyllaDB是一家成立于2012年12月的美国公司&#xff0c;总部位于加利福尼亚州桑尼维尔。作为一家数据密集型应用程序数据库供应商&#xff0c;ScyllaDB生产的NoSQL数据库兼容Apache Cassandra和Amazon DynamoDB&#xff0c;具有可靠的低延迟和10倍的吞吐量。在2023年10月17日&a…

代码随想录算法训练营第五十八天| LeetCode 583 两个字符串的删除操作、LeetCode 72 编辑距离、编辑距离总结

1 LeetCode 583 两个字符串的删除操作 题目链接&#xff1a;LeetCode 583 两个字符串的删除操作 文章讲解&#xff1a;代码随想录(programmercarl.com) 视频讲解&#xff1a;LeetCode&#xff1a;583.两个字符串的删除操作 2 LeetCode 72 编辑距离 题目链接&#xff1a;LeetCod…

JVM第十三讲:调试排错 - JVM 调优参数

调试排错 - JVM 调优参数 本文是JVM第十三讲&#xff0c;调试排错 - JVM 调优参数。对JVM涉及的常见的调优参数和垃圾回收参数进行阐述。 文章目录 调试排错 - JVM 调优参数1、Jvm参数2、垃圾回收 问题1&#xff1a;线上ECS治理问题2&#xff1a;白龙马线上服务机JVM参数配置&a…

Qt move和setGeometry的区别

move 和 setGeometry 都是用于管理窗口或小部件的位置的方法&#xff0c;通常在使用 Qt 编程时会用到。它们之间的主要区别在于&#xff1a; move 方法&#xff1a;这个方法用于设置小部件的左上角的坐标位置&#xff0c;它需要两个参数&#xff0c;即横坐标和纵坐标。使用 mov…

idea dubge 详细

目录 一、概述 二、debug操作分析 1、打断点 2、运行debug模式 3、重新执行debug 4、让程序执行到下一次断点后暂停 5、让断点处的代码再加一行代码 6、停止debug程序 7、显示所有断点 8、添加断点运行的条件 9、屏蔽所有断点 10、把光标移到当前程序运行位置 11、单步跳过 12、…

访问 github 问题解决方法

一、macOS版 PS. Windows 版的还没试&#xff0c;不过应该也差不多 1.基本信息 硬件&#xff1a;MacBook Pro 2017 (A1707) 系统&#xff1a;macOS 13.6 (Ventura) 应用&#xff1a;SwitchHosts 4.1.2 (Releases oldj/SwitchHosts GitHub) hosts内容网站&#xff1a;ht…