0%

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>

Console妙用知多少?

平时我们使用console的时候,基本都是console.log(xxx), 其实console的用途不只是打log,今天我们来看看console的其他用法。

1
2
3
console.log(xxx); // print log
console.warn(xxx); // print warning
console.error(xxx); // print error

1. console.time

console.timeconsole.timeEnd 可以用来计算代码执行时间,用法如下:

1
2
3
4
5
console.time('my-timer');
for (let i = 0; i < 1000000; i++) {
// do something
}
console.timeEnd('my-timer');

如果在代码执行中打印阶段性时间,可以使用console.timeLog

1
2
3
4
5
6
7
console.time('my-timer');
for (let i = 0; i < 1000000; i++) {
if (i % 100000 === 0) {
console.timeLog('my-timer', i);
}
}
console.timeEnd('my-timer');

2. console.table

console.table 可以将数组或对象以表格形式打印出来,用法如下:

1
2
3
4
5
6
const data = [
{ name: 'Alice', age: 18 },
{ name: 'Bob', age: 20 },
{ name: 'Cathy', age: 22 },
];
console.table(data);

3. console.assert

console.assert 可以用来断言某个条件是否为真,如果为假,则输出错误信息,用法如下:

1
console.assert(1 === 2, '1 is not equal to 2');

输出结果如下:

VM479:1 Assertion failed: 1 is not equal to 2

4. console.count

console.count 可以用来统计某个函数被调用的次数,用法如下:

1
2
3
4
5
6
7
8
9
function greet(user) {
console.count();
return `hi ${user}`;
}

greet("bob");
greet("alice");
greet();
console.count();

输出结果如下:

1
2
3
4
default: 1
default: 2
default: 3
default: 4

5. console.group

console.groupconsole.groupEnd 可以用来将输出的内容分组,用法如下:

1
2
3
4
console.group('group1');
console.log('hello');
console.log('world');
console.groupEnd();

6. console.dir

console.dir 可以用来打印对象的属性,用法如下:

1
2
const obj = { name: 'Alice', age: 18 };
console.dir(obj);

7. console.clear

console.clear 可以用来清空控制台,用法如下:

1
console.clear();

8. console.trace

console.trace 可以用来打印当前调用栈,用法如下:

1
2
3
function foo() {
console.trace();
}

What’s the differences between interface and type in typescript.

  1. types can create primitive type alias, while interface cannot.

    1
    2
    type Name = string;
    const name: Name = 'zdd';
  2. interface can do declaration merging together

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    interface Person {
    name: string;
    }

    interface Person {
    age: number;
    }

    const person: Person = {
    name: 'zdd',
    age: 18
    }

    console.log(person);

    while type can not do declaration merging

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type Person = {
    name: string;
    }

    type Person = { // Error, Duplicate identifier 'Person'
    age: number;
    }

    const person: Person = {
    name: 'zdd',
    age: 18,
    }
  3. Extends and implements

    • interface can extends another interface or class
    • class can implement interface
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Person {
    sayHi() {
    console.log('Hi')
    }
    }

    interface IPerson extends Person {
    name: string;
    }

    class Student implements IPerson {
    name: string;
    constructor(name: string) {
    this.name = name;
    }

    sayHi() {
    console.log('Hi, I am a student')
    }
    }

    Interface extends interface

    1
    2
    3
    interface PartialPointX { x: number; }
    interface Point extends PartialPointX { y: number; }

    Type alias extends type alias

    1
    2
    type PartialPointX = { x: number; };
    type Point = PartialPointX & { y: number; };

    Interface extends type alias

    1
    2
    type PartialPointX = { x: number; };
    interface Point extends PartialPointX { y: number; }

    Type alias extends interface

    1
    2
    interface PartialPointX { x: number; }
    type Point = PartialPointX & { y: number; };
  4. types can create interaction types, but interface not.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    type Name = {
    name: string;
    }

    type Age = {
    age: number;
    }

    type Person = Name & Age; // OK

    interface Name {
    name: string;
    }

    interface Age {
    age: string;
    }

    type Person = Name & Age; // OK
    interface Person = Name & Age // Error.
  5. types can create union types, but interface not.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    type Man = {
    name: string;
    }

    type Women = {
    name: string;
    }

    type Person = Man | Women; // ok

    interface Man {
    name: string;
    }

    interface Woman {
    name: string;
    }

    type Person = Man & Woman; // ok
    interface Person = Man & Woman; // Error.
  6. types can define tuple, but interface not.

    1
    2
    type Response = [string, number]; // ok
    interface Response = [string, number]; // not work

总结一下,主要的区别就是:

  • interface可以做declaration合并,但是type不能。
  • interface can be extend by class, while type can not be extend.
  • type 可以定义union interaction类型,但是interface不能。

How === works ?

Suppose the following expression

1
A === B

How strict equality works in JavaScript? Here are the steps:

  1. If the operands are of different types, return false
  2. If both operands are objects, return true only if they refer to the same object.
  3. If both operands are null or both operands are undefined. return true.
  4. If either operand is NaN, return false
  5. Otherwise, compare the two operand’s value as below
    1. Numbers must have the same numeric values.
    2. Strings must have the same characters in the same order.
    3. Booleans must be both true or both false.

Code Example

1
2
3
4
5
6
7
8
9
console.log(1 === '1'); // false, different type.
console.log({ name: 'zdd' } === { name: 'zdd' }); // false, different object with same value.
console.log([] === []); // false
console.log({} === {}); // false
console.log(null === undefined); // false, different type.

// Pay more attention to the following examples
console.log(NaN === NaN); // false
console.log(+0 === -0); // true

References

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality

Introduction

How to implement lazy load images in html page?

Use loading=lazy attribute of img tag

With html5, you can use loading="lazy" attribute to lazy load images. This attribute tells the browser to defer loading of the image until it is needed. The browser will load the image when it is about to come into view.

1
<img src="image.jpg" loading="lazy" alt="描述性文字">

For old browsers which do not support loading attribute, you can use JavaScript to implement lazy load images.

1
2
<!-- HTML -->
<img data-src="image.jpg" class="lazyload" alt="描述性文字">
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
<script>
document.addEventListener("DOMContentLoaded", function() {
let lazyImages = [].slice.call(document.querySelectorAll("img.lazyload"));

if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazyload");
lazyImageObserver.unobserve(lazyImage);
}
});
});

lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// Fallback for browsers that do not support Intersection Observer
let oldLoad = function(imgs) {
imgs.forEach(function(img) {
img.src = img.dataset.src;
});
};
oldLoad(lazyImages);
}
});
</script>

Please note that, window.load event won’t wait for lazy loaded resources.

References

  1. https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes