Angular 和 .NET 的助记笔记
C# 多线程、JS / C# GC、TypeScript 类型小技巧、Angular 生命周期与依赖注入。
~/posts/angular-dotnet-notes $ cat post.md
C#
多线程
class Program
{
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(WithOut));
Thread t2 = new Thread(new ParameterizedThreadStart(With));
t1.IsBackground = true;
t2.IsBackground = true;
t1.Start();
t2.Start("Hello");
}
public static void WithOut()
{
// Run
}
public static void With(object data)
{
string str = data as string; // "Hello"
}
}
Sleep vs Wait
- Sleep 基于时间,不需要别的线程唤醒,到点自动恢复。可以放在任何位置。
- Sleep 期间线程不会释放对象的使用权或锁。在同步方法 / 同步块里 sleep,其它线程仍然访问不到。
- Wait 让当前线程暂时放弃对象使用权进入等待。必须放在同步方法 / 同步块里。
- Wait 会释放锁。
- 唤醒方式:
Monitor.Pulse()或Monitor.PulseAll()。
线程池
线程的创建和销毁有额外开销。线程池在后台维护一组复用的线程、对刚加入的任务排队,省掉了反复创建销毁的成本。
class Program
{
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(TestMethod, "Hello");
Console.ReadKey();
}
public static void TestMethod(object data)
{
string str = data as string;
Console.WriteLine(str);
}
}
垃圾回收
JavaScript 的 GC
可达性(reachability)模型:从 Root 出发自上而下扫,所有 Root 无法到达的对象都可以被回收。相比”引用计数”有几个优势:
- 能处理相互引用的对象。
- 能整体剔除一个”孤岛”——一组互相引用但与外部无连通的对象——而无需逐个判断。
缺点是每次都要从头扫一遍。优化手段:
- 分代收集(generational collection):把对象按代分。老生代对象的清理频率更低。
- 增量收集:把扫描切成片,分散在多次运行的间隔里执行。
- 闲时收集:在空闲时段触发。
C# 还有一个 JS 没有的优化是并行回收。
C# 的 GC
引用追踪:变量没有任何引用时即可被回收,GC 在合适的时机触发。
GC 无法回收非托管资源(Unmanaged Resources)——例如文件句柄、原生指针等。非托管资源通常实现 IDisposable 接口,配合 using 语句释放。
手动触发:GC.Collect()。
非托管资源的回收模式:
- 继承
IDisposable。 - 实现
Dispose(),在其中释放托管和非托管资源,并把对象本身从 GC 中移除。 - 实现析构函数,作为兜底再次释放非托管资源。
TypeScript
函数重载的类型定义
和 Java 一样,直接声明多个同名函数、不同参数:
on(event: "close", listener: () => void): this;
on(event: "data", listener: (chunk: any) => void): this;
on(event: "end", listener: () => void): this;
自引用的递归类型
直接写 type T = ... T ... 编译器会拒绝。但通过外部传入泛型参数代替自身就能实现,因为泛型在使用前已经编译好了。
给函数加上 key/value 约束
const func = (obj, key) => obj[key].toString();
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // OK
getProperty(x, "m"); // 期望报错
可以用 K extends keyof T 把 key 约束到 obj 的实际属性集合:
const getProperty = <T, K extends keyof T>(obj: T, key: K): T[K] => obj[key];
按事件名约束回调签名
在 .d.ts 里可以写很多 overload:
on(event: "close", listener: () => void): this;
on(event: "data", listener: (chunk: any) => void): this;
on(event: "end", listener: () => void): this;
on(event: "readable", listener: () => void): this;
on(event: "error", listener: (err: Error) => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this;
例子来自
@types/node里的stream。
更现代的写法是用 extends、keyof、A[T] 组合表达:
type Listeners = {
close: () => void;
data: (chunk: any) => void;
// ...
};
const on = <T extends keyof Listeners>(event: T, listener: Listeners[T]) => {
// ...
};
Angular 2+
组件 / 指令的生命周期
适用于组件和指令:
ngOnChanges:当 Angular 设置或重设数据绑定属性时触发。ngOnInit:在第一次ngOnChanges之后,初始化组件 / 指令。最常用于从后端拉取模板需要的数据。ngDoCheck:每次变更检测运行时触发,用来在 Angular 上下文变化时手动响应。ngOnDestroy:销毁前清理,比如取消订阅、解除事件处理,防止内存泄漏。
只适用于指令:
ngAfterContentInit:组件投影内容初始化完成。ngAfterContentChecked:Angular 检查完投影内容的绑定之后。ngAfterViewInit:Angular 创建组件视图之后。ngAfterViewChecked:Angular 检查完组件视图的绑定之后。
调用顺序:
ngOnChanges → ngOnInit → ngDoCheck → ngAfterContentInit
→ ngAfterContentChecked → ngAfterViewInit → ngAfterViewChecked → ngOnDestroy
依赖注入
- 服务上加
@Injectable()。 - 组件上加
@Component。
依赖注入的本质是控制反转(IoC):应用接口和实现解耦,接口不变的前提下实现可以随意替换。这对测试非常有用。
注册 Provider 时 useClass vs useExisting 的区别:useExisting 会复用同一个已存在的实例(单例),useClass 每次会创建新实例。
变更检测
默认是自上而下:当某个组件发生变化时,它下面所有子组件依次被检查。这是 Default 模式。
OnPush 模式下,当组件的 @Input 没有变化时,整个子树的检查会被跳过。
changeDetection: ChangeDetectionStrategy.OnPush;
detach 与 reattach:把组件从变更检测队列里手动摘下来,再在合适的时机加回去。适用于”自身频繁变化但需要手动控制更新时机”的组件。
constructor(private ref: ChangeDetectorRef) {}
// 摘下
this.ref.detach();
// ...做事...
// 加回
this.ref.reattach();