0%

问题描述

一座楼梯有n个台阶,每一步可以走一个台阶,也可以走两个台阶,请问走完这座楼梯共有多少种方法?

这是一个经典的问题,俗称走台阶问题,此题有多种解法,我们分别看看每种解法的优劣。我们现推理一下递推关系,假设f(n)表示走n个台阶的方法数,那么:

  1. 当n = 1时,只有一种走法,即f(1) = 1
  2. 当n = 2时,有两种走法(每次走一个台阶:1-1,或者一次走两个台阶:2),即f(2) = 2
  3. 当n = 3时,有三种走法(1-1-1,1-2,2-1),即f(3) = 3
  4. 当n = n时,如果第一步走一个台阶,剩下n-1个台阶,有f(n-1)种走法;如果第一步走两个台阶,剩下n-2个台阶,有f(n-2)种走法,所以f(n) = f(n-1) + f(n-2)

递归解法

根据上面的递推关系,我们很容易写出递归解法,代码如下:

1
2
3
4
5
6
7
function climbStairs(n) {
if (n <= 2) {
return n;
} else {
return climbStairs(n - 1) + climbStairs(n - 2);
}
}

递归法虽好,但是效率极其低下,当n = 100时,程序就卡死了,我们来分析一下时间复杂度,当计算f(n)时,需要计算f(n-1)和f(n-2),而计算f(n-1)时,需要计算f(n-2)和f(n-3),依次类推,可以构造一个递归二叉树,其根节点是f(n),左子树是f(n-1),右子树是f(n-2),左子树的左子树是f(n-2),右子树是f(n-3),以此类推,可以看出,递归法的时间复杂度是指数级别的,即O(2^n)。

以n = 5为例,递归树如下:这里面f(3)被计算了2次,f(2)被计算了3次,f(1)被计算了2次。

1
2
3
4
5
6
7
         f(5)
/ \
f(4) f(3)
/ \ / \
f(3) f(2) f(2) f(1)
/ \
f(2) f(1)

由于每个子问题被计算多次,所以这里面有大量的重复计算,为了避免重复计算,我们可以将计算过的结果存储起来,下次用到的时候直接使用存储的结果即可。这就是记忆化搜索(备忘录)方法。

记忆化搜索(备忘录)解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function climbStairs(n, memory = []) {
if (n <= 2) {
return n;
} else {
// 如果计算过,直接使用存储的结果
if (memory[n] !== undefined) {
return memory[n];
} else {
// 计算并存储,不能直接return,必须先存放到memory[n]中
memory[n] = climbStairs(n - 1, memory) + climbStairs(n - 2, memory);
return memory[n];
}
}
}

使用记忆化搜索后,每个子问题只会被计算一次,时间复杂度降为O(n),空间复杂度为O(n)。空间复杂度就是memory数组的长度。

动态规划解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function climbStairs1(n) {
// 如果台阶数小于等于2,直接返回n(因为1阶有1种方法,2阶有2种方法)
if (n <= 2) return n;

// 定义一个数组dp,其中dp[i]表示到达第i个台阶的方法数
let dp = new Array(n + 1);
dp[0] = 0; // 第0阶没有意义,设为0
dp[1] = 1; // 到达第1阶只有1种方法
dp[2] = 2; // 到达第2阶有2种方法

// 动态规划填表
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}

// 返回到达第n阶的方法数
return dp[n];
}

还能进一步优化吗?上面的空间复杂度是O(n),我们可以看到,计算f(n)时只需要f(n-1)和f(n-2)的结果,所以我们只需要存储f(n-1)和f(n-2)的结果即可,不需要存储所有的结果。我们用两个变量来存储f(n-1)和f(n-2)的结果,然后依次计算f(n),这就是迭代法。

动态规划解法(优化空间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function climbStairs(n) {
if (n <= 2) {
return n;
} else {
let prev1 = 1;
let prev2 = 2;
let current = 0;

for (let i = 3; i <= n; i++) {
current = prev1 + prev2;
prev1 = prev2;
prev2 = current;
}

return current;
}
}

迭代法的时间复杂度是O(n),空间复杂度是O(1)。因为只使用了三个变量,所以空间复杂度是常数级别的。

打印出所有走法

我们稍微拓展一下,如果想打印出所有的走法,该怎么做呢?

1

总结

时间复杂度和空间复杂度总结如下表:

方法 时间复杂度 空间复杂度
递归 O(2^n) O(n)
记忆化搜索 O(n) O(n)
动态规划 O(n) O(n)
动态规划(优化空间) O(n) O(1)

Angular父子组件间通信的方式有很多种,比如通过@Input@OutputViewChildViewChildren等。今天我们来详细讨论一下这几种方式的使用。

@Input和@Output

这是最常见的方式,适用于父子组件之间的单向或双向数据传递。

(1) 父组件向子组件传递数据(@Input)
父组件可以通过 @Input 装饰器将数据传递给子组件。

步骤:

  1. 在子组件中使用 @Input 定义一个输入属性。
  2. 在父组件模板中通过属性绑定将数据传递给子组件。
    代码示例:

子组件 (child.component.ts):

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

@Component({
selector: 'app-child',
template: `<p>Message from parent: {{ message }}</p>`
})
export class ChildComponent {
@Input() message!: string; // 接收父组件传递的数据
}

父组件 (parent.component.html):

1
<app-child [message]="'Hello from Parent!'"></app-child>

(2) 子组件向父组件传递数据(@Output 和 EventEmitter)
子组件可以通过 @Output 和 EventEmitter 向父组件发送事件和数据。

步骤:

  1. 在子组件中定义一个 @Output 属性,并使用 EventEmitter 发送数据。
  2. 在父组件中监听子组件的事件并处理数据。
    代码示例:

子组件 (child.component.ts):

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

@Component({
selector: 'app-child',
template: `<button (click)="sendMessage()">Send Message to Parent</button>`
})
export class ChildComponent {
@Output() messageEvent = new EventEmitter<string>(); // 定义输出事件

sendMessage() {
this.messageEvent.emit('Hello from Child!'); // 触发事件并传递数据
}
}

父组件 (parent.component.html):

1
2
<app-child (messageEvent)="receiveMessage($event)"></app-child>
<p>Message from child: {{ receivedMessage }}</p>

父组件 (parent.component.ts):

1
2
3
4
5
6
7
export class ParentComponent {
receivedMessage = '';

receiveMessage(message: string) {
this.receivedMessage = message; // 处理子组件传递的数据
}
}

ViewChild和ViewChildren

父组件可以通过 @ViewChild 或 @ViewChildren 直接访问子组件的属性和方法。

步骤:

  1. 在父组件中使用 @ViewChild 获取子组件的实例。
  2. 直接调用子组件的方法或访问其属性。
    代码示例:

子组件 (child.component.ts):

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

@Component({
selector: 'app-child',
template: `<p>Child Component</p>`
})
export class ChildComponent {
message = 'Hello from Child!';

greet() {
return 'Greeting from Child!';
}
}

父组件 (parent.component.ts):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, ViewChild } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
selector: 'app-parent',
template: `
<app-child></app-child>
<button (click)="callChildMethod()">Call Child Method</button>
<p>Message from child: {{ childMessage }}</p>
`
})
export class ParentComponent {
@ViewChild(ChildComponent) child!: ChildComponent; // 获取子组件实例
childMessage = '';

callChildMethod() {
this.childMessage = this.child.greet(); // 调用子组件的方法
}
}

使用SharedService

如果父子组件之间的通信比较复杂,可以使用共享服务来管理数据。详情见这里

Falsy values in JavaScript

JavaScript has the following falsy values. When used in a boolean context, they will be coerced to false.

Value Type Description
null Null The keyword null — the absence of any value.
undefined Undefined undefined — the primitive value.
false Boolean The keyword false.
NaN Number NaN — not a number.
0 Number The Number zero, also including 0.0, 0x0, etc.
-0 Number The Number negative zero, also including -0.0, -0x0, etc.
0n BigInt The BigInt zero, also including 0x0n, etc. Note that there is no BigInt negative zero — the negation of 0n is 0n.
“” String Empty string value, also including '' and ``.
document.all Object The only falsy object in JavaScript is the built-in document.all.

注意:falsyvalue在if(...)中返回false。

Truthy values

除了上面的Falsy values,剩下的都是truthy values.

Truthy values actually ==false

注意,有些值虽然是truthy values,但是它们竟然==fasle,所以,永远不要在JS中使用==!.
以下这些比较结果都是true。至于原因嘛,请看这篇

1
2
3
4
5
6
7
console.log('0' == false); // true
console.log(new Number(0) == false); // true
console.log(new Boolean(false) == false); // true
console.log(new String('0') == false); // true
console.log([] == false); // true
console.log([[]] == false); // true
console.log([0] == false); // true

Reference: https://developer.mozilla.org/en-US/docs/Glossary/Falsy

Introduction to javascript operator ||

In JavaScript, the || operator is used to return the first truthy value among its operands. If all operands are falsy values, it returns the last operand.

1
2
3
4
5
6
x || y // return x if x is truthy, otherwise return y
x || y || z // return the first truthy value among x, y, z, or z if all operands are falsy values.
// examples
1 || 2 // 1
0 || 1 // 1
0 || "" || 1 // 1

|| can return non-boolean values

Logical operator or || in most programming languages only return boolean values, but in JavaScript, this is not true.

1
2
3
4
1 || 2 // 1, number type
"hello" || "world" // "hello", string type
0 || null // null, null type
0 || undefined // undefined, undefined type

arguments checking

|| is often used to check if an argument is provided or not. If the argument is not provided, a default value will be used.

1
2
3
4
function makeRequest(url, timeout, callback) {
timeout = timeout || 2000;
callback = callback || function() {};
}

In above example, if timeout or callback is not provided, the default value will be used.

But there is a problem with this approach, if the argument is provided but falsy value(for example, pass 0 for timeout), the default value will be used. In the following code, we intend to use 0 as the timeout value, but 0 || 2000 = 2000, so 2000 was used as the timeout value.

1
2
3
makeRequest("https://example.com", 0, function() {
console.log("done");
});

To fix this, we can explicitly check whether the arguments is undefined.

1
2
3
4
function makeRequest(url, timeout, callback) {
timeout = (timeout !== undefined) ? timeout : 2000;
callback = (callback !== undefined) ? callback : function() {};
}

Or use the nullish operator ??, which only returns the right-hand side value if the left-hand side value is null or undefined.

1
2
3
4
function makeRequest(url, timeout, callback) {
timeout = timeout ?? 2000;
callback = callback ?? function() {};
}

Short-circuit evaluation

The logical OR expression is evaluated left to right, it will stop evaluating once it finds a truthy value.

1
2
3
4
5
6
7
8
9
10
function A() {
console.log("called A");
return false;
}
function B() {
console.log("called B");
return true;
}

console.log(B() || A());

The code above will output:

  1. called B
  2. true

In the above example, B() is called first, and it returns true, so A() is not called.

Reference

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

今天在浏览Vue官网时,看到了如下代码。

1
2
3
4
5
6
7
8
9
import { createApp, ref } from 'vue'

createApp({
setup() {
return {
count: ref(0)
}
}
}).mount('#app')

生性多疑的我不禁产生了怀疑,这是什么语法?createApp方法接受一个JS对象,可是JS对象不都是冒号分隔的键值对吗?为什么这个setup()方法没有冒号?难道这是Vue的特殊语法?

于是问了一下AI,原来这是ES6的新增语法(基础知识还是不牢呀,《深入理解ES6》还要再读几遍才行),如果一个JS对象中有函数,那么ES5中必须像下面这样写才行:

1
2
3
4
5
var obj = {
setup: function() {
console.log('foo')
}
}

ES6新增了一个语法功能,对象中的函数可以省略冒号和function关键字,直接写函数体,如下:

1
2
3
4
5
var obj = {
setup() {
console.log('foo')
}
}

于是开头那段代码换成ES5的写法就是:

1
2
3
4
5
6
7
8
var obj = {
setup: function() {
return {
count: ref(0)
}
}
}
createApp(obj).mount('#app')

这样是不是就好理解多了呢?话不多说,该做饭了。昨天刚从成都旅游回来,今天家里也没有什么菜,姑且做一个圆葱炒蛋吧。

Generate project with default styling file extension

Nx based monorepo can use file nx.json to config project generate options for specific frameworks.
Take the following config as an example, when creating angular applications, nx will use scss as file extension for style files by default. If you want the terminal prompt you during the generation, remove the style option from the config.

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
35
36
"generators": {
"@nx/react": {
"application": {
"style": "less",
"linter": "eslint",
"bundler": "vite",
"babel": true
},
"component": {
"style": "less"
},
"library": {
"style": "less",
"linter": "eslint"
}
},
"@nx/angular:application": {
"style": "scss", // <--- use scss as default style file
"linter": "eslint",
"unitTestRunner": "jest",
"e2eTestRunner": "cypress"
},
"@nx/angular:library": {
"linter": "eslint",
"unitTestRunner": "jest"
},
"@nx/angular:component": {
"style": "scss" // <--- use scss as default style file
},
"@nx/web:application": {
"style": "css",
"linter": "eslint",
"unitTestRunner": "jest",
"e2eTestRunner": "none"
}
}

The built-in pipe json is used to convert a JavaScript object into a JSON string. It is useful for debugging purposes or when you need to display the raw JSON data in your Angular application.

Usage

Assume you have a data object in your components which is an javascript object:

1
2
3
4
5
6
{
"userId": 1,
"id": 20,
"title": "ullam nobis libero sapiente ad optio sint",
"completed": true
}

In your template file, you can use the json pipe to convert the object into a JSON string:

1
<pre>{{ data | json }}</pre>

You will get the following output on the rendered page:

1
{ "userId": 1, "id": 20, "title": "ullam nobis libero sapiente ad optio sint", "completed": true }

Without the json pipe, you will get [object Object] in the output.

Introduction

Deferrable views is a new feature in Angular 18 that allows you to defer the loading of a component until it is needed. This can help improve the performance of your application by reducing the initial bundle size.

How to use deferrable views

Load when browser is idle

Wrap your component with @defer {} to make it deferrable. In the following example, the app-product-detail component will be loaded only when the Browser is idle.

1
2
3
@defer {
<app-product-detail></app-product-detail>
}

Load when component enter the viewport

You can also specify a condition for when the component should be loaded. In the following example, the app-product-detail component will be loaded when it enters the viewport.

Placeholder

Note that, defer on viewport must be used in conjunction with @placeholder directive. When component is out of viewport, the placeholder will be rendered.

1
2
3
4
5
@defer(on viewport) {
<app-product-detail></app-product-detail>
} @placeholder() {
<div>Default content</div>
}

To test this, you can add a long div element before the app-product-detail component to make it out of the viewport.

Loading section.

You’d better add a section to show the loading state of the component.

1
2
3
4
5
6
7
@defer(on viewport) {
<app-product-detail></app-product-detail>
} @placeholder() {
<div>Default content</div>
} @loading() {
<div>Loading...</div>
}

To test the loading view, you can set the slow 3G network in the browser dev tools/network tab.

Error section

You can also specify an error section to show when the component fails to load.

1
2
3
4
5
6
7
8
9
@defer(on viewport) {
<app-product-detail></app-product-detail>
} @placeholder() {
<div>Default content</div>
} @loading() {
<div>Loading...</div>
} @error() {
<div>Error loading component</div>
}

This is not easy to test, not found a method yet.

Minium render time for placeholder and loading section.

To avoid flickering, you can specify a minimum render time for the placeholder and loading section. In the following example, the placeholder will be rendered for at least 200ms before the component is loaded, and the loading section will be rendered for at least 500ms.

Note that at least doesn’t mean exactly, for example, if the component didn’t load in 500ms, the loading section will keep showing until the component is loaded.

1
2
3
4
5
6
7
@defer(on viewport) {
<app-product-detail></app-product-detail>
} @placeholder( minimum 200ms) {
<div>Default content</div>
} @loading(minimum 500ms) {
<div>Loading...</div>
}

Trigger conditions

Angular provides several trigger conditions for deferrable views. You can use the following conditions to specify when the component should be loaded.

Trigger Description
idle Triggers when the browser is idle.
viewport Triggers when specified content enters the viewport
interaction Triggers when the user interacts with specified element
hover Triggers when the mouse hovers over specified area
immediate Triggers immediately after non-deferred content has finished rendering
timer Triggers after a specific duration

idle

This is the default option, the component will be loaded when the browser is idle.

1
2
3
@defer() {
<app-product-detail></app-product-detail>
}

viewport

The component will be loaded when the specified content enters the viewport. If you didn’t specify the content, the component will be loaded when itself enters the viewport.

1
2
3
4
5
@defer (on viewport) {
<app-product-detail></app-product-detail>
} @placeholder {
<div>Default view</div>
}

You can also load the component when a specific element enters the viewport. In the following example, the app-product-detail component will be loaded when the greeting element enters the viewport.

1
2
3
4
<div #greeting>Hello!</div>
@defer (on viewport(greeting)) {
<app-product-detail></app-product-detail>
}

interaction

The interaction trigger loads the deferred content when user interacts withe the specific element through click or keydown event.

By default, the placeholder acts as the interaction element. Placeholders used this way must have a single root element.

1
2
3
4
5
@defer (on interaction) {
<app-product-detail></app-product-detail>
} @placeholder {
<div>Default view</div>
}

You can also specify a different element for the interaction trigger. In the following example, the app-product-detail component will be loaded when the greeting element is clicked.

1
2
3
4
<div #greeting>Hello!</div>
@defer (on interaction(greeting)) {
<app-product-detail></app-product-detail>
}

hover

Same as interaction, you can just replace interaction with hover in the above examples.

immediate

This is different than idle, the component will be loaded immediately after the non-deferred content has finished rendering.

1
2
3
4
5
@defer (on immediate) {
<app-product-detail></app-product-detail>
} @placeholder {
<div>Default view</div>
}

timer

The timer trigger loads the deferred content after a specific duration. You can specify the duration in milliseconds or seconds.

1
2
3
4
5
@defer (on timer(500ms)) {
<app-product-detail></app-product-detail>
} @placeholder {
<div>Default view</div>
}

when

The when trigger accepts a custom conditional expression and loads the deferred content when the condition becomes true. You can control the condition in our component class.

1
2
3
4
5
@defer (when condition) {
<app-product-detail></app-product-detail>
} @placeholder {
<div>Default view</div>
}

This is a one-time operation– the @defer block does not revert back to the placeholder if the condition changes to a falsy value after becoming truthy.

Conclusion

Conditions for deferrable views

  1. Must be standalone.
  2. Must not references outside of the @defer block in the same template file.

How it works

  1. Angular use dynamic import to load the deferrable views.
  2. The component loaded in @defer was bundled in a separated chunk.

References

  1. https://angular.dev/tutorials/learn-angular/10-deferrable-views
  2. https://blog.angular.dev/introducing-angular-v17-4d7033312e4b
  3. https://angular.dev/guide/templates/defer

Introduction

在Angular开发中经常要使用一些后台数据,下面是一个封装好的DataService服务,用于获取后台数据。数据来自:JSONPlaceholder

DataService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {Injectable} from '@angular/core';

@Injectable({providedIn: 'root'})
export class DataService {
constructor() {
}

async fetchTodoById(id: number) {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
return await response.json();
}

async fetch200Todos() {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos`);
return await response.json();
}
}

Data types

Todo

1
2
3
4
5
6
export interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}

Introduction

Angular has a built-in pipe async that can be used to subscribe to an Observable or Promise and return the latest value it has emitted.

Without async pipe

Let’s see how we write the code without using the async pipe. In the following OrderComponent, we have a todo object that is fetched from the DataService using the fetchTodoById method which returns an Promise. We call the fetchTodos method in the ngOnInit lifecycle hook and assign the result to the todo property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export class OrderComponent implements OnInit {
todo: Todo | null = null;

constructor(private dataService: DataService) {
}

fetchTodos() {
return this.dataService.fetchTodoById(1);
}

ngOnInit() {
this.fetchTodos().then(todo => {
this.todo = todo
})
}
}
1
2
3
4
5
6
<div>
<div>id: {{todo?.id}}</div>
<div>userId: {{todo?.userId}}</div>
<div>title: {{todo?.title}}</div>
<div>completed: {{todo?.completed}}</div>
</div>

With async pipe

With async pipe, we don’t need to call then method on Promise, the async pipe will take care of it. We can directly assign the result of fetchTodos method to the todo property and use the async pipe in the template to subscribe to the Promise.

We need two changes in the code:

  1. Change the type of todo property to Promise<Todo> | null.
  2. Assign the result of fetchTodos method to the todo property.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class OrderComponent implements OnInit {
todo: Promise<Todo> | null = null; // <--- 1

constructor(private dataService: DataService) {
}

fetchTodos() {
return this.dataService.fetchTodoById(1);
}

ngOnInit() {
this.todo = this.fetchTodos(); // <--- 2
}
}

Note that we use the as syntax to assign the result of the async pipe to a template variable todo.

1
2
3
4
5
6
<div *ngIf="todo | async as todo">
<div>id: {{todo.id}}</div>
<div>userId: {{todo.userId}}</div>
<div>title: {{todo.title}}</div>
<div>completed: {{todo.completed}}</div>
</div>

Using with Observables

If using with Observable, just change the type of todo property to Observable<Todo> | null.

1
todo: Observable<Todo> | null = null; // If using Observable

Using with ngFor

1
2
3
4
5
6
<div *ngFor="let todo of todos | async">
<div>id: {{todo.id}}</div>
<div>userId: {{todo.userId}}</div>
<div>title: {{todo.title}}</div>
<div>completed: {{todo.completed}}</div>
</div>

Benefits

  1. When use with Observables, the async pipe automatically subscribes to the Observable, renders the output within the template, and then unsubscribes when the component is destroyed. - You don’t need to manually subscribe and unsubscribe.
  2. Works with Promises as well.

References

https://angular.dev/api/common/AsyncPipe?tab=usage-notes