Introduction
Node.js中的事件循环和javascript中的事件循环是不同的,Node.js中的队列更多,算法更复杂,今天我们来一探究竟。
JavaScript的垃圾收集机制主要是为了自动管理内存,使得开发者不需要手动去分配和释放内存。这种机制主要依赖于一些算法来追踪和标记不再使用的对象,然后回收它们所占用的内存空间。以下是简述的两个主要机制:
引用计数(Reference Counting):
标记-清除(Mark-and-Sweep):
除了上述两种基本的垃圾收集机制外,现代JavaScript引擎还可能采用其他优化技术,比如分代收集(Generational GC),它基于大多数对象很快变得不可用的假设,将对象分为不同的“代”,并更频繁地检查较新的对象以提高效率。
总的来说,JavaScript的垃圾收集机制旨在让开发者不必担心内存管理的问题,尽管如此,了解这些机制有助于编写更高效的代码,避免潜在的内存泄漏问题。
居中是CSS中永恒的话题,而垂直居中则是这个永恒话题中的重中之重,无论是日常工作,还是面试,你永远都绕不开垂直居中,今天咱们来详细讲解一下垂直居中。我们不会简单的罗列css样式,而是按不同分类来处理,这样更有利于理解。
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; |
如果是对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元素是有效的。
以如下代码为例,我们需要将子元素child垂直居中于父元素parent。
1 | <div class="parent"> |
此时,又分为两种情况,一种是我们知道子元素的高度,另一种是我们不知道子元素的高度。这两种情况有不同的处理方式。
1 | .parent { |
大多数情况下,元素的高度是未知的,这时候可以使用使用transform: translateY(-50%);代替margin-top: -50px;
1 | <div class="parent"> |
1 | .parent { |
如果你不在乎子元素会被拉伸并填满父元素的话,可以使用table-cell来实现垂直居中。
1 | <div class="parent"> |
1 | .parent { |
这是目前来讲最方便的方式了,使用flex布局可以轻松实现水平和垂直居中。首先将父元素设置为flex布局,然后设置flex-direction: column;将布局方式改为纵向排列(默认是横向排列),然后设置justify-content: center;即可实现垂直居中。
1 | <div class="parent"> |
1 | .parent { |
React的渲染可以分为两个主要阶段:Reconciler(协调阶段),和Committer(提交阶段)。
工作内容: 构建Fiber树,比较新旧虚拟DOM的不同之处,生成一个变更记录,即一系列需要对真实DOM进行的操作。此阶段的特点:异步,并发,可中断。如果执行过程中有更高优先级的任务来了,那么会中断当前Reconciler的工作,转而处理更重要的任务。
工作内容:将Reconciler阶段生成的变更记录应用到真实的DOM上。此阶段是同步的,不可中断的。
工作阶段:
注意页面的渲染和JS的执行是互斥的,只有JS代码执行完,页面才能渲染,这就是useLayoutEffect的作用,可以在页面渲染前执行一些操作,比如调整布局。
Fiber架构中采用FiberNode和FiberTree来描述虚拟DOM树。FiberNode是一个双向链表,每个节点都有一个指向父节点的指针,一个指向子节点的指针,一个指向兄弟节点的指针。FiberTree是一个树形结构,由FiberNode组成。
只做同层级结点比较,如果结点不存在了,则直接删除。不会继续比较其子树。这避免了夸层级移动操作,对于跨层级移动操作,相当于删除再重建。同层级结点移动呢?可以处理。
只做同类型的组件比较,比如div和div比较,p和p比较,只有组件的类型相同,才进入子树进行深层次比较。如果类型不一致,则删除重新创建。
对于同层级的元素结点。
setState是同步还是异步?这个问题需要区分开来看,在React18之前,如果executionContext被赋值了,代表该任务已经进入React调度流程中,此时React会对该任务进行异步处理(批量处理),如果executionContext没有被赋值,代表该任务还没有进入React调度流程,此时React会对该任务进行同步处理。想setTimeout, setInterval等函数都是不会进入React调度流程的,所以是同步处理。而合成事件都会进入到React调度流程中,所以会被异步处理。在React18后,如果使用createRoot创建根节点,那么setState会变成同步的。但是如果还是使用传统的render方式,那么和React18之前的处理逻辑一样。
虚拟DOM是React的核心概念之一,它是一个轻量级的JavaScript对象,用来描述真实DOM的层次结构。React通过比较新旧虚拟DOM的差异,然后只更新需要更新的部分,从而提高页面的渲染性能。
虚拟DOM的量大作用:
当React开始渲染过程时,它首先从上到下、从父到子地渲染组件树。这意味着父组件会先于其子组件进行渲染。一旦父组件完成它的渲染(包括执行相关的生命周期方法),React会递归地进入该父组件的子组件,并按照同样的方式渲染它们。
当组件被卸载时,React会以相反的顺序卸载组件树中的组件。也就是说,React首先卸载最深层的子组件,然后逐步向上,直到根组件。
受控组件是指组件的值是由React的state来控制的,比如下面的input,其中关键一句就是value={this.state.value} 这样就把input的value和组件的state关联起来了。
1 | class NameForm extends React.Component { |
componentDidMount当我们为useEffect的依赖传递空数组时,它只会在组件初始化时执行一次,这就相当于componentDidMount。注意,这个只是粗略的模拟,因为useEffect是异步执行,且时机是在浏览器渲染完成后,而componentDidMount是在浏览器渲染前同步执行。
1 | import {useEffect} from "react"; |
componentDidUpdatecomponentDidUpdate是在每次组件的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) => { |
Injection Context - 顾名思义,注入上下文,说的通俗点,就是在Angular代码中,什么位置可以注入,比如我们最常用的constructor就属于一个Injection Context,因为你可以在constructor中注入服务。
Angular支持的Injection Context有如下几种:
constructorconstructorconstructor是我们最常用的注入位置,比如我们在组件中注入服务,就是在constructor中注入的。
新的写法, 使用inject函数
1 | export class AppComponent { |
旧的写法
1 | export class AppComponent { |
这个是啥意思呢?就是在类的字段初始化器中,也可以注入服务,比如下面的DataService.
1 | export class AppComponent { |
有些函数被设计成可以运行在injection context中,比如我们常用的路由守卫(router guard), 之所以这样是为了能让我们在路由守卫中注入服务。比如下面的canActivateTeam函数,就是一个路由守卫。在这个函数里,我们可以注入PermissionsService和UserToken。这样就可以判断用户是否有权限访问某个页面。
1 | const canActivateTeam: CanActivateFn = |
有时候我们需要讲一个函数运行在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 { |
先看一下官方定义:
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的神秘面纱。
首先,我们来探讨一下Angular为什么要引入Signal?
在Signal之前,Angular是基于Zone.js做变更检测的,不可否认Zone.js是一个非常强大的库,但是它也有一些缺点,比如:
OnPush策略,否则Zone.js会在每次异步操作后都会触发变更检测,这样会导致性能问题。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。
1 | const count = signal(0); |
上面是一个简单的signal定义,它的初始值是0。如果要定义复杂的值,可以使用泛型:
1 | const user = signal<User>({name: 'zdd', age: 18}); // generic type |
1 | // Signals are getter functions - calling them reads their value. |
1 | count.set(1); |
1 | // Signals can also be updated by passing a function that receives the current value and returns the new value. |
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变量不同,它的读取和写入需要特殊的语法。
this.listItems() - 注意不是this.listItems,后面要加括号,有点像函数调用。this.listItems.set(newValue)1 | <!--list.component.html--> |
然后我们来定义ListItem组件:
1 | // list-item.model.ts |
Compute Signal是指依赖于其他Signal计算得到的Signal,它是只读的,当依赖的signal值变化时,compute signal的值也会相应变化。下面是一个例子:
1 | const count: WritableSignal<number> = signal(0); |
Computed 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'; |
今天这篇我们讲解一下Angular中的Input,Input是Angular中的一个装饰器,它用来接收父组件传递过来的数据。
为了方便展示,我们定义两个组件, 一个父组件: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"。
input写法以上是旧版的写法,从Angular 17.1开始,我们可以使用新版的基于signal的input语法了。
1 | import {Component, input} from '@angular/core'; // import 'input' |
由于这种类型的input的值对应的是一个signal, 所以读取值的时候,要加(),id -> id(), name -> name()。
1 | template: ` |
1 | value = input(0); // 0 is default value |
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 |
1 | value = input.required<number>(); // required |
1 | export class ListItemComponent { |
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.1 | value = input(0, {alias: 'sliderValue'}); // aliasValue is the alias name |
在模板中使用sliderValue, 注意,组件代码中仍然只能使用value。
1 | <custom-slider [sliderValue]="50" /> |
1 | import {Component} from '@angular/core'; |
今天这篇主要讨论一下Angular框架如何处理样式。
因为Angular是组件话的,每一个Component有自己的样式文件,那么Angular是如何保证多个组件之间的样式不会互相影响的呢?
Angular中有三种样式封装方式:
Emulated:默认的样式封装方式,通过给每个组件的样式添加一个唯一的属性,来实现样式的隔离。ShadowDom:使用原生的Shadow DOM来实现样式的隔离。None:不对样式进行封装,直接使用全局样式。