2024面试复盘

194 阅读51分钟

组件开发平台

1. 组件拆分的依据是什么?

总的来说,组件拆分的核心目的是提高代码的可维护性、扩展性和灵活性,从而更好地适应需求变化。关注点分离、复用性、可测试性是组件拆分的主要原则。

2. js 数据类型有哪些

原始数据类型Number、String、Boolean、null、undefined、Symbol、BigInt

引用数据类型 Object

3. String('123') 和 new String('123') 的区别

String('123') 和 new String('123') 之间也存在一些差异:

创建方式不同:

-   `String('123')` 是使用构造函数 `String()` 创建的一个原始字符串。
-   `new String('123')` 是使用 `String` 构造函数创建的一个 `String` 对象。

类型不同:

-   `String('123')` 的结果是一个原始字符串类型。
-   `new String('123')` 的结果是一个 `String` 对象类型。

内存占用不同:

-   `String('123')` 创建的是一个原始字符串,占用的内存相对更少。
-   `new String('123')` 创建的是一个 `String` 对象,占用的内存相对更多。

属性和方法不同:

-   `String('123')` 虽然也能访问一些字符串的属性和方法,但它们是临时创建的 `String` 对象。
-   `new String('123')` 创建的是一个完整的 `String` 对象,拥有丰富的属性和方法。

比较结果不同:

-   `String('123') === '123'` 返回 `true`,因为它们是相同的原始字符串。
-   `new String('123') === '123'` 返回 `false`,因为 `new String('123')` 是一个 `String` 对象,与原始字符串不相等。

总的来说,String('123') 创建的是一个原始字符串,而 new String('123') 创建的是一个 String 对象。在大多数情况下,使用字面量形式 '123' 就足够了,只有在需要使用 String 对象方法时,才考虑使用 new String('123') 的形式。

4. localStorage 可以设置过期时间嘛?

原生的 localStorage 并不提供直接设置过期时间的功能。localStorage 存储的数据是永久性的,除非手动清除。

但是我们可以通过一些技巧来模拟设置过期时间的效果。常见的方法有以下几种:

  1. 手动设置过期时间戳:

    • 在存储数据时,同时存储一个过期时间戳。
    • 读取数据时,先检查当前时间是否超过过期时间戳,如果超过则删除该数据。

javascript

复制

// 存储数据
const expirationTime = new Date().getTime() + 3600000; // 1小时后过期
localStorage.setItem('myData', JSON.stringify({ data: 'myData', expires: expirationTime }));

// 读取数据
const storedData = JSON.parse(localStorage.getItem('myData'));
if (storedData && storedData.expires > new Date().getTime()) {
  console.log(storedData.data);
} else {
  localStorage.removeItem('myData');
}
  1. 使用第三方库:

    • 一些第三方库提供了对 localStorage 进行过期时间管理的功能,比如 js-cookielocalForage 等。

javascript

复制

// 使用 localForage 库
const storage = localForage.createInstance({
  name: 'myApp',
  version: 1.0,
  storeName: 'keyValueStore',
  description: 'Some description'
});

// 存储数据,设置过期时间为1小时
storage.setItem('myData', { data: 'myData' }, { expires: 3600 });

// 读取数据
storage.getItem('myData').then(data => {
  if (data) {
    console.log(data.data);
  } else {
    console.log('Data expired or not found');
  }
});
  1. 使用 IndexedDB:

    • IndexedDB 是一种浏览器原生的键值对数据库,可以设置过期时间。

总之,虽然原生的 localStorage 没有提供直接设置过期时间的功能,但我们可以通过一些技巧来模拟实现这一需求。根据具体情况选择合适的方法,既可以满足需求,又不会过度复杂化代码。

5. Redux 设计模式

Redux 是一种应用状态管理的设计模式,它主要包含以下几个核心概念:

  1. Store:

    • Redux 应用的全局状态存储中心,存储整个应用的状态。
    • Store 是一个单一的、不可变的数据源。
  2. Action:

    • 描述应用状态变化的动作,是改变 Store 数据的唯一方式。
    • Action 是一个普通的 JavaScript 对象,必须包含一个 type 属性来标识动作类型。
  3. Reducer:

    • 纯函数,根据当前 State 和 Action 计算出新的 State。
    • Reducer 接收 (state, action) 作为参数,返回新的 State 对象。
  4. Dispatch:

    • 触发 Action 的方法,用于更新 Store 中的状态。
    • Dispatch 接收一个 Action 对象作为参数,通知 Reducer 更新 Store。
  5. Subscribe:

    • 注册监听 Store 变化的回调函数。
    • 当 Store 中的状态发生变化时,订阅的回调函数会被调用。

Redux 的设计模式主要体现在以下几个特点:

  1. 单一数据源:

    • 整个应用的状态都存储在一个单一的 Store 中。
  2. State 是只读的:

    • 唯一改变 State 的方法是触发 Action,由 Reducer 返回新的 State。
  3. 使用纯函数进行状态更新:

    • Reducer 是纯函数,不会产生任何副作用。
  4. 关注点分离:

    • Action 描述状态变化,Reducer 执行状态更新,Store 管理状态。

这种设计模式有以下优点:

  • 可预测性:应用状态的变化遵循严格的单向数据流。
  • 可测试性:由于纯函数的特性,Reducer 很容易测试。
  • 代码结构清晰:各组件职责明确,易于维护和扩展。

总之,Redux 提供了一种可预测、可测试、可维护的状态管理模式,被广泛应用于 React 等前端框架中。

6. nextTick 原理

7. vue diff 算法比较

开始和开始比较、结束和结束比较,开始和结束、结束和开始。如果tag和key一致,则认为是同一个节点,如果顺序有变化,就直接移动就可以了。

8.loader 和 plugin 相关问题

loader 中 css-loader 可以去除嘛

简单回答是,即使使用了 sass-loader,css-loader 也是必需的,不能完全去除。

原因如下:

  1. Sass-loader 的作用:
    Sass-loader 的主要作用是将 Sass/SCSS 语法转换为普通的 CSS 语法。它负责将你编写的 Sass/SCSS 代码编译成 CSS。

  2. css-loader 的作用:
    css-loader 的主要作用是解析 CSS 文件,处理 CSS 中的 @import 和 url() 等引用,并将其合并成一个 CSS 模块。

     

    即使使用了 sass-loader 将 Sass 编译成 CSS,css-loader 仍然需要进一步处理这些 CSS 文件,例如处理 CSS 中的 @import 和 url() 等引用。

  3. 两者的协作:
    通常情况下,我们会在 webpack 的 loader 链中同时使用 sass-loader 和 css-loader。sass-loader 负责将 Sass 编译成 CSS,css-loader 负责进一步处理生成的 CSS 文件。

     

    这样的组合可以确保 CSS 文件被正确地处理和打包。

因此,即使使用了 sass-loader,css-loader 仍然是必需的,不能完全去除。两者协同工作,可以确保 Sass/SCSS 代码被正确地编译和打包成最终的 CSS 文件。

webpack plugin中的实现( compiler 和 compilation)

Webpack 中的 Plugin 是通过一种插件架构来实现的。它提供了一个强大的 hooks 系统,允许开发者在 Webpack 构建过程的各个阶段注入自定义的逻辑。

下面是 Webpack 插件机制的基本原理:

  1. 插件定义:
    Webpack 插件通常是一个具有 apply 方法的 JavaScript 对象。这个 apply 方法会在 Webpack compiler 实例上被调用,compiler 对象提供了对 Webpack 构建过程的访问。

    javascript

    Copy

    class MyPlugin {
      apply(compiler) {
        // 在这里注册自定义的 hooks 和逻辑
      }
    }
    
  2. Hooks 注册:
    插件通过在 apply 方法中注册自定义的 hooks 来扩展 Webpack 的功能。Webpack 内置了大量的 hooks,插件可以监听这些 hooks 并在特定构建阶段执行自定义逻辑。

    javascript

    Copy

    class MyPlugin {
      apply(compiler) {
        compiler.hooks.emit.tap('MyPlugin', (compilation) => {
          // 在 emit 阶段执行自定义逻辑
        });
      }
    }
    
  3. 钩子类型:
    Webpack 提供了不同类型的 hooks,比如 compilation hooksenvironment hooksparser hooks 等,覆盖了构建过程的各个阶段。插件可以根据需要选择合适的 hooks 来注入自定义逻辑。

  4. Tapable 库:
    Webpack 的 hooks 系统是基于 Tapable 库实现的。Tapable 提供了一系列 hook 类,如 SyncHookAsyncSeriesHook 等,插件可以使用这些 hook 类来定义自己的 hooks。

  5. 插件配置:
    在 Webpack 配置中,插件通常以数组的形式列出,Webpack 会自动实例化并调用这些插件的 apply 方法。

    javascript

    Copy

    module.exports = {
      // ... other Webpack config
      plugins: [
        new MyPlugin()
      ]
    }
    

总的来说,Webpack 插件机制的核心思想是基于 hooks 系统,插件可以在 Webpack 构建过程的各个阶段注入自定义逻辑,从而扩展和增强 Webpack 的功能。这种灵活的插件架构使 Webpack 成为一个可高度定制的构建工具。

compiler 和 compilation

在 Webpack 插件中,compiler 和 compilation 是两个非常重要的概念,它们描述了 Webpack 构建过程中的两个关键对象。

  1. Compiler 对象:

    • Compiler 对象代表了整个 Webpack 环境配置。
    • 它在 Webpack 启动时被创建,并贯穿整个构建生命周期。
    • Compiler 对象暴露了所有的 Webpack hooks,插件可以通过这些 hooks 来钩入并扩展 Webpack 的行为。
    • Compiler 对象封装了 Webpack 配置,并提供了许多有用的方法和属性,如 compiler.optionscompiler.inputFileSystemcompiler.outputFileSystem 等。
  2. Compilation 对象:

    • Compilation 对象代表了一次单独的构建过程。
    • 每当检测到文件变更并需要重新构建时,就会创建一个新的 Compilation 对象。
    • Compilation 对象包含了当前模块资源、编译生成资源、变化的文件等。
    • 它暴露了许多 hooks,插件可以在这些 hooks 上注册自定义逻辑,例如 compilation.hooks.buildModulecompilation.hooks.afterOptimizeAssets 等。
    • Compilation 对象提供了许多用于处理模块、资源、依赖关系的方法,如 compilation.addModule()compilation.addEntry()compilation.getModule() 等。

在 Webpack 插件的 apply 方法中,我们通常会接受 compiler 对象作为参数,并在其上注册自定义的 hooks。在这些 hooks 的回调函数中,我们又可以访问 compilation 对象,以便在特定的构建阶段执行所需的自定义逻辑。

例如:

javascript

Copy

class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // 在 emit 阶段访问 compilation 对象,执行自定义逻辑
      console.log(compilation.assets);
    });
  }
}

总的来说,compiler 对象代表了整个 Webpack 环境,而 compilation 对象则代表了单次构建过程。插件开发者需要理解并熟练使用这两个对象,才能更好地扩展和定制 Webpack 的行为。

插件钩子

Webpack 插件中有许多不同类型的 hooks 可以绑定,它们对应着 Webpack 构建过程的各个阶段。选择合适的 hook 来绑定自定义逻辑是非常重要的。

以下是一些常见的 Webpack 构建阶段以及相应的 hook 事件:

  1. 初始化阶段:

    • environment: Webpack 环境准备完成时触发。
    • afterEnvironment: Webpack 环境准备完成后触发。
  2. 编译阶段:

    • compile: 一个新的 compilation 创建时触发。
    • beforeCompile: compilation 开始编译前触发。
    • compile: 编译器开始编译时触发。
    • thisCompilation: 在 compilation 创建时触发。
    • compilation: 在 compilation 完成时触发。
  3. 构建模块阶段:

    • normalModuleFactory: 在创建 normal module factory 时触发。
    • contextModuleFactory: 在创建 context module factory 时触发。
    • beforeModule: 在创建模块之前触发。
    • module: 在创建模块时触发。
    • afterModule: 在创建模块之后触发。
  4. 构建依赖图阶段:

    • beforeCompile: 在开始编译之前触发。
    • afterCompile: 在完成编译之后触发。
    • afterResolve: 在解析模块之后触发。
  5. 输出阶段:

    • emit: 在资源被放入 output 目录之前触发。
    • afterEmit: 在资源被放入 output 目录之后触发。
  6. 其他阶段:

    • done: 在 Webpack 构建完成时触发。
    • failed: 在 Webpack 构建失败时触发。

当编写 Webpack 插件时,我们需要根据插件的需求选择合适的 hook 来绑定自定义逻辑。例如,如果我们需要在输出资源之前进行某些修改,可以选择 emit 钩子;如果我们需要在构建完成时执行某些清理工作,可以选择 done 钩子。

通过熟练掌握这些 Webpack 构建阶段和相应的 hook 事件,我们就可以更好地编写出功能强大的 Webpack 插件。

钩子顺序

Webpack 的 hook 机制中有一套完整的管理策略,用于协调不同插件之间的执行顺序。这种机制有助于确保插件之间的协作性和可靠性。

主要有以下几种方式来管理 Webpack 的 hook 顺序:

  1. 钩子类型:

    • Webpack 的钩子分为同步钩子和异步钩子两种类型。同步钩子中的回调函数会按照注册的先后顺序依次执行,而异步钩子中的回调函数则是并发执行。
    • 同步钩子的执行顺序更容易控制,而异步钩子则更适合一些独立的、耗时的操作。
  2. 优先级:

    • 对于同步钩子,Webpack 允许插件指定钩子的优先级。优先级分为 highnormal(默认)、low 三种。
    • 优先级高的插件会先于优先级低的插件执行。这样可以让重要的插件先执行,而不会被其他插件干扰。
  3. tapable.js 库:

    • Webpack 基于 tapable.js 库实现了钩子机制。tapable.js 提供了丰富的 hook 类型和 API,供 Webpack 插件开发者使用。
    • 通过 tapable.js 提供的 registercalltap 等方法,Webpack 可以灵活地管理钩子的注册和执行。
  4. 钩子生命周期:

    • Webpack 的钩子遵循一定的生命周期。比如 compile 钩子会在编译开始前触发,而 afterEmit 钩子会在资源输出后触发。
    • 插件开发者需要了解钩子的触发时机,以便在适当的阶段注册自己的逻辑。
  5. 钩子数据:

    • Webpack 的钩子可以传递上下文数据。不同的钩子会传递不同的数据对象,如 compilation 对象、module 对象等。
    • 插件开发者可以利用这些数据对象来获取构建过程的上下文信息,并进行相应的处理。

总的来说,Webpack 通过钩子类型、优先级、tapable.js 库以及钩子生命周期和数据等机制,实现了对插件执行顺序的精细化管理。这确保了 Webpack 插件生态的健康发展,让不同插件可以安全、有序地协作

plugin demo

image.png

loader 和 plugin 的区别

Webpack 中的 Loader 和 Plugin 虽然都是用于扩展 Webpack 的功能,但它们在作用和使用方式上还是有一些区别的:

  1. 作用不同:

    • Loader 主要用于对模块的源代码进行转换,比如将 ES6 转换为 ES5、Sass 转换为 CSS 等。
    • Plugin 则更加广泛,可以在 Webpack 构建的各个阶段执行自定义的逻辑,如资源优化、环境变量替换、输出文件管理等。
  2. 使用方式不同:

    • Loader 在 Webpack 配置的 module.rules 中定义,用于处理特定类型的模块。
    • Plugin 在 Webpack 配置的 plugins 字段中定义,通过实例化插件类的方式添加到构建流程中。
  3. 生命周期不同:

    • Loader 只参与模块转换这一个构建阶段,
    • 而 Plugin 可以贯穿 Webpack 构建的全部生命周期,包括初始化、编译、输出等阶段。
  4. 复杂度不同:

    • Loader 相对简单,通常只需要实现一个转换函数。
    • 而 Plugin 则更加复杂,需要自定义 Webpack 构建流程中的钩子函数,实现更加灵活和强大的功能。

总的来说,Loader 和 Plugin 是 Webpack 提供的两种不同的扩展机制:

  • Loader 用于模块转换,实现不同类型源代码到 JavaScript 的转换。
  • Plugin 用于自定义 Webpack 构建过程的各个阶段,实现更加复杂和全面的功能扩展。

二者相互补充,共同为 Webpack 提供了强大的可扩展性,使得 Webpack 可以满足各种复杂的前端构建需求。

9.map 和 weakmap区别

WeakMap 和 Map 都是 JavaScript 中的数据结构,它们有一些相似和不同的地方:

相似点:

  1. 都是键值对的集合。
  2. 都可以使用任何类型的值作为键或值。

不同点:

  1. 键的类型:

    • Map 的键可以是任意类型的值(包括对象)。
    • WeakMap 的键必须是对象类型。
  2. 值的类型:

    • Map 的值可以是任意类型。
    • WeakMap 的值可以是任意类型。
  3. 垃圾回收:

    • Map 中的键和值都是强引用,即使没有其他引用指向它们,它们也不会被垃圾回收器回收。
    • WeakMap 中的键是"弱"引用,如果除了 WeakMap 之外没有其他地方引用了该对象,那么该对象可能会被垃圾回收器回收。
  4. 可枚举性:

    • Map 是可枚举的,你可以使用 for...of 循环、size 属性等来访问和遍历它。
    • WeakMap 是不可枚举的,没有 size 属性,也无法被直接遍历。
  5. 性能:

    • Map 的性能可能稍微差一点,因为它需要维护内部的迭代器。
    • WeakMap 的性能通常更好,因为它不需要维护内部的迭代器。

总的来说,Map 和 WeakMap 都是 JavaScript 中非常有用的数据结构,但它们的使用场景有所不同:

  • 如果你需要存储任意类型的键值对,并且希望能够遍历和访问它们,那么 Map 更适合。
  • 如果你只需要存储对象到任意值的映射关系,并且希望能够自动回收不再需要的对象,那么 WeakMap 更合适。

根据具体的业务需求,合理选择 Map 或 WeakMap 可以帮助你编写出更高效和内存友好的 JavaScript 代码。

10.脚手架做过哪些工作

command lerna 解析参数

template 如何加载自定义参数(title 属性不同)提供变量

11.Tree-shaking 摇树

ES6 为啥能支持tree-shaking,静态编译。 COMMONJS 动态编译

12.小程序框架

Taro React 系列 Uniapp Vue 系列

13. SSR 实现模式

14.JsBridge

H5 和 Native 之间如何相互通讯

H5 和 Native 之间的相互通讯有以下几种常用的方式:

  1. WebView 与 Native 通讯

    • 在 Native 端提供一个 WebView 组件,H5 页面可以通过 JavaScript 调用 Native 端的方法,反之亦然。
    • 这种方式需要在 Native 端和 H5 端进行约定好的通讯协议,比较灵活,但需要双方都有开发能力。
  2. URL Scheme

    • H5 页面通过 window.location.href 打开一个特定的 URL Scheme,Native 端监听到该 Scheme 后执行相应的操作。
    • 这种方式简单易实现,但功能相对受限,且存在一定的安全隐患。
  3. Postmessage

    • H5 页面通过 window.postMessage() 发送消息,Native 端通过监听 message 事件接收消息。
    • 这种方式跨域兼容性好,但需要双方都有开发能力,且存在一定的安全隐患。
  4. 第三方 SDK 桥接

    • 使用第三方 SDK (如 jsBridge) 提供的 API,在 H5 和 Native 之间建立通讯桥梁。
    • 这种方式封装了通讯细节,使用简单,但需要依赖第三方 SDK,灵活性相对较低。
  5. 文件交互

    • H5 页面生成一个文件,Native 端检测并读取该文件,执行相应操作。
    • 这种方式简单易实现,但只能单向传递信息,且可能存在兼容性问题。
  6. 二维码/条形码

    • H5 页面生成二维码/条形码,Native 端扫描并解析,执行相应操作。
    • 这种方式适用于一些特定场景,如分享、支付等,但交互体验较差。

在实际项目中,可以根据具体需求选择合适的通讯方式。如果需要复杂的交互,通常会采用 WebView 与 Native 通讯的方式;如果需求相对简单,URL Scheme 或第三方 SDK 桥接可能会更合适。此外,还可以结合多种方式来满足不同的需求。

15.多主题变化

16.type 和 interface

TypeScript 中的 type 和 interface 是两种不同的类型定义方式,它们之间有一些区别和联系:

  1. 语法差异:

    • interface: 使用 interface 关键字定义接口。
    • type: 使用 type 关键字定义类型别名。
  2. 功能差异:

    • interface: 主要用于定义对象的形状和结构。可以扩展、合并同名接口。
    • type: 可以定义基本类型、联合类型、元组等更广泛的类型。不能直接扩展或合并同名 type
  3. 扩展差异:

    • interface: 可以使用 extends 关键字扩展接口。
    • type: 可以使用交叉类型 & 来扩展类型别名。
  4. 兼容性差异:

    • interface: 可以被实现、扩展和合并。
    • type: 不能被实现,但可以用于类型推断和约束。
  5. 重复定义差异:

    • interface: 同名接口会被合并,属性会被合并。
    • type: 同名类型别名会报错,不会被合并。
  6. 表达能力差异:

    • interface: 更擅长描述对象的形状。
    • type: 可以描述更复杂的类型,如联合类型、交叉类型、元组类型等。

总的来说,interface 和 type 都可以用于定义类型,但在使用场景和表达能力上有一些区别:

  • 如果你需要定义对象的形状,使用 interface 通常是更好的选择。
  • 如果你需要定义更复杂的类型,如联合类型、交叉类型等,使用 type 会更合适。
  • 如果你需要扩展一个现有的类型,使用 interface 的 extends 关键字会更方便。
  • 如果你需要重复定义同名的类型,使用 interface 会更加灵活。

在实际开发中,根据具体的需求选择合适的类型定义方式。通常情况下,interface 和 type 可以协同使用,发挥各自的优势。

17.如何监听动画结束

在浏览器中可以使用 animationend 事件监听 CSS 动画的结束。以下是具体的步骤:

  1. 添加 CSS 动画
    在 CSS 中定义动画:

    css

    Copy

    @keyframes myAnimation {
      0% { transform: translateX(0); }
      100% { transform: translateX(100px); }
    }
    
    .animated-element {
      animation: myAnimation 2s linear;
    }
    
  2. 添加事件监听器
    在 JavaScript 中添加 animationend 事件监听器:

    javascript

    Copy

    const animatedElement = document.querySelector('.animated-element');
    
    animatedElement.addEventListener('animationend', function(event) {
      console.log('Animation ended!');
    });
    

    这里我们选择 .animated-element 作为动画元素,并添加 animationend 事件监听器。当动画结束时,就会触发该事件,并执行回调函数。

  3. 获取动画信息
    在回调函数中,可以获取更多关于动画的信息:

    javascript

    Copy

    animatedElement.addEventListener('animationend', function(event) {
      console.log('Animation ended!');
      console.log('Animation name:', event.animationName);
      console.log('Elapsed time:', event.elapsedTime, 'seconds');
    });
    
    • event.animationName: 获取动画的名称
    • event.elapsedTime: 获取动画持续的时间(单位为秒)
  4. 移除事件监听器
    如果不再需要监听动画结束事件,可以使用 removeEventListener 移除事件监听器:

    javascript

    Copy

    animatedElement.removeEventListener('animationend', myAnimationEndHandler);
    

需要注意的是,animationend 事件只会在 CSS 动画结束时触发,如果使用 JavaScript 动画,则需要使用其他事件或方法来监听动画结束。

总之,使用 animationend 事件可以很方便地监听 CSS 动画的结束,并获取相关的动画信息。这在需要在动画结束时执行其他操作时非常有用。

18. css 居中

19. flex:1 ;

flex: 1 是 CSS Flexbox 布局中的一个非常常见且重要的属性值。它代表以下三个属性的简写形式:

  1. flex-grow: 1
  2. flex-shrink: 1
  3. flex-basis: 0

让我们分别解释一下这三个属性的含义:

  1. flex-grow:

    • 定义项目的放大比例。
    • 默认值为 0,即如果存在剩余空间,也不放大。
    • 设置为 1 后,若有剩余空间,该项目将平均分享剩余空间。
  2. flex-shrink:

    • 定义了项目的缩小比例。
    • 默认值为 1,即如果空间不足,该项目将缩小。
    • 如果所有项目的 flex-shrink 属性都为 1,当空间不足时,都将等比例缩小。
  3. flex-basis:

    • 定义了在分配多余空间之前,项目占据的主轴空间。
    • 浏览器根据这个属性计算主轴是否有多余空间。
    • 它的默认值为 auto,即项目的本来大小。

综合起来,flex: 1 的含义就是:

  1. 当有剩余空间时,该项目将平分剩余空间(因为 flex-grow 为 1)。
  2. 当空间不足时,该项目将等比例缩小(因为 flex-shrink 为 1)。
  3. 在分配多余空间之前,该项目占据的主轴空间为 0(因为 flex-basis 为 0)。

这是 Flexbox 布局中最常见且最有用的一种设置,可以让项目在容器中自动伸缩,以达到理想的布局效果。

20.兼容性会考虑了多少

兼容分级策略: A B S X

遇到哪些兼容性问题?怎么解决?

处理不同浏览器内核的兼容性问题,主要有以下几种常见的方法:

  1. 特性检测

    • 使用 JavaScript 的特性检测机制,动态地根据浏览器支持情况加载不同的代码实现。
    • 例如,可以检测 document.querySelector 是否存在,以此判断浏览器是否支持 CSS 选择器 API。
    • 基于特性检测的方式可以提供优雅的降级方案,确保核心功能在不同内核下都能正常工作。
  2. 浏览器嗅探

    • 通过解析 navigator.userAgent 字符串,识别浏览器的内核类型。
    • 根据不同的内核类型,应用针对性的代码实现或样式调整。
    • 这种方式相对简单,但需要维护一份较为完整的浏览器 UA 信息库。
  3. CSS hack

    • 利用不同浏览器内核对 CSS 解析机制的差异,编写针对性的 CSS hack 代码。
    • 常见的 hack 技巧包括条件注释、属性前缀、属性值重复等。
    • 这种方式灵活性强,但可维护性较差,容易产生代码混乱。
  4. CSS 预处理器

    • 使用 Sass、Less 等 CSS 预处理器,通过条件判断或 mixin 等机制,生成针对不同内核的 CSS 代码。
    • 预处理器提供了更好的可维护性和可读性,但需要额外的构建步骤。
  5. 前端框架/库

    • 一些主流的前端框架/库,如 React、Vue 等,本身就内置了对不同浏览器内核的兼容性处理。
    • 使用这些框架可以大大简化跨浏览器内核的兼容性工作。
  6. Polyfill 和 Transpiler

    • 使用 Polyfill 技术,为不支持的特性提供兼容性实现。
    • 结合 Transpiler 工具(如 Babel)将新语法转换为兼容性更好的版本。
  7. 渐进增强

    • 采用"渐进增强"的策略,先确保网页在最低要求的浏览器内核上能正常工作,然后逐步增强功能。
    • 这种方式可以最大限度地保证核心功能的跨浏览器兼容性。

在实际项目中,通常需要综合使用上述几种方法,根据具体的业务需求和目标浏览器范围,制定合理的兼容性解决方案。同时,持续关注浏览器内核的发展动态,及时调整兼容性策略也很重要。

21.charles 为什么能劫持到请求

Charles 之所以可以劫持 HTTP/HTTPS 请求,主要有以下几个原因:

  1. 中间人攻击原理:

    • Charles 作为一个 HTTP 代理服务器,可以位于客户端(如浏览器)和服务端之间,截获和监控两者之间的通信。
    • 这种中间人攻击的方式可以让 Charles 获取并查看原本应该是"私密"的通信内容。
  2. SSL/TLS 证书伪造:

    • 对于 HTTPS 加密通信,Charles 可以生成一个自签名的 SSL/TLS 证书,并作为中间人的身份插入到客户端和服务端之间。
    • 客户端会信任 Charles 伪造的证书,从而将原本加密的内容暴露给 Charles。
  3. 操作系统信任证书:

    • Charles 会要求用户安装一个 Charles 根证书,并将其添加到操作系统的受信任证书中。
    • 这样一来,操作系统会信任 Charles 生成的所有证书,从而允许 Charles 成为 HTTPS 通信的中间人。
  4. 动态 SSL 代理:

    • Charles 会动态地为每个 HTTPS 连接生成一个新的证书,并将其注入到客户端和服务端之间。
    • 这种方式可以让 Charles 实现对 HTTPS 通信的实时监控和修改。
  5. 浏览器配合:

    • 现代浏览器一般都支持将 HTTP 代理的设置应用到整个浏览器会话。
    • 用户可以在浏览器中配置 Charles 作为 HTTP 代理,从而让浏览器的所有 HTTP/HTTPS 请求都经由 Charles 代理。

总的来说,Charles 之所以能够劫持请求,是因为它巧妙地利用了中间人攻击的原理,并通过生成自签名证书以及操作系统信任证书等方式,成功地将自己插入到客户端和服务端之间的通信链路中。这种方式使得 Charles 能够完全控制和监控 HTTP/HTTPS 数据包的传输。

22.js 中 error 报错是怎么监控的

Sentry 是一个非常流行的前端和后端错误监控和报告工具。它可以通过以下几种方式检测前端应用中的错误:

  1. 全局错误捕获:

    • Sentry 提供了全局错误捕获机制,可以监听 window.onerrorunhandledrejection(处理promise) 等事件,捕获应用中未被处理的异常。
    • 这些未捕获的异常通常会导致应用崩溃或出现意外行为,Sentry 能够及时发现并上报这些重要的错误。
  2. 框架/库集成:

    • Sentry 提供了对主流前端框架/库的集成支持,如 React、Angular、Vue.js 等。
    • 通过这些集成,Sentry 能够更深入地了解应用的结构和运行状态,捕获各种类型的错误和异常。
  3. 手动 SDK 集成:

    • Sentry 还提供了丰富的 SDK,开发者可以手动将其集成到应用中,以实现更细粒度的错误跟踪和监控。
    • 通过 SDK 提供的 API,开发者可以手动上报错误信息,记录用户上下文数据等,帮助 Sentry 更好地分析问题。
  4. 错误采样:

    • Sentry 支持错误采样,即只上报部分错误,以避免因大量错误上报而影响应用性能。
    • 开发者可以根据错误发生频率、严重程度等因素设置采样策略,确保关键错误能够及时被发现和修复。
  5. 源码映射:

    • Sentry 能够解析应用的源码映射(source map)信息,将错误堆栈信息映射回源代码位置。
    • 这有助于开发者快速定位错误发生的具体位置,提高问题的修复效率。
  6. 上下文信息收集:

    • Sentry 会自动收集错误发生时的上下文信息,如用户 ID、会话 ID、浏览器信息等。
    • 这些信息有助于开发者更好地分析错误产生的原因和影响范围。

总的来说,Sentry 通过全局错误捕获、框架/库集成、手动 SDK 集成、错误采样、源码映射以及上下文信息收集等多种方式,实现了对前端应用的全面错误检测和监控。开发者可以根据具体需求选择合适的集成方式,以确保应用的稳定性和可靠性。

23.图片懒加载的几种方式 图片懒加载有以下几种常见的实现方式:

  1. IntersectionObserver API:

    • 使用 IntersectionObserver 监听图片元素是否进入可视区域,当进入可视区域时才触发图片加载。
    • 这是一种较新的方案,可以更精确地检测元素的可见状态,减少不必要的加载。
  2. Scroll 事件监听:

    • 监听页面滚动事件,计算图片元素的位置是否在可视区域内,如果在则触发图片加载。
    • 这种方式简单易实现,但需要频繁计算元素位置,可能会影响性能。
  3. 自定义 data- 属性*:

    • 将图片的真实 src 地址存放在自定义的 data-src 属性中。
    • 当图片元素进入可视区域时,将 data-src 的值赋给 src 属性,触发图片加载。
  4. Mutation Observer API:

    • 使用 MutationObserver 监听页面 DOM 结构的变化,当图片元素加入 DOM 时触发图片加载。
    • 这种方式适用于动态插入图片的场景,能够及时捕获新图片的加载需求。
  5. Intersection Observer API + 自定义 data- 属性*:

    • 结合使用 IntersectionObserver 和自定义 data-src 属性的方式,可以实现更加可靠和高性能的懒加载。
    • 先使用 IntersectionObserver 监听图片是否进入可视区域,然后再触发 data-src 到 src 的赋值操作。
  6. Lazy Loading HTML 属性:

    • HTML5 原生提供了 loading="lazy" 属性,可以让浏览器自动实现图片的懒加载。
    • 这种方式简单易用,但目前兼容性还不太好,需要关注浏览器的支持情况。

总的来说,这些方式各有优缺点,开发者可以根据具体需求和项目情况选择合适的懒加载实现方式。一般来说,使用 IntersectionObserver 结合自定义 data-src 属性的方式是较为推荐的做法,兼具性能和可靠性。

23.Promise 的机制

好的,让我给你解释一下 Promise 机制:

Promise 是 JavaScript 中用于处理异步操作的一种机制。它提供了一种更优雅和可控的方式来处理异步代码,相比于传统的回调函数方式更加清晰和可读。

Promise 对象有三种状态:

  1. Pending: 初始状态,既不是成功,也不是失败状态。
  2. Fulfilled: 操作成功完成。
  3. Rejected: 操作失败。

Promise 对象提供了以下方法:

  • then(onFulfilled, onRejected): 用于注册当 Promise 对象状态变为 Fulfilled 或 Rejected 时的回调函数。
  • catch(onRejected): 用于注册当 Promise 对象状态变为 Rejected 时的回调函数。
  • finally(onFinally): 用于注册当 Promise 对象状态变化完成时的回调函数,无论成功还是失败。

使用 Promise 的一个典型例子是发起一个 AJAX 请求:

javascript

复制

fetch('/data.json')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

在这个例子中,fetch() 方法返回一个 Promise 对象,我们使用 .then() 方法注册成功和失败的回调函数。

Promise 机制的优点包括:

  • 更好的错误处理
  • 更清晰的异步流程控制
  • 支持链式调用
  • 可以使用 async/await 语法糖进一步简化异步代码

总之, Promise 是 JavaScript 处理异步操作的一种强大且灵活的机制,广泛应用于现代 Web 开发中。

24.Promise.all 和 Promise.race 的区别

Promise.all() 和 Promise.race() 是 Promise 的两个静态方法,它们都用于处理多个 Promise 对象,但是有以下区别:

  1. 返回值:

    • Promise.all() 会返回一个新的 Promise 对象,当所有输入的 Promise 都 fulfilled 时,该 Promise 才会 fulfilled,返回值是一个包含所有传入 Promise resolve 值的数组。如果任何一个 Promise 被 reject,则整个 Promise.all() 会立即 reject。
    • Promise.race() 也会返回一个新的 Promise 对象,但是这个 Promise 的状态取决于第一个完成的 Promise,无论成功还是失败。
  2. 执行顺序:

    • Promise.all() 会并行执行所有的 Promise,当所有 Promise 都完成后才会触发 resolved 状态。
    • Promise.race() 也是并行执行所有 Promise,但只要有一个 Promise 完成(无论成功或失败),就会立即触发 resolved 状态。
  3. 使用场景:

    • Promise.all() 适用于需要获取多个异步操作的结果,并将它们组合成一个结果的场景。
    • Promise.race() 适用于需要尽快获得异步操作结果,并且不关心其他 Promise 结果的场景,比如设置超时。

总之,Promise.all() 用于等待所有异步操作完成,Promise.race() 用于尽快获取异步操作的结果。根据具体的业务需求来选择使用哪个方法。

image.png

25.小程序了冷启动

小程序冷启动优化是提升小程序整体性能的关键所在。以下是一些常见的小程序冷启动优化策略:

  1. 代码分包:

    • 将小程序的代码按照功能模块进行分包,减少首次启动需要加载的代码量。
    • 使用 preloadRule 设置预加载规则,提升页面渲染速度。
  2. 资源优化:

    • 将图片、样式等静态资源上传至 CDN,提升资源加载速度。
    • 对资源进行压缩和适配处理,减小资源体积。
  3. 网络请求优化:

    • 减少不必要的网络请求,对必要的请求进行缓存。
    • 使用 wx.connectSocket() 建立长连接,减少重复连接的开销。
  4. 启动优化:

    • 将小程序首页设置为最小化页面,减少首屏渲染的复杂度。
    • 使用 wx.preloadGame() 实现游戏类小程序的预加载。
  5. 框架优化:

    • 使用最新版本的小程序基础库,获得更好的性能表现。
    • 根据业务需求选择合适的小程序框架,如 Taro、Chameleon 等。
  6. 监控与分析:

    • 使用 wx.reportPerformance() 收集用户的性能数据。
    • 分析性能数据,找出性能瓶颈并进行针对性优化。
  7. 开发工具优化:

    • 在开发阶段使用小程序开发者工具的性能分析功能,发现并修复性能问题。
    • 开启小程序开发者工具的 gzip 压缩功能,减小代码体积。

综上所述,小程序冷启动优化需要从多个角度入手,包括代码分包、资源优化、网络请求优化等。开发者需要根据小程序的具体情况,选择合适的优化策略,最大限度地提升小程序的冷启动性能。

26.小程序性能优化

小程序作为一种轻量级的移动端应用,性能优化是非常重要的。以下是一些常见的小程序性能优化技巧:

  1. 代码优化:

    • 减少代码体积和网络请求
    • 使用 wx.nextTick() 优化 DOM 操作
    • 合理使用 wx:if 和 wx:for 减少无用渲染
    • 使用 this.setData() 更新数据,减少整页面的重新渲染
  2. 资源优化:

    • 图片、视频等静态资源采用适当的压缩方式
    • 使用 image 标签的 lazyLoad 属性实现图片懒加载
    • 将静态资源上传至 CDN 加速访问
  3. 网络请求优化:

    • 尽量减少不必要的网络请求
    • 使用缓存技术(如 Memcached、Redis)缓存接口数据
    • 采用增量更新的方式获取数据,只更新变化部分
  4. 包体积优化:

    • 使用 ES6 模块化管理代码,按需引入
    • 开启小程序的 gzip 压缩功能
    • 分包和独立分包,将代码按模块划分
  5. 页面优化:

    • 使用 wx.createIntersectionObserver() 实现页面懒加载
    • 在页面切换时使用 wx.navigateTo() 而非 wx.redirectTo()
    • 合理使用 wx.hideLoading() 隐藏加载框
  6. 组件优化:

    • 使用自定义组件提高复用性
    • 在性能敏感的场景使用纯函数组件
    • 使用 wx:if 或 hidden 动态控制组件的显隐
  7. 运行时优化:

    • 使用 wx.reportPerformance() 监控页面性能
    • 合理使用 wx.nextTick() 延迟执行一些操作
    • 检查并优化 JavaScript 代码的执行效率

总的来说,小程序的性能优化需要从多个层面着手,包括代码优化、资源优化、网络优化等。开发者需要根据实际情况选择合适的优化方式,确保小程序的用户体验良好。

27.笔试题

promise

下面是一个使用 JavaScript Promise 实现 delay 方法的示例:


function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

delay(200)
  .then(() => {
    console.log('Delayed for 200 milliseconds.');
  })
  .catch(error => {
    console.error('Error:', error);
  });

复制js 对象

在 JavaScript 中,有几种方法可以复制对象的所有属性,除了一个:

  1. 使用 Object.assign() 方法:
const originalObj = { a: 1, b: 2, c: 3 };
const { c, ...newObj } = originalObj;
console.log(newObj); // { a: 1, b: 2 }

这里使用了对象解构赋值,将 c 属性分离出来,其余属性赋值给 newObj

  1. 使用展开运算符 ... 和对象字面量
const originalObj = { a: 1, b: 2, c: 3 };
const newObj = { ...originalObj, c: undefined };
delete newObj.c;
console.log(newObj); // { a: 1, b: 2 }

这里先使用展开运算符复制了所有属性,然后手动删除了 c 属性。

  1. 使用 JSON.parse() 和 JSON.stringify():
const originalObj = { a: 1, b: 2, c: 3 };
const { c, ...newObj } = JSON.parse(JSON.stringify(originalObj));
console.log(newObj); // { a: 1, b: 2 }

这里先将对象转换为 JSON 字符串,然后再解析回对象,同时使用对象解构赋值排除 c 属性。

  1. 使用 for...in 循环和对象字面量:
const originalObj = { a: 1, b: 2, c: 3 };
const newObj = {};
for (const key in originalObj) {
  if (key !== 'c') {
    newObj[key] = originalObj[key];
  }
}
console.log(newObj); // { a: 1, b: 2 }

这里手动遍历对象的属性,并将除了 c 之外的属性复制到新对象中。

以上方法都可以实现复制对象的所有属性,除了一个。选择使用哪种方法取决于具体的需求和个人preference。

28.缓存

image.png

协商缓存和强制缓存是 HTTP 缓存机制中的两种不同的缓存策略:

  1. 强制缓存:

    • 强制缓存是基于 HTTP 响应头中的缓存控制指令(Cache-ControlExpires)来决定资源是否可以被缓存,以及缓存时间。
    • 当浏览器再次请求该资源时,会先根据这些指令检查资源是否在缓存期内,如果在,则直接从缓存中获取,不会发送请求到服务器。
    • 强制缓存的优点是速度快,没有网络开销,但缺点是缓存时间到期后就必须向服务器发送请求。
  2. 协商缓存:

    • 协商缓存是基于 HTTP 响应头中的其他缓存验证指令(Last-ModifiedETag)来决定资源是否有更新,以及是否需要从服务器重新获取。
    • 当浏览器再次请求该资源时,会先向服务器发送带有这些验证指令的请求,服务器会根据这些指令判断资源是否有更新,如果没有更新,则返回 304 Not Modified 状态码,让浏览器继续使用缓存。
    • 协商缓存的优点是可以动态判断资源是否更新,减少不必要的网络请求,但缺点是需要多一次网络请求来验证缓存。

强制缓存和协商缓存通常结合使用,优先使用强制缓存,当强制缓存失效时再使用协商缓存。这种混合策略可以最大化缓存的效率和时效性。

29. vue 相关面试题

setup 中为啥不能使用this

在 Vue.js 中,setup() 函数没有 this 的原因主要有以下几点:

  1. 基于 Composition API 的设计:

    • Vue.js 2.x 版本使用的是基于 Options API 的设计模式,在组件选项中使用 this 来访问组件实例。
    • 而 Vue.js 3.x 引入了 Composition API,它摒弃了 this 的使用,转而采用更加函数式编程的方式。
  2. 更好的类型推导和智能提示:

    • 在 setup() 函数中,Vue.js 会自动推导出组件实例的类型,并将其作为函数参数提供。
    • 这样可以在编辑器中获得更好的类型提示和自动补全,提高开发效率。
  3. 避免 this 的上下文问题:

    • this 的上下文在 JavaScript 中一直是个头疼的问题,需要额外的绑定操作或箭头函数来解决。
    • 在 setup() 函数中完全避免 this,有助于消除这种上下文问题,使代码更加清晰和可维护。
  4. 更好的代码组织和复用:

    • 在 Composition API 中,组件的逻辑可以被更好地拆分和组织,通过自定义 Hooks 实现高度的逻辑复用。
    • 这种模式更加符合函数式编程的思想,提高了代码的可读性和可维护性。
  5. 更好的 TypeScript 支持:

    • 在使用 TypeScript 的场景下,不依赖 this 可以使得类型推导和检查更加准确和简单。
    • 这也是 Composition API 设计的一个重要目标之一。

总的来说,setup() 函数没有 this 是 Composition API 设计的一个重要特点,它有助于提高代码的可读性、可维护性和类型安全性。开发者可以通过 setup() 函数的参数来访问组件实例的属性和方法,以及其他 Vue.js 提供的工具函数。

如何拿到for 循环里面的所有组件的实例

在 Vue.js 中,如果需要拿到 v-for 循环中所有组件实例,可以使用以下几种方法:

  1. $refs 引用:

    • 在组件模板中,给每个子组件添加 ref 属性:

      html

      Copy

      <div v-for="item in items" :key="item.id" :ref="'child-' + item.id">
        <!-- 子组件内容 -->
      </div>
      
    • 在 setup() 函数或组件方法中,使用 $refs 访问所有子组件实例:

      javascript

      Copy

      import { ref, onMounted } from 'vue'
      
      export default {
        setup() {
          const childRefs = ref([])
      
          onMounted(() => {
            childRefs.value = Object.values(this.$refs)
          })
      
          return {
            childRefs
          }
        }
      }
      
  2. provide/inject 依赖注入:

    • 在父组件中,使用 provide 提供子组件实例:

      javascript

      Copy

      import { provide, ref } from 'vue'
      
      export default {
        setup() {
          const childInstances = ref([])
          provide('childInstances', childInstances)
      
          return {
            childInstances
          }
        }
      }
      
    • 在子组件中,使用 inject 获取父组件提供的子组件实例:

      javascript

      Copy

      import { inject } from 'vue'
      
      export default {
        setup() {
          const childInstances = inject('childInstances')
      
          // 将自身实例添加到 childInstances 数组中
          childInstances.value.push(/* 当前组件实例 */)
      
          return {
            // ...
          }
        }
      }
      
  3. 自定义指令:

    • 在父组件中,定义一个自定义指令来收集子组件实例:

      html

      Copy

      <div v-for="item in items" :key="item.id" v-my-directive="item"></div>
      
    • 在自定义指令的 mounted 和 unmounted 钩子中,管理子组件实例数组:

      javascript

      Copy

      import { ref } from 'vue'
      
      export default {
        directives: {
          myDirective: {
            mounted(el, binding, vnode) {
              const childInstances = ref([])
              childInstances.value.push(vnode.component)
              // 将 childInstances 绑定到父组件实例上
              vnode.props.childInstances = childInstances
            },
            unmounted(el, binding, vnode) {
              const childInstances = vnode.props.childInstances
              const index = childInstances.value.indexOf(vnode.component)
              childInstances.value.splice(index, 1)
            }
          }
        }
      }
      

以上三种方法都可以用于在 v-for 循环中获取所有子组件的实例。具体选择哪种方法,需要根据项目的具体需求和代码结构进行权衡。

30. 如何获取 dom 元素的坐标和行数

要获取 div 在页面中的坐标信息以及占用的行数,可以使用以下方法:

  1. 使用 getBoundingClientRect() :

    • 这个方法返回一个 DOMRect 对象,包含了元素的大小以及相对于视口的位置。

    • 通过 top 和 bottom 属性,我们可以计算出元素占用的行数。

    • 示例代码:

      javascript

      Copy

      const element = document.getElementById('myDiv');
      const rect = element.getBoundingClientRect();
      console.log('Left:', rect.left);
      console.log('Top:', rect.top);
      console.log('Width:', rect.width);
      console.log('Height:', rect.height);
      console.log('Lines:', Math.ceil(rect.height / 16)); // 每行约 16 像素
      
  2. 使用 offsetHeight 和 lineHeight:

    • offsetHeight 属性返回元素的像素高度,包括填充、边框和水平滚动条。

    • 通过获取元素的 lineHeight 属性,可以计算出元素占用的行数。

    • 示例代码:

      javascript

      Copy

      const element = document.getElementById('myDiv');
      const height = element.offsetHeight;
      const lineHeight = window.getComputedStyle(element).lineHeight;
      console.log('Left:', element.offsetLeft);
      console.log('Top:', element.offsetTop);
      console.log('Width:', element.offsetWidth);
      console.log('Height:', height);
      console.log('Lines:', Math.ceil(height / parseFloat(lineHeight)));
      
  3. 使用 getClientRects() :

    • 这个方法返回一个 DOMRectList 对象,包含了元素中每一个内联子元素的大小和位置。

    • 通过遍历 DOMRectList,可以获取元素占用的所有行的位置信息。

    • 示例代码:

      javascript

      Copy

      const element = document.getElementById('myDiv');
      const rects = element.getClientRects();
      console.log('Left:', rects[0].left);
      console.log('Top:', rects[0].top);
      console.log('Width:', rects[0].width);
      console.log('Height:', rects[0].height);
      console.log('Lines:', rects.length);
      

这三种方法各有优缺点,需要根据具体情况进行选择。getBoundingClientRect() 方法返回的是相对于视口的位置,offsetHeight 和 lineHeight 可以直接计算出行数,而 getClientRects() 方法可以获取元素中每一个内联子元素的位置信息。

31.了解浏览器的渲染过程嘛?

是的,我对浏览器的渲染过程有一定了解。下面简单介绍一下浏览器的主要渲染步骤:

  1. HTML 解析:

    • 浏览器首先会解析 HTML 文档,构建 DOM 树(文档对象模型)。
  2. CSS 解析:

    • 浏览器会解析 CSS 文件,构建样式表。
  3. 布局计算:

    • 浏览器会根据 DOM 树和样式表,计算出各个元素在页面上的位置和大小。
  4. 绘制:

    • 浏览器根据计算的布局信息,将各个元素绘制到屏幕上。
  5. 合成:

    • 如果页面有复杂的动画或滚动效果,浏览器会将页面分成多个图层,并使用 GPU 进行合成,提高渲染性能。
  6. 重绘和重排:

    • 当 DOM 或 CSS 发生变化时,会触发重绘(Repaint)或重排(Reflow)。重绘不会引起布局变化,而重排会引起布局变化,性能消耗较大。
  7. 优化:

    • 为了提高渲染性能,可以采取一些优化措施,如减少 DOM 操作、使用 CSS3 动画、延迟加载等。

浏览器的渲染过程是一个复杂的过程,涉及 HTML 解析、CSS 解析、布局计算、绘制、合成等多个步骤。对于前端开发者来说,理解这个过程非常重要,可以帮助我们编写更高效的代码,提高页面的性能。

32. 浏览器怎么知道几何的位置?

浏览器是如何知道页面元素的几何位置信息的呢?主要包括以下几个步骤:

  1. 计算布局(Layout) :

    • 浏览器会根据 HTML 结构和 CSS 样式,计算出每个元素在页面上的位置和大小。这个过程称为布局计算。
    • 布局计算会考虑元素的宽高、边距、填充、边框等属性,并根据元素的显示模式(块级、行内、浮动等)来确定最终的位置。
  2. 建立渲染树(Render Tree) :

    • 在完成布局计算之后,浏览器会建立一个渲染树(Render Tree),用来表示页面上实际可见的元素及其几何信息。
    • 渲染树中的每个节点都包含了元素的位置、大小、颜色等信息。
  3. 获取几何信息:

    • 通过 JavaScript 提供的一些 API,开发者可以获取元素的几何信息,比如 getBoundingClientRect()offsetWidth/offsetHeight 等。
    • 这些 API 会查询渲染树,返回元素在页面上的尺寸和位置信息。
  4. 动态更新:

    • 当页面元素发生变化时,如添加新元素、修改样式等,浏览器会重新计算布局,更新渲染树,从而更新元素的几何信息。

总的来说,浏览器是通过解析 HTML 和 CSS,计算布局,建立渲染树,然后提供 API 供开发者查询元素几何信息的。这个过程看起来似乎简单,但实际上涉及到很多复杂的算法和优化策略,以确保页面能够快速、高效地渲染出来

33.常见的加密方法

在现代密码学中,常见的加密方法主要有以下几种:

  1. 对称加密:

    • 也称为私钥加密或单密钥加密,加密和解密使用同一个密钥。常见算法有 AES、DES、Blowfish 等。
  2. 非对称加密:

    • 也称为公钥加密,使用一对密钥:公钥和私钥。公钥用于加密,私钥用于解密。常见算法有 RSA、ECC 等。
  3. 哈希算法:

    • 不可逆的数据摘要算法,将任意长度的输入映射为固定长度的输出。常见算法有 MD5、SHA-1、SHA-256 等。
  4. 数字签名:

    • 利用非对称加密的原理,使用私钥对数据进行签名,公钥可以验证签名的合法性。常见算法有 RSA、ECDSA 等。
  5. 消息认证码(MAC) :

    • 利用对称加密算法和哈希算法,为消息生成一个认证码,用于验证消息的完整性和来源。常见算法有 HMAC-SHA256 等。
  6. 密钥交换算法:

    • 用于双方在不安全的网络上安全地协商出一个共享密钥,常见算法有 Diffie-Hellman、ECDH 等。
  7. 随机数生成器:

    • 用于生成高质量的随机数,确保加密系统的安全性。常见算法有 CSPRNG、TRNG 等。

这些加密算法可以单独使用,也可以组合使用,构建出各种复杂的加密系统,满足不同场景下的安全需求。
加密算法的选择需要根据具体的应用场景、安全级别、性能要求等因素综合考虑。

对称加密和非对称加密的区别

对称加密和非对称加密(也称公钥加密)之间存在以下主要区别:

  1. 密钥管理:

    • 对称加密使用单一的共享密钥进行加密和解密。密钥必须安全地在双方之间传递和管理。
    • 非对称加密使用一对密钥:公钥和私钥。公钥可以公开分发,私钥由各自保管,密钥管理更加灵活。
  2. 加密/解密过程:

    • 对称加密中,加密和解密使用同一个密钥。
    • 非对称加密中,加密使用公钥,解密使用私钥。公钥和私钥是数学上相关的。
  3. 计算复杂度:

    • 对称加密算法,如 AES,计算速度快,加解密效率高。
    • 非对称加密算法,如 RSA,计算复杂度高,加解密速度相对较慢。
  4. 安全性:

    • 对称加密的安全性完全依赖于密钥的保护。一旦密钥被泄露,系统就会被攻破。
    • 非对称加密的安全性依赖于私钥的保护。即使公钥被泄露,系统也不会被破坏。
  5. 应用场景:

    • 对称加密适用于大量数据的加密传输,如文件加密、网络通信加密等。
    • 非对称加密适用于密钥分发、数字签名等场景,实现身份认证和数据完整性。

总的来说,对称加密和非对称加密各有优缺点,在实际应用中通常会结合使用,以发挥各自的优势。对称加密用于加密大量数据,非对称加密用于密钥分发和身份认证,两者结合可以构建出更加安全可靠的加密系统。

34.cookie 的问题

使用 Cookie 保存用户登录状态会存在以下一些潜在的问题:

  1. 安全性问题:

    • Cookie 是以明文形式存储在客户端浏览器中的,如果 Cookie 被窃取,攻击者就可以冒充用户身份访问系统。
    • 即使 Cookie 使用加密方式存储,如果密钥泄露,加密方式也可能被攻破。
  2. 跨域问题:

    • Cookie 默认是同源限制的,即只能在设置 Cookie 的域名下访问。这会给跨域请求带来问题。
    • 可以使用 domain 和 path 属性放宽 Cookie 的作用域,但这可能带来新的安全隐患。
  3. CSRF 攻击风险:

    • 因为 Cookie 会自动携带在请求中,这使得 CSRF 攻击变得较为容易。攻击者可以伪造用户请求,利用 Cookie 进行攻击。
  4. 移动端问题:

    • 移动设备上 Cookie 的容量和数量都有限制,无法满足复杂应用的需求。
  5. 退出登录问题:

    • 使用 Cookie 存储登录状态,要实现退出登录需要删除 Cookie,但删除 Cookie 并不能保证用户真正退出登录。

为了解决这些问题,通常会采用以下一些解决方案:

  1. 使用 JWT(JSON Web Token)等无状态的身份验证机制,将用户信息加密存储在客户端,减少对服务器的依赖。
  2. 采用 Session 机制,将用户登录状态保存在服务器端,客户端仅保存 Session ID。
  3. 使用 Token 机制,通过 API 调用获取 Token,并在后续请求中携带 Token 进行身份验证。
  4. 采用双 Cookie 机制,一个 Cookie 存储登录 Session ID,另一个存储 CSRF 令牌。

总之,使用 Cookie 保存登录状态存在一些安全隐患,应该结合实际需求选择合适的身份验证方式,在安全性和可用性之间寻求平衡。

35.node 是单线程还是多线程

Node.js 是一个单线程的运行时环境。它使用事件驱动、非阻塞I/O模型来实现高并发,这意味着它可以在一个单线程上处理大量的并发请求。

具体来说:

  1. Node.js 的主线程是单线程的,它使用事件循环来处理事件和异步操作。
  2. 对于 CPU 密集型的任务,Node.js 可以使用 Worker Threads 来创建子线程进行并行计算。但这些子线程是由 Node.js 主线程管理的。
  3. 对于 I/O 密集型的任务,如网络请求、文件读写等,Node.js 可以通过非阻塞 I/O 操作来实现高并发,而不需要多线程。
  4. Node.js 还支持原生的 Cluster 模块,可以启动多个子进程来实现负载均衡。但这些子进程也是由主进程管理的。

总的来说,Node.js 的核心是单线程的,但通过事件驱动和异步 I/O 操作,它可以实现高并发的能力。对于 CPU 密集型的任务,则可以使用 Worker Threads 来创建子线程进行并行计算。

36.node 是怎么处理并发的

Node.js 通过以下几种方式实现并发:

  1. 事件循环(Event Loop) : Node.js 使用单线程的事件循环来处理并发。事件循环不断检查事件队列,当有新事件到达时就立即处理。这样可以避免长时间阻塞主线程。
  2. 非阻塞 I/O: Node.js 使用非阻塞 I/O 操作,对于 I/O 密集型的任务(如网络请求、文件 I/O 等)不会阻塞主线程。相反,它会将 I/O 操作交给操作系统内核处理,然后继续处理其他任务。当 I/O 操作完成时,会触发回调函数来处理结果。
  3. 回调函数: Node.js 大量使用回调函数来实现异步编程。当一个 I/O 操作完成时,会调用相应的回调函数来处理结果,从而避免阻塞主线程。
  4. Promise 和 async/await: 为了更好地管理异步代码,Node.js 支持 Promise 和 async/await 语法。这些语法糖可以帮助开发者编写更加直观和易读的异步代码。
  5. Worker Threads: 对于 CPU 密集型的任务,Node.js 提供了 Worker Threads 模块,允许开发者创建子线程来执行并行计算。子线程之间通过消息传递来交换数据。
  6. Cluster 模块: Cluster 模块允许 Node.js 应用程序利用多核 CPU 的能力。它可以创建多个子进程来分担工作负载,从而提高整体的吞吐量。

总的来说,Node.js 通过事件循环、非阻塞 I/O、回调函数、Promise/async-await 以及 Worker Threads 和 Cluster 模块等方式实现了并发处理能力。这些机制使 Node.js 能够在单线程上高效地处理大量并发请求。

37.node 事件循环机制

38.JS 的单线程和浏览器的多进程架构

概述 此外,JS 最初是为了解决⽹⻚交互的问题⽽诞⽣的,⽽⽹⻚交互的需求⼤部分是基于⽤户事件的,⽐如点击按钮、输⼊⽂本等。这些操作的响应速度要求很⾼,如果在响应事件的同时还要处理其他任务,可能会导致⽹⻚卡顿、响应变慢等⽤户体验不佳的问题。

为了利⽤多核 CPU 的计算能⼒,HTML5 提出 Web Worker 标准,允许 JS 脚本创建多个线程,但是⼦线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JS 单线程的本质。

浏览器的多进程架构 “JS 是单线程的” 指的是执⾏ JS 的线程只有⼀个,是浏览器提供的 JS 引擎线程(主线程)。

如今的主流浏览器都是多进程架构的,以 Chrome 为例,它包含了 1 个浏览器主进程、1个 GPU 进程、1 个⽹络进程、多个渲染进程或多个插件进程。默认情况下,Chrome 会为每个 Tab 标签创建⼀个渲染进程。⼀个渲染进程通常由以下线程组成:

JS 引擎线程(主线程): JavaScript 引擎,也称为 JS 内核,负责处理 JS 脚本,执⾏代码。当主线程空闲且任务队列不为空时,会依次取出任务执⾏。注意,该线程与 GUI 渲染线程互斥,当 JS 引擎线程执⾏ JS 时间过⻓,将导致⻚⾯渲染的阻塞。 GUI 渲染线程: 主要负责⻚⾯的渲染,解析 HTML、CSS,构建DOM树,布局和绘制等。当界⾯需要重绘或者由于某种操作引发重排时,将执⾏该线程。注意:该线程与 JS 引擎线程互斥,当执⾏ JS 引擎线程时,GUI 线程会被挂起,当任务队列空闲时,主线程才会去执⾏ GUI 渲染。 事件触发线程: ⽤于控制事件循环,将准备好的事件交给 JS 引擎线程执⾏。当主线程遇到异步任务,如 setTimeOut(或 ajax 请求、⿏标点击事件),会将它们交由对应的线程处理,处理完毕后,事件触发线程会把对应的事件添加到任务队列的尾部,等待 JS 引擎的处理。注意:由于 JS 的单线程关系,队列中的待处理事件都得排队等待,只有在 JS 引擎空闲时才能被执⾏。 定时器触发线程: 负责执⾏定时器⼀类函数的线程,如 setTimeout,setInterval 等。主线程依次执⾏代码时,遇到定时器,会将定时器交由该线程进⾏计时,当计时结束,事件触发线程会将定时器的回调函数添加到任务队列的尾部,等待 JS 引擎空闲后执⾏。 异步 http 请求线程: 负责执⾏异步请求⼀类的函数的线程,如 Promise,axios,ajax 等。主线程依次执⾏代码时,遇到异步请求,会将函数交给该线程处理。当监听到状态码变更,如果设置有回调函数,事件触发线程会将相应的回调函数添加到任务队列的尾部,等待 JS 引擎空闲后执⾏。