0%

What’s Signal?

先看一下官方定义:

1
A signal is a wrapper around a value that notifies interested consumers when that value changes. Signals can contain any value, from primitives to complex data structures.

翻译一下:Signal是一个包装器,它包装了一个值,并在该值发生变化时通知感兴趣的消费者。Signal可以包含任何值,从基本类型到复杂的数据结构。据说signal借鉴了solid.js的思想(看这里),但是我没有考证过。

Signal是Angular中重要的新特性,从xxx版开始引入,至xxx版本稳定,Signal的出现,彻底更改了Angular的变更检测机制,原本基于Zone.js的变更检测机制被Signal + OnPush取代,Signal的出现,使得Angular的性能得到了极大的提升。

今天,我们就来揭开Signal的神秘面纱。

Why Signal?

首先,我们来探讨一下Angular为什么要引入Signal?

在Signal之前,Angular是基于Zone.js做变更检测的,不可否认Zone.js是一个非常强大的库,但是它也有一些缺点,比如:

  1. 性能问题:Zone.js的性能并不是很好,特别是在大型项目中,Zone.js的性能问题会暴露的更加明显。除非你使用了OnPush策略,否则Zone.js会在每次异步操作后都会触发变更检测,这样会导致性能问题。
  2. 由于Zone.js要monkey patch所有的异步操作(在Angular app启动时),所以Angular项目在启动的时候会有一些性能损失。
  3. 有些异步操作无法monkey patch,比如async/await,Angular的解决办法是将其降级到Promise。如今几乎所有浏览器都支持async/await,这种做法显然不太合理。

基于以上原因,Angular Team很早就考虑移除Zone.js,但是Zone.js的变更检测机制是Angular的核心,移除Zone.js,意味着Angular无法自动进行变更检测,也就是说变更检测的触发机制由Angular通过Zone.js自动检测,变成了需要用户手动触发,而Signal就是为此而服务的。

通过Signal定义的数据,当它变化时,Angular会自动进行变更检测,这样就不需要Zone.js了,也就解决了上面提到的问题。

signal 语法

Signal语法非常简单,下面来看看如何定义和使用signal。

定义signal

1
const count = signal(0);

上面是一个简单的signal定义,它的初始值是0。如果要定义复杂的值,可以使用泛型:

1
const user = signal<User>({name: 'zdd', age: 18}); // generic type

读取signal的值

1
2
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());

设置signal的值

1
count.set(1);

更新signal的值(根据前一个值)

1
2
// Signals can also be updated by passing a function that receives the current value and returns the new value.
count.update(value => value + 1);

signal的分类

Signal有两种类型:SignalCompute SignalSignal是指普通的signal,它是可读写的;Compute Signal是指由其他signal计算得到的signal,它是只读的。

上面的count就是普通的Signal,可读写,下面我们看一个compute signal的例子。下面代码中,doubleCount是一个compute signal,它的值是由count的值乘以2得到的。

1
2
const count = signal(0);
const doubleCount = computed(() => count() * 2); //
Signal Compute Signal
读写性 可读写 只读

一个例子

我们以一个todo list为例,来看看Signal的使用。首先我们来定义List组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// list-item.model.ts
import {Component, signal} from '@angular/core';
import {ListItemComponent} from '../list-item/list-item.component';
import {ListItem} from '../list-item/list-item.model';

@Component({
selector: 'app-list',
imports: [ListItemComponent],
templateUrl: './list.component.html',
styleUrl: './list.component.scss'
})
export class ListComponent {
readonly listItems = signal<ListItem[]>([
{id: '1', name: 'item 1'},
{id: '2', name: 'item 2'},
])

addItem() {
const nextId = (this.listItems().length + 1).toString();
this.listItems.set([...this.listItems(), {id: nextId, name: `item ${nextId}`}]);
}

removeItem(id: string) {
this.listItems.set(this.listItems().filter(item => item.id !== id));
}
}

在上面的代码中,我们使用readonly listItems = signal<ListItem[]>([...]);定义了一个signal, 它的初始值是一个包含两个ListItem的数组。我们还定义了一个addItem方法,用来添加一个新的ListItem,以及一个removeItem方法,用来删除一个ListItem。
signal与普通的js变量不同,它的读取和写入需要特殊的语法。

  • 读取signal的值:this.listItems() - 注意不是this.listItems,后面要加括号,有点像函数调用。
  • 写入signal的值:this.listItems.set(newValue)
1
2
3
4
5
6
7
8
9
<!--list.component.html--> 
<p>list works!</p>
<div class="list">
@for (item of listItems(); track item.id) {
<app-list-item [id]="item.id" [name]="item.name" (removeItem)="removeItem($event)" />
}
</div>

<button class="add-button" (click)="addItem()">Add Item</button>

然后我们来定义ListItem组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// list-item.model.ts
import {Component, input, output} from '@angular/core';

@Component({
selector: 'app-list-item',
imports: [],
template: `
<div class="list-item">
<div class="list-item-content">
<div>id: {{id()}}</div>
<div>name: {{name()}}</div>
</div>
<div class="list-item-actions">
<button (click)="onRemoveItem(id())">Remove</button>
</div>
</div>
`,
styleUrl: 'list-item.component.scss'
})
export class ListItemComponent {
id = input.required<string>();
name = input(''); // 注意,这是新的input语法。

removeItem = output<string>(); // 这是新的output语法。

onRemoveItem(id: string) {
this.removeItem.emit(id);
}
}

Compute Signal

Compute Signal是指依赖于其他Signal计算得到的Signal,它是只读的,当依赖的signal值变化时,compute signal的值也会相应变化。下面是一个例子:

1
2
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);

Computed signal有两个重要特性:

  1. lazy load - 直到你第一次读取它的值时,它才会计算。
  2. memoization - 第一次读取后缓存值,当依赖的signal值没有变化时,它不会重新计算,而是直接读取缓存的值。当依赖的signal值变化时,重新计算并缓存。

比较函数

signal是如何比较新旧值的呢?signal使用Object.is来比较新旧值,如果新旧值相等,那么signal不会触发变更检测。对于js对象来说,Object.is相当于引用(地址)比较,也就是说只有两个对象引用相等时,Object.is才会返回true。

1
2
3
4
5
const a = [1, 2, 3];
const b = [1, 2, 3];
const c = a;
console.log(Object.is(a, b)); // false
console.log(Object.is(a, c)); // true

创建signal时,你可以指定一个比较函数来改变默认的比较方式,比如使用lodashisEqual函数来进行深比较,这样当两个对象的值相等时,signal也会触发变更检测。

1
2
3
4
5
6
import _ from 'lodash';
const data = signal(['test'], {equal: _.isEqual});
// Even though this is a different array instance, the deep equality
// function will consider the values to be equal, and the signal won't
// trigger any updates.
data.set(['test']);

References

  1. https://angular.dev/guide/signals
  2. https://fullstackladder.dev/blog/2023/05/07/introduction-angular-signals/

今天这篇我们讲解一下Angular中的Input,Input是Angular中的一个装饰器,它用来接收父组件传递过来的数据。

传统的@Input()写法

为了方便展示,我们定义两个组件, 一个父组件:ListComponent, 一个子组件:ListItemComponent。为了便于展示,我们将template和style都写在component.ts中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// list-item.component.ts
import {Component, Input} from '@angular/core';

@Component({
selector: 'app-list-item',
imports: [],
template: `
<div class="list-item">
<div>id: {{id}}</div>
<div>name: {{name}}</div>
</div>
`,
styles: [`
.list-item {
border: 1px solid #ccc;
margin-bottom: 10px;
padding: 10px;
}
`]
})
export class ListItemComponent {
@Input({required: true}) id: string = '';
@Input() name: string = '';
}

ListItemComponent中,我们定义了两个属性:idname,并且使用了@Input装饰器。@Input装饰器有一个可选的参数required,如果设置为true,则表示这个属性是必须的,如果使用组件时没有给该字段赋值,则会报错。

接下来我们在ListComponent中使用ListItemComponent组件,并传递数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// list.component.ts
import { Component } from '@angular/core';
import {ListItemComponent} from '../list-item/list-item.component';

@Component({
selector: 'app-list',
imports: [ListItemComponent],
template: `
<div class="list">
@for (item of listItems; track item.id) {
<app-list-item [id]="item.id" [name]="item.name"/>
}
</div>
`,
styleUrl: './list.component.scss'
})
export class ListComponent {
listItems = [
{id: '1', name: 'item 1'},
{id: '2', name: 'item 2'},
]
}

@Input定义的字段,需要通过property binding的方式传递数据,即[id]="item.id"[name]="item.name"

基于signal的input写法

以上是旧版的写法,从Angular 17.1开始,我们可以使用新版的基于signal的input语法了。

1
2
3
4
5
6
7
8
9
import {Component, input} from '@angular/core'; // import 'input'

export class ListItemComponent {
// @Input({required: true}) id: string = '';
// @Input() name: string = '';

id = input.required<string>();
name = input<string>('');
}

由于这种类型的input的值对应的是一个signal, 所以读取值的时候,要加(),id -> id(), name -> name()

1
2
3
4
5
6
template: `
<div class="list-item">
<div>id: {{id()}}</div>
<div>name: {{name()}}</div>
</div>
`

default value

1
value = input(0); // 0 is default value

specify type

Note that, for simple values, typescript can infer the type by value, but for complex types, you need to specify the type.

1
2
value = input<number>(0); // value = input(0); // totally ok
value = input<string>(''); // value = input(''); // totally ok

required

1
value = input.required<number>(); // required

Input transform

1
2
3
4
5
6
7
8
9
export class ListItemComponent {
id = input.required<string>();
name = input('', {transform: trimString}); // apply transform
}

// define transform function, transform function should be statically analyzable and pure function.
function trimString(value: string | undefined): string {
return value?.trim() ?? '';
}

Built-in transform

  1. booleanAttribute - imitates the behavior of standard HTML boolean attributes, where the presence of the attribute indicates a “true” value. However, Angular’s booleanAttribute treats the literal string “false” as the boolean false.
  2. numberAttribute - attempts to parse the given value to a number, producing NaN if parsing fails.

Input alias

1
value = input(0, {alias: 'sliderValue'}); // aliasValue is the alias name

在模板中使用sliderValue, 注意,组件代码中仍然只能使用value

1
<custom-slider [sliderValue]="50" />

References

  1. https://angular.dev/guide/components/inputs

Reactive Forms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {Component} from '@angular/core';
import {ReactiveFormsModule, FormControl, FormGroup} from '@angular/forms';

@Component({
selector: 'app-root',
template: `
<form [formGroup]="profileForm" (ngSubmit)="handleSubmit()">
<label>
Name
<input type="text" formControlName="name" />
</label>
<label>
Email
<input type="email" formControlName="email" />
</label>
<button type="submit">Submit</button>
<h2>Profile Form</h2>
<p>Name: {{profileForm.value.name}}</p>
<p>Email: {{profileForm.value.email}}</p>
</form>
`,
imports: [ReactiveFormsModule],
})
export class AppComponent {
profileForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
});

handleSubmit() {
alert(this.profileForm.value.name + ' | ' + this.profileForm.value.email);
}
}

今天这篇主要讨论一下Angular框架如何处理样式。

Angular如何隔离样式

因为Angular是组件话的,每一个Component有自己的样式文件,那么Angular是如何保证多个组件之间的样式不会互相影响的呢?

Angular的样式封装

Angular中有三种样式封装方式:

  • Emulated:默认的样式封装方式,通过给每个组件的样式添加一个唯一的属性,来实现样式的隔离。
  • ShadowDom:使用原生的Shadow DOM来实现样式的隔离。
  • None:不对样式进行封装,直接使用全局样式。
    关于这三种样式封装方式的详细介绍,可以参考angular-view-encapsulation

ng::deep

如何处理全局样式

这篇探讨一下Angular中::ng-deep伪类的用法。

::ng-deep是什么?

以下是::ng-deep的官方描述:

(deprecated) /deep/, >>>, and ::ng-deep
Component styles normally apply only to the HTML in the component’s own template.

Applying the ::ng-deep pseudo-class to any CSS rule completely disables view-encapsulation for that rule. Any style with ::ng-deep applied becomes a global style. In order to scope the specified style to the current component and all its descendants, be sure to include the :host selector before ::ng-deep. If the ::ng-deep combinator is used without the :host pseudo-class selector, the style can bleed into other components.

为什么需要::ng-deep?

根据我的经验,使用::ng-deep的场景有:

  1. 第三方库的样式覆盖,因为第三方库的样式有时候是无法直接通过选择器来修改的,这时候就需要使用::ng-deep

为什么要配合:host一起使用?

在我们日常的项目中,::ng-deep很少单独使用,由上面对ng::deep的描述可知,它相当于将样式变成了全局样式,如果不加:host,那么这个样式会影响到所有的组件,加了:host,则只会影响到当前组件及其后代组件。

注意:

  1. 使用了::ng-deep的组件,只有显示在页面上时(该组建对应的前端路由生效时),才会影响其他组件的样式。如果该组件没有显示在页面上,那么它的样式是不会影响其他组件的。
  2. 如果当前页面只显示了使用了::ng-deep的组件,而没有显示其他组件,那么ng::deep的样式也不会影响到其他组件。

也就是说使用了::ng-deep的组件,只有和其他组件一起显示在页面上,才会影响其他组件的样式。

实际的例子,假设有组件A在其样式文件中使用了ng::deep,对于三级标题,将其文本设置为红色。

1
2
3
4
/* style for Component A */
::ng-deep h3 {
color: red;
}

组件B也有一个三级标题,但是没有设置样式。

如果组件A和组件B同时显示在页面上,那么组件A的样式会覆盖组件B的样式,此时页面上的所有h3标题都会显示为红色。

1
2
<app-component-a></app-component-a>
<app-component-b></app-component-b>

我们在浏览器中inspect,可以看到组件A设置的三级标题样式放到了整个html的head部分。

如果组件A中在::ng-deep前加上:host,则只有组件A中的h3标题显示为红色,组件B中的h3标题不会受到影响。

1
2
3
4
/* style for Component A */
:host ::ng-deep h3 {
color: red;
}

为啥加上:host后,就不影响别的组件了呢,因为:host表示这个样式只针对当前组件和其子组件生效,由生成后的css文件也可看出这点,请看下图。

h3之前多了一个限定字符串_nghost-ng-c2124967347,这个字符串正好是组件A的选择器,这样就保证了这个样式只会影响到组件A。而组件B有一个不同的选择器_nghost-ng-c2124967348,所以组件B的h3标题不会受到影响。

如果我们不加:host,那么生成的css文件中就没有这个限定字符串,这样就会影响到所有的组件。

angular-style-ngdeep

::ng-deep只对ViewEncapsulation.Emulated有效

::ng-deep只对encapsulation: ViewEncapsulation.Emulated有效,对于encapsulation: ViewEncapsulation.ShadowDomencapsulation: ViewEncapsulation.None无效。

字符串在任何编程语言中都是非常重要的数据类型,对字符串的操作是程序员必备的技能,这篇文章探讨一下Javascript中常见的字符串操作,没有什么高深的内容,算是一篇笔记吧。

字符串反转

JS中String对象上没有reverse方法,但是Array对象上有reverse方法,所以我们可以先把字符串转成数组,然后再调用reverse方法,最后再把数组转回字符串。

1
2
3
function reverseString(str) {
return str.split('').reverse().join('');
}

除了用split('')方法,我们还可以用Array.from方法,这个方法可以把类数组对象或者可迭代对象转成数组。

1
2
3
function reverseString(str) {
return Array.from(str).reverse().join('');
}

当然也可以使用...扩展运算符,这个运算符可以把可迭代对象转成数组。

1
2
3
function reverseString(str) {
return [...str].reverse().join('');
}

runOutsideAngularNgZone的一个方法,它接受一个函数作为参数,该函数会在Angular的NgZone之外执行。这个方法的作用是什么呢?

runOutsideAngular函数中运行的代码不会触发Angular变更检测。这里的outside并不是真的在Angular之外运行,而是在Angular的Zone之外运行。

  1. 在执行一些性能敏感的操作时,比如处理大量DOM事件或者动画,避免频繁的变更检测导致性能问题。比如,如果有一个画布应用,用户拖动元素的时候,每次mousemove事件都触发变更检测可能不太高效,这时候用runOutsideAngular可以让这些事件处理在Angular Zone外运行,减少不必要的检测。

  2. 第三方库的集成,比如使用D3.js或者Three.js这些库,它们可能有自己的渲染循环,这时候用runOutsideAngular可以避免Angular的变更检测干扰这些库的性能。

  3. 另外,长时间运行的计算任务,比如Web Worker中的处理,可能也需要用这个方法,确保这些任务不会触发变更检测,直到真正需要更新UI的时候再手动触发。

所有上面这些情况都需要用到runOutsideAngular方法,它可以让我们更好地控制变更检测的时机,避免不必要的性能损耗。

频繁的DOM操作

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component, NgZone } from '@angular/core';

@Component({...})
export class CanvasComponent {
constructor(private ngZone: NgZone) {
ngZone.runOutsideAngular(() => {
canvasElement.addEventListener('mousemove', (event) => {
// 处理鼠标移动,不触发变更检测
this.updateCanvasPosition(event);
});
});
}
}

集成第三方库

1
2
3
4
5
6
7
ngZone.runOutsideAngular(() => {
const chart = d3.select('#chart');
chart.on('zoom', () => {
// D3 的缩放操作,避免 Angular 频繁检查
this.handleZoom();
});
});

Web workers或耗时计算

1
2
3
4
5
6
7
ngZone.runOutsideAngular(() => {
const worker = new Worker('data-processor.worker.ts');
worker.onmessage = (result) => {
// 手动触发变更检测以更新 UI
this.ngZone.run(() => this.data = result);
};
});

回到Angular Zone

如果在runOutsideAngular中执行的代码需要更新Angular的UI,可以在需要的时候手动调用ngZone.run方法,把这些代码放回Angular的Zone中,这样就可以触发变更检测了。

这个过程相当于先跳出Angular的Zone,做一些不需要变更检测的操作,然后再手动回到Angular的Zone,触发变更检测。

1
2
3
4
5
6
ngZone.runOutsideAngular(() => {
// 长时间运行的计算任务
const result = longRunningTask();
// 手动触发变更检测以更新 UI
this.ngZone.run(() => this.data = result);
});

  1. display: inline 的局限性
  • 不能设置宽高 :因为 inline 元素的尺寸完全由内容决定,无法通过 CSS 手动调整。
  • 垂直方向的 marginpadding 无效 :虽然可以设置这些属性,但它们不会影响其他元素的布局。
  • 适合文本内容 :通常用于 <span><a> 等需要与文本混合排版的元素。
  1. display: inline-block 的优势,结合了 inlineblock 的优点 :
  • inline 一样可以同行排列。
  • block 一样可以设置宽高和垂直方向的 padding/margin
  • 适合需要精确控制尺寸的行内元素 :例如导航栏中的按钮、图片画廊等。
  1. display: block 的典型用途
  • 结构化布局 :用于页面的主要结构划分,例如标题、段落、表格等。
  • 独占一行 :确保元素不会与其他元素共享同一行。

总结

特性 display:block display:inline-block display:inline
是否独占一行
是否可以设置宽高 可以 可以 不可以
是否受空白符影响 不受影响 受影响(元素之间可能有间隙) 不受影响
默认宽度 填满父容器 仅包裹内容 仅包裹内容
是否可以设置垂直 padding/margin 可以 可以 不可以
适用场景 需要独占一行的布局(如标题、段落) 需要同行排列且能设置宽高的布局(如按钮) 需要同行排列且无需设置宽高的布局(如文本)

CSS中一共有三种样式,分别是:内联样式,内部样式,外部样式。

内联样式

所谓内联样式,是指直接写在html元素上的样式,通过给html指定style属性,比如下面的代码给h1设置文本居中。

1
2
3
<body>
<h1 style="text-align: center">Inline style</h1>
</body>

内部样式

所谓内部样式,是指直接写在html的head元素中的<style>标签上的样式。比如下面的代码中通过内部样式设置parent元素的背景颜色为红色。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.parent {
background-color: red;
}
</style>
</head>
<body>
<div class="parent"></div>
</body>
</html>

这种方式下,你可以书写多个style标签,浏览器会自动合并这些样式。下面的代码中有两个style标签,分别设置了父元素的背景颜色和子元素的文本颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.parent {
background-color: #DDDDDD;
}
</style>
<style>
.child {
color: blue;
}
</style>
</head>
<body>
<div class="parent">
<p class="child">This is a child element.</p>
</div>
</body>
</html>

外部样式

所谓外部样式,是指写在独立的css文件中的样式,这种样式会通过link标签引入到HTML文件中。下面的代码中我们引入了一个名为mystyle.css的外部样式文件。同样的,这种方式下,我们也可以引入多个外部样式文件。

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="mystyle.css">
</head>
<body>
<h1>This is a heading</h1>
<p>This is a paragraph.</p>
</body>
</html>

这三种样式的优先级如下:

1
内联样式 > 内部样式 > 外部样式

看一个列子,下面这个html包含了以上三种样式

  • div元素中的内联样式 - 设置文本为蓝色
  • head/style标签中的内部样式 - 设置文本为绿色
  • head/link标签中的外部样式 - 设置文本为红色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="mystyles.css" />
<style>
.parent {
background-color: #DDDDDD;
}
</style>
</head>
<body>
<div style="color: blue" class="parent">This is a div</div>
</body>
</html>

这个页面会显示蓝色字符串:This is a div.

用Chrome浏览器打开这个html,按键盘上的F12键进入开发者工具,并点击右侧的 Elementstab,然后点击 Stylestab,可以看到三种样式。

css-style-type

Styles标签下依次列出了四种样式,优先级从高到低

  • element.style - 内联样式,颜色是蓝色,有最高优先级。
  • .parent - 内部样式,优先级次之
  • .parent (mystyles.css:1) - 外部样式,优先级再次之
  • div (user agent stylesheet) - 浏览器默认样式,优先级最低

带删除线的样式(内部样式中的color: green, 和外部样式中的color: red)表示这个样式被覆盖了,有更高优先级的样式抢先了。我们可以将高优先级的样式勾掉(单击样式左侧的checkbox),这样浏览器就会自动应用低优先级的样式。

下图就是把内联样式和内部样式中的color全部勾掉,浏览器就是用了外部样式中的color,文本也就变成了红色。

css-chrome-tool

题目描述:
给定一个含有n个元素的整型数组a,再给定一个和sum,求出数组中满足给定和的所有元素组合,举个例子,设有数组a = [1, 2, 3, 4, 5, 6],sum = 10,则满足和为10的所有组合是

  • { 1, 2, 3, 4 }
  • { 1, 3, 6 }
  • { 1, 4, 5 }
  • { 2, 3, 5 }
  • { 4, 6 }

解题思路:

  1. 核心逻辑:回溯法
  • 递归尝试 :从数组的第一个元素开始,尝试将每个元素加入当前组合。
  • 撤销选择 :如果某个元素被加入后不满足条件,则将其移除(回溯),继续尝试其他可能性。
  • 终止条件 :
    • 如果当前组合的和等于目标值 sum,记录该组合。
    • 如果当前组合的和超过目标值 sum,停止进一步尝试(剪枝)。
  1. 关键点:避免重复组合
    在递归调用中,参数 start 表示当前遍历的起始位置。通过设置 start,确保每次递归只从当前元素或其后面的元素中选择,从而避免生成重复的组合。
  2. 剪枝优化
    如果当前组合的和已经超过目标值 sum,则直接返回,不再继续递归。这可以显著减少不必要的计算。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function findCombinations(a, sum) {
const result = []; // 用于存储所有满足条件的组合

// 回溯函数
function backtrack(start, currentCombination, currentSum) {
// 如果当前组合的和等于目标值,记录这个组合
if (currentSum === sum) {
result.push([...currentCombination]); // 深拷贝当前组合
return;
}

// 如果当前组合的和超过目标值,直接返回(剪枝)
if (currentSum > sum) {
return;
}

// 遍历数组,尝试将每个元素加入当前组合
for (let i = start; i < a.length; i++) {
currentCombination.push(a[i]); // 选择当前元素
backtrack(i + 1, currentCombination, currentSum + a[i]); // 递归调用
currentCombination.pop(); // 撤销选择(回溯)
}
}

// 调用回溯函数
backtrack(0, [], 0);

return result;
}