0%

今天这篇主要讨论一下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>

内部样式

所谓内部样式,是指直接写在<style>块上的样式,<style>块一般位于html中的head区块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta http-equiv="content-type" content="text/html">
<title>Title</title>
<style>
.parent {
display: flex;
justify-content: center;
align-items: center;
width: 800px;
height: 600px;
margin: 0 auto;
background-color: #DDDDDD;
}
</style>
</head>
<body>
<div class="parent"></div>
</body>
</html>

外部样式

所谓外部样式,是指写在独立的css文件中的样式,然后引入到html文件中供使用。

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
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta http-equiv="content-type" content="text/html">
<link rel="stylesheet" type="text/css" href="mystyles.css" />
<title>Title</title>
<style>
.parent {
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
width: 800px;
max-width: 100%;
height: 600px;
margin: 0 auto;
color: green;
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