前端面试第 74 期 - 2025.02.11 更新前端面试问题总结(20道题)

11,389 阅读1小时+

2024.11.23 - 2025.02.11 更新前端面试问题总结(20道题)
获取更多面试相关问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…

目录

中级开发者相关问题【共计 7 道题】

  1. 实现一个加法函数sum,支持sum(1)(2)(3,4)(5,6,7....)【热度: 116】【代码实现/算法】
  2. 日常开发中使用到哪些常用的 Git 命令【热度: 193】【web应用场景】
  3. 前端假如有几十个请求,如何去控制并发【热度: 590】【网络】
  4. [React] 状态管理库 Recoil 与 Redux 有何区别【热度: 210】【web框架】
  5. [React] Recoil 里面 selector 该如何使用【热度: 239】【web框架】
  6. [React] Recoil 里面 selector 支持哪些参数【热度: 239】【web框架】
  7. 将已经 push 到远端的两个 commit 合并成一个 commit 应该怎么做【热度: 103】【web应用场景】

高级开发者相关问题【共计 13 道题】

  1. 代码里console.log比较多,该怎么办【热度: 340】【web应用场景】

  2. 前端两个 dom 元素是可以拖拽的, 要实现两个 dom 之间的连接线,如何实现【热度: 55】【web应用场景、代码实现/算法】

  3. 我使用 vite 打包工程, 输出为 es6 的代码, 但是我依赖的 模块是 es5 commonjs 写的;这个他是怎么处理的【热度: 459】【工程化】【出题公司: 腾讯】

  4. 如何去衡量用户操作过程中否卡顿【热度: 492】【工程化】【出题公司: 腾讯】

  5. 前端有哪些性能指标?其中:FCP、LCP、TTFB、FID、TTI、CLS、TBT 等, 分别表示什么意思【热度: 393】【工程化】【出题公司: 腾讯】

  6. vite 开发和构建有何不同?【热度: 292】【工程化】【出题公司: 阿里巴巴】

  7. vite 在开发过程中, 是如何处理 TS 文件的【热度: 112】【工程化】【出题公司: 阿里巴巴】

  8. vite 与 esbuild 是关系【热度: 55】【工程化】【出题公司: 阿里巴巴】

  9. vite 开发过程是直接将文件给浏览器【热度: 107】【工程化】

  10. node 里面 stream 是什么, 有啥应用场景【热度: 495】【Nodejs】

  11. [React] Recoil 里面 selectorFamily 的作用是什么?和 selector 有啥区别【热度: 200】【web框架】

  12. eslint 是怎么做到用配置规则去检验代码异常【热度: 75】【web应用场景】

  13. 自定义 eslint 插件, 要求常量为大写 snake 命名方式, 该怎么做【热度: 35】【web应用场景】

中级开发者相关问题【共计 7 道题】

1079. 实现一个加法函数sum,支持sum(1)(2)(3,4)(5,6,7....)【热度: 116】【代码实现/算法】

关键词:sum 函数实现

举例:

console.log(sum(1,2,3)(4)) => 输出 10

以下是使用 JavaScript 实现满足要求的sum函数的代码:

function sum(...args) {
  let total = 0;
  for (let arg of args) {
    if (Array.isArray(arg)) {
      for (let num of arg) {
        total += num;
      }
    } else {
      total += arg;
    }
  }

  function innerSum(...innerArgs) {
    for (let innerArg of innerArgs) {
      if (Array.isArray(innerArg)) {
        for (let num of innerArg) {
          total += num;
        }
      } else {
        total += innerArg;
      }
    }
    return innerSum;
  }

  innerSum.toString = function () {
    return total;
  };

  return innerSum;
}

在上述代码中:

  1. 首先定义了外部的sum函数,它接受任意数量的参数(可以是单个数字或数字数组),并将这些参数的值累加到total变量中。
  2. 然后在sum函数内部定义了一个名为innerSum的内部函数,它同样接受任意数量的参数,并将这些参数的值累加到total变量中。每次调用innerSum都会继续累加新传入的参数值。
  3. 最后,为innerSum函数添加了一个toString方法,当在console.log等需要将函数转换为字符串的场景下,会返回累加得到的total值,这样就能得到正确的输出结果。

例如:

console.log(sum(1, 2, 3)(4));
// 输出 10

console.log(sum(1)(2)(3, 4)(5, 6, 7));
// 输出 28

补充

在现代 JavaScript 环境里,直接依赖 toString 方法可能无法满足在 console.log 里输出期望结果的需求,因为不同的浏览器或者运行环境在处理 console.log 时表现可能有差异。可以考虑在调用结束时手动调用一个方法来获取最终的累加值。

下面是改进后的代码:

function sum(...args) {
    let total = args.reduce((acc, val) => acc + val, 0);

    function innerSum(...newArgs) {
        total += newArgs.reduce((acc, val) => acc + val, 0);
        return innerSum;
    }

    innerSum.value = function () {
        return total;
    };

    return innerSum;
}

console.log(sum(1, 2, 3)(4).value()); 
console.log(sum(1)(2)(3, 4)(5, 6, 7).value()); 

1081. 日常开发中使用到哪些常用的 Git 命令【热度: 193】【web应用场景】

关键词:git 常用命令

关键词:git 常用命令

在日常生活的开发工作中,以下是一些常用的 Git 命令:

配置相关

  • git config
    • 用途:用于配置 Git 的各种参数,如用户姓名、邮箱等基本信息,这些信息会关联到每一次提交记录。
    • 示例
      • git config --global user.name "Your Name":设置全局的用户名,这个名字会出现在你提交的代码记录中。
      • git config --global user.email "your.email@example.com":设置全局的邮箱,用于标识提交者身份,方便团队协作时追踪提交来源。

仓库初始化与克隆

  • git init
    • 用途:在本地创建一个新的 Git 仓库。通常用于开始一个全新的项目,将当前目录初始化为一个可以被 Git 管理的仓库。
    • 示例:在一个新的项目文件夹下执行git init,就会在这个文件夹中创建一个隐藏的.git文件夹,用于存储仓库的相关信息。
  • git clone
    • 用途:用于从远程仓库(如 GitHub、GitLab 等)克隆代码到本地。这是获取已有项目代码的常用方法。
    • 示例git clone [远程仓库地址],例如git clone https://github.com/user/repository.git,这样就可以把名为repository的远程仓库代码克隆到本地。

状态查看与文件管理

  • git status
    • 用途:查看工作区(本地仓库目录)的状态,包括文件的修改、新增、删除情况,以及文件是否已被添加到暂存区等信息。
    • 示例:在项目目录下执行git status,可以看到类似“Changes not staged for commit”(未暂存的修改)或者“Untracked files”(未跟踪的文件)等状态信息。
  • git add
    • 用途:将工作区的文件修改添加到暂存区,告诉 Git 哪些文件的变化需要被包含在下一次提交中。
    • 示例
      • git add [文件名]:比如git add main.py,将main.py文件的修改添加到暂存区。
      • git add.:将当前目录下所有文件的修改添加到暂存区,但不包括新添加的未跟踪文件。
      • git add -A:添加所有文件的修改,包括新添加的未跟踪文件。
  • git rm
    • 用途:用于从工作区和暂存区删除文件,并且在下次提交时记录这个删除操作。
    • 示例git rm [文件名],例如git rm old_file.txt,将old_file.txt文件从工作区和暂存区删除。

提交与历史记录查看

  • git commit
    • 用途:将暂存区的内容提交到本地仓库,形成一个新的版本记录,需要添加提交注释来描述这次提交的内容。
    • 示例git commit -m "Initial commit",其中-m选项后面的内容就是提交注释,用于简要说明本次提交做了什么。
  • git log
    • 用途:查看提交历史记录,包括每次提交的作者、日期、提交注释和提交哈希值等信息,方便追踪项目的开发历程。
    • 示例:执行git log后,会以逆序(最新的提交在最上面)显示提交记录,如可以看到提交哈希值像commit 123456789abcdef...,作者名字,日期和提交注释等内容。

分支管理

  • git branch
    • 用途:用于创建、查看和删除分支。分支是 Git 中非常重要的概念,可以让开发者在不影响主分支的情况下进行功能开发或者实验。
    • 示例
      • git branch:查看本地所有分支,当前分支会以*标识。
      • git branch [分支名]:创建一个新的分支,例如git branch new-feature,会创建一个名为new - feature的新分支。
      • git branch -d [分支名]:删除一个已经合并的分支,比如git branch -d old-branch
  • git checkout
    • 用途:用于切换分支或者恢复工作区文件。可以让你在不同分支之间切换,以进行不同的开发工作。
    • 示例
      • git checkout [分支名]:切换到指定分支,如git checkout master,会将工作区切换到master分支。
      • git checkout -b [新分支名]:创建一个新分支并切换到该分支,例如git checkout -b dev-branch,相当于同时执行了git branch dev - branchgit checkout dev - branch两个操作。

远程仓库操作

  • git remote
    • 用途:用于管理远程仓库的信息,如添加、查看和删除远程仓库的关联。
    • 示例
      • git remote -v:查看已经配置的远程仓库及其对应的推送和拉取地址,会显示类似origin https://github.com/user/repository.git (fetch)origin https://github.com/user/repository.git (push)的信息。
      • git remote add [远程仓库名] [远程仓库地址]:添加一个新的远程仓库关联,例如git remote add origin https://github.com/user/new - repository.git,这里origin是远程仓库名,后面是仓库地址。
  • git push
    • 用途:将本地仓库的分支和提交记录推送到远程仓库,使得远程仓库的内容与本地保持同步。
    • 示例git push [远程仓库名] [本地分支名],如git push origin master,将本地master分支推送到名为origin的远程仓库。
  • git pull
    • 用途:从远程仓库拉取最新的分支和提交记录到本地仓库,并自动尝试合并到当前分支。
    • 示例git pull [远程仓库名] [本地分支名],通常简单地使用git pull(如果已经配置好远程仓库和分支追踪),就可以拉取并合并远程仓库对应的分支到本地当前分支。

标签管理

  • git tag
    • 用途:用于给特定的提交打上标签,方便标记重要的版本号、里程碑等。比如在发布软件版本时,可以用标签来标识版本号。
    • 示例
      • git tag -a v1.0 -m "Version 1.0 release":创建一个带有注释的标签v1.0,注释内容为Version 1.0 release
      • git tag:查看所有标签,会列出所有已创建的标签名称。
      • git push origin [标签名]:将本地标签推送到远程仓库,例如git push origin v1.0,这样远程仓库也能看到这个标签。

暂存操作

  • git stash
    • 用途:当你正在进行一项工作,但需要切换分支去处理其他紧急事情时,可以使用git stash将当前工作区和暂存区的修改暂存起来。之后可以再恢复这些暂存的修改继续工作。
    • 示例
      • git stash:暂存当前的修改。
      • git stash list:查看所有暂存记录,会显示类似stash@{0}: WIP on master: 1234567...的信息,其中stash@{0}是暂存记录的索引。
      • git stash pop:恢复最近一次暂存的修改,并将其从暂存列表中删除。如果有多个暂存记录,可以使用git stash apply stash@{1}(这里stash@{1}是指定的暂存记录索引)来恢复特定的暂存记录。

合并与变基

  • git merge
    • 用途:用于将一个分支的修改合并到另一个分支。比如将开发分支(dev)的功能合并到主分支(master)。
    • 示例
      • 首先切换到要合并到的分支,例如git checkout master,然后执行git merge dev,就可以将dev分支的修改合并到master分支。合并过程中可能会出现冲突,需要手动解决冲突后再提交。
  • git rebase
    • 用途:和git merge类似,也是用于整合分支修改,但git rebase是将提交历史“变基”,使得提交历史更加线性。一般在希望保持提交历史整洁的情况下使用。
    • 示例:假设在feature分支开发了新功能,现在想将其变基到master分支的最新提交上。首先切换到feature分支,如git checkout feature,然后执行git rebase master。在变基过程中,如果出现冲突,需要手动解决冲突,然后继续执行git rebase --continue
  • git cherry-pick
    • 用途:它允许你从一个分支中选择一个或多个提交(commit),然后将这些提交应用到当前分支。简单来说,就是 “摘取” 其他分支上的特定提交,并把它们的更改应用到你正在工作的分支上。
    • 示例
      • 场景示例:假设你有一个主分支(master)和一个开发分支(dev)。在dev分支上,开发人员发现并修复了一个紧急的软件漏洞,这个修复提交为commit - A。现在,你希望把这个修复也应用到master分支,而不是将整个dev分支合并到master,因为dev分支可能还包含其他未经过充分测试的功能。
      • 操作步骤:首先切换到master分支(git checkout master),然后使用git cherry - pick <commit - A的哈希值>。这样,commit - A中的更改就会被应用到master分支,就好像这个修复是在master分支上直接进行的一样。这在保持主分支稳定的同时,能够快速地将关键的修复传播到需要的分支。

撤销操作

  • git reset
    • 用途:用于撤销提交或者将暂存区的文件修改回退。可以根据不同的参数实现不同程度的撤销。
    • 示例
      • git reset [文件名]:将暂存区中指定文件的修改撤销,例如git reset main.py,如果main.py已经添加到暂存区,这个命令会将它从暂存区移除,恢复到未暂存的状态。
      • git reset --hard [提交哈希值或分支名]:彻底回退到指定的提交或者分支状态,这个操作会清除工作区和暂存区的所有未提交修改,例如git reset --hard HEAD~1会回退到上一个提交的状态,HEAD~1表示当前提交的前一个提交,使用时要非常谨慎。
  • git revert
    • 用途:用于撤销已经提交的修改,但会创建一个新的提交来记录这个撤销操作,这样可以保留完整的提交历史。

    • 示例git revert [提交哈希值],例如git revert 123456789abcdef...,会创建一个新的提交来撤销指定提交所做的修改。

1082. 前端假如有几十个请求,如何去控制并发【热度: 590】【网络】

关键词:并发请求处理

  1. 使用Promise.allSettled和分批处理:将请求按并发限制分成小批次,用Promise.allSettled逐批执行,如fetchWithConcurrency函数,通过循环取批次并处理,最后返回所有结果。
  2. 使用队列方式控制并发PromiseQueue类通过限制同时运行的Promise数量管理并发,有enqueue方法添加任务,runNext方法按规则执行任务,使用时创建实例并传入并发数,将请求任务入队后用Promise.all执行。
  3. 使用第三方库
    • p - limit:引入后设置最大并发数,将请求任务用其包裹后用Promise.all执行。
    • promise - pool:引入后配置请求数组、并发数和任务函数,执行promisePool获取结果。
  4. 浏览器专用:AbortController限制超时fetchWithTimeout函数利用AbortController结合超时机制,在超时后中止请求,处理请求时传入signal,捕获错误并在最后清除超时。
  5. 选择建议
    • 任务数量多但单任务时间短,适合分批处理或PromiseQueue
    • 任务数量多且复杂,建议用p - limit等库。
    • 实时性要求高,可考虑AbortController或合理设置超时策略。

1092. [React] 状态管理库 Recoil 与 Redux 有何区别【热度: 210】【web框架】

关键词:Recoil 与 Redux 区别

Recoil 和 Redux 都是用于管理 React 应用程序状态的库,但它们在设计理念、API、使用场景等方面存在一些明显的区别,下面为你详细介绍:

1. 设计理念

  • Redux
    • 采用单向数据流和单一数据源的设计理念。整个应用的状态被存储在一个单一的 store 中,并且这个状态是只读的。唯一改变状态的方式是触发 action,reducer 会根据 action 来纯函数式地计算新的状态。这种设计使得应用的状态变化可预测,便于调试和维护。
  • Recoil
    • 更强调原子性和灵活性。它将状态拆分成多个原子(atoms),每个原子代表一个独立的状态单元。组件可以独立地订阅和修改这些原子状态,并且可以通过选择器(selectors)来派生和组合状态。这种设计使得状态管理更加细粒度,易于扩展和复用。

2. API 风格

  • Redux
    • API 相对复杂,需要定义 action、reducer、store 等多个概念。通常的使用流程是:定义 action 类型和 action 创建函数,编写 reducer 函数来处理不同的 action,然后使用createStore函数创建 store。组件需要通过connect高阶组件或者useSelectoruseDispatch等钩子来连接到 store 并获取状态和分发 action。
// 定义 action 类型
const INCREMENT = "INCREMENT";

// 定义 action 创建函数
const increment = () => ({ type: INCREMENT });

// 定义 reducer
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    default:
      return state;
  }
};

// 创建 store
import { createStore } from "redux";
const store = createStore(counterReducer);

// 在组件中使用
import { useSelector, useDispatch } from "react-redux";
const Counter = () => {
  const count = useSelector((state) => state);
  const dispatch = useDispatch();
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
    </div>
  );
};
  • Recoil
    • API 更加简洁和直观。主要使用atom来定义状态原子,使用selector来定义派生状态。组件可以直接使用useRecoilStateuseRecoilValue等钩子来访问和修改状态。
import { atom, useRecoilState } from "recoil";

// 定义原子状态
const counterState = atom({
  key: "counterState",
  default: 0,
});

// 在组件中使用
const Counter = () => {
  const [count, setCount] = useRecoilState(counterState);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

3. 状态管理粒度

  • Redux
    • 倾向于将整个应用的状态集中管理在一个 store 中,状态的更新是通过全局的 action 和 reducer 来处理的。这在大型应用中可能会导致 reducer 变得复杂,难以维护。
  • Recoil
    • 支持细粒度的状态管理,每个原子状态都是独立的,可以被不同的组件独立订阅和修改。这种方式使得状态的管理更加灵活,易于扩展和复用。

4. 性能优化

  • Redux
    • 性能优化通常需要手动进行,例如使用reselect库来创建记忆化的选择器,避免不必要的重新计算。
  • Recoil
    • 内置了一些性能优化机制,例如自动记忆化选择器,只有当依赖的状态发生变化时才会重新计算选择器的值。

5. 学习成本

  • Redux
    • 由于其概念较多,如 action、reducer、store 等,学习成本相对较高,尤其是对于初学者来说。
  • Recoil
    • 设计简单直观,API 易于理解和使用,学习成本较低。

6. 使用场景

  • Redux
    • 适用于大型、复杂的应用,尤其是需要严格控制状态变化和进行时间旅行调试的场景。例如,企业级应用、电商应用等。
  • Recoil
    • 适用于中小型应用,或者需要快速迭代和灵活状态管理的场景。例如,原型开发、小型工具应用等。

1093. [React] Recoil 里面 selector 该如何使用【热度: 239】【web框架】

关键词:Recoil selector

在 Recoil 中,selector 用于创建派生状态,它可以根据一个或多个原子(atom)状态计算出新的状态。selector 的值会自动进行记忆化,只有当依赖的状态发生变化时才会重新计算。下面详细介绍 selector 的使用方法。

1. 基本使用

创建一个简单的 atomselector

import React from "react";
import { atom, selector, useRecoilValue } from "recoil";

// 定义一个原子状态
const textState = atom({
  key: "textState",
  default: "Hello, Recoil!",
});

// 定义一个 selector,它依赖于 textState
const textLengthState = selector({
  key: "textLengthState",
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  },
});

const App = () => {
  // 使用 useRecoilValue 钩子获取 selector 的值
  const textLength = useRecoilValue(textLengthState);

  return (
    <div>
      <p>Text length: {textLength}</p>
    </div>
  );
};

export default App;

在上述代码中:

  • 首先定义了一个 atom textState,它代表一个文本状态。
  • 然后定义了一个 selector textLengthState,在 get 函数中,通过 get 参数获取 textState 的值,并计算其长度。
  • 最后在组件中使用 useRecoilValue 钩子获取 textLengthState 的值并渲染。

2. 多个依赖的 selector

selector 可以依赖多个 atom 或其他 selector

import React from "react";
import { atom, selector, useRecoilValue } from "recoil";

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

const num2State = atom({
  key: "num2State",
  default: 20,
});

// 定义一个 selector,依赖于 num1State 和 num2State
const sumState = selector({
  key: "sumState",
  get: ({ get }) => {
    const num1 = get(num1State);
    const num2 = get(num2State);
    return num1 + num2;
  },
});

const App = () => {
  const sum = useRecoilValue(sumState);

  return (
    <div>
      <p>Sum: {sum}</p>
    </div>
  );
};

export default App;

在这个例子中,sumState 这个 selector 依赖于 num1Statenum2State 两个 atom,它会计算这两个状态的和。

3. 可写的 selector

除了只读的 selector,还可以创建可写的 selector,通过 set 函数来修改依赖的状态:

import React from "react";
import { atom, selector, useRecoilState } from "recoil";

// 定义一个原子状态
const counterState = atom({
  key: "counterState",
  default: 0,
});

// 定义一个可写的 selector
const doubleCounterState = selector({
  key: "doubleCounterState",
  get: ({ get }) => {
    const counter = get(counterState);
    return counter * 2;
  },
  set: ({ set }, newValue) => {
    set(counterState, newValue / 2);
  },
});

const App = () => {
  const [doubleCounter, setDoubleCounter] = useRecoilState(doubleCounterState);

  return (
    <div>
      <p>Double Counter: {doubleCounter}</p>
      <button onClick={() => setDoubleCounter(doubleCounter + 2)}>Increment Double</button>
    </div>
  );
};

export default App;

在这个例子中,doubleCounterState 是一个可写的 selector

  • get 函数根据 counterState 计算出双倍的值。
  • set 函数接收一个新值,并将其除以 2 后更新 counterState

4. 使用多个 selector 组合状态

可以将多个 selector 组合起来创建更复杂的派生状态:

import React from "react";
import { atom, selector, useRecoilValue } from "recoil";

// 定义原子状态
const priceState = atom({
  key: "priceState",
  default: 100,
});

const discountRateState = atom({
  key: "discountRateState",
  default: 0.1,
});

// 定义第一个 selector,计算折扣金额
const discountAmountState = selector({
  key: "discountAmountState",
  get: ({ get }) => {
    const price = get(priceState);
    const discountRate = get(discountRateState);
    return price * discountRate;
  },
});

// 定义第二个 selector,计算最终价格
const finalPriceState = selector({
  key: "finalPriceState",
  get: ({ get }) => {
    const price = get(priceState);
    const discountAmount = get(discountAmountState);
    return price - discountAmount;
  },
});

const App = () => {
  const finalPrice = useRecoilValue(finalPriceState);

  return (
    <div>
      <p>Final Price: {finalPrice}</p>
    </div>
  );
};

export default App;

在这个例子中,discountAmountState 计算折扣金额,finalPriceState 依赖于 priceStatediscountAmountState 计算最终价格。

1094. [React] Recoil 里面 selector 支持哪些参数【热度: 239】【web框架】

关键词:Recoil selector

在 Recoil 中,selector 函数接受一个配置对象作为参数,这个配置对象有多个可选属性,下面详细介绍这些属性。

1. key

  • 类型string
  • 描述key 是一个必需的属性,用于唯一标识这个 selector。在 Recoil 的内部状态管理系统中,每个 selector 都需要一个唯一的键,以确保状态的正确更新和管理。
  • 示例
const mySelector = selector({
  key: "mySelector",
  get: ({ get }) => {
    // 状态计算逻辑
  },
});

2. get

  • 类型({ get: GetRecoilValue }) => any
  • 描述get 函数是 selector 中最重要的部分,用于计算 selector 的值。它接收一个对象作为参数,该对象包含一个 get 函数,通过这个 get 函数可以获取其他 atomselector 的值。当依赖的状态发生变化时,get 函数会重新执行以计算新的值。
  • 示例
const textState = atom({
  key: "textState",
  default: "Hello",
});

const textLengthSelector = selector({
  key: "textLengthSelector",
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  },
});

3. set(可选)

  • 类型({ set: SetRecoilState, reset: ResetRecoilState }, newValue: any) => void
  • 描述set 函数用于使 selector 变为可写的。当调用 useRecoilState 或类似的钩子来修改这个 selector 的值时,set 函数会被执行。它接收两个参数:第一个参数是一个包含 setreset 函数的对象,set 函数用于更新其他 atomselector 的值,reset 函数用于将状态重置为默认值;第二个参数是新的值。
  • 示例
const counterState = atom({
  key: "counterState",
  default: 0,
});

const doubleCounterSelector = selector({
  key: "doubleCounterSelector",
  get: ({ get }) => {
    const counter = get(counterState);
    return counter * 2;
  },
  set: ({ set }, newValue) => {
    set(counterState, newValue / 2);
  },
});

4. dangerouslyAllowMutability(可选)

  • 类型boolean
  • 描述:Recoil 默认假设状态是不可变的,这有助于性能优化和状态管理。但在某些特殊情况下,你可能需要对状态进行可变的修改,这时可以将 dangerouslyAllowMutability 设置为 true。不过,使用这个选项需要谨慎,因为它可能会破坏 Recoil 的一些优化机制。
  • 示例
const mutableSelector = selector({
  key: "mutableSelector",
  get: () => {
    // 获取状态逻辑
  },
  dangerouslyAllowMutability: true,
});

5. cachePolicy_UNSTABLE(可选)

  • 类型{ eviction: 'most-recent' | 'lru' | 'none' }
  • 描述:这个属性用于控制 selector 的缓存策略。eviction 有三个可选值:
    • 'most-recent':只保留最近使用的值。
    • 'lru':使用最近最少使用(LRU)算法进行缓存淘汰。
    • 'none':不使用缓存,每次都重新计算。
  • 示例
const cachedSelector = selector({
  key: "cachedSelector",
  get: () => {
    // 状态计算逻辑
  },
  cachePolicy_UNSTABLE: {
    eviction: "lru",
  },
});

这些就是 selector 支持的主要参数,通过合理使用这些参数,可以实现复杂的状态计算和管理。

1096. 将已经 push 到远端的两个 commit 合并成一个 commit 应该怎么做【热度: 103】【web应用场景】

关键词:git rebase 合并 commit

当你已经将两个 commit 推送到远端仓库,现在想要将它们合并成一个 commit,可以按照以下步骤操作:

1. 克隆仓库到本地

如果你还没有在本地克隆该仓库,需要先将远程仓库克隆到本地:

git clone <远程仓库地址>
cd <仓库目录>

2. 查看提交历史

使用 git log 命令查看提交历史,确认要合并的两个 commit 的哈希值(commit hash)。一般来说,新的 commit 在前面,旧的 commit 在后面。

git log --oneline

例如,输出可能如下:

abcdefg 第三个提交
1234567 第二个提交
890abcd 第一个提交

假设你要合并的是 1234567abcdefg 这两个 commit

3. 进行交互式变基

使用 git rebase -i 命令进行交互式变基,指定要合并的 commit 的前一个 commit 的哈希值。在这个例子中,就是 890abcd

git rebase -i 890abcd

执行上述命令后,会打开一个文本编辑器,显示如下内容:

pick abcdefg 第三个提交
pick 1234567 第二个提交

# Rebase 890abcd..abcdefg onto 890abcd (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

4. 修改提交操作

将第二个 commit 前面的 pick 改为 squashs,表示将这个 commit 合并到前一个 commit 中。修改后的内容如下:

pick abcdefg 第三个提交
s 1234567 第二个提交

保存并关闭文本编辑器。

5. 修改合并后的提交信息

保存退出后,会再次打开一个文本编辑器,让你修改合并后的 commit 信息。你可以保留原有的 commit 信息,也可以根据需要进行修改。修改完成后,保存并关闭文本编辑器。

6. 强制推送修改到远程仓库

由于你已经修改了提交历史,本地的提交历史和远程仓库的提交历史不再一致,需要使用 git push -f 命令强制推送修改到远程仓库:

git push -f origin <分支名>

注意:强制推送会覆盖远程仓库的提交历史,可能会影响其他团队成员的工作,建议在操作前与团队成员沟通。

7. 通知团队成员更新本地仓库

强制推送后,通知团队成员拉取最新的提交历史:

git pull --rebase origin <分支名>

通过以上步骤,你就可以将已经推送到远端的两个 commit 合并成一个 commit

高级开发者相关问题【共计 13 道题】

1080. 代码里console.log比较多,该怎么办【热度: 340】【web应用场景】

  1. ESLint配置规则软性禁止:通过配置.eslintrc.json文件,添加"no-console": "warn"规则,使代码中使用console的地方会划上黄色波浪线警示,能一定程度削减console.log数量,但无法真正阻止其使用。
  2. git commit编写规则限制提交:找到项目中的.git/hooks文件夹下的pre-commit.sample文件,将其内容修改为若提交代码中包含console.log则报错提交失败,并将文件重命名为pre-commit。但该规则可被git commit -m 'xxx' --no-verify指令绕过。
  3. 依托于 cicd 的自动检测: 在流水线部署的时候跑 eslint, 如果 console.log 代码增加, 就 拒绝部署即可;
  4. 使用插件删除
    • VSCODE插件:可在插件商店搜索remove-console并安装,找到有console.log的文件使用插件删除,但效果可能不太理想。
    • Webpack插件
      • 可使用terser-webpack-plugin,在项目基于create-react-app脚手架时可直接搜到,在使用处配置drop_console: true,能在打包后去除全部console.log

参考文档:

1083. 前端两个 dom 元素是可以拖拽的, 要实现两个 dom 之间的连接线,如何实现【热度: 55】【web应用场景、代码实现/算法】

关键词:拖拽元素连线实现

  1. 基本思路和技术选择

    • 思路:要实现两个可拖拽 DOM 元素之间的连接线,关键在于获取两个元素的位置信息,并根据这些位置动态地绘制连线。通常可以使用 HTML5 的 Canvas 或者 SVG 来实现连线的绘制。
    • 技术对比
      • Canvas:它是一个通过 JavaScript 来绘制图形的 HTML 元素。使用 Canvas 绘制连线时,需要在每次元素位置变化时重新计算连线的起点和终点坐标,并通过 JavaScript 的绘图 API(如beginPathmoveTolineTostroke等)来绘制连线。Canvas 的优点是绘制性能高,适合绘制复杂的图形和动画;缺点是它是基于像素的绘制,对图形的操作(如修改、删除等)相对复杂。
      • SVG(Scalable Vector Graphics):它是一种基于 XML 的矢量图形格式,在 HTML 中可以直接使用 SVG 标签来定义图形。使用 SVG 绘制连线时,可以通过<line>标签来定义连线,并且可以利用 SVG 的属性(如x1y1表示起点坐标,x2y2表示终点坐标)来动态更新连线的位置。SVG 的优点是图形是矢量的,易于编辑和操作,并且可以通过 CSS 进行样式设置;缺点是在处理大量复杂图形时,性能可能不如 Canvas。
  2. 使用 SVG 实现连接线(推荐方案)

    • 步骤一:创建 SVG 元素并添加到 DOM 中

      • 在 HTML 文件中,首先创建一个 SVG 元素,并将其添加到文档的合适位置。例如:
      <div id="container">
        <svg id="svg-container" width="500" height="500"></svg>
      </div>
      
      • 这里创建了一个宽度和高度都为 500px 的 SVG 容器,并将其放置在一个idcontainerdiv元素内部。
    • 步骤二:创建连线元素并设置初始位置(使用 JavaScript)

      • 假设已经有两个可拖拽的 DOM 元素,它们的id分别为element1element2。在 JavaScript 中,可以通过以下方式创建连线并设置初始位置:
      const svgContainer = document.getElementById("svg-container");
      const element1 = document.getElementById("element1");
      const element2 = document.getElementById("element2");
      // 创建SVG连线元素
      const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
      line.setAttribute("x1", element1.offsetLeft + element1.offsetWidth / 2);
      line.setAttribute("y1", element1.offsetTop + element1.offsetHeight / 2);
      line.setAttribute("x2", element2.offsetLeft + element2.offsetWidth / 2);
      line.setAttribute("y2", element2.offsetTop + element2.offsetHeight / 2);
      line.setAttribute("stroke", "black");
      line.setAttribute("stroke - width", "2");
      // 将连线元素添加到SVG容器中
      svgContainer.appendChild(line);
      
      • 这段代码首先获取了 SVG 容器和两个可拖拽元素。然后使用createElementNS方法创建了一个 SVG 的<line>元素,这个方法是用于创建 SVG 元素的正确方式,因为 SVG 元素是在一个特定的命名空间下。接着,通过setAttribute方法设置了连线的起点(x1y1)和终点(x2y2)坐标,这里的坐标是根据元素的偏移位置(offsetLeftoffsetTop)以及元素宽度和高度的一半来计算的,这样连线就会连接到元素的中心位置。最后,设置了连线的颜色(stroke)和宽度(stroke - width),并将连线元素添加到 SVG 容器中。
    • 步骤三:更新连线位置(在元素拖拽事件中)

      • 为了在元素拖拽时更新连线的位置,需要在拖拽事件处理函数中添加代码来更新连线的起点和终点坐标。假设使用了 HTML5 的drag事件来实现元素的拖拽,以下是一个简单的示例:
      element1.addEventListener("drag", (event) => {
        line.setAttribute("x1", event.target.offsetLeft + event.target.offsetWidth / 2);
        line.setAttribute("y1", event.target.offsetTop + event.target.offsetHeight / 2);
      });
      element2.addEventListener("drag", (event) => {
        line.setAttribute("x2", event.target.offsetLeft + event.target.offsetWidth / 2);
        line.setAttribute("y2", event.target.offsetTop + event.target.offsetHeight / 2);
      });
      
      • 在这里,分别为两个可拖拽元素添加了drag事件监听器。当元素被拖拽时,会获取元素的新位置,并更新连线的起点(对于element1)或终点(对于element2)坐标,从而实现连线随着元素位置变化而动态更新的效果。
  3. 使用 Canvas 实现连接线(替代方案)

    • 步骤一:创建 Canvas 元素并获取绘图上下文

      • 在 HTML 文件中创建一个 Canvas 元素:
      <div id="container">
        <canvas id="canvas-container" width="500" height="500"></canvas>
      </div>
      
      • 然后在 JavaScript 中获取 Canvas 元素和它的绘图上下文(2d上下文用于绘制二维图形):
      const canvasContainer = document.getElementById("canvas-container");
      const ctx = canvasContainer.getContext("2d");
      
    • 步骤二:绘制初始连线(根据元素位置)

      • 同样假设已经有两个可拖拽的 DOM 元素,idelement1element2。在 JavaScript 中计算连线的起点和终点坐标并绘制连线:
      const element1 = document.getElementById("element1");
      const element2 = document.getElementById("element2");
      function drawLine() {
        const x1 = element1.offsetLeft + element1.offsetWidth / 2;
        const y1 = element1.offsetTop + element1.offsetHeight / 2;
        const x2 = element2.offsetLeft + element2.offsetWidth / 2;
        const y2 = element2.offsetTop + element2.offsetHeight / 2;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.strokeStyle = "black";
        ctx.lineWidth = 2;
        ctx.stroke();
      }
      drawLine();
      
      • 这段代码定义了一个drawLine函数,在函数内部计算了连线的起点和终点坐标,然后使用 Canvas 的绘图 API(beginPathmoveTolineTostroke)来绘制连线,设置了连线的颜色(strokeStyle)和宽度(lineWidth)。
    • 步骤三:更新连线(在元素拖拽事件中)

      • 在元素拖拽事件处理函数中,需要清除之前绘制的连线(因为 Canvas 是基于像素的绘制,每次重新绘制都需要清除之前的内容),然后重新绘制连线:
      element1.addEventListener("drag", (event) => {
        ctx.clearRect(0, 0, canvasContainer.width, canvasContainer.height);
        drawLine();
      });
      element2.addEventListener("drag", (event) => {
        ctx.clearRect(0, 0, canvasContainer.width, canvasContainer.height);
        drawLine();
      });
      
      • 这里为两个可拖拽元素添加了drag事件监听器。当元素被拖拽时,首先使用clearRect方法清除整个 Canvas 画布,然后调用drawLine函数重新绘制连线,以实现连线随着元素位置变化而更新的效果。

1084. 我使用 vite 打包工程, 输出为 es6 的代码, 但是我依赖的 模块是 es5 commonjs 写的;这个他是怎么处理的【热度: 459】【工程化】【出题公司: 腾讯】

关键词:vite 打包、es6 转 es5

  1. Vite 的模块解析机制

    • 概述:Vite 在打包过程中会对模块进行解析。当遇到 ES6 模块(ESM)和 CommonJS 模块混合的情况时,它会根据模块的类型采用不同的处理策略。Vite 内部的模块解析系统能够识别模块的语法是 ES6 还是 CommonJS。
  2. 对于 CommonJS 模块的处理

    • 转换为 ESM(在必要时):如果 Vite 发现依赖的模块是 CommonJS 模块,它会尝试将其转换为 ES6 模块格式。这是因为 Vite 的打包目标是输出 ES6 代码,所以需要统一模块格式。在转换过程中,Vite 会分析 CommonJS 模块的require语句和module.exports,将它们转换为等价的 ES6 importexport语句。
    • 示例说明转换过程:假设一个 CommonJS 模块commonjsModule.js如下:
      const add = (a, b) => a + b;
      module.exports = {
        add,
      };
      
      • Vite 会将其转换为类似这样的 ES6 模块(这是内部转换后的概念性表示):
      const add = (a, b) => a + b;
      export default {
        add,
      };
      
      • 这样就可以在 ES6 的代码环境中正确地引用这个模块了。
  3. 处理模块加载和兼容性

    • 加载器机制:Vite 使用了一套加载器系统来处理不同类型的模块。对于 CommonJS 模块转换后的 ES6 模块,加载器会确保它们在打包后的代码中能够正确地被加载和执行。这些加载器会处理模块之间的依赖关系,使得无论是原本的 ES6 模块还是转换后的 CommonJS 模块,都能按照正确的顺序加载。
    • 兼容性处理:Vite 还会考虑到浏览器的兼容性。即使输出的是 ES6 代码,它也会确保这些代码在目标浏览器环境中能够正常运行。对于一些较新的 ES6 语法特性,Vite 可能会通过插件(如@vitejs/plugin - babel)或者自身的语法转换机制来将其转换为更兼容的形式。例如,如果使用了 ES6 的async/await语法,而目标浏览器不支持,Vite 可以将其转换为基于 Promise 的等价形式或者使用 Babel 来进行语法转换,以确保代码能够在更多浏览器中运行。
  4. 插件的辅助作用(如需要)

    • 使用 Babel 插件(如果配置):如果在 Vite 项目中配置了 Babel 相关插件(如@vitejs/plugin - babel),Babel 可以在 Vite 打包过程中进一步协助处理模块的语法转换。特别是对于那些 Vite 自身转换可能不够完善或者需要更复杂语法转换的情况,Babel 插件可以发挥作用。例如,对于一些旧的 JavaScript 语法(如 ES5 的var声明、function声明等)在 CommonJS 模块中出现时,Babel 可以将它们转换为更符合现代标准的语法,以适应输出 ES6 代码的要求。

    • 其他插件用于模块处理:除了 Babel 插件,还有其他一些 Vite 插件可以用于模块处理。例如,vite - plugin - commonjs插件可以专门用于优化 CommonJS 模块在 Vite 中的处理过程,包括更好地处理模块的动态加载、命名空间等问题,以确保 CommonJS 模块和 ES6 模块能够在打包后的代码中和谐共存。

1085. 如何去衡量用户操作过程中否卡顿【热度: 492】【工程化】【出题公司: 腾讯】

关键词:卡顿、性能、用户体验

  1. 使用浏览器性能指标 - FPS(每秒帧数)

    • 基本原理:FPS 是衡量页面流畅度的重要指标。在浏览器中,动画和交互的流畅呈现通常依赖于较高的 FPS。一般来说,当 FPS 达到 60 时,用户体验会比较流畅,因为这意味着每秒有 60 帧画面更新,人眼很难察觉到卡顿。如果 FPS 低于 30,用户就可能明显感觉到页面卡顿。
    • 如何获取 FPS 数据:可以使用requestAnimationFrame函数来计算 FPS。以下是一个简单的 JavaScript 示例,用于监测页面的 FPS:
      let frameCount = 0;
      let lastTime = 0;
      const fpsArray = [];
      function calculateFps() {
        const now = performance.now();
        frameCount++;
        if (now - lastTime >= 1000) {
          const fps = frameCount;
          fpsArray.push(fps);
          frameCount = 0;
          lastTime = now;
        }
        requestAnimationFrame(calculateFps);
      }
      calculateFps();
      
      • 这个代码片段定义了一个函数calculateFps,它使用performance.now函数获取当前时间戳。每执行一次requestAnimationFrame回调函数(通常每秒执行约 60 次),frameCount就加 1。当时间间隔达到 1000 毫秒(1 秒)时,计算出 FPS 并存储在fpsArray中。通过分析fpsArray中的数据,可以了解页面在一段时间内的 FPS 情况,从而判断是否卡顿。
  2. 监测 Long Tasks(长任务)

    • 基本原理:当浏览器执行 JavaScript 任务的时间过长时,就会导致页面卡顿。Long Tasks 是指那些执行时间超过 50 毫秒的任务,因为浏览器在执行这些任务时,无法及时响应用户的其他操作,如滚动、点击等。
    • 如何监测 Long Tasks:可以使用浏览器的PerformanceObserver来监测 Long Tasks。以下是一个示例:
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.duration > 50) {
            console.log("发现长任务:", entry);
          }
        }
      });
      observer.observe({ entryTypes: ["longtask"] });
      
      • 这段代码创建了一个PerformanceObserver对象,它会监听longtask类型的性能条目。当发现执行时间超过 50 毫秒的任务时,会在控制台打印相关信息,包括任务的开始时间、持续时间等,通过这些信息可以定位导致卡顿的代码部分。
  3. 分析 First Input Delay(首次输入延迟 - FID)

    • 基本原理:FID 衡量的是用户首次与页面交互(如点击、按键等)到浏览器开始响应这个交互的时间间隔。一个较低的 FID 表示页面能够快速响应用户操作,而较高的 FID 则可能导致用户感觉到卡顿。
    • 如何获取 FID 数据:可以使用浏览器的PerformanceObserver和相关的性能 API 来测量 FID。以下是一个示例:
      let startTime;
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.name === "first - input") {
            const delay = entry.startTime - startTime;
            console.log("首次输入延迟:", delay);
          }
        }
      });
      observer.observe({ entryTypes: ["first - input"] });
      document.addEventListener("click", (event) => {
        startTime = performance.now();
        // 模拟一个任务,可能导致延迟
        setTimeout(() => {
          console.log("任务完成");
        }, 100);
      });
      
      • 在这个示例中,当用户点击页面时,记录开始时间startTime,然后通过PerformanceObserver监听first - input类型的性能条目。当监听到这个条目时,计算并打印出 FID。通过这种方式,可以评估用户操作后的响应延迟情况。

1086. 前端有哪些性能指标?其中:FCP、LCP、TTFB、FID、TTI、CLS、TBT 等, 分别表示什么意思【热度: 393】【工程化】【出题公司: 腾讯】

  1. FCP(First Contentful Paint) - 首次内容绘制

    • 含义:FCP是指浏览器首次绘制来自DOM的任何内容(如文本、图像、非白色的<canvas>或SVG)的时间点。这是用户体验的一个重要指标,因为它标志着页面开始有实际内容显示给用户,而不仅仅是空白的屏幕。
    • 重要性:对于用户来说,快速的FCP可以让他们感觉到页面正在加载,减少等待的焦虑。一般来说,良好的FCP时间应该在1.8秒以内。
  2. LCP(Largest Contentful Paint) - 最大内容绘制

    • 含义:LCP是指在视口(viewport)内可见的最大图像或文本块的绘制时间。这个指标重点关注页面主要内容的加载情况,因为主要内容通常是用户最关注的部分。例如,对于一个新闻页面,文章中的主图或者标题部分可能是LCP元素;对于一个电商页面,产品图片和主要的产品描述可能是LCP元素。
    • 重要性:LCP能够更好地反映用户感知到的页面加载速度。一个好的LCP时间有助于用户快速获取页面最重要的信息。谷歌建议LCP应在2.5秒内完成,以提供良好的用户体验。
  3. TTFB(Time to First Byte) - 首字节时间

    • 含义:TTFB是指浏览器从开始请求页面到收到第一个字节的时间。这个时间包括了网络传输时间、服务器处理时间等多个因素。它是衡量服务器响应速度和网络性能的一个重要指标。
    • 重要性:较短的TTFB意味着服务器能够快速响应浏览器的请求,为后续的页面加载提供了一个良好的开端。如果TTFB过长,可能是服务器性能问题或者网络连接不佳导致的,会影响整个页面的加载速度。
  4. FID(First Input Delay) - 首次输入延迟

    • 含义:FID衡量的是用户首次与页面交互(如点击、按键等)到浏览器开始响应这个交互的时间间隔。它反映了页面在交互事件处理上的及时性。
    • 重要性:对于提供良好的用户体验来说,快速响应用户的首次输入至关重要。如果FID过高,用户可能会感觉页面卡顿或者无响应,尤其是在需要快速交互的场景,如游戏、表单填写等。谷歌建议FID应控制在100毫秒以内。
  5. TTI(Time to Interactive) - 可交互时间

    • 含义:TTI是指页面从开始加载到变得完全可交互所花费的时间。完全可交互意味着用户可以可靠地与页面进行各种交互操作,如点击链接、输入文本等,并且页面能够及时响应这些操作。这个指标综合考虑了JavaScript加载、解析和执行,以及页面布局的稳定等多个因素。
    • 重要性:TTI直接关系到用户能够有效使用页面的时间点。一个较短的TTI可以让用户更快地开始使用页面的功能,提高用户满意度和工作效率。
  6. CLS(Cumulative Layout Shift) - 累积布局偏移

    • 含义:CLS用于衡量页面在加载过程中发生的视觉稳定性。它计算的是页面布局在加载和交互过程中的意外移动情况。例如,当一个元素最初在屏幕上的一个位置,但是由于后续资源加载导致布局改变,该元素位置发生了偏移,这就会产生CLS。
    • 重要性:良好的视觉稳定性是优质用户体验的关键。如果CLS过高,用户可能会在阅读或操作过程中感到困惑,甚至可能导致误操作。谷歌建议将CLS控制在0.1或以下。
  7. TBT(Total Blocking Time) - 总阻塞时间

    • 含义:TBT是指在FCP和TTI之间,主线程被阻塞(无法响应用户输入)的总时间。这主要是由于长时间的JavaScript执行或者其他主线程任务导致的。
    • 重要性:较低的TBT意味着页面能够更快地变得可交互,减少用户等待的时间。它有助于评估页面在加载过程中对用户交互的阻塞情况,从而优化性能,提高用户体验。

1087. vite 开发和构建有何不同?【热度: 292】【工程化】【出题公司: 阿里巴巴】

关键词:vite、开发、构建

  1. 开发阶段

    • 快速的模块加载
      • 原理:在开发阶段,Vite 充分利用浏览器对原生 ES 模块(ESM)的支持。当浏览器请求一个模块时,Vite 直接将对应的 ES 模块文件发送给浏览器,而不需要像传统构建工具那样先进行打包。这使得模块加载速度非常快,因为浏览器可以直接解析和执行这些原生模块。
      • 示例:假设在一个 Vite 项目中有main.jsutils.js两个模块,main.js通过import { someFunction } from './utils.js';来引用utils.js中的函数。在开发服务器启动后,当浏览器访问main.js并遇到这个import语句时,Vite 会直接将utils.js发送给浏览器,浏览器能够快速地加载和执行这个模块。
    • 高效的模块热替换(HMR)
      • 原理:Vite 的 HMR 功能允许在修改代码后,只更新发生变化的模块,而不需要刷新整个页面。它通过建立一个 WebSocket 连接来实时监听文件变化。当一个模块被修改后,Vite 会将更新后的模块发送给浏览器,浏览器会用新模块替换旧模块,同时保持应用的当前状态。
      • 示例:如果在上述utils.js模块中修改了someFunction的实现,Vite 会检测到这个变化,通过 WebSocket 将更新后的utils.js发送给浏览器。浏览器接收到新模块后,会更新main.js中对someFunction的引用,并且应用的其他部分状态(如组件的局部状态)可以保持不变,从而实现了快速的热替换,提高了开发效率。
    • 开发服务器的便利性
      • 原理:Vite 提供了一个轻量级的开发服务器,它可以快速启动并且自动处理一些常见的开发任务,如解析import语句、处理 CSS 的@import等。这个开发服务器还支持一些开发时的功能,如自动打开浏览器、代理请求等。
      • 示例:在启动 Vite 开发服务器后,它可以自动打开默认浏览器并访问项目的首页。如果项目需要从后端 API 获取数据,并且开发环境和生产环境的 API 地址不同,Vite 的开发服务器可以通过配置代理,将前端请求转发到正确的后端 API 地址,方便开发过程中的联调。
  2. 构建阶段(主要是vite build

    • 代码打包与优化
      • 原理:在构建阶段,Vite 使用 Rollup 作为打包工具(在vite build过程中)。Rollup 会对项目中的所有 ES 模块进行静态分析,将它们打包成一个或多个适合生产环境的文件。这个过程包括 Tree - Shaking(摇树优化),即剔除未使用的代码,以及作用域提升(Scope Hoisting)来优化代码结构,减少变量查找的层级,从而生成更紧凑、高效的代码。
      • 示例:假设有一个包含多个工具函数的utils.js模块,如export const add = (a, b) => a + b;export const subtract = (a, b) => a - b;等。在main.js中只使用了add函数,通过import { add } from './utils.js';引用。在构建过程中,Rollup 会通过 Tree - Shaking 检测到subtract等未使用的函数,不会将它们打包到最终的输出文件中,减小了文件大小。
    • 资源处理与整合
      • 原理:Vite 在构建阶段会对各种资源(如 CSS、静态图像、字体等)进行处理。对于 CSS,它会将@import语句解析并将相关的 CSS 文件合并,还可能进行压缩和代码转换。对于静态资源,它会根据配置将它们复制到输出目录,并可能对文件名进行哈希处理,以方便缓存管理。
      • 示例:如果项目中有一个styles.css文件,其中通过@import导入了其他 CSS 文件,如@import 'normalize.css';。在构建时,Vite 会将normalize.cssstyles.css合并成一个或多个 CSS 文件,并将它们放置在输出目录的合适位置,同时可能会对 CSS 进行压缩,减少文件大小,提高页面加载速度。
    • 输出格式和部署准备
      • 原理:构建阶段会根据配置生成适合生产环境部署的文件格式。Vite 可以生成多种格式的输出,如umdescjs等,以满足不同的部署场景。例如,umd格式可以用于在浏览器中通过<script>标签直接引用,es格式适合现代浏览器和模块加载器,cjs格式可以用于在 Node.js 环境或者一些支持 CommonJS 的地方使用。

      • 示例:如果配置 Vite 的输出格式为umd,并且项目名称为my - app,构建后会生成一个my - app.umd.js文件,这个文件可以直接在 HTML 文件中通过<script>标签引用,并且已经经过了打包和优化,适合在生产环境中部署。

1088. vite 在开发过程中, 是如何处理 TS 文件的【热度: 112】【工程化】【出题公司: 阿里巴巴】

关键词:vite、开发、TS

  1. 即时编译 TS 文件

    • 原理:在 Vite 开发过程中,它利用浏览器原生 ES 模块(ESM)的支持,对于 TypeScript(TS)文件,Vite 会在浏览器请求时即时将其编译为 JavaScript。这个编译过程是由 Vite 内部的插件机制和 TypeScript 编译器(tsc)相关工具完成的。当浏览器请求一个 TS 文件时,Vite 会在服务器端快速地将其转换为浏览器能够理解的 JavaScript 代码。
    • 示例操作:假设在一个 Vite 项目中有一个main.ts文件作为入口文件,其中包含了 TypeScript 代码,如const message: string = "Hello, Vite";。当浏览器访问main.ts时,Vite 会在服务器端将这个 TS 文件编译为 JavaScript 代码(类似于var message = "Hello, Vite";),然后将编译后的 JavaScript 发送给浏览器,浏览器就可以正常地解析和执行这个文件。
  2. 类型检查与开发体验优化

    • 类型检查独立于编译过程:Vite 在开发过程中会将 TypeScript 的类型检查和代码编译分开。它主要关注代码的编译,以确保浏览器能够运行代码,而类型检查可以通过单独的进程来完成。这样做的好处是,即使类型检查出现错误,也不会影响代码的即时编译和在浏览器中的运行,开发者可以在看到代码运行效果的同时,在编辑器(如 VS Code)中根据类型检查的提示来修复代码中的类型问题。
    • 利用 IDE 的类型检查功能:Vite 项目通常会配合支持 TypeScript 的 IDE(集成开发环境)来提供更好的开发体验。IDE 可以实时地对 TypeScript 代码进行类型检查,当开发者编写代码时,IDE 会根据代码中的类型定义和引用情况及时地显示错误和提示信息。例如,在 VS Code 中,当编写一个函数参数类型错误的 TypeScript 代码时,编辑器会立即在代码行旁边显示红色波浪线,并提供错误信息,帮助开发者快速定位和修复类型错误。
  3. 模块解析与 TS 模块支持

    • 正确解析 TS 模块导入和导出:Vite 能够正确地解析 TypeScript 中的模块导入(import)和导出(export)语句。无论是 ES 模块风格的导入导出,还是 CommonJS 风格(在 TypeScript 中也可以使用)的模块交互方式,Vite 都可以处理。例如,在一个 TS 文件中,如果有import { functionA } from './utils.ts';这样的导入语句,Vite 会准确地找到utils.ts文件,并将其编译和处理后提供给当前文件使用。
    • 支持 TS 模块的热替换(HMR):和 JavaScript 模块类似,Vite 也支持 TypeScript 模块的热替换。当修改了一个 TS 模块后,Vite 会通过模块热替换机制将更新后的模块发送给浏览器,浏览器会用新模块替换旧模块,同时保持应用的状态。例如,在一个 Vite + React + TS 项目中,如果修改了一个 React 组件的 TSX(TypeScript XML)文件,Vite 会检测到这个变化,通过 HMR 更新这个组件,而不会刷新整个页面。
  4. TS 配置文件(tsconfig.json)的协同工作

    • 读取和应用配置选项:Vite 会读取项目中的tsconfig.json文件来确定 TypeScript 的编译选项。这个文件包含了如目标 JavaScript 版本(target)、模块解析方式(module)、是否包含类型检查(noEmitOnError等选项)等重要信息。Vite 会根据这些配置来正确地编译 TypeScript 文件。例如,如果tsconfig.json中设置了targetES6,Vite 会将 TS 文件编译为符合 ES6 标准的 JavaScript 代码。
    • 配置文件的更新与 Vite 的响应:当tsconfig.json文件被修改时,Vite 会根据新的配置重新调整对 TypeScript 文件的编译方式。例如,如果在tsconfig.json中添加了一个新的路径别名(paths选项)用于模块引用,Vite 会识别这个变化,并在后续的模块解析过程中正确地使用这个路径别名来查找和处理相关的 TS 模块。

编译细节

  1. Vite 的 TS 插件系统工作机制

    • 利用vite-plugin-typescript插件:Vite 在开发过程中通过vite-plugin-typescript插件来处理 TypeScript 文件的即时编译。这个插件会在 Vite 开发服务器启动时被加载并初始化。它会自动检测项目中的tsconfig.json文件,以获取 TypeScript 的编译选项,如目标编译版本、模块解析规则等。
    • 监听文件变化:插件会建立一个文件监听器,用于实时监测 TypeScript 文件的变化。当有 TS 文件被修改(例如,保存文件后),插件会立即捕获到这个变化。例如,在开发过程中,当开发者在编辑器中保存了一个main.ts文件,插件会察觉到这个文件更新事件。
  2. 编译过程细节

    • 语法解析与类型检查(部分):插件会首先对 TypeScript 文件进行语法解析,这类似于 TypeScript 编译器(tsc)的前端工作。它会检查代码的语法是否正确,包括变量声明、函数定义、类型注解等方面。不过,Vite 中的 TS 插件在开发阶段主要关注代码的编译,对于严格的类型检查,它会和其他工具(如编辑器的 TS 插件)协同工作。例如,对于const num: number = "abc"这样的错误类型赋值,插件会在编译过程中识别语法问题,但可能不会像完整的tsc检查那样提供详细的类型错误链。
    • 转换为 JavaScript 代码:在解析语法之后,插件会根据tsconfig.json中的目标版本(如ESNextES6等)和其他编译规则,将 TypeScript 代码转换为 JavaScript 代码。这个转换过程涉及到诸多 TypeScript 特性的处理,如类型擦除(去除类型注解)、箭头函数转换(如果目标版本较低)、装饰器处理(如果使用)等。例如,对于一个 TypeScript 的箭头函数const add = (a: number, b: number): number => a + b;,如果目标版本是ES5,插件可能会将其转换为function add(a, b) { return a + b; }形式的 JavaScript 函数。
    • 处理模块导入和导出:插件能够正确处理 TypeScript 中的模块导入(import)和导出(export)。无论是 ES 模块风格还是 CommonJS 风格的模块交互,插件都会确保在编译后的 JavaScript 中保持正确的模块引用。例如,对于import { func } from './module.ts';这样的 TS 模块导入,插件会在编译module.ts后,将其正确地转换为 JavaScript 模块引用,使得在浏览器环境中能够正确加载和执行。
  3. 与浏览器交互和代码发送

    • 生成符合浏览器加载的代码格式:经过编译后的 JavaScript 代码会被格式化为符合浏览器 ES 模块加载的形式。Vite 会确保代码中的模块导入路径、变量声明等都符合浏览器的原生 ES 模块规范。例如,编译后的代码可能会有类似于import * as module from './module.js';这样的标准 ES 模块导入语句,其中.js扩展名是在编译过程中添加的(即使原始文件是.ts),以满足浏览器对模块文件扩展名的识别要求。

    • 即时发送给浏览器:一旦编译完成,Vite 会立即将编译后的 JavaScript 代码发送给浏览器。当浏览器请求一个 TypeScript 文件时,Vite 能够快速地完成编译并提供编译后的代码,使得浏览器可以像加载普通 JavaScript 模块一样加载和执行这些代码。例如,当浏览器通过import { app } from './main.ts';请求main.ts时,Vite 会编译main.ts为 JavaScript 并发送给浏览器,让浏览器能够顺利地执行应用程序的入口代码。

1089. vite 与 esbuild 是关系【热度: 55】【工程化】【出题公司: 阿里巴巴】

关键词:vite 与 esbuild

  1. Vite 对 Esbuild 的依赖关系(构建阶段)

    • 代码转换和打包工具:在 Vite 的构建过程(vite build)中,Esbuild 扮演了重要的角色。Esbuild 是一个超高速的 JavaScript 打包器,Vite 利用 Esbuild 来进行代码的转换和初步打包。它能够快速地将 ES 模块(ESM)进行处理,如解析importexport语句,把多个模块合并成一个或多个输出文件。
    • 性能优势体现:Esbuild 的高性能主要体现在其使用 Go 语言编写,具有高度并行化的编译能力。相比传统的打包工具,它能够以极快的速度完成任务。例如,在处理大型项目中的大量 JavaScript 模块时,Esbuild 可以在很短的时间内完成打包工作,这对于 Vite 在构建阶段提高效率非常有帮助。
  2. 功能协作关系(在 Vite 生态中的角色)

    • 与 Vite 插件的协作:Vite 有丰富的插件生态系统,Esbuild 可以和这些插件协作来完成更复杂的构建任务。例如,在处理 CSS、TS(TypeScript)等文件时,Vite 插件可以在 Esbuild 的基础上进行进一步的处理。当 Esbuild 完成对 JavaScript 模块的初步打包后,Vite 插件可以对打包后的文件进行优化,如压缩、添加代码注释等操作。
    • 在不同模块类型处理中的分工:对于不同类型的模块,Vite 和 Esbuild 有不同的处理方式。Esbuild 主要专注于 JavaScript 模块的快速打包和转换,而 Vite 则负责整体的构建流程协调,包括对 CSS 文件的处理(如解析@import语句)、静态资源的处理(如图片、字体的加载路径优化)以及模块热替换(HMR)等开发阶段的功能。例如,在处理一个包含 JavaScript、CSS 和图片的项目时,Esbuild 会快速打包 JavaScript 模块,Vite 则会确保 CSS 正确加载并且图片资源能够被正确引用。
  3. Vite 开发阶段与 Esbuild 的关联(相对较弱)

    • 开发模式下的功能侧重不同:在 Vite 开发阶段,其主要利用浏览器原生 ES 模块的支持来实现快速的模块加载和模块热替换,Esbuild 的打包功能在这个阶段没有像在构建阶段那样被大量使用。Vite 开发服务器会在浏览器请求模块时即时提供相应的 ES 模块文件,而不是依赖 Esbuild 进行预先打包。

    • 潜在的间接关联:不过,在开发阶段,Vite 的一些配置选项和底层机制可能会和 Esbuild 产生间接关联。例如,在配置vite.config.js文件时,一些关于模块解析、路径别名等设置可能会影响到后续构建阶段 Esbuild 的工作方式。同时,开发阶段对模块的处理方式也为构建阶段 Esbuild 的高效打包提供了基础,如准确的模块依赖关系等信息。

1090. vite 开发过程是直接将文件给浏览器【热度: 107】【工程化】

关键词:vite 与 esbuild

那比如我使用的是 比如我开发是使用的 less + ts + react 他是怎么处理的

  1. Vite 不是直接将原始文件给浏览器

    • Vite 是一个基于原生 ES 模块的前端构建工具。在开发过程中,它利用浏览器对原生 ES 模块的支持来提供快速的开发服务器。当你启动 Vite 开发服务器时,它会将你的项目文件作为模块加载。
    • 对于像lesstsreact这样的文件类型,Vite 有相应的处理机制。
  2. 处理 Less 文件

    • 通常情况下,不需要额外配置 Vite 就能处理 Less 文件。不过,若要对 Less 编译进行定制化配置,可在 vite.config.js 或 vite.config.ts 文件中进行设置。
    // vite.config.js
    import { defineConfig } from 'vite';
    
    export default defineConfig({
      css: {
        preprocessorOptions: {
          less: {
            // 在这里可以配置 Less 选项
            javascriptEnabled: true, // 允许在 Less 文件中使用 JavaScript 表达式
            modifyVars: {
              // 可以在这里修改 Less 变量
              '@primary-color': '#1890ff',
            },
          },
        },
      },
    });
    
  3. 处理 TypeScript 文件

    • Vite 本身对 TypeScript 有很好的支持。它利用浏览器原生的 ES 模块加载能力,在开发过程中,对于.ts.tsx文件,Vite 会将它们视为 ES 模块。
    • 当浏览器请求一个 TypeScript 文件对应的模块时,Vite 会进行即时编译(Just - in - Time,JIT)。它会根据 TypeScript 的语法规则将 TypeScript 代码编译成 JavaScript 代码。
    • 例如,对于一个简单的 TypeScript 文件main.ts
      let myVariable: number = 10;
      console.log(myVariable);
      
      Vite 会在内存中即时将其编译成等价的 JavaScript 代码:
      let myVariable = 10;
      console.log(myVariable);
      
      然后将编译后的 JavaScript 代码发送给浏览器,浏览器就能够正常执行这些代码了。而且 Vite 会根据 TypeScript 的模块导入和导出规则正确地处理模块之间的关系。
  4. 处理 React 文件(.tsx文件)

    • 对于 React + TypeScript(.tsx文件),Vite 同样利用上述 TypeScript 的即时编译机制。

    • 例如,对于一个简单的 React 组件文件App.tsx

      import React from "react";
      const App: React.FC = () => {
        return <div>Hello, Vite with React and TS!</div>;
      };
      export default App;
      
    • Vite 会先将tsx文件中的 TypeScript 部分编译成 JavaScript,同时会保留 React 的 JSX 语法。因为现代浏览器虽然不能直接理解 JSX 语法,但是 Vite 会通过@vitejs/plugin - react插件等手段来处理 JSX。

    • 这个插件会将 JSX 语法在发送给浏览器之前转换为浏览器能够理解的React.createElement函数调用形式(或者其他等价的高效形式,比如使用jsx - runtime)。例如,上面的App.tsx中的 JSX 部分可能会被转换为类似以下的 JavaScript 代码:

      import React from "react";
      const App = () => {
        return React.createElement("div", null, "Hello, Vite with React and TS!");
      };
      export default App;
      
    • 这样转换后的代码就可以在浏览器中正常运行,并且能够正确地渲染 React 组件。

1091. node 里面 stream 是什么, 有啥应用场景【热度: 495】【Nodejs】

关键词:node stream

一、stream 的概念与应用场景

  1. 概念引入与文件读取对比:文章以一个简单的 HTTP 服务读取文件并返回响应的示例引入。起初,使用fs.readFileSync读取小文件并返回响应,这种方式在文件较小时可行。但当文件增大到几百 M 时,全部读取完再返回会导致长时间等待,此时便引出了 stream 的概念。通过fs.createReadStream创建文件读取流,并使用pipe方法将其连接到响应流,实现了流式返回,解决了大文件读取的效率问题。
  2. HTTP 传输中的流:在 HTTP 传输大文件时,有两种常见的确定文件下载结束的方式。一种是在header里带上Content-Length,浏览器下载到指定长度即结束;另一种是设置transfer-encoding:chunked,服务器以不固定长度分块返回内容,当返回一个空块时代表传输结束。这种分块传输的方式,使得服务器可以在不知道文件总长度的情况下,持续向客户端发送数据,提高了传输的灵活性和效率。
  3. Shell 命令与 Node 脚本中的流:在 Shell 命令中,ls | grep pack展示了ls命令的输出流作为grep命令的输入流的应用。同时,Node 脚本也能接收 Shell 命令的输出流作为输入,如ls | grep pack | node src/read.mjs,其中process.stdin作为输入流,通过监听readable事件并使用read方法读取数据,体现了流在不同命令和程序间的传递和交互。

二、四种底层 stream 类型

  1. Readable(可读流)
    • 实现方式与示例:可读流需要实现_read方法,通过push方法返回具体的数据,当push(null)时,表示流结束。文章给出了多种创建可读流的方式,包括直接创建实例、通过继承Readable类创建,以及结合生成器创建。例如,通过直接创建实例的方式如下:
import { Readable } from "node:stream";
const readableStream = new Readable();
readableStream._read = function () {
  this.push("阿门阿前一棵葡萄树,");
  this.push("阿东阿东绿的刚发芽,");
  this.push("阿东背着那重重的的壳呀,");
  this.push("一步一步地往上爬。");
  this.push(null);
};
readableStream.on("data", (data) => {
  console.log(data.toString());
});
readableStream.on("end", () => {
  console.log("done");
});
- **与其他API的关联**:文件读取流`fs.createReadStream`是基于`Readable`封装的,`http`服务的`request`对象也是`Readable`的实例。这意味着在处理HTTP请求时,可以像操作普通可读流一样读取请求数据。

2. Writable(可写流)

  • 实现方式与特点:可写流要实现_write方法,用于接收写入的内容。其特点是可以通过控制next方法的调用时机,来控制消费数据的频率。例如,以下代码实现了一个简单的可写流,每 1 秒处理一次写入的数据:
import { Writable } from "node:stream";
class WritableDong extends Writable {
  constructor(iterator) {
    super();
    this.iterator = iterator;
  }
  _write(data, enc, next) {
    console.log(data.toString());
    setTimeout(() => {
      next();
    }, 1000);
  }
}
function createWriteStream() {
  return new WritableDong();
}
const writeStream = createWriteStream();
writeStream.on("finish", () => console.log("done"));
writeStream.write("阿门阿前一棵葡萄树,");
writeStream.write("阿东阿东绿的刚发芽,");
writeStream.write("阿东背着那重重的的壳呀,");
writeStream.write("一步一步地往上爬。");
writeStream.end();
- **与其他API的关联**`fs.createWriteStream``Writable`的常见封装应用,`http`服务的`response`对象也是`Writable`实例,这使得在处理HTTP响应时,可以方便地向客户端写入数据。

3. Duplex(双工流)

  • 实现方式与功能:双工流需要同时实现_read_write方法,具备可读可写的功能。文章通过一个示例展示了双工流的实现:
import { Duplex } from "node:stream";
class DuplexStream extends Duplex {
  _read() {
    this.push("阿门阿前一棵葡萄树,");
    this.push("阿东阿东绿的刚发芽,");
    this.push("阿东背着那重重的的壳呀,");
    this.push("一步一步地往上爬。");
    this.push(null);
  }
  _write(data, enc, next) {
    console.log(data.toString());
    setTimeout(() => {
      next();
    }, 1000);
  }
}
const duplexStream = new DuplexStream();
duplexStream.on("data", (data) => {
  console.log(data.toString());
});
duplexStream.on("end", (data) => {
  console.log("read done");
});
duplexStream.write("阿门阿前一棵葡萄树,");
duplexStream.write("阿东阿东绿的刚发芽,");
duplexStream.write("阿东背着那重重的的壳呀,");
duplexStream.write("一步一步地往上爬。");
duplexStream.end();
duplexStream.on("finish", (data) => {
  console.log("write done");
});
- **实际应用**:TCP协议中的`socket``Duplex`的典型实现,通过`net`模块创建的TCP服务端和客户端,可以实现双向通信,其中`write`方法用于发送数据,`data``end`事件用于接收和处理数据。

4. Transform(转换流)

  • 实现方式与功能:转换流继承自Duplex,需要实现_transform方法,对写入的内容进行转换后提供给消费者读取。例如,以下代码实现了一个将输入内容反转的转换流:
import { Transform } from "node:stream";
class ReverseStream extends Transform {
  _transform(buf, enc, next) {
    const res = buf.toString().split("").reverse().join("");
    this.push(res);
    next();
  }
}
var transformStream = new ReverseStream();
transformStream.on("data", (data) => console.log(data.toString()));
transformStream.on("end", (data) => console.log("read done"));
transformStream.write("阿门阿前一棵葡萄树");
transformStream.write("阿东阿东绿的刚发芽");
transformStream.write("阿东背着那重重的的壳呀");
transformStream.write("一步一步地往上爬");
transformStream.end();
transformStream.on("finish", (data) => console.log("write done"));
- **实际应用**`zlib`模块中的`createGzip`是转换流的重要应用,可用于文件的压缩。通过`source.pipe(gzip).pipe(destination)`,可以将文件读取流经过`gzip`转换流后,传输到文件写入流,实现文件的压缩功能,也可以使用`pipeline` API简化操作。

三、总结

Stream 是 Node.js 中非常重要且常用的 API,在文件读写、网络通信等场景中发挥着关键作用。文章详细介绍了四种底层 stream 类型:Readable通过实现_read方法和push操作提供数据;Writable通过实现_write方法和next操作消费数据;Duplex同时具备可读可写功能;TransformDuplex基础上对写入内容进行转换。在面试中,不仅要了解这四种类型的定义和实现方式,还需能够举例说明常见 API 所属的 stream 类型,如fs.createReadStreamReadable的实现,fs.createWriteStreamWritable的实现等。掌握 stream 的知识,对于理解和开发高效的 Node.js 应用程序至关重要。

参考文档

juejin.cn/post/744918…

1095. [React] Recoil 里面 selectorFamily 的作用是什么?和 selector 有啥区别【热度: 200】【web框架】

关键词:Recoil selector 和 selectorFamily

在 Recoil 中,selectorFamilyselector 都是用于创建派生状态的工具,但它们在使用场景和功能上存在一些差异,下面为你详细介绍它们的作用以及区别。

selector 的作用

selector 用于创建派生状态,它可以根据一个或多个原子(atom)状态计算出新的状态。selector 的值会自动进行记忆化,只有当依赖的状态发生变化时才会重新计算。以下是一个简单的 selector 示例:

import { atom, selector, useRecoilValue } from "recoil";

// 定义一个原子状态
const textState = atom({
  key: "textState",
  default: "Hello, Recoil!",
});

// 定义一个 selector,它依赖于 textState
const textLengthState = selector({
  key: "textLengthState",
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  },
});

const App = () => {
  const textLength = useRecoilValue(textLengthState);
  return (
    <div>
      <p>Text length: {textLength}</p>
    </div>
  );
};

在这个例子中,textLengthState 是一个 selector,它根据 textState 的值计算文本的长度。

selectorFamily 的作用

selectorFamilyselector 的一种扩展,它允许你创建一系列相关的 selector,这些 selector 可以根据传入的参数动态生成。这在需要根据不同的输入生成不同的派生状态时非常有用,比如根据不同的 ID 获取不同的数据。以下是一个 selectorFamily 的示例:

import { atom, selectorFamily, useRecoilValue } from "recoil";

// 模拟一个数据集合
const dataState = atom({
  key: "dataState",
  default: {
    1: { name: "Item 1" },
    2: { name: "Item 2" },
    3: { name: "Item 3" },
  },
});

// 定义一个 selectorFamily
const itemSelectorFamily = selectorFamily({
  key: "itemSelectorFamily",
  get:
    (itemId) =>
    ({ get }) => {
      const data = get(dataState);
      return data[itemId];
    },
});

const App = () => {
  const item1 = useRecoilValue(itemSelectorFamily(1));
  const item2 = useRecoilValue(itemSelectorFamily(2));

  return (
    <div>
      <p>Item 1: {item1?.name}</p>
      <p>Item 2: {item2?.name}</p>
    </div>
  );
};

在这个例子中,itemSelectorFamily 是一个 selectorFamily,它根据传入的 itemIddataState 中获取相应的数据。通过不同的 itemId 可以获取不同的派生状态。

两者的区别

  • 参数化能力
    • selector:没有参数化的能力,它的依赖和计算逻辑是固定的,每次使用时都会计算相同的派生状态。
    • selectorFamily:支持参数化,可以根据传入的不同参数生成不同的 selector 实例,从而计算出不同的派生状态。
  • 使用场景
    • selector:适用于派生状态的计算逻辑固定,不依赖外部参数的场景,比如根据一个固定的原子状态计算其长度、总和等。
    • selectorFamily:适用于需要根据不同的输入动态生成派生状态的场景,比如根据不同的 ID 获取不同的数据、根据不同的筛选条件获取过滤后的数据等。
  • 记忆化机制
    • selector:对整个 selector 进行记忆化,只要依赖的状态不变,就不会重新计算。
    • selectorFamily:对每个根据不同参数生成的 selector 实例进行记忆化,不同参数对应的实例之间相互独立,每个实例的计算结果会分别进行记忆化。

综上所述,selectorFamilyselector 的增强版本,当你需要根据不同的参数动态生成派生状态时,应该使用 selectorFamily;而当派生状态的计算逻辑固定时,使用 selector 即可。

1097. eslint 是怎么做到用配置规则去检验代码异常【热度: 75】【web应用场景】

关键词:eslint 原理

ESLint 是一个用于识别和报告 JavaScript 代码中模式问题的工具,它通过配置规则来检验代码异常,下面详细介绍其工作原理和实现过程。

1. 规则配置

ESLint 允许用户通过配置文件来定义代码检查规则。常见的配置文件格式有 .eslintrc.js.eslintrc.json.eslintrc.yml 等。在配置文件中,可以为不同的规则设置不同的级别,规则级别主要有以下三种:

  • off0:关闭该规则。
  • warn1:开启规则,违反规则时给出警告。
  • error2:开启规则,违反规则时给出错误。

以下是一个 .eslintrc.js 配置文件的示例:

module.exports = {
  rules: {
    // 要求使用分号结尾
    semi: ["error", "always"],
    // 要求使用两个空格缩进
    indent: ["error", 2],
  },
};

2. 解析代码

ESLint 使用 JavaScript 解析器(如 Espree)将 JavaScript 代码解析成抽象语法树(Abstract Syntax Tree,简称 AST)。AST 是代码的一种树形表示,它以节点的形式描述了代码的语法结构。例如,对于以下代码:

function add(a, b) {
  return a + b;
}

解析后会生成一个包含函数定义、参数、返回语句等节点的 AST。

3. 规则检查

ESLint 根据配置文件中的规则,遍历 AST 节点,对代码进行检查。每个规则都定义了一个或多个检查函数,这些函数会在遍历到特定类型的 AST 节点时被调用。例如,对于 semi 规则,当遍历到语句结束的节点时,会检查该语句是否以分号结尾。

以下是一个简单的自定义规则示例:

module.exports = {
  meta: {
    type: "problem",
    docs: {
      description: "Ensure that all function names start with a capital letter",
      category: "Best Practices",
      recommended: true,
    },
    schema: [],
    messages: {
      functionNameShouldStartWithCapital: "Function name should start with a capital letter",
    },
  },
  create(context) {
    return {
      FunctionDeclaration(node) {
        const functionName = node.id.name;
        if (functionName[0] !== functionName[0].toUpperCase()) {
          context.report({
            node,
            messageId: "functionNameShouldStartWithCapital",
          });
        }
      },
    };
  },
};

在这个自定义规则中,create 函数返回一个对象,对象的属性名是 AST 节点类型(如 FunctionDeclaration),对应的属性值是一个检查函数。当遍历到函数声明节点时,会调用该检查函数,检查函数名是否以大写字母开头。

4. 报告问题

如果代码违反了配置的规则,ESLint 会记录问题信息,包括问题所在的文件路径、行号、列号、规则名称和错误信息等。最后,ESLint 会将这些问题信息输出到控制台,方便开发者查看和修复。

5. 自动修复

对于一些简单的规则违反情况,ESLint 支持自动修复功能。在配置文件中,可以为规则设置 fixable 属性,并在规则的检查函数中实现修复逻辑。开发者可以使用 --fix 选项来让 ESLint 自动修复代码中的问题。例如:

eslint --fix yourfile.js

1098. 自定义 eslint 插件, 要求常量为大写 snake 命名方式, 该怎么做【热度: 35】【web应用场景】

关键词:eslint 插件实现

1. 项目结构

假设你的项目根目录为 your-project,在项目中创建插件相关的目录结构:

your-project/
├── .eslintrc.js
├── src/
│   └── ...
├── eslint-plugin-local/
│   ├── lib/
│   │   ├── rules/
│   │   │   └── uppercase-snake-case.js
│   │   └── index.js

2. 编写插件代码

规则组件(eslint-plugin-local/lib/rules/uppercase-snake-case.js
module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "Enforce uppercase snake case naming convention",
      category: "Stylistic Issues",
      recommended: false,
    },
    schema: [],
    messages: {
      invalidNaming: 'Variable name "{{name}}" should be in uppercase snake case.',
    },
  },
  create(context) {
    return {
      VariableDeclarator(node) {
        const { id } = node;
        if (id.type === "Identifier") {
          const name = id.name;
          const isUppercaseSnakeCase = /^[A-Z_]+$/.test(name);
          if (!isUppercaseSnakeCase) {
            context.report({
              node: id,
              messageId: "invalidNaming",
              data: {
                name,
              },
            });
          }
        }
      },
    };
  },
};
插件入口文件(eslint-plugin-local/lib/index.js
module.exports = {
  rules: {
    "uppercase-snake-case": require("./rules/uppercase-snake-case"),
  },
  configs: {
    recommended: {
      rules: {
        "eslint-plugin-local/uppercase-snake-case": "error",
      },
    },
  },
};

3. 配置 ESLint

在项目根目录下的 .eslintrc.js 文件中进行配置,让 ESLint 能够识别并使用本地插件:

module.exports = {
  // 告诉 ESLint 从哪里查找插件
  plugins: [
    {
      rules: require("./eslint-plugin-local/lib/index.js").rules,
      configs: require("./eslint-plugin-local/lib/index.js").configs,
    },
  ],
  extends: ["plugin:eslint-plugin-local/recommended"],
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: "module",
  },
};
代码解释:
  • plugins 数组中的对象用于直接引入本地插件的规则和配置。
  • extends 用于应用插件中定义的推荐配置。

4. 运行 ESLint

确保你的项目已经安装了 ESLint,然后在项目根目录下运行 ESLint 检查:

npx eslint src

这里的 src 是你的项目代码所在目录,你可以根据实际情况修改为其他目录。

通过以上步骤,你就可以在自己的项目中使用本地编写的 ESLint 插件了。当你修改插件代码后,再次运行 ESLint 检查,就能看到最新的检查结果。