0%

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;
}

问题描述

一座楼梯有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 ||

在JavaScript中,逻辑或运算符(||)用于返回其操作数中的第一个真值,如果所有操作数都是假值,则返回最后一个操作数。

在大多数其他编程语言中,比如Java、C#等,逻辑或运算符只返回布尔值truefalse。但是在JavaScript中,||运算符返回的是操作数本身,而不是布尔值。

运算规则

1
2
x || y // 如果x为真值,返回x,否则返回y
x || y || z // 返回x、y、z中的第一个真值,如果都为假值,则返回z。

看几个例子

1
2
3
1 || 2 // 1
0 || 1 // 1
0 || "" || undefined // undefined

|| 可以返回非布尔值

在大多数编程语言中,逻辑或运算符||只返回布尔值,但在JavaScript中,它返回的是操作数本身,而不是布尔值。

1
2
3
4
1 || 2 // 返回 1, 数字类型
"hello" || "world" // 返回 "hello", 字符串类型
0 || null // 返回 null, null类型
0 || undefined // 返回 undefined, undefined类型

参数守卫

|| 通常用于检查函数中的某个参数是否被传递,如果没有则提供一个默认的值。

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

在上面的例子中,如果没有提供timeoutcallback,则会使用默认值。

但是,这种方法存在一个问题,如果提供了参数但其值为假(例如,将0传递给timeout),则不会生效(依然会使用||后面的值)。在以下代码中,我们打算将0用作超时值,但0 || 2000 = 2000,因此使用了2000作为超时值。

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

为了修复这个问题,我们可以显式检查参数是否为undefined

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

或者使用空值合并运算符??,它只在左侧值为nullundefined时返回右侧值。

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

短路求值

|| 运算符具有短路求值的特性,即如果第一个操作数为真值,则不会计算第二个操作数。这在某些情况下可以提高性能,避免不必要的计算。运算规则是:逻辑或表达式从左到右依次对每个操作数进行求值,一旦找到一个真值,就会停止求值。

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());

上述代码会输出:

  1. called B
  2. true

在上面的例子中,首先调用了B()函数,它返回true,此时满足短路条件,因此函数A()不再被执行。

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')

这样是不是就好理解多了呢?关于Object中方法的详情,请看这里

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"
}
}