前端面试常用一句话原理

196 阅读42分钟

打包

Vite 的原理是什么

Vite 的核心原理是浏览器原生支持的 ESM 特性来实现快速开发和构建。有如下特点:

  • 快速的冷启动: No Bundle + esbuild 预构建
  • 即时的模块热更新: ,同时利用浏览器缓存策略提升速度
  • 真正的按需加载: 利用浏览器ESM支持,实现真正的按需加载

Vite HMR

主要是通过WebSocket创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。

  • 基于 ESM 的 import,浏览器会发出请求,Vite劫持浏览器的HTTP请求,在后端进行相应的处理将项目中使用的文件通过简单的分解与整合(比如编译 TypeScript、解析 Vue 单文件组件等),最后将处理后的结果返回给浏览器。

Vite 的生产态

Vite 的插件

VS Webpack

  • Webpack: 重新编译,请求变更后模块的代码,客户端重新加载

Vite 没有打包的过程,三方库使用了预编译,Webpack是先解析依赖、打包构建再启动开发服务器,Dev Server 必须等待所有模块构建完成,当我们修改了 bundle模块中的一个子模块, 整个 bundle 文件都会重新打包然后输出。项目应用越大,启动时间越长。

Vite: 请求变更的模块,再重新加载

ESM 是什么?

ESM 是 JavaScript 提出的官方标准化模块系统。ESM 编译的可以分为三个步骤:

  • 下载:编译 ajs,遇到 a.js 中的 import 语句,浏览器会发出 HTTP 请求来下载整个 b.js
  • 解析:浏览器解析 bjs ,识别所有 export 模块,解析为模块记录
  • 链接:将模块记录转换为一个模块实例,为所有的模块分配内存空间,依照导出、导入语句把模块指向对应的内存地址。 上面在编译阶段就完成了 ESM 的依赖关系,而不是在运行时。 下面为运行时阶段
  • 运行:运行代码,将内存空间填充。 bjs 整个文件参与编译和链接,但是 ajs 只会时机运行 bjs 导入的部分。

ESM 的优点是:

  • 实时绑定的模式,导出和导入的模块都指向相同的内存地址。而CJS采用的是值拷贝,即所有导出值都是拷贝值。
  • 由于 esm 模块间明确的静态关系,很多工具支持 tree shaking 来移除没有使用的代码。import 和 export 是静态的结构的,不能放在 if 中。
  • 非运行时加载:
    • import() 动态引入是一个例外,它是在运行时执行的,并且会动态地下载、编译和执行指定的模块。

CommonJS 是什么?

  • 运行时加载:模块和依赖时代码运行时同步加载的。
  • 同步加载:ComonJS 通常用于服务端,模块是从本地文件系统同步加载完成的,因为服务端这种加载时瞬间完成的。

但是浏览器环境的原生并不支持 ComonJS 规范,require 和 module.require 是未知的,用浏览器可执行的代码替换规范语法,

ESM 和 COMMONJS 解决循环渲染的不同

//index.js
var a = require('./a')
console.log('入口模块引用a模块:',a)

// a.js
exports.a = '原始值-a模块内变量'
var b = require('./b')
console.log('a模块引用b模块:',b)
exports.a = '修改值-a模块内变量'

// b.js
exports.b ='原始值-b模块内变量'
var a = require('./a')
console.log('b模块引用a模块',a)
exports.b = '修改值-b模块内变量'

commonjs

image.png

这种AB模块间的互相引用,本应是个死循环,但是实际并没有,因为CommonJS做了特殊处理——模块缓存。
依旧使用断点调试,可以看到变量require上有一个属性cache,这就是模块缓存

es module

image.png 值得一提的是,import语句有提升的效果,实际执行可以看作这样:

// index.mjs
import * as a from './a.mjs'
console.log('入口模块引用a模块:',a)

// a.mjs
import * as b from "./b.mjs"
let a = "原始值-a模块内变量"
export { a }
console.log("a模块引用b模块:", b)
a = "修改值-a模块内变量"

// b.mjs
import * as a from "./a.mjs"
let b = "原始值-b模块内变量"
export { b }
console.log("b模块引用a模块:", a)
b = "修改值-b模块内变量"

可以看到,在b模块中引用a模块时,得到的值是uninitialized,接下来一步步分析代码的执行。 在代码执行前,首先要进行预处理,这一步会根据import和export来构建模块地图(Module Map),它类似于一颗树,树中的每一个“节点”就是一个模块记录,这个记录上会标注导出变量的内存地址,将导入的变量和导出的变量连接,即把他们指向同一块内存地址。不过此时这些内存都是空的,也就是看到的uninitialized。

接下来就是代码的一行行执行,import和export语句都是只能放在代码的顶层,也就是说不能写在函数或者if代码块中。

【入口模块】首先进入入口模块,在模块地图中把入口模块的模块记录标记为“获取中”(Fetching),表示已经进入,但没执行完毕,

import * as a from ‘./a.mjs’ 执行,进入a模块,此时模块地图中a的模块记录标记为“获取中”

【a模块】import * as b from ‘./b.mjs’ 执行,进入b模块,此时模块地图中b的模块记录标记为“获取中”,

【b模块】import * as a from ‘./a.mjs’ 执行,检查模块地图,模块a已经是Fetching态,不再进去,

let b = ‘原始值-b模块内变量’ 模块记录中,存储b的内存块初始化,

console.log(‘b模块引用a模块:’, a) 根据模块记录到指向的内存中取值,是{ a:}

b = ‘修改值-b模块内变量’ 模块记录中,存储b的内存块值修改

【a模块】let a = ‘原始值-a模块内变量’ 模块记录中,存储a的内存块初始化,

console.log(‘a模块引用b模块:’, b) 根据模块记录到指向的内存中取值,是{ b: ‘修改值-b模块内变量’ }

a = ‘修改值-a模块内变量’ 模块记录中,存储a的内存块值修改

【入口模块】console.log(‘入口模块引用a模块:’,a) 根据模块记录,到指向的内存中取值,是{ a: ‘修改值-a模块内变量’ }

ES Module来处理循环使用一张模块间的依赖地图来解决死循环问题,标记进入过的模块为“获取中”,所以循环引用时不会再次进入;使用模块记录,标注要去哪块内存中取值,将导入导出做连接,解决了要输出什么值。

Webpack 原理是什么?

Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。它主要用于处理 JavaScript 文件,但也能够转换、打包 HTML、CSS、图片以及其他资源。Webpack 的原理涉及以下几个关键概念:

  1. 入口(Entry) : 在 Webpack 中,入口是指项目中的起始点,Webpack 从入口开始分析和构建项目的依赖关系图。一个 Webpack 应用通常有多个入口,每个入口对应一个 JavaScript 文件。
  2. 依赖图(Dependency Graph) : Webpack 根据入口文件,递归地构建出整个项目的依赖关系图。它分析模块之间的依赖关系,包括 JavaScript 文件之间的 import、require 关系,以及其他资源(如 CSS、图片等)之间的引用关系。
  3. 加载器(Loaders) : Webpack 的加载器用于对项目中的不同类型的文件进行预处理,例如将 ES6/ES7 代码转换为 ES5、将 Sass/LESS 转换为 CSS 等。加载器允许你在导入文件时进行转换,从而扩展了 Webpack 对不同类型文件的支持。
  4. 插件(Plugins) : Webpack 的插件是用于扩展其功能的工具。插件可以处理更多的任务,例如代码压缩、拆分代码、资源优化等。插件能够在 Webpack 构建流程的不同阶段执行自定义的操作,从而更灵活地满足项目的需求。
  5. 输出(Output) : 输出是指 Webpack 打包后生成的文件。通过配置输出选项,你可以指定 Webpack 输出的文件名、路径、格式等。通常,Webpack 将所有的模块打包成一个或多个 bundle 文件,可以通过 script 标签在 HTML 中引入这些文件。
  6. 模式(Mode) : Webpack 提供了不同的构建模式,包括开发模式和生产模式。开发模式会启用一些辅助工具,如更加详细的错误信息和更快的构建速度;生产模式则会对代码进行优化,以提高性能并减小文件大小。

基于以上原理,Webpack 能够将多个模块及其依赖打包成一个或多个静态资源文件,使得前端开发更加高效和便捷。

Webpack 插件有哪些生命周期

Webpack 的插件(Plugin)是用来扩展 Webpack 功能的机制,它们可以在 Webpack 构建流程的各个阶段插入自定义逻辑。常用的插件钩子函数包括:

  1. apply: 这是插件的基本方法,在使用插件时会被调用。可以在这个方法内部访问 Webpack 的 Compiler 对象,然后通过 Compiler 对象的钩子函数实现对构建过程的干预。
  2. entryOption: 在配置 entry 选项之前调用,允许修改入口的配置。
  3. beforeRun: 在开始读取记录前执行。
  4. run: 在读取记录后、编译前执行。
  5. compile: 编译器开始编译文件时触发。
  6. compilation: 在编译创建的新 compilation 对象之前执行。
  7. emit: 在生成资源并将其输出到目录之前执行,可以在这个时候获取所有的输出资源并对它们进行处理。
  8. afterEmit: 在生成资源并将其输出到目录之后执行。
  9. done: 完成编译后触发,可以在这个时候执行一些额外的操作,比如输出构建统计信息。
  10. failed: 如果编译过程遇到错误,会触发这个钩子。
  11. invalid: 当监听模式下,一个文件发生改变,重新构建前触发。
  12. watchRun: 监听模式下,在编译之前触发。

以上是一些常用的插件钩子函数,开发者可以根据需要选择适合的钩子函数来扩展 Webpack 的功能。

Rollup 和 Webpack 的区别是什么?

Rollup也是一款ESModule打包器,可以将项目中的细小模块打包成整块代码,使得划分的模块可以更好的运行在浏览器环境或者是Nodejs环境。Rollup与Webpack作用非常类似,不过Rollup更为小巧。webpack结合插件可以完成前端工程化的绝大多数工作,而Rollup仅仅是一款ESM打包器,没有其他功能,例如Rollup中并不支持类似HMR这种高级特性。Rollup并不是要与Webpack全面竞争,而是提供一个充分利用ESM各项特性的高效打包器。

Rollup 优势

  • 输出结果更加扁平,执行效率更高
  • 自动移除未引用的代码
  • 打包结果依然完全可读

Rollup 缺点

  • 加载非ESM的第三方模块比较复杂
  • 模块最终都会被打包到一个函数中,无法实现HMR
  • 浏览器环境中,代码拆分功能依赖AMD库

TreeShaking 的区别

问的是webpack和rollup都用到了tree-shaking,但rollup的打包体积更小,为什么? 尝试解答如下,请老师帮忙看下: 首先webpack打包后会生成__webpack_require__等runtime代码,这就会占用一定的体积。而rollup不存在这个问题。 Tree-shaking的原理是通过ES Module的方式进行静态分析,同时对程序流进行分析,判断变量是否被使用。之后就是比对二者Tree-Shaking的优劣:二者都是通过ES Module的方法进行静态分析,但rollup有程序流分析的功能,可以更好地判断代码是否有副作用,从而进行更彻底的Tree-shaking;而webpack是在production模式中通过uglifyJS进行Tree-Shaking,对于程序流的分析不完善,所以依然会有无法删除的代码。 能不能针对这段,再细化一下,举例叫我一下

function doSomething(condition) {
  if (condition) {
    return function() {
      console.log("Condition is true");
    };
  } else {
    return function() {
      console.log("Condition is false");
    };
  }
}

const func = doSomething(false);
func();

在这个例子中,doSomething 函数根据传入的 condition 参数返回不同的函数。如果 conditionfalse,那么返回的函数永远不会被调用。

Rollup 在进行静态分析时,会尝试理解整个代码的逻辑结构,并且尽可能地分析代码的执行流程。在这个例子中,Rollup 能够通过静态分析确定 condition 的值是 false,因此能够知道返回的函数永远不会被调用。基于这一信息,Rollup 在打包时会移除掉整个 doSomething 函数,因为它知道在这种情况下这个函数是没有用到的。

因此,Rollup 在进行 Tree Shaking 时能够更加深入地分析代码的程序流,从而更准确地识别和移除无用代码,相比之下,它在这方面的表现要优于 Webpack。

image.png

与 Webpack 不同的是,Rollup 不仅仅针对模块进行依赖分析,它的分析流程如下:

  1. 从入口文件开始,组织依赖关系,并按文件生成 Module
  2. 生成抽象语法树(Acorn),建立语句间的关联关系
  3. 为每个节点打标,标记是否被使用
  4. 生成代码(MagicString+ position)去除无用代码

ESBuild 为什么快?

  • 编译运行 VS 解释运行,Esbuild 则选择使用 Go 语言编写
  • 多线程 VS 单线程,GO天生的多线程优势。
  • 对构建流程进行了优化,充分利用 CPU 资源

计算机基础

什么是编译型语言,什么是解释型语言?

  1. 编译型语言:

    • 编译型语言经过编译器的处理,将源代码转换成机器语言或者字节码等被直接执行的形式。
    • 整个源代码文件编译成目标文件(通常是二进制文件),然后链接成可执行文件。
    • 会生成与特定平台相关的可执行文件,因此在不同平台上需要重新编译和链接才能执行。
  2. 解释型语言:

    • 不需要预先编译成目标代码,而是通过解释器逐行解释执行源代码。
    • 会逐行读取源代码,并将其转换成计算机可以执行的机器语言或者中间代码。
    • 执行过程是动态的,不需要生成可执行文件,可以在任何支持相应解释器的平台上直接执行。
    • 解释型语言的执行速度通常比编译型语言慢,因为它在运行时需要逐行解释源代码。

机器码是直接由硬件执行的二进制代码,而字节码是一种中间形式的代码,需要由解释器或虚拟机进行解释执行。字节码的使用使得程序具有了更好的跨平台性和灵活性

V8

V8 是一款高性能的 JavaScript 引擎,它并不是严格意义上的解释器,而是一个即时编译器。 即时编译器是一种将源代码动态编译成机器码的编译器,而不是简单地逐行解释执行源代码。V8 在执行 JavaScript 代码时,会将 JavaScript 代码先解析成抽象语法树(AST),然后通过解释器将 AST 转换成字节码。接着,V8 使用即时编译器将字节码编译成本地机器码,最终由 CPU 执行。 V8 提升 JavaScript 性能的主要方式有以下几个方面:

  1. 即时编译(JIT Compilation):V8 使用即时编译器将 JavaScript 代码直接编译成本地机器码,从而避免了逐行解释执行带来的性能损失。
  2. 内联缓存(Inline Cache):V8 使用内联缓存技术来提高属性访问的性能。它会将对象的属性访问进行缓存,避免了每次访问都要进行属性查找的开销。
  3. 内存优化:V8 对内存的管理进行了优化,包括对象的分配、垃圾回收等方面,提高了内存的使用效率。
  4. 隐藏类和内联缓存:V8 使用隐藏类来跟踪对象的属性和方法,以提高访问速度。此外,V8 还使用内联缓存来加速对对象属性和方法的访问。
  5. 热点代码优化:V8 会监视 JavaScript 代码的执行情况,并根据代码的执行频率进行优化,将热点代码编译成高效的机器码,提高了性能。

举个简单的例子,假设有一个包含加法运算的循环:

for (let i = 0; i < 1000000; i++) {
    let result = i + 1;
}

如果使用解释器执行这段代码,每次循环时都会解释执行 i + 1 这个加法运算。即,解释器会逐行解释这个代码,每次执行时都需要重新解释一次加法运算。这会导致每次循环的执行速度相对较慢。

而当使用即时编译器执行同样的代码时,即时编译器会在程序开始执行时将整个循环体编译成机器码或者中间代码。这样,在循环执行过程中,不需要重新解释加法运算,而是直接执行编译后的机器码或中间代码。因此,执行速度会更快。

浮点数原理?

JS 使用了 IEEE二进制浮点

分别是1个符号位+11个指数位+52个尾数位 image.png

各家计算机公司的各个型号的计算机,有着千差万别的浮点数表示,却没有一个业界通用的标准。这给数据交换、计算机协同工作造成了极大不便。IEEE的浮点数专业小组于七十年代末期开始酝酿浮点数的标准。

在这种存储方式下,由于尾数的位数是有限的,因此浮点数并不能精确地表示所有的十进制数。例如,0.1 在二进制浮点表示中无法精确表示,会有一定的误差。这就是为什么在 JavaScript 中执行 0.1 + 0.2 不等于 0.3,而是一个非常接近 0.3 的值。这种误差被称为浮点数精度问题。

计算机硬件中通常会包含专门的浮点运算器(Floating Point Unit,FPU),用于执行浮点数的加法、减法、乘法和除法等运算。这些硬件单元通常具有高度优化的电路结构,以便于快速执行浮点数运算,并支持浮点数的并行计算和向量化处理。

image.png

进程和线程是什么?

进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元

进程是操作系统中的一个独立的执行实例,它包含了程序代码、虚拟内存资源、寄存器资源以及文件描述符表。每个进程都是相对独立的,有自己的内存空间,不会直接共享变量。进程具有自己的资源,包括内存、文件描述符等,这些资源是在进程创建时分配的,进程之间不能直接访问对方的资源。进程间的通信相对复杂,需要通过进程间通信(IPC)机制来实现,比如管道、消息队列、共享内存等。进程切换的开销较大,涉及保存和恢复整个进程的状态。

Linux内核是没有线程这个概念的,只有内核调度实体,线程是进程中的一个执行单元,一个进程可以包含多个线程,它们共享相同的代码和数据段,但拥有独立的栈空间和寄存器。线程是轻量级的,相对于进程而言更容易创建、销毁和切换。线程共享进程的资源,包括内存、文件描述符等,但每个线程有自己的栈空间和寄存器。线程之间通信相对简单,因为它们共享相同的地址空间,可以通过共享变量直接进行通信。线程切换的开销较小,因为只需要保存和恢复线程的运行时数据,不涉及整个进程的状态。

总的来说,进程是操作系统分配资源的最小单元,而线程是操作系统调度的最小单元。进程提供了更高的隔离性和安全性,但是线程的创建和切换更加轻量级,可以提供更高的并发性能。

RPC

RPC(Remote Procedure Call)是一种用于实现分布式系统中进程间通信的技术。它允许在不同的计算机或进程间通过网络调用远程的函数或方法,就像调用本地函数一样。

RPC和直接使用HTTP服务进行通信相比,有一些区别,其中性能是其中一个因素,但不是唯一的原因。以下是RPC和直接HTTP服务之间的一些主要区别:

  1. 性能:
    • RPC通常被设计为更轻量、更高效的协议,因为它们致力于减少通信的开销。通常情况下,RPC的序列化和反序列化过程更为紧凑和高效。
    • 一些RPC框架还可以使用二进制协议(如Protocol Buffers或MessagePack),相较于文本协议(如JSON)在数据传输和解析上更加高效。
    • gRPC,一个基于Protocol Buffers的RPC框架,以其高性能和低开销而闻名。
  1. 协议支持:
    • 直接使用HTTP服务通常使用RESTful API,而RPC框架可以使用不同的协议,包括但不限于HTTP/2、Protocol Buffers、Thrift等。
    • 一些RPC框架支持双向流式通信,使得客户端和服务器之间可以更灵活地进行交互。
  1. 灵活性和功能性:
    • RPC框架通常提供更丰富的功能,如服务发现、负载均衡、中间件支持等,这些功能可以简化分布式系统的构建和管理。
    • RPC框架支持多种语言,使得不同语言的服务能够无缝地进行通信。

IPC

IPC(Inter-Process Communication,进程间通信)是指在操作系统中,不同进程之间进行数据交换、共享资源或通信的机制。 常见的IPC机制

  • 管道(Pipe) :一种半双工的通信方式,用于在父进程和子进程之间进行通信。
  • 消息队列(Message Queue) :允许一个或多个进程通过消息进行通信,消息可以是结构化的数据,由操作系统进行管理。
  • 信号(Signal) :用于在进程之间发送简单的通知或中断信息。
  • 共享内存(Shared Memory) :允许多个进程共享同一块内存区域,从而实现高效的数据交换。
  • 信号量(Semaphore) :用于控制多个进程对共享资源的访问,防止竞争条件的发生。
  • 套接字(Socket) :一种在网络编程中常用的IPC方式,允许不同主机上的进程进行通信。

框架

React 的整体原理

Fiber 是什么?

  • v16 前,React 的渲染流程是 JSX -> render function -> VDOM
  • v16 后,React 的渲染流程变为 JSX -> render function -> VDOM -> Fiber。Fiber 是一个链表树的结构。

React Concurrent Mode 是什么?

React 15 之前是同步递归渲染模式,React 16.17 默认是 Fiber 架构下的同步遍历渲染模式 React 18 使用并发特性的 API 支持的开启并发更新 时间分片机制是并发模型的核心,原理是基于 MessageChannel 等宏任务 API 来推迟 Fiber 处理逻辑到下一轮事件循环,从而释放用户交互并实现渲染恢复。中断机制也是类似的原理。

  • Scheduler(调度器):调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器):负责找出变化的组件,更新工作从递归变成了可中断的循环过程
  • Renderer(渲染器):负责将变化的组件渲染到页面上

调度器和时间分片机制的工作流程总结如下:

  • 用户初始操作触发调度器开始工作,假设任务执行过程中存在并发特性API,就会进行处理
  • 在当前渲染更新结束后,时间分片机制开启,之后就会触发宏任务,每一次宏任务都会触发Fiber工作单元的同步遍历处理逻辑
  • Fiber工作单元遍历处理过程中就会统计执行时长,一旦超过时长就会停止处理,继而触发下一次宏任务,异步API保证了控制权的释放以及后续恢复渲染
  • 支持渲染完所有时间分片的任务,之后就是非并发API下其他工作单元的同步遍历渲染的逻辑

为什么 react 要设计合成事件

react使用合成事件主要有三个目的:1、进行浏览器兼容,实现更好的跨平台;react提供的合成事件可用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。2、避免垃圾回收;react事件对象不会被释放掉,而是存放进一个数组中,当事件触发,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收)。3、方便事件统一管理和事务机制。

react 事件代理为什么会改变

  1. 可以在jquery里使用react不会有event.stopPropagation()失效的问题,可以同时存在多个react版本,对于想在旧项目里尝试新版本react的开发者来说应该是一个福音;
  2. 比较容易实现事件重放(replaying events)。

事件委托的节点从 React16 的 document 更改为 React17 的 React 树的根 DOM 容器。

在 React16 中,对 document 的事件委托都委托在冒泡阶段,当事件冒泡到 document 之后触发绑定的回调函数,在回调函数中重新模拟一次 捕获-冒泡 的行为,所以 React 事件中的e.stopPropagation()无法阻止原生事件的捕获和冒泡,因为原生事件的捕获和冒泡已经执行完了。

image.png

在 React 16 或更早版本中,React 会由于事件委托对大多数事件执行 document.addEventListener()。但是一旦你想要局部使用 React,那么 React 中的事件会影响全局,如下面这个例子,当把 React 和 jQuery 一起使用,那么当点击 input 的时候,document 上和 React 不相关的事件也会被触发,这符合 React 的预期,但是并不符合用户的预期。

在 React17 中,对 React 应用根 DOM 容器的事件委托分别在捕获阶段和冒泡阶段。即:

  • 当根容器接收到捕获事件时,先触发一次 React 事件的捕获阶段,然后再执行原生事件的捕获传播。所以 React 事件的捕获阶段调用e.stopPropagation()阻止原生事件的传播。
  • 当根容器接受到冒泡事件时,会触发一次 React 事件的冒泡阶段,此时原生事件的冒泡传播已经传播到根了,所以 React 事件的冒泡阶段调用e.stopPropagation()不能阻止原生事件向根容器的传播,但是能阻止根容器到页面顶层的传播。

recoil 对性能有哪些帮助

当涉及到状态管理时,Recoil 提供了一些性能优势。下面是针对每个优势的具体例子:

  1. 精细的粒度控制:
jsxCopy code
import { atom, useRecoilState } from 'recoil';

// 定义两个原子状态
const firstNameState = atom({
  key: 'firstNameState',
  default: ''
});

const lastNameState = atom({
  key: 'lastNameState',
  default: ''
});

// 一个组件只关注 firstNameState
function FirstNameInput() {
  const [firstName, setFirstName] = useRecoilState(firstNameState);

  return (
    <input 
      type="text" 
      value={firstName} 
      onChange={(e) => setFirstName(e.target.value)} 
      placeholder="First Name" 
    />
  );
}

// 另一个组件只关注 lastNameState
function LastNameInput() {
  const [lastName, setLastName] = useRecoilState(lastNameState);

  return (
    <input 
      type="text" 
      value={lastName} 
      onChange={(e) => setLastName(e.target.value)} 
      placeholder="Last Name" 
    />
  );
}

在上面的例子中,FirstNameInput 组件和 LastNameInput 组件分别关注于不同的原子状态。这使得每个组件只更新它们自己关心的状态,而不会因为其他状态的变化而触发不必要的重新渲染。

  1. 惰性更新:
jsxCopy code
import { atom, useRecoilState, RecoilRoot } from 'recoil';

const countState = atom({
  key: 'countState',
  default: 0
});

function Counter() {
  const [count, setCount] = useRecoilState(countState);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <RecoilRoot>
      <Counter />
    </RecoilRoot>
  );
}

在上面的例子中,Counter 组件只在点击按钮时才更新状态,这是因为 Recoil 使用了惰性更新的策略,只有在状态真正发生变化时才会触发重新渲染。

  1. 数据流优化:
jsxCopy code
import { atom, selector, useRecoilValue } from 'recoil';

const firstNameState = atom({
  key: 'firstNameState',
  default: ''
});

const lastNameState = atom({
  key: 'lastNameState',
  default: ''
});

// Selector 从两个原子状态派生
const fullNameSelector = selector({
  key: 'fullNameSelector',
  get: ({get}) => {
    const firstName = get(firstNameState);
    const lastName = get(lastNameState);
    return `${firstName} ${lastName}`;
  }
});

function FullNameDisplay() {
  const fullName = useRecoilValue(fullNameSelector);

  return <p>Full Name: {fullName}</p>;
}

在上面的例子中,FullNameDisplay 组件只会在 firstNameStatelastNameState 发生变化时才重新计算,并更新自己的显示。Recoil 使用了数据流优化算法,确保只有相关的状态变化时才会重新渲染组件。

  1. 并发模式支持:
jsxCopy code
import { atom, useRecoilState } from 'recoil';

const countState = atom({
  key: 'countState',
  default: 0
});

function Counter() {
  const [count, setCount] = useRecoilState(countState);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <React.StrictMode>
      <Counter />
    </React.StrictMode>
  );
}

在上面的例子中,App 组件包裹了 Counter 组件,并启用了 React 的严格模式(StrictMode)。Recoil 对 React 的并发模式提供了支持,可以正确地处理并发渲染,保证状态更新的一致性和可靠性。

这些例子展示了 Recoil 如何通过其特性来提高性能,包括精细的粒度控制、惰性更新、数据流优化和对并发模式的支持。

为什么 React 实现了自己的事件机制

  • 将事件都代理到了根节点上,减少了事件监听器的创建,节省了内存。

  • 磨平浏览器差异,开发者无需兼容多种浏览器写法。如想阻止事件传播时需要编写event.stopPropagation() 或 event.cancelBubble = true,在 React 中只需编写event.stopPropagation()即可。

  • 对开发者友好。只需在对应的节点上编写如onClickonClickCapture等代码即可完成click事件在该节点上冒泡节点、捕获阶段的监听,统一了写法。

  1. 浏览器兼容,统一行为,比如事件对象有统一的属性和方法,又比如,移除不想要的点击事件(Firefox右键点击会生成点击事件),再比如无论注册onMouseLeave还是onMouseOut都会映射成原生的mouseout事件;
  2. 多平台适配,ReactNative也能使用;
  3. 实现事件委托,避免大量创建事件监听;

在 React 类组件中,为什么修改状态要使用 setState 而不是用 this.state.xxx = xx

在 React 中,直接修改 this.state.xxx 的值来更新状态并不是响应式的,这是因为 React 状态更新的异步性质和状态合并操作的特性。直接修改 this.state 可能会导致组件的状态更新出现问题,因为 setState 是唯一能够保证状态更新正确应用的方法。

当你调用 this.setState 时,React 会合并你提供的对象到当前状态中,而不是替换掉 this.state。因此,如果你直接修改 this.state.xxx,你可能会丢失那次状态更新,因为这种修改不会被 React 的状态合并机制捕捉到。

此外,setState 方法是异步的,也就是说,直接在调用 setState 后立即通过 this.state 来获取新状态是不可靠的。setState 会把状态的更新排入队列,并且可能会批量更新,因此实际的状态值不会立即改变,直到状态更新流程结束。

总结:为了保证状态更新能够被响应式地处理,React 推荐使用 setState 方法来修改状态。直接修改 this.state 可能会导致状态不一致和其他问题。

class component 的生命周期函数和 function component hook 的对应关系是什么

以下是 React 类组件的生命周期函数与 React 函数组件的 Hook 的对应关系:

  1. componentDidMount() <=> useEffect() with empty dependency array

    • componentDidMount() 在组件挂载后调用,适合执行一次性的初始化操作。对应的函数组件写法是使用 useEffect(),并传递一个空的依赖数组,确保回调函数只在组件挂载后执行一次。
  2. componentDidUpdate() <=> useEffect() with dependency array

    • componentDidUpdate() 在组件更新后调用,可以在此执行与前一次渲染相关的操作。对应的函数组件写法是使用 useEffect(),并传递一个包含所依赖的状态或 props 的依赖数组,以确保只在相关依赖更新时执行回调函数。
  3. componentWillUnmount() <=> useEffect() with cleanup function

    • componentWillUnmount() 在组件即将卸载时调用,适合执行清理操作,如取消订阅、清除定时器等。对应的函数组件写法是使用 useEffect(),并在回调函数中返回一个清理函数,在组件即将卸载时执行清理操作。
  4. componentDidCatch() <=> useErrorBoundary()

    • componentDidCatch() 用于捕获组件树中的错误。对应的函数组件写法是使用错误边界(Error Boundary),可以通过自定义的错误边界组件来捕获组件树中的错误,并进行处理。
  5. getDerivedStateFromProps() <=> useReducer()

    • getDerivedStateFromProps() 用于根据 props 更新 state。对应的函数组件写法可以使用 useReducer() 钩子来管理组件的状态,使用 reducer 来处理 state 的更新逻辑。
  6. shouldComponentUpdate() <=> useMemo()

    • shouldComponentUpdate() 用于优化性能,决定是否需要重新渲染组件。对应的函数组件写法是使用 useMemo() 来 memoize 值,避免在不必要的情况下触发组件重新渲染。

以上是常见的生命周期函数与对应的 Hook 关系。但需要注意的是,并非所有的生命周期函数都有直接对应的 Hook,因为在函数组件中,一些生命周期函数的功能已经被合并或者重组到了不同的 Hook 中。

setState 之后发生了什么

当调用 setState() 后,React 会对状态更新进行处理,其中涉及到 Reconciler 和 Fiber 架构:

  1. 更新状态: setState() 负责更新组件的状态。这个状态更新被放置在 React 内部的数据结构中,具体来说,React 使用 fiber 节点来表示组件树中的每个节点。
  2. 确定优先级: React 使用调度器(Scheduler)来确定更新的优先级。调度器会将更新划分到不同的优先级级别(lanes)中,以便在后续的调度过程中按优先级处理它们。
  3. 将更新加入更新队列: 更新被添加到与其优先级相对应的更新队列中。这些队列通常以链表或其他数据结构的形式存储在内存中,每个队列对应一个优先级级别。
  4. 调度更新: 调度器定期检查更新队列,并根据一些策略来决定何时以及如何处理这些更新。React 使用时间切片技术来分割渲染工作,以确保不会阻塞主线程。
  5. Reconciler 和 Fiber 架构: 当调度器决定开始重新渲染时,React 的调解器(Reconciler)负责协调组件树的更新。Reconciler 使用 Fiber 架构来构建一个任务调度器。每个 fiber 节点都有一个 shouldUpdate 标志,用于指示节点是否需要重新渲染。这个标志会在调和过程中被检查和更新。
  6. 执行渲染流程: 在重新渲染阶段,Reconciler 通过递归遍历 fiber 树,对比新旧节点,找出需要更新的部分。如果某个节点的状态发生了变化,或者它的 props 发生了变化,那么它的 shouldUpdate 标志就会被设置为 true,表示需要重新渲染该节点及其子树。否则,React 将跳过该节点及其子树的渲染过程。
  7. 更新实际 DOM: 一旦 Reconciler 完成协调过程,它会生成一系列 DOM 操作指令,用于更新实际的 DOM。这些指令描述了如何将最新的虚拟 DOM 树更新到实际的 DOM 中。

废弃三个Will钩子的原因

componentWillMount、componentWillUpdate、componentWillReceiveProps这三个钩子常年被开发者滥用,是产生副作用的重灾区,在Fiber中,因为所有的工作单元都是可中断,可继续的,所以会导致这三个钩子重复调用,导致不可想象的局面,所以这三个钩子随着Fiber的出现废弃是必然的。

React Hooks 实现原理

useState: 获取当前 Fiber 实例的 hookId,hooks 列表, 如果没有缓存,执行初始化函数,初始化状态,返回 setState 函数,函数会对比新老状态是否变更, 如果变更设置新状态,如果当前实例在更新状态,就标记需要调度,如果不是更新状态,则立即更新状态。 如果之前的 hooks[id] 有缓存,则对比新状态和老状态,如果不一致标记它为更新状态。

useEffect: hookId,hooks 原理类似,

useId: 生成客户端&&服务端都唯一的 id,为了防止水合两边内容不一致,useId 是根据父节点路径生成的。

useInsertionEffect: 可以在插入到 DOM 前执行一些函数,它可以确保 style 标签可以在其他 Effect 运行之前被注入。如果可能出现样式过时。如果在渲染期间注入,那么浏览器将在渲染组件数时重新计算样式,可能会非常慢

use:使用 use 包裹的 fetch 函数会激活 Suspense。

useState 的原理是什么,背后怎么执行的,它怎么保证一个组件中写多个 useState 不会串

useState 的原理是通过闭包和链表数据结构实现的。当你在一个组件中多次调用 useState 时,每次调用都会创建一个新的状态和与之对应的更新函数,并将它们存储在一个链表中。

  1. 闭包: 在函数组件中,每次渲染时,useState 函数都会创建一个闭包环境,其中包含了该状态的当前值和更新函数。这样可以确保每个状态在组件多次渲染之间保持独立。
  2. 链表数据结构: React 使用链表来存储组件中的状态。每个状态都有一个指向下一个状态的指针,这样就能够在组件的生命周期中跟踪状态的变化。当你调用 useState 多次时,React 会按照调用的顺序依次创建状态,并将它们连接成一个链表。
  3. 状态更新: 当你调用状态的更新函数时,React 不会立即更新状态的值,而是将更新函数和对应的新值加入到状态的更新队列中。然后,在组件下一次渲染时,React 会依次执行更新队列中的操作,并更新状态的值。这样做的好处是可以将多次状态更新合并成一次更新,以提高性能。
  4. 保证多个 useState 不会串: 每次渲染组件时,React 都会根据调用 useState 的顺序来确定当前组件实例中的状态链表的顺序。这意味着每个状态都有自己的位置,不会受到其他状态的影响。因此,即使你在一个组件中多次调用 useState,每个状态都会独立地管理自己的状态值和更新函数。

React SSR 实现原理

  1. 服务器端渲染逻辑: 首先,在服务器端,创建一个用于渲染 React 组件的渲染引擎。常见的选择是 Node.js 上的 ReactDOMServer 模块。使用该模块中的 renderToStringrenderToNodeStream 方法将 React 组件渲染为 HTML 字符串或流。
  2. 路由匹配: 如果应用程序使用了路由,服务器端需要匹配请求的 URL,并确定渲染哪个组件。通常使用路由库(如 React Router)来处理路由匹配。
  3. 数据获取: 在服务器端渲染期间,可能需要获取组件所需的数据。这可以通过调用相应的数据获取函数或调用 API 来实现。数据获取完成后,将数据传递给组件以进行渲染。
  4. 渲染 React 组件: 使用 ReactDOMServer 中的 renderToString 方法将 React 组件渲染为 HTML 字符串。这会生成组件的静态表示,包括组件的初始状态和数据。
  5. 嵌入 HTML 模板: 将渲染得到的 HTML 字符串嵌入到 HTML 模板中。通常,服务器端会使用一个 HTML 模板文件,其中包含基本的 HTML 结构、样式表链接和 JavaScript 文件引用。渲染得到的 HTML 字符串会替换模板中的特定标记(如 <div id="root"></div>)。
  6. 将 HTML 发送到客户端: 最后,将包含 React 组件渲染结果的完整 HTML 页面发送到客户端。这可以通过 HTTP 响应发送到浏览器。

Suspense:支持嵌套,不同级别的结构支持自己的加载级别。服务端渲染报错时,Suspense 会捕获错误,局部让客户端重新渲染,水合作用不匹配是会丢弃部分 HTML,Suspense loading,然后重新渲染。 Better Stream:

  • 以前需要服务端准备好所以数据,再把组件渲染为 HTML。
    • 现在通过 use,每个组件都能独立管理 Fetch,Render,Load,Hydration。可以先返回 Suspense 的,等 Fetch 完成了,将 Render 后的 SSR 流发送到之前同一个流中,用内联 script 将之前的 Suspense 换成最新的内容。
  • 以前必须加载客户端所有组件代码,然后才开始水合。
    • Suspense + lazy 的可以适用 SSR,使用 Suspense 包裹的组件,应用程序会先一步这些组件,进行水合,然后 包裹的组件 脚本加载完成后,再进行水合。如果加载完成快于上一步的 SSR 组件 Fetch,水合自己先行完成。
  • 必须等待所以组件都水合完成,才能与其中的组件进行交互。
    • 水合阶段的点击行为,都会被捕获,等加载完,再还原。

React Diff 算法

  1. 树的比较:当组件状态或属性发生变化时,React 会生成新的 Virtual DOM 树。React Diff 算法首先会逐层比较新旧两棵 Virtual DOM 树的节点。

  2. 同级比较:React Diff 算法会从根节点开始比较,对于同一层级的节点,React 会尝试寻找具有相同类型的节点进行比较。

  3. 策略优化

    • 如果节点类型不同,React 会直接销毁旧节点,创建新节点,并进行替换。
    • 如果节点类型相同,React 会进一步比较节点的属性和子节点。
    • 对于同一层级的节点,React 会使用唯一的 key 进行对比,以确定哪些节点是新增的、删除的、或者需要更新的。
  4. 递归遍历:对于需要更新的节点,React 会递归比较其子节点。这样就可以在需要更新的部分进行深度优先的比较。

  5. 最小化操作:React Diff 算法尽可能地减少实际 DOM 操作的数量。例如,在确定需要更新的节点后,React 会生成最小的一组操作来更新实际 DOM。这些操作可能包括添加、移除、替换节点等。

小程序的框架

支付宝小程序框架中 js 框架叫 appx,分为 appx,appx-web,appx-naive。 appx 是小程序框架的核心。

  • 管理 app 和 page 的创建和生命周期回调
  • bridge 的调用和转发
  • appx-web 和 appx-native 分别是小程序 web 版本和小程序 native 版本 其中 appx 在运行时又分为 af-app.min.js 和 af-app.worker.js。worker 通常运行在 ServiceWorker 中。双方通过 Native 注入的 postMessage 进行通信。
  • 业务层的代码也会在打包后分为 index.js 和 index.worker.js 启动流程为:
  • 加载 html 时,启动 af-app.min.js 和 index.js,调用 af-app.min.js 的能力注册 Bridge 和回调,运行 SW 中的 worker 代码。
  • Nebula 容器完成 AliBridge 加载后,出发 BridgeReady 事件,继而执行 af-app.min.js 注册的 onReady 回调,启动 SW ,开始渲染。 更新流程为:
  • SW 更新 dom 逻辑触发,如 pageScrollTo -> 变为 SW postMessage 调用 -> 变为 af-app.min.js 使用 window.scrollTo

小程序高性能 tip

启动:分包、JSAPI Libs 按需加载,减少全局代码,取消同步 API 首屏:数据离线化,分离渲染,快照,取消同步 API 运行时性能:setData("a.b"), spliceData, Key for lists, a:if

LWP 是什么?

Native JS Bridge

  • JS -> Native:
    • API 注入,提供 postBridgeMessage 方法。
    • URL 拦截。
  • Native -> JS:
    • JS 在全局注册一个可访问的函数可被 Native 调用。
    • loadURL,url 中写入脚本 为了解决并发调用:
const callbacks = []
const event = []
window.JSBridge = {
    invoke: (bridgeName, callback, data) => {
        this.id++
        callbacks[this.id] = callback;
        bridge.postMessage({
            bridgeName,
            data,
            callbackId
        })
    }
    receiveMessage: (msg) => {
        const { bridgeName, callbackId, data } = msg;
        if (callbackId) {
           callbacks[callbackId](data);
        } else {
            event...
        }
    }
    register (bridgeName, callback) {
        event[bridgeName].push(callback)
    }
}

LLM

RAG 是什么?

  • 多模态 RAG 对图片进行 summary,然后将图片存入向量化检索器中
  • SELF-RAG: 使用高级模型来对问题打标,然后训练小的决策模型来评价检索的信息。防止 RAG 造成的召回数据不匹配和矛盾。
  • 联合检索:使用 RASA 提取问题的实体,如何实体能匹配知识图谱中的对应节点,直接从图谱中检索答案。如果图谱缺少信息,使用 ES 检索和向量检索。向量检索可以从语义上找到最匹配的文档,ES 检索则基于关键字匹配文档。最后将结果融合和排序。
  • 知识图谱:首先从页面中提取文本内容,使用 BERT 来识别文本中的实体,识别出三元组关系,将识别的实体和知识库链接。然后通过之前的关系抽取模型,构建知识图谱,2个节点一个边,接着将知识融合,不同文本源的相同实体,合并到一个节点。
  • 低代码相关:
    • 风格匹配:检索风格和页面的关系,页面和组件的关系。
    • 组件选择:基于商品列表等活动实体,找到对应的组件实体,并确认他们与风格匹配。
    • 页面组装:根据页面实体和组件实体的 HTML,生产一个包含所需组件的页面 HTML。
    • 图片匹配:根据组件相关的图片描述,选择合适的图片实体,并获取他们实际的地址嵌入页面
  • LLM && 知识图谱:将问题和图的模式提供给 LLM,LLM 利用这些信息生产 Cypher 查询。Neo4j 根据查询返回结果。

LLM 用到的技术

  • 基于知识蒸馏,用昂贵的大模型先上次 QA 结构对,再用便宜等的大模型针对 QA 对进行微调。
  • 基于种子场景的知识抽取,选取一些代表性场景,可以覆盖该场景群的偏好,然后基于向量检索的推荐 LLM 知识。

深度学习,机器学习你知道哪些?

机器学习:

  1. 监督学习(Supervised Learning)

    • 回归(Regression) :用于预测连续型变量的值,例如房价预测。
    • 分类(Classification) :用于将实例分配到预定义的类别中,例如垃圾邮件过滤。
  2. 无监督学习(Unsupervised Learning)

    • 聚类(Clustering) :将数据分成不同的组,使得同一组内的数据相似度较高。
    • 降维(Dimensionality Reduction) :减少数据的维度,保留最重要的特征。
  3. 半监督学习(Semi-supervised Learning) :结合了监督学习和无监督学习的方法,通常在数据标记有限的情况下使用。

  4. 强化学习(Reinforcement Learning) :智能系统通过与环境的交互学习最优的行为策略,例如AlphaGo。

深度学习:

  1. 神经网络(Neural Networks)

    • 多层感知器(Multi-layer Perceptrons, MLP) :由多个神经元组成的前馈神经网络。
    • 卷积神经网络(Convolutional Neural Networks, CNN) :专用于处理网格化数据,如图像。
    • 循环神经网络(Recurrent Neural Networks, RNN) :具有循环连接的神经网络,用于处理序列数据。
  2. 深度学习模型

    • 自动编码器(Autoencoder) :用于数据压缩和特征学习的神经网络模型。
    • 生成对抗网络(Generative Adversarial Networks, GAN) :由生成器和判别器组成的对抗性训练框架,用于生成逼真的新数据。
    • 变分自编码器(Variational Autoencoder, VAE) :一种生成模型,与传统的自动编码器相比,具有更强的概率建模能力。
  3. 深度学习技术

    • 迁移学习(Transfer Learning) :将在一个任务上训练好的模型迁移到另一个相关任务上,以提高学习效果。
    • 批量归一化(Batch Normalization) :通过规范化神经网络的输入来加速训练过程和提高模型的稳定性。
    • 残差网络(Residual Networks, ResNets) :通过引入跨层连接来解决深度神经网络训练过程中的梯度消失和梯度爆炸问题。

LoRA 是什么?

LoRA 通常是指低秩分解(Low-Rank Decomposition)算法,是一种低资源微调大模型方法。预训练模型拥有极小的内在维度(instrisic dimension),即存在一个极低维度的参数,微调它和在全参数空间中微调能起到相同的效果。

Z 字打印

let matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

let rows = matrix.length;
let cols = matrix[0].length;

let result = [];

for (let i = 0; i < rows + cols - 1; i++) {
    if (i % 2 === 0) {
        for (let j = Math.min(i, rows - 1); j >= Math.max(0, i - cols + 1); j--) {
            result.push(matrix[j][i - j]);
        }
    } else {
        for (let j = Math.min(i, cols - 1); j >= Math.max(0, i - rows + 1); j--) {
            result.push(matrix[i - j][j]);
        }
    }
}

console.log(result);

TypeScript 泛型