interview(1)

Posted on:October 19, 2023 at 02:05 PM
预计阅读时长:147 min read 字数:29326
面试(其一)—2023-10-19=>2024-06-07

目录

basic

Hydration

Hydration Hydration(即“水合”)在前端框架和库中是一个重要的概念.它主要与服务器渲染(Server-Side Rendering,SSR)和客户端渲染(Client-Side Rendering,CSR)相结合.下面我将详细解释它的意义和应用:

服务器渲染 (SSR): 当一个页面首次加载时,如果使用服务器渲染,服务器会生成一个完整的 HTML 文档,并将其发送到客户端.这意味着浏览器可以立即看到内容,而不需要等待 JavaScript 下载和执行.

客户端渲染 (CSR): 在客户端渲染中,页面的大部分或全部内容由 JavaScript 在浏览器中动态生成.这意味着浏览器首先需要下载和执行 JavaScript,然后才能看到完整的内容.

水合 (Hydration): 当使用服务器渲染(SSR)并且随后启动客户端渲染(CSR)时,需要确保两者之间的内容一致性和可交互性.这就是“水合”所起的作用.水合是指客户端 JavaScript“接管”由服务器渲染生成的静态 HTML,将其转换为可交互的动态内容,同时保持用户界面的状态和事件处理程序.

在 React、Vue 和其他一些现代前端框架中,水合是自动处理的.框架会比较服务器渲染生成的 HTML 与客户端 JavaScript 生成的内容,并确保它们之间的一致性.

为什么水合很重要?

性能优化:通过使用服务器渲染和水合,您可以提供更快的首次加载时间和更好的性能体验.

SEO 友好:服务器渲染生成的静态 HTML 对搜索引擎更友好,这有助于提高页面的搜索排名.

用户体验:通过尽快显示内容并立即添加交互性,可以提供更好的用户体验.

总之,水合是前端开发中一个关键的概念,它结合了服务器渲染和客户端渲染的优点,为用户提供了更好的性能和体验.

Closure

闭包是对外部作用域变量的实时引用,而非复制

2024/01/08 js闭包以及应用

闭包(Closure)是JavaScript中的一个重要概念,它与作用域、变量生命周期等紧密相关.闭包在JavaScript中有着广泛的应用,下面我将首先简要解释闭包的概念,然后列举一些常见的闭包应用场景.

什么是闭包?

闭包是指有权访问另一个函数作用域中的变量的函数.简单来说,当一个函数在另一个函数内部定义时,并且这个内部函数引用了外部函数的变量,那么就产生了一个闭包.

闭包的形成条件:

  1. 函数嵌套:一个函数定义在另一个函数内部.
  2. 内部函数引用了外部函数的变量.

闭包的应用:

  1. 封装私有变量和方法

    function createCounter() {
      let count = 0;
      return {
        increment: function () {
          count++;
          return count;
        },
        decrement: function () {
          count--;
          return count;
        },
      };
    }
    const counter = createCounter();
    console.log(counter.increment()); // 输出 1
    console.log(counter.decrement()); // 输出 0
    
  2. 模块模式:通过闭包可以创建私有变量和方法,从而实现模块化的编程.

  3. 事件监听器

    function attachEvent() {
      let count = 0;
      document.getElementById("btn").addEventListener("click", function () {
        count++;
        console.log(`Button clicked ${count} times.`);
      });
    }
    attachEvent();
    
  4. 循环与闭包:在循环中使用闭包时,需要特别注意闭包与循环变量的交互.

    for (var i = 0; i < 5; i++) {
      setTimeout(function () {
        console.log(i); // 输出 5, 5, 5, 5, 5
      }, 1000);
    }
    

    上述代码会输出五次5,因为循环结束后i的值为5.为了解决这个问题,可以使用IIFE(立即调用函数表达式).

    for (var i = 0; i < 5; i++) {
      (function (j) {
        setTimeout(function () {
          console.log(j); // 输出 0, 1, 2, 3, 4
        }, 1000);
      })(i);
    }
    
  5. 实现函数柯里化:通过闭包,可以实现函数柯里化,即将多参数的函数转化为一系列使用一个参数的函数.

  6. 缓存:通过闭包缓存计算结果,提高性能.

    function memoize(fn) {
      const cache = {};
      return function (...args) {
        const key = JSON.stringify(args);
        if (cache[key] === undefined) {
          cache[key] = fn(...args);
        }
        return cache[key];
      };
    }
    

总结:闭包是JavaScript中的一个强大而灵活的特性,可以用于创建私有变量和方法、实现模块化、事件监听等.但同时也需要注意闭包可能带来的内存泄漏问题,因为闭包会保留外部函数的作用域链.

面经

此一处陌生的地方,不过是心魂之旅中的一处景观、一次际遇,未来的路途一样还是无限之问.

牛客1-偏算法

您了解的排序算法有哪些?/这些排序算法是如何实现的?

Click to expand 我了解的排序算法包括但不限于以下几种:

  1. 冒泡排序(Bubble Sort): 通过不断交换相邻元素,将最大(或最小)的元素逐步“冒泡”到最后(或最前)的位置.

  2. 选择排序(Selection Sort): 每次从未排序的元素中选取最小(或最大)的元素,放到已排序部分的末尾.

  3. 插入排序(Insertion Sort): 将待排序的元素逐个插入到已排序部分的合适位置,从而实现排序.

  4. 快速排序(Quick Sort): 选择一个基准元素,将小于基准的元素放到基准的左侧,大于基准的元素放到右侧,然后递归地对左右两部分进行排序.

  5. 归并排序(Merge Sort): 将待排序的数组不断地分割成更小的数组,直到每个数组只有一个元素,然后将这些小数组合并成一个新的有序数组.

  6. 堆排序(Heap Sort): 将待排序的数组构建成一个二叉堆,然后不断地移除堆顶元素(最大或最小),并重新调整堆,直到所有元素都被移除.

  7. 计数排序(Counting Sort): 统计数组中每个元素的出现次数,然后根据元素的大小顺序,将其放置到正确的位置上.

  8. 桶排序(Bucket Sort): 将待排序的元素分配到不同的桶中,对每个桶中的元素进行排序,然后按照顺序合并所有桶.

  9. 基数排序(Radix Sort): 将待排序的数字按照位数进行分组,然后按照每一位的顺序依次进行排序,直到所有位都排序完成.

每种排序算法都有其优缺点,适用于不同的场景.例如,对于小规模的数据集,插入排序可能更快;对于大规模的数据集,快速排序或归并排序可能更合适.选择合适的排序算法取决于数据的特点、排序的稳定性要求、内存空间等因素.

分析一下冒泡排序的时间复杂度?

Click to expand

冒泡排序的时间复杂度取决于比较和交换的次数.在冒泡排序中,每次都会比较相邻的两个元素,并根据需要交换它们的位置,直到整个数组排序完成.

假设待排序的数组长度为 n,冒泡排序的基本思想是通过多次遍历数组,每次都比较相邻的两个元素,将较大的元素交换到数组的末尾.在第一次遍历结束后,最大的元素会被交换到数组的最后一个位置;在第二次遍历中,第二大的元素会被交换到倒数第二个位置,依次类推.

最坏情况下的时间复杂度:

在最坏的情况下,即待排序数组是逆序排列的情况下,冒泡排序需要进行 n-1 轮比较和交换.在每一轮比较中,需要比较 n-i-1 次(其中 i 是当前轮数),因为每次都会将当前轮最大的元素交换到数组的末尾.因此,总的比较次数为:

[(n1)+(n2)+...+1=n(n1)2][ (n-1) + (n-2) + ... + 1 = \frac{n \cdot (n-1)}{2} ]

因此,冒泡排序的最坏情况下的时间复杂度为 O(n2)O(n^2)

最好情况下的时间复杂度:

在最好的情况下,即待排序数组已经是有序的情况下,冒泡排序只需要进行一轮遍历,每次比较都不需要交换元素.因此,最好情况下的时间复杂度为 O(n)O(n).

平均情况下的时间复杂度:

在平均情况下,冒泡排序需要进行 (n(n1)4)( \frac{n \cdot (n-1)}{4} ) 次比较和 (n(n1)4)( \frac{n \cdot (n-1)}{4} ) 次交换,因此平均时间复杂度仍为 (O(n2))( O(n^2) ).

总结:

冒泡排序的时间复杂度在最坏和平均情况下都是 (O(n2))( O(n^2) ),因此它是一种效率较低的排序算法,不适用于大规模数据的排序.然而,由于其简单易懂的实现方式,冒泡排序在学习排序算法和简单数据集的排序中仍然具有一定的价值.

js实现冒泡排序

function bubbleSort(arr) {
  const len = arr.length;
  let swapped; // 用于标记本轮是否有元素交换的标志

  // 外层循环控制轮数
  for (let i = 0; i < len - 1; i++) {
    swapped = false; // 在每轮开始前重置标志为 false

    // 内层循环控制比较和交换
    for (let j = 0; j < len - 1 - i; j++) {
      // 如果前面的元素比后面的元素大,则交换它们的位置
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 使用解构赋值交换元素
        swapped = true; // 标记本轮有元素交换
      }
    }

    // 如果本轮没有元素交换,则数组已经有序,直接退出循环
    if (!swapped) {
      break;
    }
  }

  return arr;
}

// 示例
const arr = [64, 34, 25, 12, 22, 11, 90];
console.log("原始数组:", arr);
console.log("排序后数组:", bubbleSort(arr));

分析一下快速排序的时间复杂度

Click to expand

快速排序是一种常用且高效的排序算法,其平均时间复杂度为 (O(nlogn))(O(n \log n)),最坏情况下的时间复杂度为 (O(n2))(O(n^2)),其中 (n)(n) 是待排序数组的长度.以下是快速排序时间复杂度的分析:

平均情况下的时间复杂度:

在平均情况下,快速排序的时间复杂度为 (O(nlogn))(O(n \log n)).这是因为快速排序的基本操作是通过将数组分成两部分,使得左边部分的元素都小于右边部分的元素,并且基准元素(pivot)在排序后处于正确的位置.然后,递归地对左右两部分进行排序.在每一次分割过程中,需要遍历数组一次,因此快速排序的时间复杂度是 (O(n))(O(n));而在平均情况下,每次分割都能将数组划分为大小相近的两部分,因此需要的递归层数为 (O(logn))(O(\log n)).因此,平均情况下快速排序的时间复杂度为 (O(nlogn))(O(n \log n)).

最坏情况下的时间复杂度:

在最坏情况下,快速排序的时间复杂度为 (O(n2))(O(n^2)).最坏情况发生在每次分割后,基准元素选取不合适,导致左右两部分的大小差别很大,从而导致递归树呈现出极端不平衡的状态.例如,当待排序数组已经有序时,如果每次选择的基准元素都是第一个或最后一个元素,那么快速排序的时间复杂度将会退化到 (O(n2))(O(n^2)).因此,为了避免最坏情况的发生,一般采用随机选择基准元素或者三数取中法等方法来提高快速排序的性能.

最好情况下的时间复杂度:

在最好情况下,快速排序的时间复杂度也是 (O(nlogn))(O(n \log n)).最好情况发生在每次分割后,左右两部分的大小相等,递归树呈现出平衡状态.这样,在每一层递归中,都会将数组大小缩减为原来的一半,因此需要的递归层数为 (O(logn))(O(\log n)),每次分割的时间复杂度为 (O(n))(O(n)),因此最好情况下的时间复杂度为 (O(nlogn))(O(n \log n)).

综上所述,快速排序的平均时间复杂度为 (O(nlogn))(O(n \log n)),最坏情况下的时间复杂度为 (O(n2))(O(n^2)),最好情况下的时间复杂度为 (O(nlogn))(O(n \log n)).由于其平均时间复杂度较低,因此快速排序是一种高效的排序算法,常被应用于实际开发中.

了解heap内部是如何实现的?构造函数和push函数的时间复杂度是多少?分别详细讲解这两个函数实现了什么,如何得到该时间复杂度?

Click to expand

在 JavaScript 中,通常使用数组来实现堆(Heap).堆是一种特殊的二叉树结构,其中每个节点的值都大于等于(或小于等于)其子节点的值,根据这个特性,堆可以分为最大堆和最小堆.

构造函数的实现和时间复杂度:

构造函数主要用于创建一个空的堆或者根据给定的数组构建一个堆.在构造函数中,通常会执行一次堆化操作,将无序的数组转换为满足堆的特性的数组.

构造函数的实现步骤通常如下:

  1. 将给定数组赋值给堆的内部数组.
  2. 从数组的中间位置开始向前遍历,对每个非叶子节点执行下沉操作(sinkDown),以确保每个节点都满足堆的特性.

构造函数的时间复杂度取决于堆化操作的复杂度,通常为 (O(n))(O(n)),其中 (n)(n) 是数组的长度.因为堆化操作中,我们需要对每个非叶子节点执行一次下沉操作,而非叶子节点的数量约为数组长度的一半.

push 函数的实现和时间复杂度:

push 函数用于向堆中添加一个新的元素,并保持堆的特性.通常,push 操作是将新元素添加到数组的末尾,然后执行一次上浮操作(percolateUp).

push 函数的实现步骤如下:

  1. 将新元素添加到数组的末尾.
  2. 对新添加的元素执行一次上浮操作(percolateUp),将其与父节点进行比较并交换,直到满足堆的特性.

push 函数的时间复杂度取决于上浮操作的复杂度,通常为 (O(logn))(O(\log n)),其中 (n)(n) 是堆的大小.因为上浮操作中,我们需要将新添加的元素与其父节点进行比较并交换,直到满足堆的特性.最坏情况下,新添加的元素可能需要上浮到堆的根节点位置,因此上浮操作的时间复杂度与堆的高度有关,堆的高度通常为 (logn)(\log n).

总结:
  • 构造函数的时间复杂度为 (O(n))(O(n)),其中 (n)(n) 是数组的长度.
  • push 函数的时间复杂度为 (O(logn))(O(\log n)),其中 (n)(n) 是堆的大小.

这些时间复杂度是根据堆的特性以及 push 和构造函数的实现步骤得出的.因为堆是一种树状结构,其操作的时间复杂度通常与树的高度相关,而堆的高度通常为 (logn)(\log n),因此 push 函数的时间复杂度为 (O(logn))(O(\log n)).

牛客2-偏框架

React 组件的实现原理

React 组件通过 React.createElement 创建元素,通过虚拟 DOM 维护状态变化,类组件和函数组件分别通过生命周期方法和 Hooks 处理状态和副作用.组件的渲染和更新机制包括初始化渲染、状态更新、diff 算法和 DOM 更新.

不同文件在浏览器中的存放形式

请看vcr

在浏览器中,网页的不同文件类型通过不同的机制加载和存储.以下是一些常见文件类型及其在浏览器中的存放和处理方式:

HTML 文件

存放形式

  • 文档对象模型 (DOM):浏览器将 HTML 文件解析成 DOM 树,这是 HTML 文档的内存表示.每个 HTML 元素和属性在 DOM 中都作为节点存在.

处理

  • 浏览器从服务器加载 HTML 文件,将其解析为 DOM 树,然后通过 DOM API 可以动态操作这棵树.
CSS 文件

存放形式

  • 层叠样式表对象模型 (CSSOM):浏览器将 CSS 文件解析成 CSSOM 树,这与 DOM 树类似,是 CSS 样式的内存表示.

处理

  • 浏览器将 CSS 文件与 HTML 文件结合,通过 CSSOM 和 DOM 树共同决定页面的呈现样式.
JavaScript 文件

存放形式

  • 脚本引擎内存:JavaScript 文件被浏览器加载并由 JavaScript 引擎(如 V8、SpiderMonkey 等)解析和执行.脚本内容会被保存在内存中.

处理

  • JavaScript 文件可以通过 <script> 标签内联在 HTML 文件中,或者作为外部文件通过 <script src="..."> 引入.
  • 浏览器会解析并执行这些脚本,脚本可以操作 DOM、CSSOM,以及执行其他任务.
图像文件(如 JPEG、PNG、GIF 等)

存放形式

  • 内存中的位图:浏览器将图像文件解码成内存中的位图,具体实现方式依赖于浏览器和操作系统.

处理

  • 图像文件通过 <img> 标签加载,或作为背景图像在 CSS 中定义.浏览器会从网络加载这些文件,并在解码后渲染到页面上.
视频和音频文件(如 MP4、WebM、MP3 等)

存放形式

  • 媒体缓冲区:浏览器将视频和音频文件加载到媒体缓冲区中,以便进行流式传输和播放.

处理

  • 这些文件通过 <video><audio> 标签加载,并由浏览器的内置媒体播放器解码和播放.
字体文件(如 TTF、WOFF、WOFF2 等)

存放形式

  • 内存中的字体对象:浏览器将字体文件加载并解析成内存中的字体对象.

处理

  • 字体文件通过 CSS 中的 @font-face 规则加载.加载后的字体会用于页面中的文本渲染.
JSON、XML 等数据文件

存放形式

  • 内存中的数据对象:JSON 和 XML 文件在加载后会被解析成 JavaScript 对象或 DOM 对象,保存在内存中.

处理

  • 这些文件通常通过 JavaScript 的 fetchXMLHttpRequest 加载,并用于 AJAX 请求的数据交换.
例子:HTML 页面加载过程

当浏览器加载一个包含 HTML、CSS 和 JavaScript 的网页时,典型的过程如下:

  1. HTML 加载和解析

    • 浏览器请求 HTML 文件并开始解析,构建 DOM 树.
    • 在解析过程中,如果遇到 CSS 文件引用,浏览器会并行请求这些 CSS 文件.
  2. CSS 加载和解析

    • CSS 文件被加载和解析,构建 CSSOM 树.
    • 浏览器将 CSSOM 树与 DOM 树结合,生成渲染树.
  3. JavaScript 加载和执行

    • 如果 HTML 中有 <script> 标签,浏览器会按顺序加载和执行 JavaScript 文件.
    • JavaScript 可以操作已构建的 DOM 树和 CSSOM 树.
  4. 资源加载

    • 图像、字体、视频等资源文件被并行加载.
    • 浏览器在资源加载完成后,将其解码并渲染到页面上.
  5. 页面渲染

    • 浏览器根据渲染树计算每个节点的布局和样式.
    • 最终,浏览器将内容绘制到屏幕上.
浏览器缓存
  • 缓存机制:浏览器会缓存已加载的文件(HTML、CSS、JavaScript、图像等),以减少重复请求,提升性能.
  • 缓存位置:文件缓存通常存储在浏览器的本地缓存中,可以通过 HTTP 头(如 Cache-ControlETag)管理缓存策略.

通过理解不同文件在浏览器中的存放形式和处理方式,可以更好地优化网页性能和用户体验.

vuex在哪个阶段挂在在vue实例上

Vuex 是一个独立的状态管理库,它并不是在 Vue 实例上挂载的,而是通过 Vue.use() 方法在 Vue 应用中进行安装.通常,Vuex 是在 Vue 根实例创建之前被安装的.

import Vue from "vue";
import Vuex from "vuex";
import App from "./App.vue";

// 1. 使用 Vuex 插件
Vue.use(Vuex);

// 2. 创建 Vuex store 实例
const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
  },
});

// 3. 创建根 Vue 实例
new Vue({
  el: "#app",
  store, // 将 Vuex store 实例挂载到根实例上
  render: h => h(App),
});

在实际开发中shouldComponentUpdate有什么作用

  • 避免在shouldComponentUpdate中进行过多或复杂的计算,以免影响性能.
  • 如果shouldComponentUpdate返回false,那么render、componentDidUpdate等生命周期方法将不会被执行.
  • 使用PureComponent或React.memo可以在某种程度上自动实现shouldComponentUpdate的功能,它们会对组件的属性进行浅比较,来决定是否重新渲染组件.

某瓣-面试(未回答上/回答不好)

Ts typeof keyof

  • typeof 操作符有两个主要用途:

    1.在运行时获取变量的类型

    2.在编译时获取变量或对象的类型

interface Person {
  name: string;
  age: number;
}

const person: Person = { name: "John", age: 25 };

// 使用ts 的 typeof 获取 person 的类型
type PersonType = typeof person;

const anotherPerson: PersonType = { name: "Jane", age: 30 };
  • keyof

keyof 操作符用于获取某种类型的所有键,并将其联合成一个联合类型.

keyof 操作符用于获取某种类型的所有键,并将其联合成一个联合类型.

ts Omit

用于创建一个新的类型,该类型从另一个类型中排除指定的键

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

示例:假设我们有一个用户类型 User,包含 id, name, 和 email 三个属性.我们希望创建一个新的类型,排除 email 属性.

type User = {
  id: number;
  name: string;
  email: string;
};

type UserWithoutEmail = Omit<User, "email">;

const user: UserWithoutEmail = {
  id: 1,
  name: "John Doe",
  // email 属性不需要包含在 UserWithoutEmail 类型中
};

在 React 项目中,Omit 常用于处理组件的 Props,特别是当我们需要复用一些通用 Props 并排除某些特定属性时.

type BaseButtonProps = {
  onClick: () => void;
  label: string;
  disabled?: boolean;
};

type IconButtonProps = Omit<BaseButtonProps, "label"> & {
  icon: string;
};

const IconButton: React.FC<IconButtonProps> = ({ onClick, icon, disabled }) => (
  <button onClick={onClick} disabled={disabled}>
    <i className={`icon-${icon}`} />
  </button>
);

结合其他高级类型如 Partial 使用,Omit 能够大大增强 TypeScript 类型系统的表达能力,帮助开发者编写更健壮和可维护的代码

type User = {
  id: number;
  name: string;
  email: string;
  age: number;
};

type PartialUserWithoutAge = Partial<Omit<User, "age">>;

const partialUser: PartialUserWithoutAge = {
  name: "ajn404",
  email: "ajn404@example.com",
  // id 属性是可选的,并且 age 属性被排除在 PartialUserWithoutAge 类型之外
};

react redux具体流程

Click to expand 创建 Store:使用 createStore 函数创建 Redux store.


定义 Action:创建表示用户行为或事件的 action 对象.


编写 Reducer:根据 action 来修改 store 中的 state.


连接 React 组件:使用 Provider 和 connect,将 Redux store 与 React 组件连接起来.


通用的弹窗(Modal)组件

设计一个通用的弹窗(Modal)组件

设计一个通用的弹窗(Modal)组件 API 可以让你的 React 应用更加模块化和可维护.以下是一个如何在 React 中实现通用弹窗组件的步骤和示例.

1. 创建 Modal 组件

首先,我们需要创建一个基础的 Modal 组件,它可以接受不同的内容和配置.

// Modal.js
import React from "react";
import "./Modal.css"; // 你可以创建一个 CSS 文件来定义 Modal 的样式

const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        <button className="modal-close-button" onClick={onClose}>
          X
        </button>
        {children}
      </div>
    </div>
  );
};

export default Modal;
2. 创建 ModalProvider 和 useModal Hook

为了让弹窗在整个应用中可用,我们可以创建一个上下文(Context)来管理弹窗的状态和提供相关的 API.

// ModalContext.js
import React, { createContext, useContext, useState, useCallback } from "react";
import Modal from "./Modal";

const ModalContext = createContext();

export const ModalProvider = ({ children }) => {
  const [modalContent, setModalContent] = useState(null);
  const [isOpen, setIsOpen] = useState(false);

  const openModal = useCallback(content => {
    setModalContent(content);
    setIsOpen(true);
  }, []);

  const closeModal = useCallback(() => {
    setModalContent(null);
    setIsOpen(false);
  }, []);

  return (
    <ModalContext.Provider value={{ openModal, closeModal }}>
      {children}
      <Modal isOpen={isOpen} onClose={closeModal}>
        {modalContent}
      </Modal>
    </ModalContext.Provider>
  );
};

export const useModal = () => {
  return useContext(ModalContext);
};
3. 使用 ModalProvider 包裹应用

在应用的根组件中,使用 ModalProvider 包裹整个应用.

// App.js
import React from "react";
import { ModalProvider } from "./ModalContext";
import HomePage from "./HomePage";

const App = () => {
  return (
    <ModalProvider>
      <HomePage />
    </ModalProvider>
  );
};

export default App;
4. 使用 useModal Hook 打开和关闭弹窗

在你希望打开弹窗的任何组件中,可以使用 useModal Hook 来访问 openModalcloseModal 方法.

// HomePage.js
import React from "react";
import { useModal } from "./ModalContext";

const HomePage = () => {
  const { openModal } = useModal();

  const handleOpenModal = () => {
    openModal(<div>This is the content of the modal!</div>);
  };

  return (
    <div>
      <h1>Home Page</h1>
      <button onClick={handleOpenModal}>Open Modal</button>
    </div>
  );
};

export default HomePage;
5. 样式

为了使弹窗看起来更好,可以添加一些基本的 CSS 样式.

/* Modal.css */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 5px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  position: relative;
}

.modal-close-button {
  position: absolute;
  top: 10px;
  right: 10px;
  background: none;
  border: none;
  font-size: 1.2rem;
  cursor: pointer;
}

通过这种方式,我们创建了一个通用的弹窗 API,可以在应用的任何地方轻松打开和关闭弹窗,并且弹窗的内容可以动态变化.我们利用了 React 的 Context API 和 Hooks 来管理弹窗的状态,并确保弹窗组件是可复用的和灵活的.

这使得我们的代码更加模块化,弹窗逻辑集中管理,同时还确保了使用的方便性.如果有更多需求,例如支持多种类型的弹窗(确认弹窗、表单弹窗等),可以在 ModalContext 中扩展相应的逻辑和 API.

如何设计弹窗的动画,比如打开时淡入,关闭时淡出
// Modal.js
import React from "react";
import "./Modal.css";

const Modal = ({ isOpen, onClose, children }) => {
  return (
    <div className={`modal-overlay ${isOpen ? "fade-in" : "fade-out"}`}>
      <div className={`modal-content ${isOpen ? "slide-in" : "slide-out"}`}>
        <button className="modal-close-button" onClick={onClose}>
          X
        </button>
        {children}
      </div>
    </div>
  );
};

export default Modal;

当然这里也可以添加一个prop来控制动画的持续时间/控制是否显示动画

/* Modal.css */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  transition: opacity 0.5s;
}

.modal-overlay.fade-in {
  opacity: 1;
}

.modal-overlay.fade-out {
  opacity: 0;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 5px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  position: relative;
  transform: translateY(-50px);
  transition: transform 0.5s;
}

.modal-content.slide-in {
  transform: translateY(0);
}

.modal-content.slide-out {
  transform: translateY(-50px);
}

.modal-close-button {
  position: absolute;
  top: 10px;
  right: 10px;
  background: none;
  border: none;
  font-size: 1.2rem;
  cursor: pointer;
}

为了确保动画完成后才移除弹窗,在 ModalProvider 中稍微修改关闭逻辑

// ModalContext.js
import React, { createContext, useContext, useState, useCallback } from "react";
import Modal from "./Modal";

const ModalContext = createContext();

export const ModalProvider = ({ children }) => {
  const [modalContent, setModalContent] = useState(null);
  const [isOpen, setIsOpen] = useState(false);

  const openModal = useCallback(content => {
    setModalContent(content);
    setIsOpen(true);
  }, []);

  const closeModal = useCallback(() => {
    setIsOpen(false);
    // 延迟清理内容,等待动画完成
    setTimeout(() => setModalContent(null), 500);
  }, []);

  return (
    <ModalContext.Provider value={{ openModal, closeModal }}>
      {children}
      <Modal isOpen={isOpen} onClose={closeModal}>
        {modalContent}
      </Modal>
    </ModalContext.Provider>
  );
};

export const useModal = () => {
  return useContext(ModalContext);
};

两数之和

Click to expand 哈希表

function twoSum(nums, target) {
  const numIndices = {}; // 用对象代替哈希表

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];

    if (numIndices.hasOwnProperty(complement)) {
      return [numIndices[complement], i];
    }

    numIndices[nums[i]] = i;
  }

  return null;
}

// 示例
const nums = [2, 7, 11, 15];
const target = 9;
const result = twoSum(nums, target);
console.log(result); // 输出: [0, 1]

数组拆分多个和近似的数组

Click to expand

贪心

function splitArrayIntoApproxEqualSums(nums, k) {
  // 步骤1:对数组进行降序排序
  nums.sort((a, b) => b - a);

  // 步骤2:初始化子数组
  const result = Array.from({ length: k }, () => []);

  // 步骤3:逐个元素分配到和最小的子数组中
  const sums = Array(k).fill(0);

  for (let num of nums) {
    // 找到和最小的子数组
    let minIndex = 0;
    for (let i = 1; i < k; i++) {
      if (sums[i] < sums[minIndex]) {
        minIndex = i;
      }
    }
    // 分配当前元素到该子数组
    result[minIndex].push(num);
    sums[minIndex] += num;
  }

  return result;
}

// 示例
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const k = 3;
const result = splitArrayIntoApproxEqualSums(nums, k);
console.log(result);

ts中范型的作用

范型类允许类的属性和方法使用范型,从而可以适用于多种类型.
范型接口使得接口中的属性和方法可以使用范型,从而可以适用于多种类型.
有时你需要对范型进行约束,使其只能是某些类型的子类型.可以使用 extends 关键字来实现范型约束.
通过范型,函数、类和接口可以适用于多种类型,而不丧失类型检查的优势.

博客项目如何实现模糊

css blur(2px)

文字渐变

color结合animation-delay

@keyframes anim-text-flow-keys {
  @for $i from 0 through $animationSteps {
    #{percentage($i * calc(1 / $animationSteps))} {
      color: hsla(random(365), 60%, 60%, 1);
    }
  }
}

...

$totalDelayTime: $animationElementsCount * $delayBetweenLetters;

    @for $i from 1 through $animationElementsCount {
      #{$animationElement}:nth-of-type(#{$i}) {
        animation-delay: #{($i * $delayBetweenLetters) - $totalDelayTime}s;
      }
    }

react ts 设计Table组件,设计option组件,table组件中的数据可以选择,通过option组件

参考@tanstack/react-table的table组件设计

NameAgeCountry
John Doe28USA
Jane Smith34UK
Sam Green23Canada
小小table,拿下拿下
import { useState } from "react";

// 定义类型

export interface Column {
  header: string;
  accessor: string;
}

export interface Row {
  [key: string]: any;
}

export interface TableOptions {
  className?: string;
}

export interface OptionConfig {
  label: string;
  name: string;
  type: "text" | "number" | "select";
  value: string | number;
  options?: Array<{ label: string; value: string | number }>; // Only for select type
}

export interface TableState {
  selectedRows: Set<number>;
  allSelected: boolean;
}

interface UseTableOptions {
  columns: Column[];
  data: Row[];
}

//使用 useTable hook 来管理表格的状态和渲染逻辑.
//在 useTable Hook 中增加管理选中状态的逻辑
//通过 state 管理选中的行和全选状态.

export const useTable = ({ columns, data }: UseTableOptions) => {
  const [rows, setRows] = useState<Row[]>(data);
  const [state, setState] = useState<TableState>({
    selectedRows: new Set<number>(),
    allSelected: false,
  });

  const toggleRowSelection = (index: number) => {
    setState(prevState => {
      const selectedRows = new Set(prevState.selectedRows);
      if (selectedRows.has(index)) {
        selectedRows.delete(index);
      } else {
        selectedRows.add(index);
      }
      return {
        ...prevState,
        selectedRows,
        allSelected: selectedRows.size === rows.length,
      };
    });
  };

  const toggleAllSelection = () => {
    setState(prevState => {
      const allSelected = !prevState.allSelected;
      const selectedRows = allSelected
        ? new Set(rows.map((_, index) => index))
        : new Set<number>();
      return { ...prevState, selectedRows, allSelected };
    });
  };

  const getTableProps = () => ({
    // Add additional table props here
  });

  const getHeaderProps = () => ({
    // Add additional header props here
  });

  const getRowProps = (row: Row, index: number) => ({
    // Add additional row props here
    key: index,
  });

  const getCellProps = (cell: any) => ({
    // Add additional cell props here
  });

  return {
    rows,
    columns,
    state,
    toggleRowSelection,
    toggleAllSelection,
    getTableProps,
    getHeaderProps,
    getRowProps,
    getCellProps,
  };
};

// 在表头和每一行中添加复选框.
// 使用 toggleAllSelection 方法处理全选逻辑.
// 使用 toggleRowSelection 方法处理行选中逻辑.

interface TableProps {
  columns: Column[];
  data: Row[];
  options: TableOptions;
}

const Table: React.FC<TableProps> = ({ columns, data, options }) => {
  const {
    rows,
    state,
    toggleRowSelection,
    toggleAllSelection,
    getTableProps,
    getHeaderProps,
    getRowProps,
    getCellProps,
  } = useTable({ columns, data });

  return (
    <table className={options?.className || ""} {...getTableProps()}>
      <thead>
        <tr {...getHeaderProps()}>
          <th>
            <input
              type="checkbox"
              checked={state.allSelected}
              onChange={toggleAllSelection}
            />
          </th>
          {columns.map((col, index) => (
            <th key={index}>{col.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {rows.map((row, rowIndex) => (
          <tr key={rowIndex} {...getRowProps(row, rowIndex)}>
            <td>
              <input
                type="checkbox"
                checked={state.selectedRows.has(rowIndex)}
                onChange={() => toggleRowSelection(rowIndex)}
              />
            </td>
            {columns.map((col, colIndex) => (
              <td key={colIndex} {...getCellProps(row[col.accessor])}>
                {row[col.accessor]}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

// 管理表格数据和选项的状态.
// 将初始数据和列配置传递给 Table 组件.
// 通过 Option 组件来实现数据的选择和过滤,并将结果传递给 Table 组件进行渲染.

const App: React.FC = () => {
  const initialData: Row[] = [
    { name: "John Doe", age: 28, country: "USA" },
    { name: "Jane Smith", age: 34, country: "UK" },
    { name: "Sam Green", age: 23, country: "Canada" },
  ];

  const columns: Column[] = [
    { header: "Name", accessor: "name" },
    { header: "Age", accessor: "age" },
    { header: "Country", accessor: "country" },
  ];

  const [data, setData] = useState<Row[]>(initialData);
  const [tableOptions, setTableOptions] = useState({ className: "my-table" });
  const [filterOptions, setFilterOptions] = useState<OptionConfig[]>([
    {
      label: "Filter by Country",
      name: "country",
      type: "select",
      value: "",
      options: [
        { label: "All", value: "" },
        { label: "USA", value: "USA" },
        { label: "UK", value: "UK" },
        { label: "Canada", value: "Canada" },
      ],
    },
  ]);

  const handleOptionChange = (name: string, value: string | number) => {
    setFilterOptions(prevOptions =>
      prevOptions.map(option =>
        option.name === name ? { ...option, value } : option
      )
    );

    if (name === "country") {
      setData(
        value ? initialData.filter(row => row.country === value) : initialData
      );
    }
  };

  return (
    <div>
      <Table columns={columns} data={data} options={tableOptions} />
    </div>
  );
};

export default App;

Webpack项目实现按需引入

关键在于 Babel 的 import 插件配置,它使得 Webpack 只打包实际使用的组件及其依赖,而不是整个库:

{
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd",
        "libraryDirectory": "es",
        "style": "css"
      }
    ]
  ]
}

React高阶组件及应用场景

简介

React高阶组件(Higher Order Components,HOC)是一种设计模式,用于在React应用中复用组件逻辑.它们本质上是函数,接受一个组件作为参数,并返回一个新的组件.

应用场景:
  1. 逻辑复用:当多个组件需要共享相同的逻辑时,可以将该逻辑封装在高阶组件中,并在需要时应用于其他组件.

  2. 渲染劫持:高阶组件可以拦截并修改组件的props,state或渲染结果,以达到特定的目的.比如,认证组件可以拦截未登录用户的请求并重定向到登录页面.

  3. 渲染助手:高阶组件可以提供一些辅助函数或渲染逻辑,使得组件更容易使用或更加灵活.

示例:
// 高阶组件示例:为组件添加日志记录功能
function withLogger(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log(`Component ${WrappedComponent.name} mounted`);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

// 使用高阶组件包装组件
const LoggedComponent = withLogger(MyComponent);

// 使用LoggedComponent渲染
ReactDOM.render(<LoggedComponent />, document.getElementById("root"));

在这个例子中,withLogger是一个高阶组件,它接受一个组件作为参数,并返回一个新的组件,该新组件在挂载时会输出日志信息.通过将MyComponent传递给withLogger,我们创建了一个具有日志功能的新组件LoggedComponent.

注意事项:
  • 命名约定:高阶组件的命名约定是以”With”开头,以便清晰地表明它是一个高阶组件.

  • 不要窃取props:在高阶组件内部,应该避免直接修改传入的props,而是应该使用props传递给包装组件.

  • 组件树修改:高阶组件可能会更改组件树的结构,因此要谨慎使用,并确保不会破坏组件的预期行为.

高阶组件是React中非常强大和灵活的工具,但也需要谨慎使用,以避免复杂化组件树和降低可维护性.

具体应用场景

当涉及到特定的应用场景时,高阶组件可以提供一些具体的解决方案:

  1. 认证和授权:在需要认证用户才能访问的页面中,可以使用高阶组件来包装需要授权的组件.这个高阶组件可以检查用户的登录状态,并根据需要进行重定向或显示相应的UI.
function withAuth(WrappedComponent) {
  return class extends React.Component {
    render() {
      if (isUserAuthenticated()) {
        return <WrappedComponent {...this.props} />;
      } else {
        return <Redirect to="/login" />;
      }
    }
  };
}
  1. 数据获取和状态管理:在需要获取和管理数据的组件中,可以使用高阶组件来处理数据的获取和状态管理逻辑.这可以使组件更加专注于UI渲染,而将数据逻辑与UI分离.
function withDataFetching(WrappedComponent, dataURL) {
  return class extends React.Component {
    state = {
      data: null,
      isLoading: true,
      error: null,
    };

    componentDidMount() {
      fetchData(dataURL)
        .then(data => this.setState({ data, isLoading: false }))
        .catch(error => this.setState({ error, isLoading: false }));
    }

    render() {
      const { data, isLoading, error } = this.state;
      return (
        <WrappedComponent
          data={data}
          isLoading={isLoading}
          error={error}
          {...this.props}
        />
      );
    }
  };
}
  1. 性能优化:在需要进行性能优化的组件中,可以使用高阶组件来封装一些性能相关的逻辑,比如数据的缓存或者懒加载等.
function withCache(WrappedComponent) {
  const cache = new Map();
  return class extends React.Component {
    render() {
      const cacheKey = JSON.stringify(this.props);
      if (cache.has(cacheKey)) {
        return cache.get(cacheKey);
      } else {
        const result = <WrappedComponent {...this.props} />;
        cache.set(cacheKey, result);
        return result;
      }
    }
  };
}

这些是一些常见的高阶组件应用场景,但实际上,高阶组件的应用范围非常广泛,可以根据具体的需求进行灵活使用和定制.

36*面试准备

js合并两个数组

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];

const mergedArray = array1.concat(array2);
console.log(mergedArray); // 输出: [1, 2, 3, 4, 5, 6]
const array1 = [1, 2, 3];
const array2 = [4, 5, 6];

const mergedArray = [...array1, ...array2];
console.log(mergedArray); // 输出: [1, 2, 3, 4, 5, 6]

内存泄漏

Click to expand

前端内存泄漏是指在应用程序运行过程中,一些不再需要使用的内存没有被正确释放,从而导致内存占用不断增加,最终可能导致应用程序性能下降甚至崩溃.以下是一些常见的前端内存泄漏问题及其解决方法:

常见的前端内存泄漏问题

  1. 未清理的事件监听器

    • 当给DOM元素添加事件监听器,但没有在元素被移除时移除这些监听器,可能导致内存泄漏.
  2. 未清理的定时器

    • 使用setIntervalsetTimeout创建定时器,但没有在不需要时清除它们,也会导致内存泄漏.
  3. 闭包

    • 闭包可以导致某些变量被引用而无法被垃圾回收器回收,从而导致内存泄漏.
  4. DOM引用

    • 保留对已经从DOM中移除的元素的引用,导致这些元素不能被垃圾回收.
  5. 全局变量

    • 全局变量或挂在全局对象(如window)上的属性不会自动释放,可能会导致内存泄漏.
解决方法
  1. 清理事件监听器
    • 在元素被移除前移除相关的事件监听器,或者使用像addEventListenerremoveEventListener来动态管理监听器.
function handleClick() {
  // 事件处理逻辑
}
element.addEventListener("click", handleClick);
// 当不再需要时移除监听器
element.removeEventListener("click", handleClick);
  1. 清理定时器
    • 在组件卸载或不再需要时,使用clearIntervalclearTimeout清除定时器.
const timerId = setInterval(() => {
  // 定时器逻辑
}, 1000);
// 当不再需要时清除定时器
clearInterval(timerId);
  1. 正确使用闭包
    • 尽量避免不必要的闭包,或在闭包内部确保没有多余的引用保留.
function createClosure() {
  let largeData = new Array(1000000).fill("*");
  return function () {
    console.log(largeData.length);
  };
}
let closure = createClosure();
// 当不再需要时可以显式地清理大对象
closure = null;
  1. 移除不必要的DOM引用
    • 当从DOM中移除元素时,确保也删除相关的JavaScript引用.
let element = document.getElementById("myElement");
document.body.removeChild(element);
// 删除JavaScript引用
element = null;
  1. 避免使用全局变量
    • 尽量避免使用全局变量,可以使用模块化的方法来组织代码,或者使用闭包来限制变量的作用域.
(function () {
  let localVar = "I am a local variable";
  // 代码逻辑
})();
检测工具
  1. 浏览器开发者工具

    • Chrome和Firefox的开发者工具中都包含用于内存分析的工具,如Heap snapshots、Allocation profiler等.
  2. 第三方库

    • 使用像leakagememlab等库来检测内存泄漏.
示例

以下是一个简单的示例,展示如何可能产生内存泄漏以及如何解决它.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Memory Leak Example</title>
  </head>
  <body>
    <button id="leakButton">Click me</button>
    <script>
      function createLeak() {
        const element = document.getElementById("leakButton");
        element.addEventListener("click", function onClick() {
          console.log("Button clicked");
        });
        // 内存泄漏:即使元素被移除,事件监听器仍然存在
      }

      createLeak();

      // 解决方法:在元素被移除前移除事件监听器
      function removeLeak() {
        const element = document.getElementById("leakButton");
        element.removeEventListener("click", onClick);
      }

      // 模拟移除元素
      document.getElementById("leakButton").remove();
      removeLeak();
    </script>
  </body>
</html>

通过上述步骤和方法,可以有效地避免和解决前端内存泄漏问题,从而提高应用程序的性能和稳定性.

二分查找的时间复杂度分析

初始查找范围为n.


每次查找后,范围减半,即变为n/2, n/4, n/8, …


经过k次查找后,查找范围缩小到1,即n / 2^k = 1.


通过方程求解得出k = log_2(n),因此时间复杂度为O(log n)


http建立连接过程,为什么不是四次或两次握手

Click to expand

HTTP协议本身是基于TCP协议的.TCP协议使用三次握手(Three-Way Handshake)来建立连接,这一过程确保了客户端和服务器之间的连接是可靠的,并且双方都准备好进行数据传输.以下是TCP三次握手建立连接的详细过程,以及为什么不是四次或两次握手.

TCP三次握手过程
  1. 第一次握手:客户端发送SYN

    • 客户端向服务器发送一个SYN(Synchronize)报文段,其中包含一个初始序列号(Sequence Number),表示客户端请求建立连接.
    • 客户端进入SYN_SENT状态.
  2. 第二次握手:服务器发送SYN-ACK

    • 服务器收到SYN报文段后,向客户端发送一个SYN-ACK(Synchronize-Acknowledge)报文段,其中包含服务器的初始序列号,以及对客户端SYN报文段的确认(ACK = 客户端的序列号 + 1).
    • 服务器进入SYN_RCVD状态.
  3. 第三次握手:客户端发送ACK

    • 客户端收到SYN-ACK报文段后,向服务器发送一个ACK(Acknowledgement)报文段,确认接收到服务器的SYN报文段(ACK = 服务器的序列号 + 1).
    • 客户端和服务器都进入ESTABLISHED状态,连接建立完成.
为什么不是四次握手?

四次握手会显得多余,因为在三次握手中已经可以确认双方的通信能力:

  • 第一次握手:客户端发起连接请求,服务器知道了客户端想要建立连接.
  • 第二次握手:服务器同意连接请求并确认了客户端的序列号,同时提供了自己的序列号.
  • 第三次握手:客户端确认了服务器的序列号,并且通知服务器,客户端的连接请求被确认.

在这三次交互中,客户端和服务器已经成功同步了初始序列号,并确认了彼此的通信能力,多一次握手并不能提供额外的可靠性保证.

为什么不是两次握手?

两次握手不能确保连接的可靠性,无法防止历史连接请求的干扰.假设使用两次握手:

  1. 第一次握手:客户端发送SYN

    • 客户端向服务器发送SYN报文段.
  2. 第二次握手:服务器发送SYN-ACK

    • 服务器收到SYN报文段后,发送SYN-ACK报文段.

在这种情况下,客户端无法确认服务器是否收到了SYN报文段并且愿意建立连接.假设由于网络问题,服务器的SYN-ACK报文段丢失,那么客户端会一直等待,导致资源浪费.同时,如果有旧的SYN报文段被服务器误认为是新的连接请求,也可能导致意外的连接建立.

三次握手的优势

三次握手通过以下方式确保了连接的可靠性和有效性:

  1. 确认双方通信能力:确保双方都能发送和接收数据.
  2. 防止旧的重复连接请求:通过序列号,能够区分不同的连接请求,防止由于网络延迟导致的历史报文干扰.
  3. 同步初始序列号:确保双方都能够正确地接收和处理数据包.

因此,TCP选择三次握手作为建立连接的过程,以确保连接的可靠性和有效性.

对rust的了解程度

我对Rust有一定的了解和使用经验.Rust是一种系统编程语言,注重安全性和性能.它的主要特点包括内存安全性、所有权模型和无数据竞争的并发编程.我曾在项目中使用Rust开发过高性能的服务器应用,利用其所有权系统来管理内存,避免常见的内存泄漏和并发问题.此外,我还熟悉Rust的包管理工具Cargo和其生态系统中的一些常用库,如Serde用于序列化和反序列化,以及Tokio用于异步编程

rust基础

当然,介绍Rust的基础知识可以帮助你在面试中展示对这门语言的了解.以下是一些关键的基础概念和示例代码,能够帮助你更好地理解Rust,并准备面试中的相关问题.

Rust基础概念
1. 所有权与借用
  • 所有权(Ownership): Rust的核心特性之一,每个值在任意时刻都有一个所有者.
  • 借用(Borrowing): 可以通过引用借用值,但不能改变所有权.

示例代码:

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1; // 借用 s1
    println!("{}", s2);
    // s1 仍然可以使用
    println!("{}", s1);
}
2. 生命周期(Lifetimes)
  • 生命周期确保引用的有效性,防止悬空引用.

示例代码:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}
3. 模式匹配(Pattern Matching)
  • 使用match表达式处理不同的模式.

示例代码:

fn main() {
    let number = 7;
    match number {
        1 => println!("One!"),
        2 => println!("Two!"),
        3 => println!("Three!"),
        _ => println!("Not one, two, or three!"),
    }
}
4. 结果类型与错误处理(Result and Error Handling)
  • 使用Result类型处理可能的错误.

示例代码:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");
    let f = match f {
        Ok(file) => file,
        Err(ref error) if error.kind() == ErrorKind::NotFound => {
            match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
            }
        },
        Err(error) => {
            panic!("There was a problem opening the file: {:?}", error)
        },
    };
}
5. 结构体(Structs)与枚举(Enums)
  • 使用结构体定义自定义类型,使用枚举定义可能的值集合.

结构体示例:

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    println!("User: {}", user1.username);
}

枚举示例:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    println!("{:?}, {:?}", four, six);
}
6. 泛型(Generics)
  • 使用泛型来编写能够处理不同数据类型的代码.

示例代码:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest(&char_list);
    println!("The largest char is {}", result);
}
总结

Rust语言通过其独特的所有权系统、严格的类型检查和模式匹配等特性,提供了高效、安全和可靠的编程体验.即使你刚开始学习Rust,这些基础概念和示例代码可以帮助你在面试中展示对Rust的理解和兴趣.如果你有更多时间,可以进一步了解Rust的异步编程、错误处理和并发模型等高级特性.

node bff

Click to expand

BFF(Backend for Frontend)是一种后端架构模式,旨在为每个前端应用程序(如移动端、Web端等)提供专门的后端服务.Node.js在实现BFF模式方面有许多优点,以下是其中的一些主要优点:

1. 单线程非阻塞I/O

Node.js基于单线程非阻塞I/O模型,能够高效处理大量并发请求.BFF通常需要处理来自多个前端的高并发请求,Node.js的这一特性使其非常适合用于构建BFF.

2. JavaScript全栈开发

Node.js使用JavaScript,这意味着开发者可以使用同一种语言进行前端和后端的开发.这种一致性带来了以下好处:

  • 代码重用:可以在前端和后端共享一些代码库和工具.
  • 开发效率:开发团队只需掌握一种编程语言即可,减少了学习和维护成本.
3. 丰富的生态系统

Node.js拥有庞大的包管理器npm,提供了丰富的库和工具,可以快速实现各种功能,例如:

  • 请求处理:如Express.js、Koa等轻量级框架,使开发BFF变得简单.
  • 数据处理:如Lodash、Moment.js等库,简化数据处理操作.
  • API集成:如Axios、Request等库,方便进行第三方API集成.
4. 高效的JSON处理

Node.js天然支持JavaScript对象和JSON格式的数据处理,这与大多数前端应用的数据交换格式(JSON)高度契合,使得数据的序列化和反序列化操作非常高效.

5. 实时功能

Node.js对WebSocket等实时通信协议有良好的支持,可以轻松实现实时数据推送和双向通信,这对于需要实时交互的前端应用非常重要.

6. 微服务架构

Node.js适合构建微服务架构,可以将BFF作为多个独立的微服务之一.这种架构可以带来更好的可扩展性和灵活性,并且每个微服务可以独立部署和扩展.

7. 开发和部署速度

Node.js的轻量级和高效特性使得开发和部署BFF服务非常快速:

  • 快速启动:Node.js应用通常启动速度很快,适合频繁的开发和部署迭代.
  • 轻量级:Node.js应用占用资源较少,可以快速部署在各种环境中,包括云服务和容器中.
8. 社区支持和文档

Node.js拥有强大的社区支持和丰富的文档,开发者可以轻松找到各种问题的解决方案和最佳实践,从而提高开发效率.

总结

使用Node.js作为BFF的实现技术具有许多优点,包括单线程非阻塞I/O模型、高效的JSON处理、JavaScript全栈开发、丰富的生态系统、强大的实时功能支持、适合微服务架构、高效的开发和部署速度,以及良好的社区支持.这些优点使得Node.js成为构建BFF的理想选择.

http3/http2

Click to expand

HTTP的下一代是HTTP/2和HTTP/3,它们在HTTP/1.1的基础上进行了改进,以解决一些性能和效率方面的问题.以下是HTTP/2和HTTP/3分别解决的主要问题:

HTTP/2

HTTP/2于2015年正式发布,是对HTTP/1.1的重大升级,主要解决了以下问题:

  1. 多路复用(Multiplexing)

    • 问题:HTTP/1.1中,每个请求都需要建立一个单独的TCP连接,容易导致连接的数量和资源的浪费.浏览器通常限制同一域名下并发TCP连接的数量.
    • 解决方案:HTTP/2通过多路复用允许在一个TCP连接中同时发送多个请求和响应,从而提高了传输效率.
  2. 头部压缩(Header Compression)

    • 问题:HTTP/1.1中,头部信息在每个请求中都是冗余的,尤其是在使用Cookies等情况时,这会导致额外的开销.
    • 解决方案:HTTP/2引入了HPACK头部压缩算法,可以有效地压缩头部信息,减少传输的数据量.
  3. 服务器推送(Server Push)

    • 问题:在HTTP/1.1中,服务器只能响应客户端的请求,无法主动推送数据到客户端.
    • 解决方案:HTTP/2允许服务器在客户端请求前主动推送资源(如CSS、JavaScript文件),减少了请求的往返次数,提高了页面加载速度.
  4. 二进制分帧(Binary Framing)

    • 问题:HTTP/1.1使用文本格式传输数据,解析起来相对复杂且效率较低.
    • 解决方案:HTTP/2使用二进制格式传输数据,解析和处理起来更加高效和可靠.
HTTP/3

HTTP/3是基于QUIC协议的,它在HTTP/2的基础上进一步优化,主要解决以下问题:

  1. 基于UDP的QUIC协议

    • 问题:TCP连接建立时间较长(三次握手),以及丢包重传机制容易导致延迟增加.
    • 解决方案:HTTP/3基于UDP传输协议的QUIC,通过减少连接建立时间(零次握手或一次握手)和改进丢包处理机制(丢包只影响单个流而非整个连接),大大减少了延迟,提高了传输效率.
  2. 连接迁移

    • 问题:在TCP连接中,客户端IP地址或网络变化会导致连接中断,需要重新建立连接.
    • 解决方案:QUIC支持连接迁移,即使客户端的IP地址或网络发生变化,也能保持连接的持续性,避免重新建立连接的开销.
  3. 内置加密

    • 问题:在HTTP/1.1和HTTP/2中,安全性主要通过TLS层提供,但这引入了额外的复杂性和握手延迟.
    • 解决方案:QUIC协议内置加密机制,简化了加密的实现,并且由于是设计之初就考虑到安全性,因此性能和安全性都得到了优化.
总结

HTTP/2和HTTP/3分别通过多路复用、头部压缩、服务器推送、二进制分帧等技术,以及基于QUIC协议的低延迟连接、连接迁移和内置加密等特性,解决了HTTP/1.1中的性能、效率和安全性问题.这些改进旨在提升网络传输的速度、可靠性和用户体验,特别是在现代Web应用程序中,HTTP/2和HTTP/3的应用越来越广泛.

restful接口架构的优缺点

RESTful接口架构具有简单性、可扩展性、统一接口、状态无关性和可缓存性等优点,但也存在一些缺点,如过度网络传输、安全性、灵活性、性能和不适用于所有场景等问题.在选择API设计风格时,需要根据具体的业务需求和系统要求进行综合考虑,并灵活选择合适的API设计风格

前端个人规划

不断提升自己的技能水平和竞争力,才能在不断变化的行业中保持领先并实现个人价值

mvvm

Click to expand

MVVM(Model-View-ViewModel)是一种前端架构模式,旨在将应用程序的用户界面(View)、业务逻辑(ViewModel)和数据模型(Model)进行分离,以提高代码的可维护性和可测试性.下面是MVVM结构的主要组成部分和工作原理:

1. Model(模型)
  • 定义:Model代表应用程序的数据模型,负责处理数据的存储、获取和处理.
  • 责任:负责与服务器端通信、数据库交互、数据校验、业务逻辑等操作,提供数据给ViewModel.
  • 特点:通常是纯数据对象,不包含任何业务逻辑.
2. View(视图)
  • 定义:View是应用程序的用户界面,负责展示数据给用户,并接收用户的输入和操作.
  • 责任:负责展示ViewModel提供的数据,以及将用户的操作反馈给ViewModel.
  • 特点:通常是由HTML、CSS和JavaScript构成的页面或组件,负责展示数据和处理用户交互.
3. ViewModel(视图模型)
  • 定义:ViewModel是View和Model之间的连接器,负责处理View和Model之间的通信和交互.
  • 责任:负责从Model中获取数据,并将数据转换成View可以展示的格式,同时监听View的用户交互事件,将用户操作转发给Model.
  • 特点:通常包含业务逻辑、数据格式化、数据转换和状态管理等功能,但不涉及具体的DOM操作.

MVVM工作原理:

  1. 初始化:View加载时,ViewModel会初始化,并开始与Model进行交互,获取初始数据.
  2. 数据绑定:ViewModel将数据绑定到View上,使得View能够动态展示ViewModel提供的数据.
  3. 用户交互:ViewModel监听View上的用户交互事件(如点击、输入等),并根据用户的操作更新内部状态或者向Model发送请求.
  4. 数据更新:当Model的数据发生变化时,ViewModel会及时地将新的数据更新到View上,保持View的显示与数据的同步.
  5. 数据传递:ViewModel负责将用户的操作转换成Model能够理解的数据格式,并将数据传递给Model进行处理.
  6. 状态管理:ViewModel负责管理View的状态(如显示/隐藏、可用/禁用等),并响应用户的操作来更新状态.

优点:

  • 分离关注点:将UI逻辑和业务逻辑分离,使得代码更易于理解和维护.
  • 可测试性:ViewModel通常是纯JavaScript对象,便于编写单元测试.
  • 重用性:ViewModel可以在不同的View中重用,增加了代码的复用性.

缺点:

  • 学习成本:相比传统的MVC结构,MVVM需要学习额外的概念和技术,可能会增加学习曲线.
  • 复杂性:对于简单的应用程序,引入MVVM可能会增加不必要的复杂性.
  • 性能开销:MVVM框架通常会增加一些性能开销,如数据绑定、依赖追踪等.

总结:

MVVM是一种将数据、UI和业务逻辑进行分离的前端架构模式,通过ViewModel实现数据的双向绑定和交互,提高了代码的可维护性和可测试性,但也需要在学习成本和性能开销上进行权衡.

vue v-model

v-model 的原理是通过在表单元素上绑定数据属性,并监听元素的输入事件,实现了视图与数据之间的双向绑定,使得数据的变化能够实时反映到视图上,同时用户的操作也能够同步更新到数据层.

监控

Click to expand

前端项目监控是指对前端项目的性能、错误、用户行为等进行监控和分析,以便及时发现和解决问题,提高用户体验和系统稳定性.以下是一些常见的前端项目监控内容:

1. 性能监控
  • 页面加载时间:监控页面的加载时间,包括DNS解析、TCP连接、DOM渲染等各个环节的耗时.
  • 资源加载时间:监控各种资源(如图片、脚本、样式表)的加载时间,以及资源的缓存命中率.
  • 页面性能指标:监控关键性能指标,如首次内容渲染时间(FCP)、首次有效渲染时间(FMP)、可交互时间(TTI)等.
2. 错误监控
  • JavaScript错误:监控页面中的JavaScript错误,包括语法错误、运行时错误、跨域错误等.
  • 资源加载错误:监控页面中资源加载失败的情况,如404错误、跨域加载限制等.
  • API请求错误:监控与后端接口的请求错误,包括网络错误、超时错误、服务端错误等.
3. 用户行为监控
  • 页面PV/UV:统计页面的访问量(PV)和独立访客数(UV),了解页面的流量情况.
  • 用户行为分析:监控用户在页面上的操作行为,如点击、滚动、输入等,以及用户的停留时长和转化路径.
  • 设备信息:收集用户的设备信息,包括浏览器类型、操作系统、分辨率等,以优化页面的兼容性和性能.
4. 实时监控和报警
  • 实时监控:及时收集和展示监控数据,以便开发者实时了解项目的运行情况.
  • 异常报警:设置异常阈值和报警规则,当监控数据超出设定范围时,及时发送报警通知,以便开发者及时处理问题.
5. 数据分析和可视化
  • 数据统计:对监控数据进行统计和分析,生成报表和趋势图,以便开发者了解项目的运行趋势和性能瓶颈.
  • 可视化展示:将监控数据以可视化的方式展示,如图表、地图、仪表盘等,使得数据更加直观和易于理解.
6. 安全监控
  • XSS攻击:监控页面中的跨站脚本攻击(XSS),防止恶意脚本对页面进行篡改和窃取用户信息.
  • CSRF攻击:监控页面中的跨站请求伪造攻击(CSRF),防止恶意请求对用户数据进行操作和篡改.

综合以上内容,前端项目监控旨在全面了解项目的性能、错误和用户行为情况,及时发现和解决问题,提升用户体验和项目稳定性.可以借助各种监控工具和服务来实现监控需求,如Google Analytics、Sentry、New Relic、Datadog等.

微服务

微服务是一种软件架构风格,它将应用程序构建为一组小型、独立的服务,每个服务都围绕着特定的业务功能进行构建,并通过轻量级通信机制(通常是HTTP API)进行通信

自动化

Click to expand

前端自动化开发和测试是一种提高开发效率和软件质量的重要手段,通常包括自动化构建、自动化测试、持续集成和持续交付等流程.将这些自动化过程产品化,可以进一步提高开发团队的工作效率和软件质量.下面是一些常见的前端自动化开发/测试过程产品化的步骤和方法:

1. 自动化构建

  • 产品化步骤
    1. 设计和实现统一的构建工具链,如Webpack、Parcel等,以实现前端资源的打包、压缩和优化.
    2. 定制化构建流程,根据项目需求和团队规范配置构建任务,包括编译、打包、代码检查、资源合并等.
    3. 集成自动化构建到版本控制系统中,并配置持续集成工具,如Jenkins、Travis CI等,实现代码提交后自动触发构建.

2. 自动化测试

  • 产品化步骤
    1. 设计和实现自动化测试框架,如Jest、Mocha、Cypress等,用于编写和运行单元测试、集成测试和端到端测试.
    2. 编写和维护自动化测试用例,覆盖项目的各个功能模块和边界情况,保证代码的稳定性和可靠性.
    3. 集成自动化测试到持续集成流程中,确保每次代码提交都会触发自动化测试,及时发现和修复问题.

3. 持续集成和持续交付

  • 产品化步骤
    1. 设计和实现持续集成和持续交付流水线,通过配置CI/CD工具实现自动化构建、测试、部署和发布.
    2. 配置自动化测试和静态代码检查任务,确保每次代码提交都会自动进行测试和代码质量检查.
    3. 实现自动化部署和发布,将构建好的代码自动部署到测试环境、预生产环境和生产环境,实现快速迭代和持续交付.

4. 监控和反馈

  • 产品化步骤
    1. 配置监控和告警系统,监控项目的构建、测试、部署和运行状态,及时发现和处理异常情况.
    2. 实现持续反馈机制,定期汇总和分析自动化测试和构建的结果,生成报告和可视化数据,为团队提供决策支持和持续改进.

5. 文档和培训

  • 产品化步骤
    1. 编写和维护自动化开发/测试流程的文档和手册,包括工具的使用方法、流程的配置步骤、常见问题解答等内容.
    2. 定期组织培训和分享会议,向团队成员介绍和演示自动化开发/测试工具和流程,提高团队的技术水平和工作效率.

通过以上产品化的步骤和方法,可以将前端自动化开发/测试过程标准化和规范化,提高团队的工作效率和软件质量,从而实现快速迭代和持续交付.

聊聊缓存

Click to expand

缓存是指将数据临时存储在计算机内存或磁盘中,以便在后续访问时能够更快地获取数据.在Web开发中,缓存是提高网站性能和用户体验的重要手段之一.下面是与缓存相关的一些重要概念:

1. ETag(实体标签)

  • 作用:ETag是由服务器为每个资源分配的唯一标识符,用于标识资源的版本号或内容是否发生变化.
  • 工作原理:当客户端请求一个资源时,服务器会生成该资源的ETag,并在响应头中返回给客户端.客户端在后续请求时,可以将该ETag通过请求头的If-None-Match字段发送给服务器,用于验证资源是否发生变化.
  • 优点:可以更精确地判断资源是否发生变化,减少不必要的数据传输.
  • 示例ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

2. Expires(过期时间)

  • 作用:Expires是指定资源的过期时间,告诉客户端该资源在何时过期,需要重新获取新的资源.
  • 工作原理:服务器在响应头中返回Expires字段,告诉客户端该资源的过期时间.客户端在收到资源后,将会在过期时间后重新请求该资源.
  • 示例Expires: Wed, 21 Oct 2026 07:28:00 GMT

3. Cache-Control(缓存控制)

  • 作用:Cache-Control是用来指定客户端和代理服务器对缓存进行控制的指令,包括缓存的存储方式、过期时间、验证方式等.
  • 常用指令
    • max-age=<seconds>:指定缓存存储的最长时间.
    • no-cache:强制客户端对资源进行重新验证,不直接使用缓存.
    • no-store:不缓存资源的任何内容.
    • public:表示响应可以被任何缓存所缓存.
    • private:表示响应只能被单个用户缓存.
  • 示例Cache-Control: max-age=3600, public

这些缓存相关的概念在Web开发中经常被使用,可以通过设置合适的缓存策略来提高网站性能、减少服务器负载,并改善用户体验.

栈与堆

Click to expand

栈和堆是计算机内存中用于存储数据的两种不同的区域,它们在内存管理和存储方式上有一些重要的区别.

栈(Stack)

  • 特点

    • 栈是一种线性的数据结构,遵循后进先出(LIFO)的原则.
    • 栈的操作通常包括入栈(push)和出栈(pop).
    • 栈的大小通常是固定的,由操作系统提前分配好内存空间.
  • 应用场景

    • 存储局部变量和函数调用的上下文信息.
    • 存储函数的调用参数、返回地址和临时变量.
  • 特点

    • 栈的分配和释放都是自动的,由编译器负责管理,无需手动释放内存.
    • 栈的分配速度比较快,因为只需要移动栈指针即可.
    • 栈的空间相对较小,通常只能存储局部变量和函数调用信息,不能存储大型对象.

堆(Heap)

  • 特点

    • 堆是一种动态分配的内存区域,大小不固定.
    • 堆的数据存储没有特定的顺序,分配和释放的顺序由程序员控制.
    • 堆的内存空间通常比栈大,可以存储大型对象和数据结构.
  • 应用场景

    • 存储动态分配的对象、数组和数据结构.
    • 存储全局变量和静态变量.
  • 特点

    • 堆的分配和释放需要手动管理,程序员负责申请内存和释放内存,否则会出现内存泄漏.
    • 堆的分配速度相对较慢,因为需要考虑内存碎片和地址分配的问题.
    • 堆的空间相对较大,可以存储大型对象和数据结构,但也容易出现内存碎片问题.

总的来说,栈和堆是计算机内存中两种不同的数据存储区域,它们在内存管理、分配方式和存储内容上有着明显的区别.在编程中,栈通常用于存储局部变量和函数调用信息,而堆用于存储动态分配的对象和数据结构.

水平垂直居中

Click to expand 在CSS中实现内容水平垂直居中有多种方案,下面列举了一些常用的方法:

1. 使用 Flexbox 布局

利用 Flexbox 可以轻松实现内容的水平和垂直居中.

.container {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
}

2. 使用 Grid 布局

同样,Grid 布局也提供了简单的方式来实现内容的水平和垂直居中.

.container {
  display: grid;
  place-items: center; /* 同时水平和垂直居中 */
}

3. 使用绝对定位和负边距

将内容设置为绝对定位,并利用负边距的方式实现水平和垂直居中.

.container {
  position: relative;
}

.content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

4. 使用表格布局

利用表格布局的特性,可以将内容轻松地水平和垂直居中.

.container {
  display: table;
}

.content {
  display: table-cell;
  text-align: center; /* 水平居中 */
  vertical-align: middle; /* 垂直居中 */
}

5. 使用 line-height

如果内容只有一行文本,可以利用 line-height 实现垂直居中.

.container {
  line-height:; /* 和容器高度相同的值 *;
  text-align: center; /* 水平居中 */
}

以上这些方法都可以实现内容的水平和垂直居中,具体使用哪种方法取决于项目的需求和个人偏好.

js写一个快排

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }

  const pivotIndex = Math.floor(arr.length / 2);
  const pivot = arr.splice(pivotIndex, 1)[0];
  const left = [];
  const right = [];

  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }

  return quickSort(left).concat([pivot], quickSort(right));
}

// 示例
const array = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
console.log(quickSort(array)); // 输出 [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]

ts内置工具类

Click to expand
Partial<T>
将类型 T 中的所有属性设置为可选属性.
Required<T>
将类型 T 中的所有属性设置为必选属性.
Readonly<T>
将类型 T 中的所有属性设置为只读属性.
Record<K, T>
创建一个由类型 K 中的键和类型 T 中的值组成的对象类型.
Pick<T, K>
从类型 T 中选取部分属性,组成一个新的类型.
Omit<T, K>
从类型 T 中排除指定的属性,组成一个新的类型.

iframe页面通信

Click to expand

在 Web 开发中,如果在一个页面(父页面)中嵌入了另一个页面(子页面)的 iframe,通常会涉及到两个页面之间的通信问题.下面是一些常见的 iframe 页面通信方法:

1. Window.postMessage()

postMessage() 方法允许安全地实现跨域通信.通过在父页面中使用 postMessage() 方法向子页面发送消息,或者在子页面中使用 postMessage() 方法向父页面发送消息,可以实现双向通信.

父页面中发送消息:

const iframeWindow = document.getElementById("myIframe").contentWindow;
iframeWindow.postMessage("Hello from parent!", "https://example.com");

子页面中接收消息:

window.addEventListener("message", event => {
  if (event.origin === "https://example.com") {
    console.log("Message from parent:", event.data);
  }
});

2. Window.parent

子页面可以通过 window.parent 属性直接访问父页面的 window 对象,从而进行通信.

子页面向父页面发送消息:

window.parent.postMessage("Hello from iframe!", "https://example.com");

父页面接收消息:

window.addEventListener("message", event => {
  if (event.origin === "https://example.com") {
    console.log("Message from iframe:", event.data);
  }
});

3. Window.frames

父页面可以通过 window.frames 属性访问到子页面的 window 对象,从而进行通信.

父页面向子页面发送消息:

const iframeWindow = window.frames["myIframe"];
iframeWindow.postMessage("Hello from parent!", "https://example.com");

子页面接收消息:

window.addEventListener("message", event => {
  if (event.origin === "https://example.com") {
    console.log("Message from parent:", event.data);
  }
});

通过以上方法,父页面和子页面可以实现跨域的安全通信,实现双向通信,从而实现更丰富的交互体验.在实际使用中,需要注意安全性和数据的有效性,以防止恶意攻击和信息泄露.

async && defer

Click to expand

deferasync 是用于优化脚本加载和执行顺序的 HTML <script> 标签的属性.

1. defer 属性

defer 属性告诉浏览器,脚本在执行的过程中不会影响文档的解析,即脚本会被延迟执行,直到文档解析完成后再执行.多个带有 defer 属性的脚本会按照它们在文档中出现的顺序依次执行.

<script src="script.js" defer></script>

特点

  • 脚本会在文档解析完成后执行,但在 DOMContentLoaded 事件之前执行.
  • 多个带有 defer 属性的脚本会按照它们在文档中出现的顺序依次执行.
  • 适合用于不需要立即执行的脚本,可以减少页面加载时间.

2. async 属性

async 属性告诉浏览器立即下载脚本文件,但不阻塞文档解析,一旦脚本下载完成,立即开始执行.多个带有 async 属性的脚本在下载完成后会并行执行,不会按照它们在文档中出现的顺序依次执行.

<script src="script.js" async></script>

特点

  • 脚本会在下载完成后立即执行,不会阻塞文档解析.
  • 多个带有 async 属性的脚本会并行下载,并且在下载完成后立即执行,执行顺序不确定.
  • 适合用于不依赖于文档内容的脚本,可以提高页面加载的并行度.

选择使用场景

  • 如果脚本依赖于文档中的 DOM 结构,应该使用 defer 属性,以确保脚本在 DOMContentLoaded 事件之前执行.
  • 如果脚本不依赖于文档内容,且可以在任何时候执行,可以使用 async 属性,以提高脚本的并行下载和执行效率.

在选择使用 defer 还是 async 属性时,需要根据脚本的具体需求和依赖关系来决定,以确保脚本能够正确加载和执行,并且不会影响页面性能和用户体验.

url到页面呈现的过程

Click to expand

将 URL 输入到浏览器地址栏并按下回车键后,浏览器开始加载并呈现页面的过程通常包括以下几个步骤:

1. URL 解析

浏览器首先会对输入的 URL 进行解析,提取出协议、主机名、端口号、路径和查询参数等信息.

2. DNS 解析

浏览器需要将主机名解析成对应的 IP 地址,这个过程称为 DNS 解析.浏览器会首先检查本地 DNS 缓存,如果找不到对应的 IP 地址,则向 DNS 服务器发送查询请求,获取对应的 IP 地址.

3. 建立 TCP 连接

浏览器通过获取到的 IP 地址和端口号与服务器建立 TCP 连接.这个过程包括三次握手,即客户端向服务器发送 SYN 报文,服务器回复 SYN+ACK 报文,最后客户端发送 ACK 报文,建立起 TCP 连接.

4. 发送 HTTP 请求

建立好 TCP 连接后,浏览器向服务器发送 HTTP 请求.请求中包含请求方法、URL、请求头和请求体等信息.服务器接收到请求后开始处理,并返回响应.

5. 接收响应并解析

浏览器接收到服务器返回的响应后,开始解析响应头和响应体.根据响应头中的 Content-Type 等信息确定响应的类型,如 HTML、CSS、JavaScript 或者其他资源类型.

6. 渲染页面

如果响应的内容为 HTML 文档,则浏览器开始解析 HTML,构建 DOM 树;接着解析 CSS 样式文件,构建 CSSOM 树;然后将 DOM 树和 CSSOM 树结合,生成渲染树(Render Tree);最后根据渲染树进行布局和绘制,将页面内容显示在浏览器窗口中.

7. 下载并执行 JavaScript

如果响应中包含 JavaScript 文件,浏览器会下载并执行 JavaScript 代码.JavaScript 的执行可能会改变 DOM 结构和样式,影响页面的渲染.

8. 加载其他资源

页面中可能包含其他的资源,如图片、字体、视频等.浏览器会根据 HTML 和 CSS 中的引用下载这些资源,并在需要时进行渲染.

9. 页面加载完成

当所有的资源都加载完成,页面渲染完成后,浏览器触发 DOMContentLoaded 事件,表示 HTML 文档已经完全加载和解析;如果页面中包含图片等外部资源,当这些资源都加载完成后,浏览器触发 load 事件,表示页面已经全部加载完成.

36*一面总结

vuex中mutation和action的区别

Click to expand

在Vuex中,mutation和action是用于管理和修改状态(state)的两个核心概念,但它们有一些关键的区别:

Mutation

  • 同步操作:mutation只能执行同步操作.当你提交一个mutation时,状态的改变是立即生效的.
  • 直接修改state:mutation是唯一允许直接修改Vuex状态的方法.每个mutation都有一个类型(type)和一个回调函数(handler),回调函数接收state作为第一个参数.
  • 简单而明确:因为它们是同步的,mutation非常容易追踪和调试,开发工具(如Vue Devtools)能够明确地展示每个mutation的历史和它们的影响.
const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state, payload) {
      state.count += payload.amount;
    },
  },
});

// 提交一个mutation
store.commit("increment", { amount: 10 });

Action

  • 异步操作:action可以包含异步操作(如API调用).它们用于调度mutation,而不是直接改变状态.
  • 封装业务逻辑:action可以包含复杂的业务逻辑,可以在其中执行异步操作,然后在合适的时机提交mutation.
  • 返回Promise:action本身返回一个Promise,可以在外部链式调用.
const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state, payload) {
      state.count += payload.amount;
    },
  },
  actions: {
    incrementAsync({ commit }, payload) {
      setTimeout(() => {
        commit("increment", payload);
      }, 1000);
    },
  },
});

// 分发一个action
store.dispatch("incrementAsync", { amount: 10 });

总结

  • Mutation:用于同步地、直接地修改状态.
  • Action:用于处理异步操作,并最终提交mutation来修改状态.

通过这种方式,Vuex保证了状态管理的一致性和可预测性,使得应用程序的状态流动更加清晰和可控.

js冒泡排序

Click to expand 冒泡排序是一种简单的排序算法,其基本思想是重复地遍历待排序的数组,每次比较相邻的两个元素,如果它们的顺序错误就交换它们,直到整个数组有序.下面是JavaScript中实现冒泡排序的代码:

function bubbleSort(arr) {
  let n = arr.length;
  // 外层循环控制遍历的次数
  for (let i = 0; i < n - 1; i++) {
    // 内层循环控制每次遍历时的比较和交换
    for (let j = 0; j < n - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        // 交换相邻元素
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
  return arr;
}

// 测试冒泡排序
let array = [64, 34, 25, 12, 22, 11, 90];
console.log("Unsorted array:", array);
let sortedArray = bubbleSort(array);
console.log("Sorted array:", sortedArray);

代码说明

  1. 外层循环 (for (let i = 0; i < n - 1; i++)):控制需要遍历的次数.每次遍历将把当前未排序部分中最大的元素移到正确的位置.
  2. 内层循环 (for (let j = 0; j < n - 1 - i; j++)):在每次遍历中,比较相邻的元素并进行交换.由于每一轮结束后,最大的元素已经到达正确位置,所以在下一轮中可以少比较一次.
  3. 交换相邻元素 (let temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp;):如果当前元素大于下一个元素,则交换它们的位置.

优化

可以进行一些优化,例如如果在某次遍历中没有进行任何交换,说明数组已经是有序的,可以提前终止算法:

function bubbleSortOptimized(arr) {
  let n = arr.length;
  let swapped;
  for (let i = 0; i < n - 1; i++) {
    swapped = false;
    for (let j = 0; j < n - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
        swapped = true;
      }
    }
    // 如果没有发生交换,数组已经有序
    if (!swapped) break;
  }
  return arr;
}

// 测试优化后的冒泡排序
let arrayOptimized = [64, 34, 25, 12, 22, 11, 90];
console.log("Unsorted array:", arrayOptimized);
let sortedArrayOptimized = bubbleSortOptimized(arrayOptimized);
console.log("Sorted array:", sortedArrayOptimized);

这个优化版的冒泡排序通过swapped标志位来检测在一轮遍历中是否发生了交换,如果没有,则说明数组已经排序完成,可以提前退出循环.这样可以减少不必要的比较,提高效率.

js手写防抖

Click to expand 防抖(debounce)是一种编程技术,用于在特定事件频繁触发时,延迟执行事件处理函数,直到事件停止触发一段时间后再执行.这在处理诸如窗口大小调整、搜索框输入等高频率事件时非常有用,可以有效减少事件处理函数的调用次数,从而提高性能.

下面是一个简单的JavaScript防抖函数的实现:

function debounce(func, wait) {
  let timeout;
  return function (...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, wait);
  };
}

// 示例:防抖处理窗口大小调整事件
window.addEventListener(
  "resize",
  debounce(function () {
    console.log("Window resized");
  }, 300)
);

// 示例:防抖处理输入框事件
const input = document.getElementById("searchInput");
input.addEventListener(
  "input",
  debounce(function () {
    console.log("Input value:", this.value);
  }, 500)
);

代码说明

  1. debounce函数:接受两个参数,func是要执行的函数,wait是等待的时间(以毫秒为单位).
  2. timeout变量:用于保存定时器的ID,以便后续清除.
  3. 返回一个新的函数:这个函数将在事件触发时执行.它会清除之前的定时器,并设置一个新的定时器.
  4. clearTimeout(timeout):清除之前的定时器,防止之前的函数执行.
  5. setTimeout:设置一个新的定时器,在指定的等待时间后执行传入的函数.
  6. func.apply(context, args):在定时器触发时执行传入的函数,保持上下文和参数一致.

使用示例

  1. 窗口大小调整事件:当窗口大小调整时,通过防抖函数将处理函数的调用频率限制为每300毫秒一次.
  2. 输入框事件:在输入框中输入时,通过防抖函数将处理函数的调用频率限制为每500毫秒一次.

通过这种方式,防抖函数可以有效减少事件处理函数的调用次数,提高应用的性能和响应速度.

将get参数转对象

将url中get参数转对象

将URL中的GET参数转换为JavaScript对象,可以使用以下方法:

  1. 使用正则表达式手动解析URL.
  2. 使用URLSearchParams API(现代浏览器支持).

下面是两种方法的详细实现:

方法一:使用正则表达式手动解析URL

这种方法适用于任何浏览器,无论是否支持现代API.

function getQueryParams(url) {
  let queryParams = {};
  let queryString = url.split("?")[1];
  if (queryString) {
    let pairs = queryString.split("&");
    for (let pair of pairs) {
      let [key, value] = pair.split("=");
      queryParams[decodeURIComponent(key)] = decodeURIComponent(value || "");
    }
  }
  return queryParams;
}

// 示例
let url = "http://example.com/page?name=John&age=30&city=New%20York";
let params = getQueryParams(url);
console.log(params); // { name: "John", age: "30", city: "New York" }

方法二:使用URLSearchParams API

这种方法更简洁,推荐在现代浏览器中使用.

function getQueryParams(url) {
  let queryParams = {};
  let queryString = url.split("?")[1];
  if (queryString) {
    let urlSearchParams = new URLSearchParams(queryString);
    for (let [key, value] of urlSearchParams.entries()) {
      queryParams[key] = value;
    }
  }
  return queryParams;
}

// 示例
let url = "http://example.com/page?name=John&age=30&city=New%20York";
let params = getQueryParams(url);
console.log(params); // { name: "John", age: "30", city: "New York" }

使用说明

  1. getQueryParams函数:接收一个URL字符串,返回一个包含GET参数的对象.
  2. url.split('?')[1]:提取URL中的查询字符串部分.
  3. 正则表达式方法:将查询字符串分割成键值对,然后逐个解析并存储到对象中.
  4. URLSearchParams API方法:利用URLSearchParams内置的解析能力,直接将键值对存储到对象中.

示例输出

对于URL http://example.com/page?name=John&age=30&city=New%20York,解析后的对象是:

{
  name: "John",
  age: "30",
  city: "New York"
}

这两种方法都能有效地将URL中的GET参数转换为JavaScript对象,可以根据需要选择适合的实现方式.

代码混淆的原理

Click to expand 在Cordova应用中,代码混淆(obfuscation)是一种技术手段,用于使代码更难以理解和逆向工程.代码混淆的主要原理是通过改变代码的结构和名称,使得代码仍然能够正常运行,但对于人类来说变得难以理解.

代码混淆的基本原理

  1. 变量和函数名重命名

    • 将有意义的变量名和函数名替换为无意义的短名称或随机字符.例如,将getUserData重命名为a.
  2. 字符串加密

    • 对代码中的字符串进行加密或编码,使其在源代码中不可读.执行时再进行解密或解码.
  3. 移除注释和空白

    • 删除所有注释、空行和多余的空格,以减少代码的可读性.
  4. 代码结构变换

    • 重排代码结构,例如将函数内联,或者将多段代码合并为一段复杂的代码块.
  5. 插入无用代码

    • 插入一些无用的代码(dead code),增加混淆度,使得代码逻辑更加复杂和难以理解.

在Cordova中实现代码混淆

虽然Cordova本身不直接提供代码混淆功能,但你可以借助一些第三方工具来实现对Cordova项目的代码混淆.常见的工具包括UglifyJS、Terser和ProGuard(用于混淆Java代码).

使用Terser混淆JavaScript代码

Terser是一个广泛使用的JavaScript压缩工具,可以用于混淆和压缩Cordova项目的JavaScript代码.

  1. 安装Terser

    npm install terser -g
    
  2. 使用Terser混淆代码

    terser yourfile.js -o yourfile.min.js --compress --mangle
    
  3. 集成到Cordova构建流程: 你可以在Cordova构建流程中添加一个步骤,在打包之前使用Terser混淆你的JavaScript代码.你可以使用Cordova hooks来实现这一点:

    hooks/after_prepare目录下创建一个脚本,例如hooks/after_prepare/terser.js

    const terser = require("terser");
    const fs = require("fs");
    const path = require("path");
    
    module.exports = function (context) {
      const wwwPath = path.join(context.opts.projectRoot, "www");
      const jsFiles = fs
        .readdirSync(wwwPath)
        .filter(file => file.endsWith(".js"));
    
      jsFiles.forEach(file => {
        const filePath = path.join(wwwPath, file);
        const code = fs.readFileSync(filePath, "utf8");
        const result = terser.minify(code);
        if (result.error) {
          console.error(`Error minifying ${file}: `, result.error);
        } else {
          fs.writeFileSync(filePath, result.code, "utf8");
          console.log(`Successfully minified ${file}`);
        }
      });
    };
    

    确保在config.xml中添加这个hook:

    <hook src="hooks/after_prepare/terser.js" type="after_prepare" />
    

总结

代码混淆是通过重命名、字符串加密、移除注释和空白、代码结构变换和插入无用代码等手段,使代码难以理解.虽然Cordova本身不直接支持代码混淆,但你可以使用Terser等工具,在构建过程中对JavaScript代码进行混淆.通过集成这些工具到Cordova的构建流程中,可以有效地提升应用代码的安全性.

vue单向数据流

Click to expand

在Vue.js中,单向数据流是一种数据管理的设计原则,用于确保数据在应用程序中流动的方向是明确和可预测的.这有助于提高应用程序的可维护性和可调试性.

单向数据流的基本概念

  1. 父子组件数据流:在Vue.js中,数据是从父组件通过props传递给子组件的.子组件不能直接修改从父组件接收到的props,而只能通过事件通知父组件更新数据.

  2. 数据的唯一来源:每个数据片段都应该有一个明确的来源.通常,这意味着数据应该在尽可能高的层级上定义,然后传递给需要的子组件.

  3. 单向绑定:Vue.js的数据绑定是单向的,即从模型到视图.当模型的数据发生变化时,视图会自动更新.然而,视图的变化不会直接导致模型的变化,除非通过事件或绑定.

单向数据流的好处

  1. 数据管理简单:数据流动方向明确,减少了数据管理的复杂性.
  2. 可预测性:数据的变化是可预测的,便于追踪和调试.
  3. 提高组件复用性:组件的输入(props)和输出(事件)明确,提高了组件的复用性.

单向数据流在Vue.js中的实现

父子组件通信

  1. 父组件传递数据给子组件

    • 父组件通过props向子组件传递数据.
    • 子组件使用props接收父组件传递的数据.
  2. 子组件向父组件传递数据

    • 子组件通过事件向父组件传递数据.
    • 父组件监听子组件的事件并接收数据.

示例

<!-- 父组件 -->
<template>
  <div>
    <child-component
      :message="parentMessage"
      @update-message="updateMessage"
    ></child-component>
    <p>Parent Message: {{ parentMessage }}</p>
  </div>
</template>

<script>
  import ChildComponent from "./ChildComponent.vue";

  export default {
    components: { ChildComponent },
    data() {
      return {
        parentMessage: "Hello from parent",
      };
    },
    methods: {
      updateMessage(newMessage) {
        this.parentMessage = newMessage;
      },
    },
  };
</script>

<!-- 子组件 -->
<template>
  <div>
    <p>Child Message: {{ message }}</p>
    <input
      v-model="newMessage"
      @input="sendMessage"
      placeholder="Update parent message"
    />
  </div>
</template>

<script>
  export default {
    props: ["message"],
    data() {
      return {
        newMessage: "",
      };
    },
    methods: {
      sendMessage() {
        this.$emit("update-message", this.newMessage);
      },
    },
  };
</script>

解释

  1. 父组件

    • 定义一个数据属性parentMessage.
    • 通过propsparentMessage传递给子组件.
    • 监听子组件的update-message事件并调用updateMessage方法更新数据.
  2. 子组件

    • 使用props接收父组件传递的数据message.
    • 通过v-model绑定输入框的值并在输入事件中调用sendMessage方法.
    • sendMessage方法通过$emit向父组件发送update-message事件,并传递新的消息数据.

总结

在Vue.js中,单向数据流确保了数据从父组件流向子组件,而子组件通过事件将数据变化传递回父组件.通过这种设计,可以使数据流动方向明确,减少数据管理的复杂性,提高应用程序的可维护性和可预测性.

react hoc

Click to expand

高阶组件(Higher-Order Component,HOC)是React中一种用于代码复用的技术.它是一个函数,接收一个组件并返回一个新的组件.通过这种方式,高阶组件可以将共享的逻辑抽象出来,应用到多个组件中.

高阶组件的原理

高阶组件的本质是一个函数,它接受一个组件作为参数,并返回一个增强后的组件.这个增强后的组件通常会包裹原始组件,添加额外的功能或修改其行为.

基本实现

以下是一个简单的高阶组件示例:

import React from "react";

// 这是一个简单的高阶组件
const withLogging = WrappedComponent => {
  return class extends React.Component {
    componentDidMount() {
      console.log(`Component ${WrappedComponent.name} mounted`);
    }

    componentWillUnmount() {
      console.log(`Component ${WrappedComponent.name} will unmount`);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

// 这是一个普通的React组件
class MyComponent extends React.Component {
  render() {
    return <div>My Component</div>;
  }
}

// 使用高阶组件
const MyComponentWithLogging = withLogging(MyComponent);

export default MyComponentWithLogging;

在上面的例子中:

  1. withLogging是一个高阶组件函数,它接收一个组件WrappedComponent.
  2. withLogging返回一个新的组件,这个新组件在挂载和卸载时会记录日志.
  3. 新组件在渲染时,传递所有的props给WrappedComponent.

使用场景

高阶组件主要用于以下几种场景:

  1. 代码复用:将多个组件中重复的逻辑抽取到一个高阶组件中,避免代码重复.
  2. 状态提升:将状态提升到高阶组件中,实现跨组件的状态共享.
  3. 条件渲染:根据条件决定是否渲染某个组件或修改组件的行为.
  4. 增强组件:添加额外的功能或行为,如日志记录、权限控制等.

实例:权限控制

假设我们有一个需要权限控制的组件,我们可以创建一个高阶组件来实现这一功能.

import React from "react";

const withAuthorization = (WrappedComponent, userRole) => {
  return class extends React.Component {
    render() {
      if (userRole !== "admin") {
        return <div>Access Denied</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  };
};

class AdminPanel extends React.Component {
  render() {
    return <div>Admin Panel</div>;
  }
}

// 使用高阶组件
const AdminPanelWithAuthorization = withAuthorization(AdminPanel, "user"); // 假设当前用户角色是'user'

export default AdminPanelWithAuthorization;

在这个示例中:

  1. withAuthorization高阶组件接受一个组件和一个用户角色作为参数.
  2. 如果用户角色不是admin,则渲染Access Denied提示;否则,渲染WrappedComponent.

注意事项

  1. 避免嵌套地狱:过度使用高阶组件会导致组件嵌套过深,代码难以维护.可以考虑使用其他模式,如Render Props或Hooks来解决类似的问题.
  2. 不要改变原组件的静态方法:高阶组件返回的新组件不会继承原组件的静态方法.可以使用hoist-non-react-statics库来解决这个问题.
  3. 传递props:确保高阶组件正确地传递props给原组件,以保持组件的可用性和灵活性.
import hoistNonReactStatics from "hoist-non-react-statics";

const withLogging = WrappedComponent => {
  class WithLogging extends React.Component {
    componentDidMount() {
      console.log(`Component ${WrappedComponent.name} mounted`);
    }

    componentWillUnmount() {
      console.log(`Component ${WrappedComponent.name} will unmount`);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  hoistNonReactStatics(WithLogging, WrappedComponent);
  return WithLogging;
};

总结

高阶组件是React中用于复用组件逻辑的一种技术.它通过将共享逻辑抽象到一个函数中,使得不同的组件可以共享这部分逻辑.在使用高阶组件时,需要注意避免嵌套过深,正确传递props,并考虑使用其他模式(如Render Props或Hooks)来解决类似的问题.

webp的优缺点

Click to expand WebP是一种现代图像格式,由谷歌开发,旨在提供较小的文件大小,同时保持高质量的图像.它支持有损压缩和无损压缩,并且可以包含动画和透明度.以下是WebP格式的主要优缺点:

优点

  1. 高压缩效率

    • 有损压缩:相比JPEG,WebP的有损压缩可以提供相似质量的图像,但文件大小通常要小25%-34%.
    • 无损压缩:相比PNG,WebP的无损压缩文件大小通常小26%.
  2. 支持透明度

    • WebP支持透明度(alpha通道),而且透明度压缩率更高.无损模式下,支持透明度的WebP文件比PNG文件小很多;有损模式下,支持透明度的WebP文件也能大大减小文件大小.
  3. 支持动画

    • WebP支持动画,可以作为GIF的替代品,提供更好的压缩效率和更高的质量.
  4. 减少带宽消耗

    • 由于WebP图像文件较小,可以显著减少网页加载时间和带宽消耗,提高网页的加载速度和用户体验.
  5. 广泛的浏览器支持

    • 现代浏览器(如Chrome、Firefox、Edge和Opera)都支持WebP格式.Safari从版本14开始也支持WebP.

缺点

  1. 浏览器兼容性

    • 尽管大多数现代浏览器支持WebP,但仍有一些较旧的浏览器和特定环境(如某些版本的Internet Explorer和较老版本的Safari)不支持WebP.因此,需要使用回退机制,为不支持WebP的浏览器提供JPEG或PNG格式的图像.
  2. 工具和软件支持

    • 虽然WebP的支持在不断增加,但并不是所有图像编辑软件和工具都支持WebP格式.某些情况下,可能需要使用专门的工具或插件来处理WebP图像.
  3. 转换成本

    • 如果现有的网站和应用程序大量使用JPEG或PNG图像,转换为WebP格式需要时间和资源.需要考虑批量转换工具和自动化处理方式.
  4. 压缩时间

    • WebP的压缩算法更复杂,有时候压缩时间比JPEG或PNG长,特别是无损压缩模式下.这可能会影响实时图像处理或动态生成图像的性能.

适用场景

  1. 网站和网页优化:WebP非常适合用于网站图像优化,通过减少文件大小来提高网页加载速度和用户体验.
  2. 移动应用:由于WebP的高压缩效率,适合在移动应用中使用,以减少数据传输量和存储空间.
  3. 动画替代GIF:WebP的动画支持和更好的压缩效率,使其成为GIF的优秀替代品.
  4. 透明图像:对于需要透明度的图像,WebP提供了比PNG更高的压缩效率.

总结

WebP是一种高效的图像格式,提供优良的压缩率和图像质量,支持透明度和动画,适合用于网页和移动应用的图像优化.然而,开发者在使用WebP时需要考虑浏览器和工具的兼容性,并采取适当的回退机制来保证在不支持WebP的环境中也能正常显示图像.

webp的浏览器兼容性问题如何解决

spa应用的优缺点及改进方案

Click to expand 单页应用(Single Page Application,SPA)是一种Web应用程序,它通过动态加载内容和更新页面部分而不需要刷新整个页面.这种方法提供了更快的用户体验,但也有一些缺点.以下是SPA的优缺点以及可能的改进方案:

优点

  1. 快速响应和流畅的用户体验

    • 由于不需要刷新整个页面,SPA在用户操作时能够提供更快的响应和更流畅的体验.
  2. 减少带宽消耗

    • SPA只加载一次HTML、CSS和JavaScript,之后通过AJAX请求数据,减少了带宽消耗.
  3. 代码复用和模块化

    • 由于前后端分离,前端可以更加模块化和复用,提高了代码的可维护性和可测试性.
  4. 离线能力

    • 通过缓存和服务工作线程(Service Workers),SPA可以在离线状态下提供部分功能.

缺点

  1. 首次加载时间较长

    • 由于需要加载大量的JavaScript文件,SPA的首次加载时间可能较长,影响用户体验.
  2. SEO问题

    • 传统搜索引擎对JavaScript渲染的内容抓取不友好,可能导致SEO效果不佳.
  3. 浏览器兼容性

    • 对于一些老旧的浏览器,SPA的某些功能可能无法正常工作,可能需要额外的polyfill和兼容性处理.
  4. 前进和后退导航问题

    • 需要手动处理浏览器的前进和后退按钮,维护浏览历史和状态.
  5. 内存泄漏

    • 由于长时间运行在浏览器中,如果不正确地管理资源和事件监听,可能会导致内存泄漏.

改进方案

  1. 优化首次加载时间

    • 代码拆分:使用Webpack等工具进行代码拆分(Code Splitting),按需加载模块,减少初始包大小.
    • 服务器端渲染(SSR):使用Next.js或Nuxt.js等框架实现服务器端渲染,减少客户端渲染的负担.
    • 预渲染(Prerendering):对静态页面进行预渲染,提升首屏加载速度.
  2. 改善SEO

    • SSR:通过服务器端渲染生成静态HTML,确保搜索引擎能够抓取页面内容.
    • 动态渲染:使用Rendertron等工具,在检测到搜索引擎爬虫时动态生成HTML内容.
  3. 提高浏览器兼容性

    • Polyfill:使用Babel等工具自动添加所需的Polyfill,以支持较旧的浏览器.
    • 渐进增强和优雅降级:为现代浏览器提供最佳体验,同时确保旧浏览器的基本功能.
  4. 管理导航和状态

    • 使用React Router或Vue Router:通过前端路由库管理路由,确保浏览器的前进和后退按钮正常工作.
    • 状态管理:使用Redux、Vuex等状态管理库,集中管理应用状态,确保一致性和可维护性.
  5. 防止内存泄漏

    • 事件解绑:在组件销毁时正确地解绑事件监听,释放不再需要的资源.
    • 优化资源管理:监控和优化资源的使用,及时释放不再需要的内存.

实际示例

代码拆分示例

使用React和Webpack进行代码拆分:

// Webpack配置
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: "all",
    },
  },
};

在组件中使用动态导入:

import React, { Suspense, lazy } from "react";

const OtherComponent = lazy(() => import("./OtherComponent"));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

服务器端渲染(SSR)示例

使用Next.js进行服务器端渲染:

// pages/index.js
import React from "react";

const Home = ({ data }) => (
  <div>
    <h1>Home Page</h1>
    <p>{data}</p>
  </div>
);

export async function getServerSideProps() {
  // Fetch data from external API
  const res = await fetch("https://api.example.com/data");
  const data = await res.json();

  // Pass data to the page via props
  return { props: { data } };
}

export default Home;

动态渲染示例

使用Rendertron进行动态渲染:

  1. 部署Rendertron.
  2. 配置反向代理,将搜索引擎爬虫的请求转发给Rendertron.
# Nginx配置示例
location / {
  if ($http_user_agent ~* "Googlebot|Bingbot|Yahoo|DuckDuckBot|Baiduspider") {
    set $prerender 1;
  }
  if ($prerender = 1) {
    proxy_pass http://localhost:3000/render/https://yourwebsite.com$request_uri;
    break;
  }
  proxy_pass http://localhost:8080;
}

通过这些改进方案,可以有效地提升SPA的性能和用户体验,同时解决SEO和兼容性等问题.

自我介绍

1版

我是一名具有3年专业前端开发经验的终身学习者,对技术探索充满热情.我性格温和,善于沟通,乐于接受挑战,致力于通过不断学习来提升自身能力.

我精通JavaScript、CSS和HTML,并且熟练运用Vue和React等前端框架.我也在探索Rust和Tauri以及后端技术如Nest.js和TypeORM,致力于开发跨平台应用.

此外,我具备Shell脚本编写和Python编程经验,熟悉Jenkins持续集成,也有着丰富的PostgreSQL数据库使用经验.

欢迎访问我的GitHub: ajn404,了解更多我的项目和技术探索.

详细讲讲vue3重构vue2的过程及技术难点

Click to expand Vue 3是Vue.js框架的下一个主要版本,带来了许多改进和新特性,但与Vue 2相比,它也带来了一些重大变化.下面我将详细讲解Vue 3重构Vue 2的过程以及其中的技术难点.

1. Vue 3的重构过程

1.1 Vue 3的架构重构

  • 模块化架构:Vue 3使用TypeScript重写,并采用模块化的架构,使得代码更易于维护和扩展.
  • Composition API:引入了Composition API,提供了更灵活和可复用的组合式API,取代了Vue 2的Options API.

1.2 Vue 3的核心重写

  • 虚拟DOM重写:Vue 3对虚拟DOM进行了重写,引入了更高效的算法和优化,提高了渲染性能.
  • 响应式系统重写:Vue 3的响应式系统进行了重写,引入了Proxy作为底层实现,提供了更好的性能和扩展性.

1.3 Vue 3的新特性

  • Teleport:引入了Teleport,允许将组件的子树渲染到DOM中的任何位置.
  • Suspense:引入了Suspense组件,用于处理异步组件和代码分割的加载状态.
  • 新的编译器:Vue 3使用了新的编译器,生成更高效和精简的代码.

2. 技术难点

2.1 Composition API的理解和应用

  • 学习曲线:Composition API是一种全新的API,相对于Vue 2的Options API,有较高的学习曲线,需要花费一定的时间来理解和掌握.
  • 迁移现有代码:将现有的Vue 2代码迁移到Composition API需要重写和调整,尤其是对于大型项目来说可能会面临一定的挑战.

2.2 基于Proxy的响应式系统

  • Proxy的使用:Vue 3的响应式系统基于Proxy进行实现,需要深入理解Proxy的工作原理和使用方式.
  • 迁移现有代码:Vue 3的响应式系统与Vue 2有一些不同,需要对现有代码进行适当的修改和调整.

2.3 新的编译器和虚拟DOM算法

  • 编译器优化:新的编译器带来了许多优化,但也需要深入了解其工作原理,以便编写更高效和可维护的代码.
  • 虚拟DOM算法:新的虚拟DOM算法提高了渲染性能,但也可能需要调整现有的代码以充分利用其优势.

3. 解决方案和建议

  • 逐步迁移:对于现有的Vue 2项目,可以采取逐步迁移的方式,先将部分功能或组件迁移到Vue 3,逐步完善和调整.
  • 学习和实践:学习和实践Composition API、Proxy和新的特性,掌握其使用方式和最佳实践,以便更好地应用到项目中.
  • 借助工具和社区资源:借助Vue Devtools、Vue CLI和Vue Router等工具,以及Vue社区的文档和教程,加快学习和开发的进度.

总的来说,Vue 3的重构过程需要理解和掌握新的API和特性,面临一些挑战和技术难点,但也带来了更好的性能和开发体验.通过学习和实践,逐步迁移现有代码,并结合工具和社区资源,可以更顺利地完成Vue 3的重构和应用.

至诚**面试总结

相比于前面的简单一些,但同样答得不好,还是八股不会背(不想背)

vuex

Click to expand Vuex是Vue.js的官方状态管理库,用于管理应用中的状态.它基于Flux和Redux的概念,提供了一种集中式管理应用状态的机制.下面是Vuex的基本流程:

1. 定义状态(State)

在Vuex中,状态(State)是存储在应用中的数据.你需要在Vuex的store中定义状态,这些状态可以是任何类型的数据,如对象、数组、字符串等.例如:

const store = new Vuex.Store({
  state: {
    count: 0,
  },
});

2. 提交变更(Committing Mutations)

要改变状态,你需要提交一个mutation.mutation是一个包含type和handler的对象,用于同步修改状态.每个mutation都有一个字符串类型的事件类型(type)和一个回调函数(handler),回调函数接收当前状态和可选的参数作为参数,并修改状态.例如:

const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
  },
});

store.commit("increment");

3. 分发行动(Dispatching Actions)

有时,我们需要在异步操作中修改状态.在Vuex中,你可以通过分发一个action来实现异步修改状态.action是一个包含type和handler的对象,type是一个字符串类型的事件类型,handler是一个回调函数,用于执行异步操作,并在操作完成后提交mutation来修改状态.例如:

const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit("increment");
      }, 1000);
    },
  },
});

store.dispatch("incrementAsync");

4. 访问状态(Accessing State)

你可以通过store.state来访问状态.例如:

console.log(store.state.count); // 输出当前状态中的count值

5. 订阅状态(Subscribing to State)

Vuex还提供了一种订阅状态变更的机制.你可以通过store.subscribe方法来订阅状态变更,当状态发生变化时,订阅的回调函数将被调用.例如:

store.subscribe((mutation, state) => {
  console.log(mutation.type);
  console.log(mutation.payload);
});

以上是Vuex的基本流程,通过定义状态、提交变更、分发行动、访问状态和订阅状态,你可以实现对应用中状态的管理和控制.

平时怎么学习技术的

Click to expand

前端学习技术的方法因人而异,但通常包括以下几个步骤和方法:

1. 设定学习目标

  • 明确目标:确定你想要学习或掌握的技术或领域,如Vue.js、React、JavaScript算法等.
  • 分解目标:将大目标分解为小目标,逐步实现,避免过度压力和失望感.

2. 阅读文档和教程

  • 官方文档:查阅官方文档是学习新技术的首要途径,它提供了最权威和全面的资料.
  • 教程和博客:阅读教程和博客文章可以加深对某一技术点的理解,获取实战经验和最佳实践.

3. 实践项目和练习

  • 实际项目:通过实际项目应用所学知识,加深理解和掌握,同时积累项目经验.
  • 练习题和挑战:参与练习题和编程挑战可以提高编码能力和解决问题的能力,如LeetCode、CodeWars等.

4. 参与社区和交流

  • 技术社区:加入技术社区(如GitHub、Stack Overflow、Reddit等),参与讨论和分享经验.
  • 线上课程:参与线上课程(如Coursera、Udemy、Codecademy等)学习和交流.

5. 持续学习和反思

  • 定期复习:定期复习所学知识,加深记忆和理解,巩固基础.
  • 反思和总结:及时总结学习经验和教训,发现不足和改进空间,持续提升自己的学习效率和技能水平.

6. 探索新技术和趋势

  • 跟进行业动态:关注前端技术的最新发展和趋势,掌握行业热点和前沿技术.
  • 尝试新技术:勇于尝试新技术和工具,拓展自己的技术栈和视野.

总的来说,前端学习技术的过程是一个持续不断的探索和学习的过程,需要不断地实践、反思、交流和尝试新技术,才能不断提升自己的技术水平和能力.

vue computed 和 watch的区别

Click to expand

computedwatch都是Vue.js中用于监听数据变化并执行相应逻辑的选项,但它们之间有一些重要的区别:

1. computed

  • 特点
    • computed是一个计算属性,它的值会根据其依赖的数据动态计算得出,并且会有缓存机制,只有依赖的数据发生变化时才会重新计算.
    • 通常用于根据其他数据计算衍生出来的新数据,例如对数据进行过滤、排序、格式化等操作.
  • 示例
    computed: {
      fullName() {
        return this.firstName + ' ' + this.lastName;
      }
    }
    

2. watch

  • 特点
    • watch用于观察特定数据的变化,并在数据变化时执行自定义的逻辑.它可以监听数据的变化,执行一些异步操作或者在数据变化时执行复杂逻辑.
    • 通常用于需要在数据变化时执行异步或者开销较大的操作,或者需要监听非响应式数据的变化.
  • 示例
    watch: {
      firstName(newValue, oldValue) {
        console.log('firstName changed from', oldValue, 'to', newValue);
      }
    }
    

区别总结

  • computed适用于对已有数据进行计算和衍生出新的数据,且计算逻辑不依赖于外部数据.
  • watch适用于监听特定数据的变化,并在数据变化时执行自定义逻辑,通常用于执行异步操作或监听非响应式数据的变化.
  • computed具有缓存机制,只有依赖的数据变化时才会重新计算,而watch每次都会执行.

在实际开发中,根据具体需求选择合适的选项进行数据监听和处理,computed用于简单的计算逻辑和衍生数据,而watch用于复杂的数据变化监听和异步操作.

vue3 computed Vue 3仍然保留了computed的功能,但是在Vue 3中,computed的实现略有不同.在Vue 3中,computed的使用方式与Vue 2基本相同,但是它们的内部实现有所改进,以提高性能和可维护性.

在Vue 3中使用computed

import { computed } from "vue";

export default {
  setup() {
    const firstName = ref("John");
    const lastName = ref("Doe");

    const fullName = computed(() => {
      return firstName.value + " " + lastName.value;
    });

    return {
      firstName,
      lastName,
      fullName,
    };
  },
};

在Vue 3中,computed函数接受一个返回计算属性的箭头函数.与Vue 2相似,computed的值会根据其依赖的响应式数据动态计算得出,但是Vue 3的内部实现进行了优化,以提高性能和响应速度.

因此,尽管Vue 3中对一些API进行了更改和优化,但是computed仍然是Vue的一个重要特性,并且在Vue 3中仍然可以使用它来计算衍生数据.

vue2中数组的值修改了但是没有触发更新,该怎么应对

Click to expand 在Vue 2中,如果修改了数组的值但没有触发更新,通常是因为Vue无法检测到数组内部变化.Vue会在使用push()pop()shift()unshift()splice()sort()reverse()这些方法时进行响应式地更新,但是直接修改数组的索引或者长度时,Vue无法检测到变化,因此不会触发更新.

要解决这个问题,有几种方法:

1. 使用Vue.set或this.$set

Vue提供了Vue.set方法(或this.$set)来修改数组的指定索引值,并触发响应式更新.

Vue.set(arr, index, newValue); // Vue 2.x
this.$set(this.arr, index, newValue); // Vue 2.x 组件中

2. 使用splice方法

使用splice方法删除和插入数组元素时,会触发响应式更新.

this.arr.splice(index, 1, newValue);

3. 使用数组展开运算符或concat方法

将数组重新赋值给一个新数组,然后使用展开运算符或concat方法来修改数组的值,这样也会触发响应式更新.

this.arr = [
  ...this.arr.slice(0, index),
  newValue,
  ...this.arr.slice(index + 1),
];
// 或者
this.arr = this.arr.concat(newValue);

4. 直接修改数组内部

如果要直接修改数组的某个索引,可以使用Vue.setsplice方法,而不是直接赋值.

// 使用Vue.set
Vue.set(this.arr, index, newValue);

// 使用splice
this.arr.splice(index, 1, newValue);

通过以上方法,你可以确保在Vue 2中修改数组的值时触发响应式更新.

面包屑组件设计

Click to expand 在Vue项目中,面包屑导航组件用于显示用户当前位置以及导航路径.实现一个面包屑组件通常需要以下步骤:

1. 设计路由结构

确保你的路由结构能够支持面包屑导航.通常每个路由对象会包含一些自定义元信息,用于面包屑的显示.

// router/index.js
import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);

const routes = [
  {
    path: "/",
    name: "Home",
    component: () => import("@/views/Home.vue"),
    meta: { breadcrumb: "Home" },
  },
  {
    path: "/about",
    name: "About",
    component: () => import("@/views/About.vue"),
    meta: { breadcrumb: "About" },
  },
  {
    path: "/about/team",
    name: "Team",
    component: () => import("@/views/Team.vue"),
    meta: { breadcrumb: "Team" },
  },
];

const router = new Router({
  routes,
});

export default router;

2. 创建面包屑组件

创建一个面包屑组件,该组件将根据当前路由动态生成面包屑导航.

<!-- components/Breadcrumb.vue -->
<template>
  <nav aria-label="breadcrumb">
    <ol class="breadcrumb">
      <li
        v-for="(breadcrumb, index) in breadcrumbs"
        :key="index"
        class="breadcrumb-item"
        :class="{ active: index === breadcrumbs.length - 1 }"
      >
        <router-link
          v-if="index < breadcrumbs.length - 1"
          :to="breadcrumb.path"
        >
          {{ breadcrumb.meta.breadcrumb }}
        </router-link>
        <span v-else>{{ breadcrumb.meta.breadcrumb }}</span>
      </li>
    </ol>
  </nav>
</template>

<script>
export default {
  computed: {
    breadcrumbs() {
      let currentRoute = this.$route;
      let breadcrumbs = [];

      while (currentRoute) {
        if (currentRoute.meta && currentRoute.meta.breadcrumb) {
          breadcrumbs.unshift({
            path: currentRoute.path,
            meta: currentRoute.meta,
          });
        }
        currentRoute = this.$router.options.routes.find(
          route => route.path === currentRoute.meta.parent
        );
      }

      return breadcrumbs;
    },
  },
};
</script>

<style>
.breadcrumb {
  padding: 8px 15px;
  margin-bottom: 20px;
  list-style: none;
  background-color: #f5f5f5;
  border-radius: 4px;
}

.breadcrumb-item + .breadcrumb-item::before {
  content: ">";
  padding: 0 5px;
  color: #6c757d;
}
</style>

3. 使用面包屑组件

将面包屑组件引入到你的页面布局中,以便在每个页面顶部显示面包屑导航.

<!-- App.vue -->
<template>
  <div id="app">
    <header>
      <Breadcrumb />
    </header>
    <router-view />
  </div>
</template>

<script>
import Breadcrumb from "./components/Breadcrumb.vue";

export default {
  components: {
    Breadcrumb,
  },
};
</script>

4. 动态生成面包屑路径

如果路由结构比较复杂,需要动态生成面包屑路径,可以在计算属性中递归遍历当前路由的父路由,生成面包屑数组.

computed: {
  breadcrumbs() {
    const breadcrumbs = [];
    let matched = this.$route.matched.concat();
    matched.forEach(routeRecord => {
      if (routeRecord.meta && routeRecord.meta.breadcrumb) {
        breadcrumbs.push({
          path: routeRecord.path,
          meta: routeRecord.meta
        });
      }
    });
    return breadcrumbs;
  }
}

通过以上步骤,你可以在Vue项目中实现一个动态的面包屑导航组件,以便为用户提供清晰的导航路径.

还有其二