2024年虚拟DOM技术将何去何从?

news2024/9/28 23:35:33

d6923afa3229722729ceeb6adab12485.jpeg

从诞生之初谈起,从命令式到声明式,Web开发的演变之路

Web开发的起源与jQuery的统治

在Web开发的早期阶段,操作DOM元素主要依赖命令式编程。当时,jQuery因其易用性而广受欢迎。使用jQuery,开发者通过具体的命令操作DOM,比如:

// jQuery 示例
$("ol li").click(function() {});
let li = $("<li>我是一个列表项</li>");
$("ol").append(li);

这种方式虽然直观,但随着应用复杂度的提高,代码管理变得越来越困难。

声明式编程的兴起:React与JSX

2f427fc9bc954997432434892ce4cf4e.jpeg

2013年,Facebook的Jordan Walke提出了一个革命性的想法:将FaceBook在2010年开发的XHP功能迁移到JavaScript,借助JSX扩展形成新的编码风格。与命令式不同,声明式编程不再关注如何操作DOM,而是描述希望DOM是什么样子。例如:

// React组件示例
const Component = (
  <ul>
    {data.map(item => <MyItem data={item} />)}
  </ul>
);

声明式编程使得组件化开发成为可能,极大地提高了代码的可维护性和可扩展性。

虚拟DOM的出现

React的一个关键概念是虚拟DOM(Virtual DOM)。虚拟DOM是代码与实际DOM操作之间的中间层。这个概念允许代码先修改虚拟DOM树,然后再映射到真实的DOM树上。

虚拟DOM的优点包括:

  • 促进函数式UI编程:抽象组件,简化代码维护。

  • 跨平台能力:虚拟DOM本质上是JavaScript对象,可用于小程序、iOS、Android等应用的抽象层。

  • 数据绑定减少DOM操作:通过合并多个DOM操作为一次操作,减少浏览器重排。例如:

// 利用DocumentFragment优化DOM操作
const fragment = document.createDocumentFragment();
for(let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  fragment.appendChild(div);
}
const container = document.getElementById('container');
container.appendChild(fragment);
  • 轻量级JavaScript操作进行DOM差异化(Diffing):避免过度查询和存储实际DOM,提高性能。

  • 通过DOM Diff减少不必要的操作:减少页面重排和重绘。

  • 缓存DOM和保存节点状态:优化DOM更新。

通过这些技术的发展和应用,Web前端开发从繁琐的命令式操作转变为更加高效、可维护的声明式编程。虚拟DOM的概念不仅为React所采纳,也被许多其他框架所使用,从而推动了整个前端生态的发展。

虚拟DOM的现状及其争议

虚拟DOM的挑战

虽然虚拟DOM曾是前端开发的一大创新,但随着技术的发展,一些框架开始质疑并摒弃虚拟DOM。这背后的原因主要包括:

  • 首次渲染性能:虚拟DOM在首次渲染大量DOM时,由于额外的计算开销,可能会比直接使用innerHTML插入慢。

  • 内存占用:维护一份虚拟DOM树的内存副本。

  • 频繁更新的开销:在频繁更新时,虚拟DOM需要更多时间进行计算工作。

  • 大型项目的性能成本:即使现代框架进行了优化,比较和计算虚拟DOM的成本依然存在,特别是在构建虚拟DOM树时。

不同框架的应对策略

Uber:例如Uber,通过广泛且手动使用shouldComponentUpdate来最小化渲染调用。

React Fiber:React 16引入了React Fiber。通过优先级分割的中断机制,改善了因虚拟DOM树深度遍历造成的主进程阻塞问题。

Vue 3:Vue的创始人尤雨溪在“Vue 3的设计”中提到,他致力于寻找突破虚拟DOM的性能瓶颈。

Svelte:Svelte的作者Rich Harris提出了“虚拟DOM纯属开销”的观点,强调在某些情况或频繁更新下,虚拟DOM数据驱动模型带来的不必要开销。

2024年的虚拟DOM:还需要吗?

fcac80e0fef842d4f51aba592e000831.jpeg

当前非虚拟DOM框架的主力:Svelte

虚拟DOM的现状

目前,虚拟DOM仍然是主流框架中的主导技术。React持续在迭代中探索更合理的调度模式,而Vue3专注于优化虚拟DOM的diff算法。ivi和Inferno在虚拟DOM框架的性能前沿领先。尽管虚拟DOM在主流框架中仍占主导地位,但像Svelte和Solidjs这样的非虚拟DOM框架开始将它们的新模式引入公众视野。

Svelte的创新

Rich Harris,Svelte和rollup的作者,将他在代码打包策略上的专长带入了JavaScript框架领域。

理念:“最好的API是根本没有API” —— Rich Harris

Svelte3:Svelte3经过重大改变,成为一个更轻量级、语法更简洁、代码量更少的JavaScript框架,用于实现响应性。

Svelte在编译阶段,直接将声明式代码转换为更高效的命令式代码,并减少运行时代码。例如:

<script>
 let count = 0;

 function handleClick() {
  count += 1;
 }

 $: {
  console.log(`当前计数为 ${count}`);
 }
</script>

<div class="x-three-year" on:click={handleClick}>
  <div class="no-open" style={{ color: 'blue' }}>{`计数: ${count}`}</div>
</div>

在这个示例中,我们通过基本声明获得了一个响应性变量。然后,通过将其绑定到点击事件,我们得到了一个通过点击驱动视图数据的普通组件。

编译后,Svelte会自动标记响应式数据,例如:

function instance($$self, $$props, $$invalidate) {
 let count = 0;

 function handleClick() {
  $$invalidate(0, count += 1);
 }

 $$self.$$.update = () => {
  if ($$self.$$.dirty & /*count*/ 1) {
   $: {
    console.log(`当前计数为 ${count}`);
   }
  }
 };

 return [count, handleClick];
}

Svelte的优势

Svelte的主要优势在于:

  • 编译时优化:它在构建时而不是运行时处理组件逻辑,将声明式代码编译为高效的命令式代码,从而减少了运行时的开销。

  • 更少的代码:由于编译时优化,Svelte能够生成更少的代码来实现相同的功能。

  • 无需虚拟DOM:Svelte避免了虚拟DOM的使用,直接在编译时将组件转换为优化的JavaScript代码,这减少了运行时的性能开销。

总结来说,Svelte代表了一种新的前端开发范式,通过编译时优化和减少代码量,提供了一种高效、简洁的开发体验。这对于追求性能和简洁性的开发者来说,是一个有吸引力的选择。

Vue的蒸汽模式(Vapor Mode)

概述

Vue3引入了一种新的编译优化策略,称为“蒸汽模式”(Vapor Mode),这是对Svelte预编译概念的响应。蒸汽模式利用编译时信息优化虚拟DOM。这种模式主要体现在编译阶段为一些静态节点附加编译信息,从而在遍历虚拟DOM树时减少不必要的开销,并在一定程度上优化了虚拟DOM带来的问题。

优化的关键点

  • 静态节点优化:在编译阶段,Vue能够识别出模板中的静态节点,并为它们添加特定的编译信息。这意味着在组件更新时,Vue可以跳过这些静态节点的重新渲染,因为它们不会改变。

  • 减少运行时开销:通过在编译时就处理一部分工作,Vue减少了虚拟DOM在运行时的负担。这使得组件在更新时更快,尤其是在处理大型或复杂的DOM结构时。

性能和体积方面的考虑

  • 性能提升:蒸汽模式通过优化静态节点的处理,提高了整体渲染性能。

  • 体积轻量化:这种模式不仅关注性能,也注重于减轻最终打包文件的体积。通过在编译时进行优化,Vue能够生成更加精简的代码。

对前端框架未来的探索

Vue通过引入蒸汽模式,展示了前端框架未来的一个可能趋势:更多地依赖编译时优化,而不仅仅是运行时的动态处理。这种方法不仅提高了性能,还降低了框架的体积,为开发者提供了更高效的开发体验。此外,这也表明了前端技术的不断进步,以及框架开发者对于提高Web应用性能和用户体验的持续追求。

总的来说,Vue的蒸汽模式是对传统虚拟DOM概念的一种重要补充和优化,它在保持虚拟DOM带来的好处的同时,减少了其带来的一些性能开销。这种模式对于那些追求高性能且关注应用大小的项目尤为有益。

Solidjs:一种基于编译的响应式系统

1、Solidjs概述

Solidjs(或称为Solid)是一个类似于Svelte的现代前端框架。它们都基于编译的响应式系统,但在响应性的实现方式上有所不同。Solidjs通过数据驱动的发布-订阅模式来实现细粒度的响应。它因其卓越的性能而闻名,甚至在js-framework-benchmark中排名第一。与此同时,Solidjs的语法更接近于React,对于习惯使用React的开发者而言更为友好。

2、编译阶段的转换

在Solidjs的官方playground中,我们可以看到框架在编译阶段将JSX转换为HTML的输出结果。这一过程体现了Solidjs如何将声明式的代码编译为能够直接操作DOM的命令式代码,从而提高运行时性能。

3、“真正的响应式”

Solidjs在其官网上被标榜为“真正的响应式”。这种响应式并非指React中的虚拟DOM基于状态变化进行修改和重新渲染,而是指Solidjs和Svelte在数据层面上具有更细粒度的响应。相比之下,React是在组件层面上进行响应的。

4、Solidjs的“细粒度响应”设计与实现

Solidjs的“细粒度响应”是指它能够精确地跟踪和响应每个独立的状态变化,而不是整个组件树的变化。这种方法减少了不必要的组件更新和重新渲染,从而提高了性能。

例如,在Solidjs中,当一个状态值改变时,只有依赖于这个状态的部分会重新计算和渲染,而不会影响其他不相关的组件或状态。这种方式使得Solidjs在处理大型应用或复杂交互时具有更高的效率和更好的性能。

5、createSignal 详解

在Solidjs中,createSignal 是实现状态管理和响应式更新的核心。与一些文章误解的不同,Solidjs并不完全基于Proxy来实现响应式,而是依赖类似于Knockout的发布-订阅数据响应系统。createSignal 的关键在于两种角色:SignalState(信号状态)和Computation(计算)。

export function createSignal<T>(
  value?: T,
  options?: SignalOptions<T | undefined>
): Signal<T | undefined> {
  options = options ? Object.assign({}, signalOptions, options) : signalOptions;

  const s: SignalState<T | undefined> = {
    value,
    observers: null,
    observerSlots: null,
    comparator: options.equals || undefined
  };

  if ("_SOLID_DEV_" && !options.internal) {
    if (options.name) s.name = options.name;
    registerGraph(s);
  }

  const setter: Setter<T | undefined> = (value?: unknown) => {
    if (typeof value === "function") {
      if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue);
      else value = value(s.value);
    }
    return writeSignal(s, value);
  };

  return [readSignal.bind(s), setter];
}

SignalState 和 Computation 的角色

SignalState:主要存储在类型为SignalState的对象中。它包含当前值(value)、观察者数组(observers,类型为Computation)、观察者在数组中的位置(observerSlots)和比较器(comparator,用于比较变化,默认为浅比较)。

export interface SignalState<T> extends SourceMapValue {
  value: T;
  observers: Computation<any>[] | null;
  observerSlots: number[] | null;
  tValue?: T;
  comparator?: (prev: T, next: T) => boolean;
}

Computation:在全局作用域中,有一个Listener用来临时存储类型为Computation的观察者。在组件渲染(createRenderEffect)或调用createEffect时,通过updateComputation方法为全局Listener赋值,为后续的依赖跟踪打下基础。

let Listener: Computation<any> | null = null;
export interface Computation<Init, Next extends Init = Init> extends Owner {
  fn: EffectFunction<Init, Next>;
  state: ComputationState;
  tState?: ComputationState;
  sources: SignalState<Next>[] | null;
  sourceSlots: number[] | null;
  value?: Init;
  updatedAt: number | null;
  pure: boolean;
  user?: boolean;
  suspense?: SuspenseContextType;
}
function updateComputation(node: Computation<any>) {
  if (!node.fn) return;
  cleanNode(node);
  const owner = Owner,
    listener = Listener,
    time = ExecCount;
  Listener = Owner = node;
  runComputation(
    node,
    Transition && Transition.running && Transition.sources.has(node as Memo<any>)
      ? (node as Memo<any>).tValue
      : node.value,
    time
  );
//...
  Listener = listener;
  Owner = owner;
}

由于信号的读取,通过函数调用获取数据。

<div class="no-open" style={{ color: 'blue' }}>{`count: ${count()}`}</div>

信号的读取和写入

读取信号:在任何地方读取SignalState时,都会调用readSignal函数。当前临时存储在全局上下文中的“观察者”Listener(指引用SignalState的地方)将被放入其观察者数组中,观察者源将指向当前信号,实现数据绑定。最后,返回相应的SignalState值。

export function readSignal(this: SignalState<any> | Memo<any>) {
  
  const runningTransition = Transition && Transition.running;
  if (
    (this as Memo<any>).sources &&
    (runningTransition ? (this as Memo<any>).tState : (this as Memo<any>).state)
  ) {
    if ((runningTransition ? (this as Memo<any>).tState : (this as Memo<any>).state) === STALE)
      updateComputation(this as Memo<any>);
    else {
      const updates = Updates;
      Updates = null;
      runUpdates(() => lookUpstream(this as Memo<any>), false);
      Updates = updates;
    }
  }
  
  if (Listener) {
    const sSlot = this.observers ? this.observers.length : 0;
    if (!Listener.sources) {
      Listener.sources = [this];
      Listener.sourceSlots = [sSlot];
    } else {
      Listener.sources.push(this);
      Listener.sourceSlots!.push(sSlot);
    }
    if (!this.observers) {
      this.observers = [Listener];
      this.observerSlots = [Listener.sources.length - 1];
    } else {
      this.observers.push(Listener);
      this.observerSlots!.push(Listener.sources.length - 1);
    }
  }
  if (runningTransition && Transition!.sources.has(this)) return this.tValue;
  return this.value;
}

写入信号:写入信号时,调用writeSignal函数。在闭包内更改当前SignalState后,遍历在readSignal阶段收集的观察者数组,并将观察者推入当前Effect执行列表。

export function writeSignal(node: SignalState<any> | Memo<any>, value: any, isComp?: boolean) {
  let current =
    Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value;
  if (!node.comparator || !node.comparator(current, value)) {
    if (Transition) {
      const TransitionRunning = Transition.running;
      if (TransitionRunning || (!isComp && Transition.sources.has(node))) {
        Transition.sources.add(node);

        .tValue = value;
      }
      if (!TransitionRunning) node.value = value;
    } else node.value = value;
    if (node.observers && node.observers.length) {
      runUpdates(() => {
        for (let i = 0; i < node.observers!.length; i += 1) {
          const o = node.observers![i];
          const TransitionRunning = Transition && Transition.running;
          if (TransitionRunning && Transition!.disposed.has(o)) continue;
          if (TransitionRunning ? !o.tState : !o.state) {
            if (o.pure) Updates!.push(o);
            else Effects!.push(o);
            if ((o as Memo<any>).observers) markDownstream(o as Memo<any>);
          }
          if (!TransitionRunning) o.state = STALE;
          else o.tState = STALE;
        }
        if (Updates!.length > 10e5) {
          Updates = [];
          if ("_SOLID_DEV_") throw new Error("Potential Infinite Loop Detected.");
          throw new Error();
        }
      }, false);
    }
  }
  return value;
}

消息重新分发:此时,Effect列表保存了当时的观察者。然后遍历并执行runEffects来重新分发消息。在相应的节点(Computation)中,重新执行readSignal函数,此时可以获取最新的数据结果。

6、组件更新机制解析

在Solidjs中,组件的更新和createEffect类似,但组件的引用通过createRenderEffect和updateComputation来进行处理。

示例:App 组件

function App() {
    const [count, setCount] = createSignal(0);

    return (
        <div class="x-three-year" onClick={() => setCount((pre) => pre + 1)}>
            <div class="no-open">foobar</div>
            <div class="no-open">{count()}</div>
        </div>
    );
}

在这个例子中,我们有一个计数器,每次点击时count会增加。这是通过setCount函数实现的,它是createSignal的一部分。

点击事件触发更新过程

当点击事件发生时,会触发setCount,进而触发writeSignal的行为,如之前所述。这会导致updateComputation被触发,接着进行readSignal以获取SignalState。整个调用栈过程如下:

4a2e6421e86eee47a8659e02d0cc1449.jpeg

7、Solid中需注意的几点

属性的解构和合并

在Solid中,有一些特别需要注意的地方,特别是关于属性(props)的处理:

不能直接解构和合并响应式属性:不能直接使用剩余(rest)和展开(spread)语法来解构和合并响应式属性数据。这是因为通过浅拷贝形式的解构(同样Object.assign方法也不可用)进行的拷贝会割断信号的更新,导致其失去响应性并脱离跟踪范围。

解构会导致失去响应性:直接解构会导致解构出的值失去响应性,并从跟踪中分离。通常,在Solid的原语或JSX之外访问props对象上的属性可能导致失去响应性。此外,像展开操作符和Object.assign这样的函数也可能导致失去响应性。

示例

//no
function Other({count}) {
    return (
    <div>
        <div>{count}</div>
    </div>
    );
}

//yes
function Other(props) {
    return (
    <div>
        <div>{props.count}</div>
    </div>
    );
}

function App() {
    const [count, setCount] = createSignal(0);
    return (
        <div class="x-three-year" onClick={() => setCount((pre: any) => pre + 1)}>
            <div class="no-open">foobar</div>
            <div class="no-open">{count()}</div>
            <Other count={count()}></Other>
        </div>
    );
}
//yes
function Other({count}) {
    return (
        <div>
            <div>{count()}</div>
        </div>
    );
}

function App() {
    const [count, setCount] = createSignal(0);
    return (
        <div class="x-three-year" onClick={() => setCount((pre: any) => pre + 1)}>
            <div class="no-open">foobar</div>
            <div class="no-open">{count()}</div>
            <Other count={count}></Other>
        </div>
    );
}

同时,Solid官方也提供了像mergeProps和splitProps这样的API,用于子组件修改响应式props数据。内部它使用Proxy代理来实现动态跟踪。

依赖跟踪的同步性

Solid的依赖跟踪仅适用于同步跟踪。

异步操作中的依赖跟踪问题:如果在createEffect中使用setTimeout来异步直接访问SignalState,将无法跟踪SignalState的更新,如下示例所示:

const [count, setCount] = createSignal(100);

createEffect(() => {
  setTimeout(() => {
    // 这种方式无法跟踪
    console.log('count', count());
  }, 100);
});

这是因为当readSignal函数在这个时候读取Listener时,基本过程已经完成,数据已被清除(Listener = null Owner = null),因此在读取时无法跟踪SignalState。

避免方法:然而,可以通过某些方法避免这种情况。

createEffect(() => {
  const tempCount = count();
  setTimeout(() => {
    console.log('count', tempCount;
  }, 100);
});

前端框架比较

npm下载次数查询网站

d0fcc345789d51ca83dab4d0bc714913.jpeg

目前,State of js 只有 2022 年的数据(仅供参考),但从数据中可以看出,React、Vue、Angular 在使用量上仍然占据主导地位。但从满意度来看,已经出现了两大非虚拟DOM主力。

59605d3f3aee499e4f61e0e704534896.jpeg

f1e3d50fdc985b061e3bab40d56b4524.jpeg

Svelte和Solid:超越虚拟DOM的前端框架

Svelte和Solid的崛起不仅标志着虚拟DOM的淡出,更多的编译时任务的加入,也展示了开发的新可能性。这两个框架都不使用虚拟DOM,但提供了高效的更新机制和优化的编译过程。

性能比较

根据最新的js-framework-benchmark(Chrome 119 — OSX)数据,Svelte和Solid在性能上相似。在DOM操作时间方面,Solid似乎表现更佳,而Svelte在内存使用和启动时间方面有更好的数据。

2cf71d3b936d7599e67ef298380d5516.jpeg

与其他框架的对比

这边我提取了 js-framework-benchmark (Chrome 119 — OSX) 的公开状态,选取了 ivi、Inferno、Solid、Svelte、Vue、React 进行整体对比。从结果来看,Svelte 和 Solid 的表现略好于大家熟知的 Vue 和 React。但相比像 ivi、Inferno 这样以性能着称的虚拟 DOM 框架,并没有什么优势。

28729c12ddbcdc0ffe636cb5a4a634a5.jpeg

国外大佬 Ryan Carniato在他的研究《DOM渲染的最快方法》一文中,使用标签模板和HyperScript作为Solid的渲染模板,并将其与其他在js-framework-benchmark上表现良好的JavaScript框架进行了比较。结果显示,虚拟DOM框架和非虚拟DOM框架的性能相似,特别是一些高性能的虚拟DOM框架。

最终结果表明,虚拟DOM框架和非虚拟DOM框架具有相似的性能(严格来说,这是针对一些高性能的虚拟DOM框架)。因此,没有最好的技术。在历史上不断修改和优化的过程中,虚拟 DOM 的速度并不慢。不断探索是对技术最大的尊重。

结束

在2024年的前端框架领域,我们看到了多样化的技术选择和不断的创新。每种技术都有其适用的场景和优势,开发者应根据项目的具体需求和上下文来选择最适合的框架。虚拟DOM的主导地位表明它在许多情况下仍然是一个有效的选择,但Svelte和Solid等新兴框架的出现也为开发者提供了更多的选择和可能性。在前端开发的世界里,不断的学习和适应新技术是每个开发者的必经之路。

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

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

相关文章

qt自定义控件的封装

刚学了一个很有意思的东西,前面学了list,Tree,Table三大控件和一部分常用基础控件,但感觉没啥意思,就是用别人的直接用,刚学了一个自定义控件的封装,流程如下: 想把两个不相关的组件封装在一块,直接用ui不行,所以先新添加了qt设计师页面,新添加了一个SmallWidget *ui 在smal…

FCN-8s源码理解

FCN网络用于对图像进行分割&#xff0c;由于是全卷积网络&#xff0c;所以对输入图像的分辨率没有要求。本文重点对fcn8s.py中图像降采样和上采样后图像分辨率的变换进行理解。 相关知识 为准确理解图像分辨率的变换&#xff0c;对网络结构中影响图像分辨率变换的几个函数进行…

Linux基础命令@echo、tail、重定向符

目录 echo概念语法作用演示一演示二 反引号作用 tail概念语法作用不带选项&#xff0c;演示一带选项 -num&#xff0c;演示二带选项 -f &#xff0c; 持续跟踪 重定向符概念作用覆盖重定向&#xff0c;>演示一演示二 追加重定向&#xff0c;>>演示一演示二 总结 echo …

腾讯云取消免费10G CDN流量包:免费CDN时代结束

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 免费送了7-8年的腾讯云10G免费流量包&#xff0c;从2024年开始&#xff0c;停止赠送了!自此&#xff0c;国内绝大多数互联网大厂的CDN都开收费了! 大概从2016年开始&#xff0c;腾讯云为了抢夺CDN客户&#xff0…

CentOS使用docker安装mysql并使用navicat 远程链接

这篇文章没用开启mysql的挂载功能&#xff0c;如果想开启的话可以和我的下篇文章结合着看。 CentOS中开启mysql挂载-CSDN博客 docker在之前的文章中已经安装完成了 这里输入命令查询已被上传的MySQL镜像 docker search mysql这里stars代表点赞数&#xff0c;official代表官…

瓢虫目标检测数据集VOC格式400张

瓢虫&#xff0c;一种小巧玲珑、色彩鲜艳的昆虫&#xff0c;因其独特的形态和生态习性而受到广泛欢迎。 瓢虫的体型小巧&#xff0c;一般为圆球形&#xff0c;体色鲜艳&#xff0c;有红、黄、黑等多种颜色。它们通常有一个坚硬的外壳&#xff0c;可以保护自己不受天敌的侵害。…

一文讲透使用SPSS统计分析软件绘制双轴线图

双轴线图主要用来展示两个因变量和一个自变量的关系&#xff0c;并且两个因变量的数值单位不同时的情形。具体来说&#xff0c;双轴线图是指在一幅图上有一个横轴和两个纵轴&#xff0c;适用于三个变量。两个纵轴分别表示一个变量&#xff0c;横轴变量同时适用于两个纵轴上的变…

案例098:基于微信小程序的电子购物系统的设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

【LeetCode每日一题】383. 赎金信(计数模拟)

2024-1-7 文章目录 [383. 赎金信](https://leetcode.cn/problems/ransom-note/)思路&#xff1a;计数模拟 383. 赎金信 思路&#xff1a;计数模拟 1.通过数组对字母进行计数 2.magazine 中的每个字符只能在 ransomNote 中使用一次。 3.判断减一后&#xff0c;是否小于等于0。…

VS2019出现按F12或Ctrl+鼠标左键跳转到函数位置不准的解决

程序有时会出现大部分函数都不能准确的从头文件中正确定位到函数定位,这是因为数据库错乱造成的,可以通过重构数据库来解决,操作方法如下&#xff1a; 1、菜单栏&#xff1a;工具——选项 2、在打开选项对话框左边的树形框中选择C——高级&#xff1a; 3、然后在右边的浏览/导…

如何找到 niche 出海细分市场的 IDEA

先说结论就是&#xff1a;看榜单 Why&#xff1a;为什么看榜单&#xff1f; 大家会问为什么&#xff1f;原因很简单&#xff1a; 熟读唐诗三百首&#xff0c;不会作诗也会吟不天天看榜单上相关的优秀同行&#xff0c;你想干啥 心法就是下苦功夫坚持&#xff0c;量变引起质变…

微信小程序 获取地址信息(uniapp)

参考API地址&#xff1a;微信小程序JavaScript SDK | 腾讯位置服务 <script> // 引入SDK核心类&#xff0c;js文件根据自己业务&#xff0c;位置可自行放置var QQMapWX require(../../js/uploadImg/qqmap-wx-jssdk.js);export default {data(){return{qqmapsdk:}},onL…

[VUE]2-vue的基本使用

目录 vue基本使用方式 1、vue 组件 2、文本插值 3、属性绑定 4、事件绑定 5、双向绑定 6、条件渲染 7、axios 8、⭐跨域问题 &#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在读&#xff0c;阿里云专家博主&#xff0c;专注于Java领域学习&#xff0c;擅…

RabbitMQ(八)消息的序列化

目录 一、为什么需要消息序列化&#xff1f;二、常用的消息序列化方式1&#xff09;Java原生序列化&#xff08;默认&#xff09;2&#xff09;JSON格式3&#xff09;Protobuf 格式4&#xff09;Avro 格式5&#xff09;MessagePack 格式 三、总结 RabbitMQ 是一个强大的消息中间…

Java 11中的新字符串APIs详解

第1章 引言 大家好&#xff0c;我是小黑&#xff0c;咱们都知道&#xff0c;Java作为一种广泛使用的编程语言&#xff0c;每一次更新都会带来不少新鲜事物。而Java 11&#xff0c;作为长期支持&#xff08;LTS&#xff09;版本之一&#xff0c;更是引起了广大开发者的关注。好…

数据结构期末复习

章节知识点分析 第一章绪论 基本概念 数据 数据元素&#xff08;记录、表目&#xff0c;是数据集合中一个个体&#xff09; 数据项&#xff1a;一个数据元素可由若干数据项组成 数据对象&#xff1a;性质相同的数据元素的集合&#xff0c;是数据的一个子集 数据结构&…

超维空间M1无人机使用说明书——51、ROS无人机使用AR二维码识别与定位

引言&#xff1a;二维码识别与定位是指ROS通过创建AR标签并且对AR标签进行识别&#xff0c;标签可以由自己任意创建&#xff0c;具体方法会在文中给出&#xff0c;摄像头可以通过识别AR标签大小和姿态获取到标签对应的ID和位置等信息&#xff0c;实现识别与定位 注意&#xff…

深入分析-Spring BeanDefinition构造元信息

**## Spring BeanDefinition元信息定义方式 Bean Definition是一个包含Bean元数据的对象。它描述了如何创建Bean实例、Bean属性的值以及Bean之间的依赖关系。可以使用多种方式来定义 Bean Definition 元信息&#xff0c;包括&#xff1a; XML 配置文件&#xff1a;使用<be…

Qt/QML编程学习之心得:Linux下读写文件File(24)

在Linux嵌入式系统中,经常会使用Qt来读写一个文件,判断一个文件是否存在,具体如何实现呢? 首先,要使用linux系统中相关的头文件: #include <unistd.h> #include <stdio.h> #include <stdlib.h> 其次,判断路径是否存在, if(!dir.exists()){mkdir(…

C#,字符串匹配算法(模式搜索)Z算法的源代码与数据可视化

Z算法也是模式搜索&#xff08;Pattern Search Algorithm&#xff09;的常用算法。 本文代码的运算效果&#xff1a; 一、Z 算法 线性时间模式搜索算法的Z算法&#xff0c;在线性时间内查找文本中模式的所有出现。 假设文本长度为 n&#xff0c;模式长度为 m&#xff0c;那么…