0%

什么是背压

Back pressure 翻译成中文叫做背压,也有叫回压的,那么到底啥是背压呢?望文思义一下,背压就是来自背后的压力,那么背后是指哪里呢?在RxJS中,背后就是指产生数据的Observable,我们姑且称之为生产者,而订阅这个Observable的Observer就是消费者。背压就是生产者生产数据的速度大于消费者消费的速度,导致大量的数据积压。

现实生活中的例子,你老婆一分钟能烙五个饼,而你每分钟只能吃一个,那么烙好的饼就会越积越多,你就吃不消了。

解决背压

解决背压基本就两种方法:缓冲和丢弃,我们在时髦一下,在泛化一下,就是无损处理和有损处理。

  • Lossy Operation
  • Lossless Operation

有损方式

  • debounce
  • throttle

无损方式

  • bufferCount - 按数量缓冲,攒够Count个数据再发出
  • bufferTime - 按时间缓冲,攒够Time时间内的数据再发出
  • buffer - 按条件缓冲,满足条件时发出缓冲的数据, buffer(xxx), xxx是一个Observable,当xxx发出数据时,发出缓冲的数据。

Introduction

Angular中有三种样式封装方式,分别是:Emulated, ShadowDom, and None

  • ViewEncapsulation.Emulated (Default value)
  • ViewEncapsulation.ShadowDom
  • ViewEncapsulation.None

可以用过Component中的encapsulation属性来指定封装方式,如果不指定的话,默认是Emulated

1
2
3
4
5
6
7
8
@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.scss'],
encapsulation: ViewEncapsulation.ShadowDom, // <--- here
})
export class ProductComponent {
}

下面我们分别讲解三种封装方式。

Emulated

这是默认的封装方式,如果你没有在@Component中显式指定encapsulation的话,那么使用的就是该方式。在该方式下,Angular会生成一个随机字符串做为组件的属性,然后将样式应用到这个属性上。
这种封装方式是模拟Shadow DOM的方式,Angular会更改组件的CSS选择器,以便样式只应用于组件的视图。这意味着组件的样式不会影响应用程序的其他部分。

以下面的Product组件为例,product-home.component.scss这个样式文件里面的样式只会影响ProductHomeComponent这个组件对应的视图.

1
2
3
4
5
6
7
8
@Component({
selector: 'app-product-home',
templateUrl: './product-home.component.html',
styleUrl: './product-home.component.less',
})
export class ProductHomeComponent {
protected readonly name = 'Philip';
}

运行app后,我们可以在浏览器中inspect生成后的页面,可以看到app-product-home元素有一个唯一的属性_nghost-ng-c3352351300:

1
2
3
<app-product-home _nghost-ng-c3352351300>
<!-- ... -->
</app-product-home>

再看一下实际的例子,假如我们有如下html template, ProductHomeComponent中引入了ProductDetailComponent

1
2
3
<!-- product-home.component.html -->
<p>product-home works!</p>
<app-product-detail></app-product-detail>

那么生成的HTML如下:
emulated

可以看到,Angular为生成的Dom节点都添加了额外的属性,这些属性大概分为两类:

  • _nghost-xxxx - 用来标记组件的host element,比如上图中第一个红框内
  • _ngcontent-xxxx - 用来标记组件的子元素,比如上图中app-product-home内的p元素,它就是一个纯内容,所以标记是_ngcontent-xxxx
  • 有些元素既是host, 又是content,比如上图的第二个红框。因为ProductDetailComponent既是ProductHomeComponent的子元素,而它本身又是一个组件。所以它同时拥有_nghost-xxxx_ngcontent-xxxx两个属性。

如果我们给上面的p标签添加一个背景色的话,那么观察生成后的css样式,你会发现,样式文件里面也添加了额外的属性,这样就确保了样式的唯一性。

观察下图中左侧p标签的属性,和右侧css文件中product-container的属性,可以看到他们是一一对应的。
emulated-css

Emulated模式生成的样式最终会插入到html文件的head标签中。(可以运行Angular app,然后鼠标右键-inspect查看对应的html文件代码)

ShadowDom

这种方式使用浏览器内置的Shadow DOM API来封装组件的样式,该模式下,组件被放到一个ShadowRoot之下,这个Shadow Root相当于组件的host element, 这意味着组件的样式不会影响应用程序的其他部分。

该模式生成的样式会直接放到shadow-root中。

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-product-home',
templateUrl: './product-home.component.html',
styleUrl: './product-home.component.less',
encapsulation: ViewEncapsulation.ShadowDom,
})
export class ProductHomeComponent {
protected readonly name = 'Philip';
}

注意观察生成的html文件,app-product-home元素会有一个shadow-root:

1
2
3
4
<app-product-home>
#shadow-root (open)
<!-- ... -->
</app-product-home>

None

这种模式不进行任何封装,相当于裸奔,其效果等于直接在html中引入样式文件。这种模式谨慎使用,因为会有样式污染。

该模式生成的样式会直接放到html文件的head中。

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-product-home',
templateUrl: './product-home.component.html',
styleUrl: './product-home.component.less',
encapsulation: ViewEncapsulation.None,
})
export class ProductHomeComponent {
protected readonly name = 'Philip';
}

The generated HTML will not have any unique attributes or shadow root:

1
2
3
<app-product-home>
<!-- ... -->
</app-product-home>

注意:ViewEncapsulation.None ViewEncapsulation.Emulated中的样式同时会插入到ViewEncapsulation.ShadowDom的组件中。

References

  1. https://v17.angular.io/guide/view-encapsulation - 这里面的例子要仔细研究一下,有很多细节。

Introduction

ngSwitch is a structural directive in Angular, it is used to conditionally render a template based on the value of an expression.

Use ngSwitch with enum.

注意:我发现下面每个*ngSwitchCase分支都只包含一个<p>元素,所以我们可以将ng-container拿掉,直接把*ngSwitchCase放在<p>标签上。
但是如果case分支中有多个元素,那么就需要用ng-container包裹起来。

default分支用*ngSwitchDefault来标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--app.component.html-->
<div class="account-feature-container" >
<ng-container [ngSwitch]="accountStatus">
<ng-container *ngSwitchCase="AccountStatus.CLOSED">
<p>This is a closed account</p>
</ng-container>
<ng-container *ngSwitchCase="AccountStatus.AUTHORIZED">
<p>This is an authorized account</p>
</ng-container>
<ng-container *ngSwitchCase="AccountStatus.NORMAL">
<p>This is an normal account</p>
</ng-container>
<p *ngSwitchDefault>This is the default branch</p>
</ng-container>
</div>

在Component中定义一个枚举类型,注意AccountStatus = AccountStatus;这一行是必须的,否则编译报错。

1
2
3
4
5
6
7
8
9
10
11
12
// app.component.ts
@Component({
selector: 'app-ng-switch',
templateUrl: './ng-switch.component.html',
styleUrls: ['./ng-switch.component.less']
})
export class NgSwitchComponent implements OnInit {
AccountStatus = AccountStatus; // Make enum visible in component.ts file.
public accountStatus: AccountStatus = AccountStatus.AUTHORIZED;
constructor() {}
ngOnInit(): void {}
}

Use built-in structural directive @switch.

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="use_built-in_control_flow">
@switch (accountStatus) {
@case (AccountStatus.CLOSED) {
<p>This is a closed account</p>
} @case (AccountStatus.AUTHORIZED) {
<p>This is an authorized account</p>
} @case (AccountStatus.NORMAL) {
<p>This is an normal account</p>
} @default {
<p>This is the default branch</p>
}
}
</div>

Tips:

  1. Remember to add the @default branch at the end, if no case matched, and no @default is provided, nothing will be rendered.
  2. There is no fallthrough in the @switch directive, so you don’t need to add break or return in each @case branch.

Introduction

ngIf is a structural directive in Angular, it is used to conditionally render content in a template. Remember to import CommonModule in the module where you want to use ngIf.

Full syntax with ng-template

Structural directives are used in ng-template, here is an example:

1
2
3
<ng-template [ngIf]="hero">
<div class="name">{{hero.name}}</div>
</ng-template>

Shorthand syntax

But, in practice, we often use ngIf in a more concise way by prefix a * before it, this is called the shorthand syntax:

1
<div *ngIf="hero" class="name">{{hero.name}}</div>

Note, even we write it in the shorthand syntax, Angular will still convert it to the ng-template syntax after compiling.

The above template will create a div element in the template, What if you don’t want to create a div element? You can use ng-container instead of div:

1
2
3
<ng-container *ngIf="condition">
Content to render when condition is true
</ng-container>

In this way, Angular will render the text directly without creating a div element.

New syntax with @if

With the new built-in structural @if directive, you can use the following syntax:

1
2
3
@if (condition) {
Content to render when condition is true
}

If else then with *ngIf

1
2
3
4
5
6
7
8
9
10
<div class="case-3">
<ng-container *ngIf="condition; then thenBlock else elseBlock">
</ng-container>
<ng-template #thenBlock>
Content to render when condition is true
</ng-template>
<ng-template #elseBlock>
Content to render when condition is false
</ng-template>
</div>

If else then with @if

With @if directive, you can use the following syntax, it’s more simple and readable:

1
2
3
4
5
6
7
<div class="case-3">
@if (condition) {
Content to render when condition is true
} else {
Content to render when condition is false
}
</div>

Conclusion

既然Angular已经有ngIf, ngFor, ngSwitch等内置的结构性指令,为什么还要引入新的@if@for, @switch呢?主要有以下原因

  1. 更简洁的语法,比如要实现if - else if - … - else的逻辑,使用ngIf就会显得很臃肿。而使用@if就可以很好的解决这个问题。
  2. 更好的类型检查机制,这个不知道是怎么实现的。
  3. 更好的运行时性能,Angular编译器会将@if@for@switch编译成更高效的代码,这样可以减少运行时的开销。
  4. 更小的包体积,最多可以减少30kb的包体积。
  5. 不需要额外导入CommonModule模块,直接使用即可,ngIf等指令则需要导入CommonModule模块。

RxJS Use Case

统计页面点击次数

注意,RxJS中的scan相当于JavaScript中的Array.prototype.reduce,下面的例子展示了如何统计页面点击次数。

1
2
3
4
const { fromEvent, scan } = rxjs;
fromEvent(document, "click")
.pipe(scan((count) => count + 1, 0))
.subscribe((count) => console.log(count));

节流

Resize窗口

resize时间触发非常频繁,我们可以使用throttleTime来节流,下面的例子展示了如何节流resize事件。

1
2
3
4
5
6
7
const { fromEvent, throttleTime } = rxjs;
fromEvent(window, "resize")
.pipe(throttleTime(500))
.subscribe(() => {
console.log(`window width: ${window.innerWidth}`);
console.log(`window height: ${window.innerHeight}`);
});

统计点击次数

throttleTime可以用来实现节流,下面的例子展示了如何实现节流,无论用户点击的有多快,我们都是每隔1秒统计一次点击次数。

1
2
3
4
5
6
7
const { fromEvent, throttleTime, scan } = rxjs;
fromEvent(document, "click")
.pipe(
throttleTime(1000),
scan((count) => count + 1, 0)
)
.subscribe((count) => console.log(count));

设置最大并发请求数

mergeMap with concurrency参数可以设置最大并发请求数,下面的例子展示了如何设置最大并发请求数。

1
2
3
4
import { interval, mergeMap, of } from 'rxjs';

const source = interval(1000);
source.pipe(mergeMap(() => of('request'), 2)).subscribe(console.log);

避免重复请求

使用switchMap可以避免重复请求,下面的例子展示了如何避免重复请求。

忽略某个值

使用skip, 以下代码忽略了interval的前两个值,然后取3个值。所以忽略0和1,取2, 3, 4。关于interval的详细信息,请看这里

1
2
3
import { interval, skip, take } from 'rxjs';

interval(1000).pipe(skip(2), take(3)).subscribe(console.log); // 2, 3, 4

Introduction

RxJS中有许多创建Observable的方法,如fromofinterval等,本文一一介绍。

from

from方法可以将一个数组、类数组对象、Promise、可迭代对象、Observable-like对象转换为Observable。

将数组转换为Observable

RxJS社区有一个约定,以$结尾的变量代表一个Observable对象。下面的例子将一个数组转换为Observable。

1
2
3
4
5
import { from } from 'rxjs';

const array = [1, 2, 3];
const array$ = from(array);
array$.subscribe(console.log); // 1, 2, 3

上面的代码可以简化为:

1
2
3
import { from } from 'rxjs';

from([1, 2, 3]).subscribe(console.log); // 1, 2, 3

将Promise转换为Observable

下面的例子将一个Promise对象转换为Observable。

1
2
3
4
5
6
const p = new Promise((resolve) => {
setTimeout(() => {
resolve([1, 2, 3]);
}, 1000);
});
from(p).subscribe(console.log);

将可迭代对象转换为Observable

其实数组就是可迭代对象,所以前面数组的例子已经包含该例子,下面的例子将一个可迭代对象转换为Observable。

1
2
3
4
5
6
7
function* generate123() {
yield 1;
yield 2;
yield 3;
}

from(generate123()).subscribe(console.log);

使用asapScheduler

使用asapScheduler可以让from方法在当前任务队列的末尾执行,下面的例子展示了如何使用asapScheduler。你知道以下代码输出什么吗?如果不加asapScheduler,输出会是什么?

1
2
3
console.log('from start...');
from([1, 2, 3], asapScheduler).subscribe(console.log);
console.log('from end...');

以上代码输出如下:

1
2
3
4
5
from start...
from end...
1
2
3

如果去掉asapScheduler,则from同步执行,输出如下:

1
2
3
4
5
from start...
1
2
3
from end...

from的以下书写方式已经被废弃,将在RxJS 8中移除.

1
from([1, 2, 3], asapScheduler).subscribe(console.log);

推荐使用下面的方式,详情看这里

1
scheduled([1, 2, 3], asapScheduler).subscribe(console.log);

将Dom事件转换为Observable

使用fromEvent方法可以将Dom事件转换为Observable。
以下代码监听documentclick事件,当点击document时,会在控制台输出clicked!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
<title>rxjs demo</title>
</head>
<body>
<script>
const { fromEvent } = rxjs;
fromEvent(document, "click").subscribe(() => {
console.log("clicked!");
});
</script>
</body>
</html>

默认情况下,浏览器的事件模型是冒泡模型,也可以改为捕获模型,只需要传递{ capture: true }作为fromEvent的第三个参数即可。

1
2
3
fromEvent(document, "click", { capture: true }).subscribe(() => {
console.log("clicked!");
});

of

of把输入的值转换为Observable。与from不同的是,of不会做任何flatten操作,它会把输入的值当做一个整体。这一点体现在数组上,from会把数组的每个元素当做一个值,而of会把整个数组当做一个值。

1
2
3
import { of, from } from 'rxjs';
of([1, 2, 3]).subscribe(console.log); // [1, 2, 3]
from([1, 2, 3]).subscribe(console.log); // 1, 2, 3

of通常用在要一次性生成多个值的场景,比如下面的例子:

1
2
import { of } from 'rxjs';
of(1, 2, 3).subscribe(console.log); // 1, 2, 3

range

Emit a sequence of numbers in a range.

在一个范围内生成一个Observable。range方法有三个参数,分别是startcountschedulerstart表示起始值,count表示生成的个数,scheduler表示调度器。

1
2
3
import { range } from 'rxjs';

range(1, 3).subscribe(console.log); // 1, 2, 3

interval

interval方法会每隔一段时间发出一个递增的数字。interval方法有一个参数period,表示时间间隔,单位是毫秒。interval默认使用asyncScheduler

1
2
3
import { interval } from 'rxjs';

interval(1000).subscribe(console.log); // 0, 1, 2, 3, 4, ...

有三点需要注意:

  1. interval方法是异步的,默认使用asyncScheduler
  2. interval方法会从0开始发出数字。
  3. interval方法第不是立即执行的,而是在第一个时间间隔之后执行。

以下代码输出什么?

1
2
3
console.log('interval start...');
interval(1000).subscribe(console.log);
console.log('interval end...');

答:因为interval默认使用asyncScheduler,所以interval方法是异步的,所以interval start...interval end...会先输出,然后再输出0, 1, 2, 3, 4, ...

timer

可以将timer看成是高配版的interval,为啥这么说呢?因为interval只能设置时间间隔,却无法控制开始时间(interval的开始时间就是其时间间隔),而timer既能控制间隔时间,也能控制开始时间。

来看这样一个需求,每个1秒产生一个数字(从0开始),但是要求第一个数字立即产生,而不是等待1秒。interval无法满足这个需求,但是timer可以。

1
2
3
import { timer } from 'rxjs';

timer(0, 1000).subscribe(console.log); // 0, 1, 2, 3, 4, ...

timer方法的第一个参数就是开始时间,除了可以指定一个具体的毫秒数之外,还可以指定一个Date对象,表示在指定的时间开始。

1
timer(new Date(2024, 11, 28, 17, 0, 0), 1000).subscribe(console.log); // 0, 1, 2, 3, 4, ...

Introduction

Zoneless change detection是Angular 18引入的一个新特性,该特性能让Angular在不依赖zone.js的情况下进行更新检测。下面我们来看一下如何使用这个新特性。

Why remove Zone.js?

  1. Reducing initial bundle size, Zone.js is about 30KB raw and around 10KB gzipped. Remove it can significantly save the initial load time.
  2. Avoid unnecessary change detection cycles: Zone.js notify Angular to run change detection on every async operation, but it doesn’t actually know whether these operations change any data.
  3. Improve debugging experience, Zone.js can make stack traces harder to read.

Integration steps

Create an Angular 18 project

1
npx @angular/cli@18 new my-zoneless-app

Enable zoneless change detection in standalone bootstrap app.

  1. Open file app.config.ts under src/app folder.
  2. Remove the following line
    1
    provideZoneChangeDetection({ eventCoalescing: true }),
  3. Add the following line
    1
    provideExperimentalZonelessChangeDetection(),

Enable zoneless change detection in traditional Module based app.

1
2
3
4
5
6
// NgModule bootstrap
platformBrowser().bootstrapModule(AppModule);
@NgModule({
providers: [provideExperimentalZonelessChangeDetection()]
})
export class AppModule {}

Remove zone.js from your project

Remove zone.js from file angular.json or project.json for Nx based project.

  1. Open file angular.json or project.json.
  2. Remove the following line from architect | build | options
    1
    2
    3
    "polyfills": [
    "zone.js"
    ],
  3. Remove the following line from architect | test | options
    1
    2
    3
    4
    "polyfills": [
    "zone.js",
    "zone.js/testing"
    ],

Old app with polyfills.ts

For old Angular app with an explicit polyfill.ts file, you can remove import 'zone.js' and import 'zone.js/testing' from polyfills.ts.

Uninstall zone.js

1
npm uninstall zone.js

Start your app

1
ng serve # or npm run start

How to make your app work with zoneless change detection

对于开启了zoneless change detection的应用,Angular需要依赖核心API的通知才能进行更新检测,这些通知包括:

  • ChangeDetectorRef.markForCheck (called automatically by AsyncPipe)
  • ComponentRef.setInput
  • Updating a signal that’s read in a template
  • Bound host or template listeners callbacks
  • Attaching a view that was marked dirty by one of the above

除了以上几种情况,Angular不会自动进行更新检测,比如setTimeoutsetIntervalPromise.thenfetch等异步操作,这些操作不会触发更新检测,需要手动调用ChangeDetectorRef.markForCheck来通知Angular进行更新检测。

Use signal for simple value

1
2
3
4
5
6
7
8
// app.component.ts
name = signal('zdd');

ngOnInit() {
setTimeout(() => {
this.name.set('Philip')
}, 1000)
}

Note that, you need to use name() instead of name in your template.

1
2
<!-- app.component.html -->
<p>Hello, I'm {{name()}}</p>

Use signal for complex value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app.component.ts
interface User {
id: string;
title: string;
}

user = signal<User | null>(null);

fetchData() {
return fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
}

ngOnInit() {
this.fetchData().then(res => {
this.user.set(res);
});
}

Note again, you need to use user() instead of user in your template.

1
2
3
4
5
<!-- app.component.html -->
<div>
user id: {{user()?.id}}
user name: {{user()?.title}}
</div>

Manually call cdf.markForCheck

Note, setTimeout won’t call change detection automatically, you need to call cdf.markForCheck manually.

1
2
3
4
5
6
7
8
9
constructor(private cdf: ChangeDetectorRef) {}
name = 'zdd'

ngOnInit() {
setTimeout(() => {
this.name = 'Philip';
this.cdf.markForCheck();
}, 1000)
}

for fetch api, you need to call cdf.markForCheck manually as well.

1
2
3
4
5
6
7
8
9
10
11
fetchData() {
return fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
}

ngOnInit() {
this.fetchData().then(res => {
this.user = res;
this.cdf.markForCheck();
});
}

References

  1. https://angular.dev/guide/experimental/zoneless
  2. https://angular.love/the-latest-in-angular-change-detection-zoneless-signals

Introduction

柯里化(Currying)是一种函数式编程的技术,它是将一个接受多个参数的函数转换为一系列只接受一个参数的函数的过程。这些只接受一个参数的函数被称为柯里化函数。

下面是一个Currying实现的例子,需要好生领悟。

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
34
// 使用rest parameters
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
}
return function (...args2) {
return curried.apply(this, [...args, ...args2]);
};
};
}

// 使用arguments,此方案仅供参考,优先使用上面的rest parameters方案。
function curry1(func) {
return function curried() {
if (arguments.length >= func.length) {
return func.apply(this, arguments);
}
const oldArguments = arguments;
return function () {
return curried.apply(this, [...oldArguments, ...arguments]);
};
};
}

function sum(a, b, c) {
return a + b + c;
}

const currySum = curry(sum);
console.log(currySum(1)(2)(3)); // 6
console.log(currySum(1, 2)(3)); // 6
console.log(currySum(1)(2, 3)); // 6
console.log(currySum(1, 2, 3)); // 6

How to lazy load a script in JavaScript?

  • use defer attribute in script tag.
    1
    <script src="path/to/script.js" defer></script>
  • use async attribute in script tag.
    1
    <script src="path/to/script.js" async></script>
  • use window.load event to load script.
    1
    2
    3
    4
    5
    6
    7
    <script>
    window.addEventListener('load', function() {
    var script = document.createElement('script');
    script.src = 'path/to/script.js';
    document.body.appendChild(script);
    });
    </script>
  • use document.DomContentLoaded event to load script.
    1
    2
    3
    4
    5
    6
    7
    <script>
    document.addEventListener('DOMContentLoaded', function() {
    var script = document.createElement('script');
    script.src = 'path/to/script.js';
    document.body.appendChild(script);
    });
    </script>