nodejs-event-loop
javascript-garbage-collection
JavaScript的垃圾收集机制主要是为了自动管理内存,使得开发者不需要手动去分配和释放内存。这种机制主要依赖于一些算法来追踪和标记不再使用的对象,然后回收它们所占用的内存空间。以下是简述的两个主要机制:
引用计数(Reference Counting):
- 在引用计数机制中,每个对象都维护着一个记录它被引用次数的计数器。每当有新的引用指向该对象时,计数器加一;当某个引用失效或被移除时,计数器减一。一旦某个对象的引用计数降为0,就说明这个对象不再被使用,可以安全地将其从内存中删除。
- 但是,引用计数有一个显著的问题是它无法处理循环引用的情况,即两个或多个对象互相引用,但实际上这些对象已经不可达(无法从根对象访问到)。这会导致这些对象永远不会被回收,从而造成内存泄漏。
标记-清除(Mark-and-Sweep):
- 标记-清除是现代JavaScript引擎中最常用的垃圾收集算法。在这个机制下,垃圾收集器会定期从根对象(如全局变量、当前调用栈中的局部变量等)开始遍历所有可达的对象,并给这些对象做上标记。完成标记阶段后,任何没有被标记的对象就被认为是不可达的,因此可以从内存中清除。
- 这种方法解决了引用计数中的循环引用问题,因为它不关心对象之间的相互引用,只关注对象是否能通过根对象访问到。
除了上述两种基本的垃圾收集机制外,现代JavaScript引擎还可能采用其他优化技术,比如分代收集(Generational GC),它基于大多数对象很快变得不可用的假设,将对象分为不同的“代”,并更频繁地检查较新的对象以提高效率。
总的来说,JavaScript的垃圾收集机制旨在让开发者不必担心内存管理的问题,尽管如此,了解这些机制有助于编写更高效的代码,避免潜在的内存泄漏问题。
css-vertical-align-center
简介
垂直居中是CSS中一个重要的知识点,也是面试中常考的内容,今天咱们来详细讲解一下垂直居中。我们不会简单的罗列css样式,而是按不同分类来处理,这样更有利于理解。
inline或者inline-*元素的垂直居中(本质是文本垂直居中)
padding-top = padding-bottom
inline元素是指不会独占一行的元素,比如span
, a
, img
等等。inline元素的垂直居中,可以通过设置padding-top和padding-bottom为相同的值来实现垂直居中。考虑如下html代码:
1 | <span>This is a span</span> |
我们先设置个背景色,方便查看span
元素的高度,然后设置padding-top
和padding-bottom
为16px,这样span元素内的文本就垂直居中了。此方案也适用于多行文本。
1 | background-color: green; |
line-height = height
如果是对block element里面的文本垂直居中(比如div
, p
等),那么可以尝试设置line-height
等于height
,这样也可以实现垂直居中。- 此方案不适用于多行文本。(多行文本时,文本会超出容器外,因为line-height本质上设置的时行与行之间的垂直距离)
1 | <div class="content">This is a div</div> |
1 | background-color: green; |
需要注意的是span
属于inline元素,height
对于inline元素是无效的。inline元素的宽度和高度由内容决定,所以height
及width
对于inline元素是无效的。但是line-height
对于inline元素是有效的。
block elements的垂直居中
知道元素的高度
- 设置父元素position: relative, 子元素position: absolute
- 设置子元素height: 100px, 这个是必须的,此条件就是元素高度已知。
- 设置子元素top: 50%, margin-top: -height/2(50px)
1 | <div class="parent"> |
1 | .parent { |
###不知道元素高度
大多数情况下,元素的高度是未知的,这时候可以使用如下方法,可见我们使用transform: translateY(-50%);
代替了margin-top: -50px;
1 | <div class="parent"> |
1 | .parent { |
使用table-cell
如果你不在乎子元素会被拉伸并填满父元素的话,可以使用table-cell来实现垂直居中。
1 | <div class="parent"> |
1 | .parent { |
使用flex布局
这是目前来讲最方便的方式了,使用flex布局可以轻松实现水平和垂直居中。首先将父元素设置为flex布局,然后设置flex-direction: column;
将布局方式改为纵向排列(默认是横向排列),然后设置justify-content: center;
即可实现垂直居中。
1 | <div class="parent"> |
1 | .parent { |
References
React-interview
React Fiber架构
React的渲染可以分为两个主要阶段:Reconciler(协调阶段),和Committer(提交阶段)。
Reconciler(协调阶段)
工作内容: 构建Fiber树,比较新旧虚拟DOM的不同之处,生成一个变更记录,即一系列需要对真实DOM进行的操作。此阶段的特点:异步,并发,可中断。如果执行过程中有更高优先级的任务来了,那么会中断当前Reconciler的工作,转而处理更重要的任务。
Committer阶段
工作内容:将Reconciler阶段生成的变更记录应用到真实的DOM上。此阶段是同步的,不可中断的。
工作阶段:
- Dom更新前 - useEffect在这个阶段执行,通过微任务队列异步执行(页面渲染后执行)
- Dom更新 - 执行真实DOM的更新
- Dom更新后 - useLayoutEffect在这个阶段执行,同步(页面渲染前执行)
注意页面的渲染和JS的执行是互斥的,只有JS代码执行完,页面才能渲染,这就是useLayoutEffect
的作用,可以在页面渲染前执行一些操作,比如调整布局。
数据结构
Fiber架构中采用FiberNod
e和FiberTree
来描述虚拟DOM树。FiberNode
是一个双向链表,每个节点都有一个指向父节点的指针,一个指向子节点的指针,一个指向兄弟节点的指针。FiberTree
是一个树形结构,由FiberNode
组成。
React diff算法
树比较
只做同层级结点比较,如果结点不存在了,则直接删除。不会继续比较其子树。这避免了夸层级移动操作,对于跨层级移动操作,相当于删除再重建。同层级结点移动呢?可以处理。
组件比较
只做同类型的组件比较,比如div和div比较,p和p比较,只有组件的类型相同,才进入子树进行深层次比较。如果类型不一致,则删除重新创建。
元素比较
对于同层级的元素结点。
- 元素在新集合中,但是不在原来的集合中,属于全新的结点,对集合进行插入操作。
- 元素在原来的集合中,但是不在新的集合中,则删除该元素。
- 元素在新集合中,也在原来的集合中,且元素并未更新,只是位置发生了变化,则进行移动操作。
双缓冲策略:
- current树负责呈现当前页面,而所有的更新都由workInProgress树来承接,当变更完成需要渲染时,将workInProgress树变成current树。
setState
是同步还是异步?
这个问题需要区分开来看,在React18之前,如果executionContext
被赋值了,代表该任务已经进入React调度流程中,此时React会对该任务进行异步处理(批量处理),如果executionContext
没有被赋值,代表该任务还没有进入React调度流程,此时React会对该任务进行同步处理。想setTimeout
, setInterval
等函数都是不会进入React调度流程的,所以是同步处理。而合成事件都会进入到React调度流程中,所以会被异步处理。在React18后,如果使用createRoot
创建根节点,那么setState
会变成同步的。但是如果还是使用传统的render
方式,那么和React18之前的处理逻辑一样。
虚拟DOM
虚拟DOM是React的核心概念之一,它是一个轻量级的JavaScript对象,用来描述真实DOM的层次结构。React通过比较新旧虚拟DOM的差异,然后只更新需要更新的部分,从而提高页面的渲染性能。
虚拟DOM的量大作用:
- 跨平台,因为虚拟DOM只是一个数据结构,所以可以在不同平台上使用,比如浏览器,移动端App等。这是虚拟DOM的重要特性,很多人没有意识到。
- 高性能,通过比较新旧虚拟DOM的差异,只更新需要更新的部分,减少了对真实DOM的操作,提高了页面的渲染性能。
React中嵌套组件的渲染顺序,生命周期,销毁顺序。
渲染顺序
当React开始渲染过程时,它首先从上到下、从父到子地渲染组件树。这意味着父组件会先于其子组件进行渲染。一旦父组件完成它的渲染(包括执行相关的生命周期方法),React会递归地进入该父组件的子组件,并按照同样的方式渲染它们。
生命周期顺序
- 首次挂载时:
- 父组件:constructor -> static getDerivedStateFromProps -> render -> 子组件重复相同流程 -> 子组件componentDidMount -> 父组件componentDidMount
- 更新时(由于props变化或state变化):
- 父组件:static getDerivedStateFromProps -> shouldComponentUpdate -> render -> 子组件重复相同流程 -> 子组件componentDidUpdate -> 父组件componentDidUpdate
销毁顺序
当组件被卸载时,React会以相反的顺序卸载组件树中的组件。也就是说,React首先卸载最深层的子组件,然后逐步向上,直到根组件。
受控组件和非受控组件
受控组件是指组件的值是由React的state来控制的,比如下面的input,其中关键一句就是value={this.state.value}
这样就把input的value和组件的state关联起来了。
1 | class NameForm extends React.Component { |
自定义hook
模拟componentDidMount
当我们为useEffect
的依赖传递空数组时,它只会在组件初始化时执行一次,这就相当于componentDidMount
。注意,这个只是粗略的模拟,因为useEffect
是异步执行,且时机是在浏览器渲染完成后,而componentDidMount
是在浏览器渲染前同步执行。
1 | import {useEffect} from "react"; |
模拟componentDidUpdate
componentDidUpdate是在每次组件的state或props更新是调用的,但是组件mount的时候不调用,所以我们需要一个变量来标志组件是否是第一次加载,这里我们使用useRef
来标志组件是否是第一次加载。因为useRef
包裹的值在整个组件声明周期内引用不变(有点像静态变量),并且这个值改变后不会触发组件的重新渲染。
1 | export const useUpdate = (callback, deps) => { |
需要注意的是,如果在React18+严格模式下运行该函数,还是会执行一次,因为React18+的严格模式会执行两次。
1 | createRoot(document.getElementById('root')).render( |
模拟componentWillUnmount
这个就比较简单了,componentWillUnmount
是在组件卸载时调用的,我们可以使用useEffect
的返回值来模拟这个生命周期。
1 | export const useUnmount = ((callback) => { |
css-interview
- How to implement responsive layout with CSS?
- What’s flex layout in CSS?
- 简述CSS中盒模型是什么?
- grid布局极其优点?
- 手写一个圣杯布局。
- 如何实现垂直居中?除了flex布局还有其他方案吗?
- float不是完全脱离文档流,这点和position: absolute有区别的。
- CSS中有哪些实现透明的方式?
- CSS中选择器有哪些?优先级是怎样的?
- 如何实现一个三角形?
- 如何实现一个圆形?
- 如何实现一个梯形?
angular-di-injection-context
Injection Context
- 顾名思义,注入上下文,说的通俗点,就是在Angular代码中,什么位置可以注入,比如我们最常用的constructor
就属于一个Injection Context
,因为你可以在constructor
中注入服务。
Angular支持的Injection Context
有如下几种:
- In class
constructor
- In the initializer for fields of such classes.
- In the factory function specified for useFactory of a Provider or an @Injectable
- In the factory function specified for an InjectionToken.
- Within a stack frame that runs in an injection context. - 这是个啥?我咋看不懂捏?
In class constructor
constructor
是我们最常用的注入位置,比如我们在组件中注入服务,就是在constructor
中注入的。
新的写法, 使用inject
函数
1 | export class AppComponent { |
旧的写法
1 | export class AppComponent { |
In the initializer for fields of such classes
这个是啥意思呢?就是在类的字段初始化器中,也可以注入服务,比如下面的DataService.
1 | export class AppComponent { |
Stack frame in an injection context
有些函数被设计成可以运行在injection context中,比如我们常用的路由守卫(router guard), 之所以这样是为了能让我们在路由守卫中注入服务。比如下面的canActivateTeam
函数,就是一个路由守卫。在这个函数里,我们可以注入PermissionsService
和UserToken
。这样就可以判断用户是否有权限访问某个页面。
1 | const canActivateTeam: CanActivateFn = |
Run within an injection context
有时候我们需要讲一个函数运行在injection context中,但是当前上下文并不是injection context
, 这时,我们可以使用runInInjectionContext
函数来创建一个新的injection context, 然后在这个新的injection context中运行我们的函数。
比如Angular框架要求effect
函数是必须运行在injection context
中,所以我们通常在构造函数体中运行effect
函数,如果我们想在ngOnInit
函数中运行effect
函数呢?因为ngOnInit
函数并不是injection context
, 这时我们就可以使用runInInjectionContext
函数来运行effect
函数。
注意:使用runInInjectionContext
函数需要一个EnvironmentInjector
1 | export class CountComponent implements OnInit { |
References
angular-signal
What’s Signal?
先看一下官方定义:
1 | A signal is a wrapper around a value that notifies interested consumers when that value changes. Signals can contain any value, from primitives to complex data structures. |
翻译一下:Signal是一个包装器,它包装了一个值,并在该值发生变化时通知感兴趣的消费者。Signal可以包含任何值,从基本类型到复杂的数据结构。据说signal借鉴了solid.js
的思想(看这里),但是我没有考证过。
Signal是Angular中重要的新特性,从xxx版开始引入,至xxx版本稳定,Signal的出现,彻底更改了Angular的变更检测机制,原本基于Zone.js
的变更检测机制被Signal
+ OnPush
取代,Signal的出现,使得Angular的性能得到了极大的提升。
今天,我们就来揭开Signal的神秘面纱。
Why Signal?
首先,我们来探讨一下Angular为什么要引入Signal?
在Signal之前,Angular是基于Zone.js做变更检测的,不可否认Zone.js是一个非常强大的库,但是它也有一些缺点,比如:
- 性能问题:Zone.js的性能并不是很好,特别是在大型项目中,Zone.js的性能问题会暴露的更加明显。除非你使用了
OnPush
策略,否则Zone.js会在每次异步操作后都会触发变更检测,这样会导致性能问题。 - 由于Zone.js要monkey patch所有的异步操作(在Angular app启动时),所以Angular项目在启动的时候会有一些性能损失。
- 有些异步操作无法monkey patch,比如
async/await
,Angular的解决办法是将其降级到Promise
。如今几乎所有浏览器都支持async/await
,这种做法显然不太合理。
基于以上原因,Angular Team很早就考虑移除Zone.js,但是Zone.js的变更检测机制是Angular的核心,移除Zone.js,意味着Angular无法自动进行变更检测,也就是说变更检测的触发机制由Angular通过Zone.js自动检测,变成了需要用户手动触发,而Signal就是为此而服务的。
通过Signal定义的数据,当它变化时,Angular会自动进行变更检测,这样就不需要Zone.js了,也就解决了上面提到的问题。
signal 语法
Signal语法非常简单,下面来看看如何定义和使用signal。
定义signal
1 | const count = signal(0); |
上面是一个简单的signal定义,它的初始值是0。如果要定义复杂的值,可以使用泛型:
1 | const user = signal<User>({name: 'zdd', age: 18}); // generic type |
读取signal的值
1 | // Signals are getter functions - calling them reads their value. |
设置signal的值
1 | count.set(1); |
更新signal的值(根据前一个值)
1 | // Signals can also be updated by passing a function that receives the current value and returns the new value. |
signal的分类
Signal有两种类型:Signal
和Compute Signal
。Signal
是指普通的signal,它是可读写的;Compute Signal
是指由其他signal计算得到的signal,它是只读的。
上面的count就是普通的Signal,可读写,下面我们看一个compute signal的例子。下面代码中,doubleCount是一个compute signal,它的值是由count的值乘以2得到的。
1 | const count = signal(0); |
Signal | Compute Signal | |
---|---|---|
读写性 | 可读写 | 只读 |
一个例子
我们以一个todo list为例,来看看Signal的使用。首先我们来定义List组件:
1 | // list-item.model.ts |
在上面的代码中,我们使用readonly listItems = signal<ListItem[]>([...]);
定义了一个signal, 它的初始值是一个包含两个ListItem的数组。我们还定义了一个addItem
方法,用来添加一个新的ListItem,以及一个removeItem
方法,用来删除一个ListItem。
signal与普通的js变量不同,它的读取和写入需要特殊的语法。
- 读取signal的值:
this.listItems()
- 注意不是this.listItems
,后面要加括号,有点像函数调用。 - 写入signal的值:
this.listItems.set(newValue)
1 | <!--list.component.html--> |
然后我们来定义ListItem组件:
1 | // list-item.model.ts |
Compute Signal
Compute Signal
是指依赖于其他Signal计算得到的Signal,它是只读的,当依赖的signal值变化时,compute signal的值也会相应变化。下面是一个例子:
1 | const count: WritableSignal<number> = signal(0); |
Computed signal
有两个重要特性:
- lazy load - 直到你第一次读取它的值时,它才会计算。
- memoization - 第一次读取后缓存值,当依赖的signal值没有变化时,它不会重新计算,而是直接读取缓存的值。当依赖的signal值变化时,重新计算并缓存。
比较函数
signal是如何比较新旧值的呢?signal使用Object.is
来比较新旧值,如果新旧值相等,那么signal不会触发变更检测。对于js对象来说,Object.is
相当于引用(地址)比较,也就是说只有两个对象引用相等时,Object.is
才会返回true。
1 | const a = [1, 2, 3]; |
创建signal时,你可以指定一个比较函数来改变默认的比较方式,比如使用lodash
的isEqual
函数来进行深比较,这样当两个对象的值相等时,signal也会触发变更检测。
1 | import _ from 'lodash'; |
References
angular-component-input
今天这篇我们讲解一下Angular中的Input,Input是Angular中的一个装饰器,它用来接收父组件传递过来的数据。
传统的@Input()写法
为了方便展示,我们定义两个组件, 一个父组件:ListComponent
, 一个子组件:ListItemComponent
。为了便于展示,我们将template和style都写在component.ts中。
1 | // list-item.component.ts |
在ListItemComponent
中,我们定义了两个属性:id
和name
,并且使用了@Input
装饰器。@Input
装饰器有一个可选的参数required
,如果设置为true
,则表示这个属性是必须的,如果使用组件时没有给该字段赋值,则会报错。
接下来我们在ListComponent
中使用ListItemComponent
组件,并传递数据。
1 | // list.component.ts |
@Input
定义的字段,需要通过property binding的方式传递数据,即[id]="item.id"
和[name]="item.name"
。
基于signal的input
写法
以上是旧版的写法,从Angular 17.1开始,我们可以使用新版的基于signal的input
语法了。
1 | import {Component, input} from '@angular/core'; // import 'input' |
由于这种类型的input的值对应的是一个signal, 所以读取值的时候,要加()
,id -> id()
, name -> name()
。
1 | template: ` |
default value
1 | value = input(0); // 0 is default value |
specify type
Note that, for simple values, typescript can infer the type by value, but for complex types, you need to specify the type.
1 | value = input<number>(0); // value = input(0); // totally ok |
required
1 | value = input.required<number>(); // required |
Input transform
1 | export class ListItemComponent { |
Built-in transform
booleanAttribute
- imitates the behavior of standard HTML boolean attributes, where the presence of the attribute indicates a “true” value. However, Angular’s booleanAttribute treats the literal string “false” as the boolean false.numberAttribute
- attempts to parse the given value to a number, producing NaN if parsing fails.
Input alias
1 | value = input(0, {alias: 'sliderValue'}); // aliasValue is the alias name |
在模板中使用sliderValue
, 注意,组件代码中仍然只能使用value
。
1 | <custom-slider [sliderValue]="50" /> |
References
angular-forms
Reactive Forms
1 | import {Component} from '@angular/core'; |