乐闻世界logo
搜索文章和话题

前端面试题手册

如何优化 Prettier 的性能?有哪些最佳实践?

Prettier 的性能优化和最佳实践在使用 Prettier 进行大型项目开发时,性能优化和最佳实践对于提高开发效率至关重要。性能优化策略1. 使用 lint-staged 只格式化暂存文件{ "lint-staged": { "*.{js,jsx,ts,tsx,json,css,scss,md}": [ "prettier --write" ] }}2. 配置 .prettierignore 忽略不必要的文件node_modulesdistbuildcoverage*.min.js*.min.csspackage-lock.jsonyarn.lockpnpm-lock.yaml3. 使用缓存机制Prettier 2.0+ 版本内置了缓存功能,可以显著提升重复格式化的速度:npx prettier --write --cache "**/*.{js,jsx,ts,tsx}"4. 限制格式化范围避免格式化整个项目,只格式化修改的文件:npx prettier --write "src/**/*.{js,jsx,ts,tsx}"最佳实践1. 团队统一配置将 .prettierrc 提交到版本控制使用 package.json 的 engines 字段锁定 Node 版本在 README 中说明 Prettier 配置2. 编辑器集成VS Code: 安装 Prettier 扩展,配置 formatOnSaveWebStorm: 内置 Prettier 支持Vim/Neovim: 使用 coc-prettier 或 vim-prettier3. 与 ESLint 协作// .eslintrc.jsmodule.exports = { extends: [ 'eslint:recommended', 'plugin:prettier/recommended' // Prettier 作为 ESLint 规则 ]};4. CI/CD 集成在 PR 检查中添加 Prettier 检查使用 --check 模式而非 --write 模式失败时提供清晰的修复指引5. 版本管理锁定 Prettier 版本避免格式不一致定期更新 Prettier 版本记录版本变更日志常见问题解决1. 格式化冲突使用 eslint-config-prettier 禁用冲突规则定期同步团队配置2. 性能问题使用 --cache 选项减少格式化文件数量升级到最新版本3. 配置继承使用 overrides 为不同文件类型设置不同规则在 monorepo 中使用共享配置包通过遵循这些最佳实践,可以充分发挥 Prettier 的优势,提高团队开发效率和代码质量。
阅读 0·2月21日 16:56

Prettier 的 overrides 配置如何使用?有哪些常见场景?

Prettier 的 overrides 配置详解Prettier 的 overrides 配置允许为不同的文件或目录设置不同的格式化规则,这对于多语言项目或需要特殊格式化的文件非常有用。基本语法{ "semi": true, "overrides": [ { "files": "*.css", "options": { "singleQuote": false } } ]}文件匹配模式1. 单一文件扩展名{ "overrides": [ { "files": "*.json", "options": { "tabWidth": 4 } } ]}2. 多个文件扩展名{ "overrides": [ { "files": ["*.css", "*.scss", "*.less"], "options": { "singleQuote": false } } ]}3. 目录匹配{ "overrides": [ { "files": "src/styles/**/*", "options": { "printWidth": 120 } } ]}4. 复杂匹配模式{ "overrides": [ { "files": ["**/*.test.js", "**/*.spec.js"], "options": { "printWidth": 100 } } ]}常见使用场景1. 不同语言的格式化规则{ "semi": true, "singleQuote": true, "overrides": [ { "files": "*.css", "options": { "singleQuote": false } }, { "files": "*.json", "options": { "tabWidth": 4 } }, { "files": "*.md", "options": { "proseWrap": "preserve" } } ]}2. 特定目录的格式化{ "overrides": [ { "files": "legacy/**/*", "options": { "tabWidth": 4, "useTabs": true } }, { "files": "src/**/*", "options": { "tabWidth": 2, "useTabs": false } } ]}3. 测试文件的特殊格式化{ "overrides": [ { "files": ["**/*.test.js", "**/*.spec.js", "**/__tests__/**/*"], "options": { "printWidth": 100, "trailingComma": "all" } } ]}4. 配置文件的特殊处理{ "overrides": [ { "files": [".prettierrc", ".eslintrc", "package.json"], "options": { "tabWidth": 2 } } ]}高级用法1. 排除特定文件{ "overrides": [ { "files": ["**/*.js", "!**/*.min.js"], "options": { "printWidth": 80 } } ]}2. 指定解析器{ "overrides": [ { "files": "*.vue", "options": { "parser": "vue" } }, { "files": "*.mdx", "options": { "parser": "mdx" } } ]}配置优先级文件特定的 overrides 配置优先级最高全局配置作为默认值后定义的 overrides 会覆盖先定义的相同配置合理使用 overrides 可以让 Prettier 在复杂项目中灵活适应不同的格式化需求。
阅读 0·2月21日 16:56

使用 Prettier 时常见的问题有哪些?如何解决?

Prettier 的常见问题和解决方案在使用 Prettier 的过程中,开发者可能会遇到各种问题。了解这些常见问题及其解决方案可以帮助更高效地使用 Prettier。配置相关问题1. 配置文件不生效问题: 修改了 .prettierrc 但格式化没有变化解决方案:确认配置文件位置正确(项目根目录)检查配置文件语法是否正确(JSON 格式)使用 prettier --find-config-path <file> 查看实际使用的配置清除编辑器缓存2. 多个配置文件冲突问题: 项目中有多个配置文件,不知道使用哪个解决方案:Prettier 按照特定优先级查找配置文件使用 --config 选项明确指定配置文件删除不需要的配置文件在 .prettierrc 中使用 extends 继承配置格式化相关问题3. 格式化后代码不符合预期问题: 格式化后的代码与预期不符解决方案:检查配置选项是否正确使用 --check 模式查看差异使用 --diff 查看具体变化考虑使用 overrides 为特定文件设置不同规则4. 某些文件不想被格式化问题: 某些文件或目录不想被 Prettier 格式化解决方案:在 .prettierignore 中添加忽略规则使用 --ignore-path 指定自定义忽略文件在代码中使用 // prettier-ignore 注释// prettier-ignoreconst matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9],];性能相关问题5. 格式化速度慢问题: 大项目中格式化速度很慢解决方案:使用 --cache 选项启用缓存配置 .prettierignore 忽略不必要的文件只格式化暂存的文件(lint-staged)升级到最新版本的 Prettier6. 内存占用过高问题: 格式化大量文件时内存占用过高解决方案:分批格式化文件使用 --write 而非 --check 减少内存使用增加系统内存限制考虑使用 CI 环境进行格式化集成相关问题7. 与 ESLint 冲突问题: Prettier 和 ESLint 规则冲突解决方案:安装 eslint-config-prettier 禁用冲突规则在 ESLint 配置中正确设置 extends 顺序使用 eslint-plugin-prettier 将 Prettier 作为 ESLint 规则module.exports = { extends: [ 'eslint:recommended', 'plugin:prettier/recommended' // 必须放在最后 ]};8. Git hooks 不工作问题: pre-commit hook 没有自动格式化代码解决方案:检查 Husky 是否正确安装确认 .husky/pre-commit 文件存在且可执行验证 lint-staged 配置正确检查 Git hooks 是否被启用编辑器相关问题9. 编辑器格式化不一致问题: 不同编辑器格式化结果不同解决方案:确保所有编辑器使用相同的 Prettier 配置在项目中配置 .editorconfig使用项目根目录的配置文件统一编辑器的 Prettier 扩展版本10. VS Code 格式化不工作问题: VS Code 中 Prettier 不自动格式化解决方案:确认安装了 Prettier 扩展检查 settings.json 中的配置确认 editor.formatOnSave 设置为 true验证 editor.defaultFormatter 设置正确版本相关问题11. 版本升级导致格式变化问题: 升级 Prettier 版本后代码格式发生变化解决方案:在 package.json 中锁定 Prettier 版本仔细阅读版本更新日志在单独的分支中测试新版本逐步升级并验证格式变化12. 团队成员使用不同版本问题: 不同团队成员使用不同版本的 Prettier解决方案:使用 engines 字段锁定 Node 版本在 CI 中使用特定版本的 Prettier文档中说明推荐的 Prettier 版本使用 npm ci 确保依赖版本一致通过了解这些常见问题和解决方案,可以更高效地使用 Prettier,减少开发过程中的阻碍。
阅读 0·2月21日 16:56

Prettier 的 ignore 文件如何配置?有哪些常用规则?

Prettier 的 ignore 文件配置详解Prettier 的 .prettierignore 文件允许指定不需要格式化的文件和目录,这对于优化性能和避免不必要的格式化非常重要。基本语法.prettierignore 文件使用类似 .gitignore 的语法:# 忽略 node_modules 目录node_modules# 忽略构建产物distbuildout# 忽略特定文件*.min.js*.min.css# 忽略特定目录coverage.next.nuxt# 忽略配置文件package-lock.jsonyarn.lockpnpm-lock.yaml忽略模式1. 目录忽略# 忽略整个目录node_modules# 忽略嵌套目录**/dist# 忽略特定路径的目录src/dist2. 文件扩展名忽略# 忽略所有 .min.js 文件*.min.js# 忽略多个扩展名*.min.js*.min.css*.min.html3. 路径模式# 忽略特定路径下的文件src/temp/*.js# 忽略所有子目录中的文件**/*.test.js# 使用通配符src/**/*.generated.js4. 否定模式# 忽略所有 .js 文件,但保留 src 目录下的*.js!src/**/*.js# 忽略 dist 目录,但保留 dist/publicdist!dist/public常见忽略规则1. 依赖和构建产物node_modulesdistbuildout.next.nuxt.cache2. 压缩文件*.min.js*.min.css*.min.html*.bundle.js3. 测试覆盖率coverage.nyc_output4. 锁定文件package-lock.jsonyarn.lockpnpm-lock.yaml5. 生成的文件*.generated.js*.generated.ts*.d.ts6. 临时文件*.tmp*.temp*.swp高级用法1. 使用自定义 ignore 文件# 指定自定义 ignore 文件prettier --write --ignore-path .prettierignore.custom "**/*.js"2. 禁用默认忽略# 忽略 node_modules 也会被格式化prettier --write --ignore-unknown "**/*.js"3. 命令行指定忽略# 在命令行中指定忽略模式prettier --write "**/*.js" --ignore "node_modules/**"最佳实践1. 性能优化# 忽略大型目录以提高性能node_modulesdistbuildcoverage2. 避免格式化第三方代码# 不格式化第三方库lib/vendorpublic/assets/vendor3. 保护特殊文件# 不格式化需要保持原样的文件*.min.js*.min.css*.bundle.js4. 项目特定规则# 根据项目需求自定义src/generated/**scripts/temp/**常见问题1. ignore 规则不生效检查 .prettierignore 文件位置(应在项目根目录)确认语法正确(类似 .gitignore)使用 --debug-check 查看详细信息2. 想要格式化被忽略的文件# 使用绝对路径绕过 ignore 规则prettier --write /absolute/path/to/file.js# 临时修改 ignore 文件3. 复杂的忽略模式# 组合使用多种模式node_modulesdist**/*.min.js!src/vendor/*.js与其他工具的 ignore 文件1. 与 .gitignore 协作# 可以在 .prettierignore 中引用 .gitignore# 但需要手动复制规则2. 与 ESLint ignore 协作# .eslintignore 和 .prettierignore 可以有不同的规则# 根据工具特性分别配置通过合理配置 .prettierignore,可以优化 Prettier 的性能,避免不必要的格式化,提高开发效率。
阅读 0·2月21日 16:56

Prettier 如何在团队协作中发挥作用?有哪些最佳实践?

Prettier 在团队协作中的应用Prettier 在团队协作中扮演着重要角色,通过强制统一的代码风格,可以有效减少代码审查中的格式争议,提高团队开发效率。团队协作的价值1. 消除风格争议统一代码格式,避免团队成员之间的风格偏好争论减少代码审查时的格式相关评论让开发者专注于代码逻辑而非格式2. 提高代码可读性统一的格式使代码更易于阅读和理解新成员可以更快适应项目代码风格降低代码维护成本3. 自动化流程通过 Git hooks 自动格式化代码在 CI/CD 中强制检查代码格式减少手动格式化的工作量团队配置策略1. 统一配置文件// .prettierrc - 团队统一配置{ "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 80, "bracketSpacing": true}2. 配置文件版本控制将 .prettierrc 和 .prettierignore 提交到版本控制确保所有团队成员使用相同的配置在 README 中说明 Prettier 配置3. 编辑器配置统一// .vscode/settings.json - VS Code 团队配置{ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true }}工作流程集成1. Git Hooks 集成# 使用 Husky 和 lint-stagednpm install --save-dev husky lint-staged prettier# 配置 pre-commit hooknpx husky add .husky/pre-commit "npx lint-staged"2. CI/CD 检查# GitHub Actions- name: Check code format run: npx prettier --check "**/*.{js,jsx,ts,tsx,json,css,md}"3. Pull Request 模板在 PR 模板中提醒开发者格式化代码:## 提交前检查- [ ] 代码已通过 Prettier 格式化- [ ] 运行 `npm run format:check` 确认格式正确团队最佳实践1. 新成员入职在 onboarding 文档中说明 Prettier 使用方法提供编辑器配置指南安装必要的依赖和插件2. 代码审查流程使用 Prettier 自动格式化,减少审查中的格式讨论在 CI 中强制检查格式,不合格的 PR 不能合并提供清晰的格式化错误提示3. 定期更新定期更新 Prettier 版本评估新的配置选项记录配置变更历史4. 文档维护在 README 中说明 Prettier 配置提供常见问题解答记录团队约定的格式规范解决常见问题1. 配置不一致使用统一的配置文件定期同步团队成员的配置在 CI 中强制使用项目配置2. 性能问题使用 lint-staged 只格式化暂存文件配置 .prettierignore 忽略不必要的文件使用缓存机制提高性能3. 历史代码格式化逐步格式化历史代码,避免大量变更使用 --write 选项批量格式化在单独的 PR 中进行格式化通过合理配置和使用,Prettier 可以显著提升团队协作效率,让团队专注于代码质量而非格式问题。
阅读 0·2月21日 16:56

如何在 VS Code、WebStorm 等编辑器中配置 Prettier?

Prettier 在不同编辑器中的配置Prettier 支持在主流代码编辑器中集成,实现自动格式化和实时预览,提高开发效率。VS Code 配置1. 安装扩展在 VS Code 扩展市场搜索 "Prettier - Code formatter"安装官方 Prettier 扩展2. 基本配置在 .vscode/settings.json 中配置:{ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.formatOnPaste": true, "editor.formatOnType": false, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }}3. 高级配置{ "prettier.requireConfig": true, "prettier.useEditorConfig": false, "prettier.documentSelectors": ["**/*.js", "**/*.ts", "**/*.json"], "prettier.enableDebugLogs": false}WebStorm 配置1. 内置支持WebStorm 内置了 Prettier 支持,无需额外安装2. 配置步骤打开 Settings/Preferences导航到 Languages & Frameworks > JavaScript > Prettier启用 "On 'Reformat Code' action"指定 Prettier 包路径3. 保存时格式化在 Settings > Tools > Actions on Save 中:勾选 "Reformat code"选择 Prettier 作为格式化工具Vim/Neovim 配置1. 使用 coc-prettier" 安装 coc.nvim 和 coc-prettier:CocInstall coc-prettier" 配置自动格式化autocmd BufWritePre *.js,*.jsx,*.ts,*.tsx,*.json :call CocAction('format')2. 使用 vim-prettier" 安装 vim-prettierPlug 'prettier/vim-prettier', { \ 'do': 'yarn install', \ 'for': ['javascript', 'typescript', 'css', 'less', 'scss', 'json', 'graphql'] }" 配置let g:prettier#autoformat = 1let g:prettier#exec_cmd_async = 13. 使用 null-ls (Neovim)local null_ls = require("null-ls")null_ls.setup({ sources = { null_ls.builtins.formatting.prettier.with({ extra_args = { "--no-semi", "--single-quote" }, }), },})-- 自动格式化vim.cmd([[autocmd BufWritePre * lua vim.lsp.buf.formatting_sync()]])Sublime Text 配置1. 安装 Package Control通过 Package Control 安装 "JsPrettier"2. 配置在 Preferences > Package Settings > JsPrettier > Settings - User:{ "prettier_cli_path": "/path/to/prettier", "node_path": "/path/to/node", "auto_format_on_save": true, "auto_format_on_save_excludes": ["*.min.js"]}Atom 配置1. 安装插件通过 Atom 设置安装 "prettier-atom"2. 配置在 Settings > Packages > prettier-atom:启用 "Format on Save"配置 Prettier 选项Emacs 配置1. 使用 prettier-emacs(use-package prettier :ensure t :hook ((js-mode . prettier-mode) (typescript-mode . prettier-mode) (json-mode . prettier-mode)) :config (setq prettier-pre-warn nil))通用最佳实践1. 项目级配置在项目根目录创建 .prettierrc确保编辑器使用项目配置将配置文件提交到版本控制2. 编辑器配置管理使用 .editorconfig 统一基本配置在 .vscode/settings.json 中配置 VS Code 特定设置在项目文档中说明编辑器配置3. 团队协作提供编辑器配置指南在 README 中说明推荐配置使用统一的配置文件4. 性能优化只在必要时启用格式化使用缓存机制配置忽略规则常见问题1. 格式化不工作确认 Prettier 已安装检查配置文件路径验证编辑器扩展是否启用2. 配置冲突优先使用项目级配置禁用编辑器默认格式化器检查多个配置文件的优先级3. 性能问题减少自动格式化的触发频率使用增量格式化配置忽略规则通过合理配置编辑器集成,可以充分发挥 Prettier 的优势,提高开发效率。
阅读 0·2月21日 16:56

RxJS 中 switchMap、mergeMap、concatMap 有什么区别?

switchMap、mergeMap、concatMap 的核心区别这三个操作符都是用于处理高阶 Observable(Observable of Observable)的,但它们的处理策略完全不同:1. switchMap特点: 取消之前的内部 Observable,只处理最新的工作原理:当新的值到达时,取消之前未完成的 Observable只保留最新的 Observable 的结果适合需要取消旧请求的场景示例代码:import { of, interval } from 'rxjs';import { switchMap, take, delay } from 'rxjs/operators';// 模拟异步请求function makeRequest(id) { return of(`Result ${id}`).pipe(delay(1000));}interval(500).pipe( switchMap(id => makeRequest(id)), take(5)).subscribe(console.log);// 输出: Result 4// 解释: 因为每500ms触发一次,但请求需要1000ms// 所以每次新请求来时,之前的请求都被取消了// 最终只有最后一个请求完成了实际应用场景:搜索框输入:每次输入都取消上一次的搜索请求自动完成:只显示最新输入的结果导航切换:取消未完成的页面加载// 搜索框示例searchInput.pipe( switchMap(query => searchAPI(query))).subscribe(results => { // 只显示最新搜索的结果 displayResults(results);});2. mergeMap特点: 并行处理所有内部 Observable工作原理:同时订阅所有内部 Observable所有 Observable 的结果都会被发出不保证顺序,结果可能交错示例代码:import { of } from 'rxjs';import { mergeMap, delay } from 'rxjs/operators';function makeRequest(id) { return of(`Result ${id}`).pipe(delay(id * 200));}of(1, 2, 3).pipe( mergeMap(id => makeRequest(id))).subscribe(console.log);// 输出: Result 1, Result 2, Result 3// 解释: 所有请求并行执行,按完成顺序输出实际应用场景:并行加载多个资源批量处理独立任务不需要顺序的并发请求// 并行加载用户数据示例merge( getUserProfile(userId), getUserPosts(userId), getUserComments(userId)).pipe( mergeMap(response => response.json())).subscribe(data => { // 所有数据并行加载完成 updateUI(data);});3. concatMap特点: 顺序处理内部 Observable,一个完成后再处理下一个工作原理:按顺序订阅内部 Observable当前 Observable 完成后才订阅下一个保证结果的顺序示例代码:import { of } from 'rxjs';import { concatMap, delay } from 'rxjs/operators';function makeRequest(id) { return of(`Result ${id}`).pipe(delay(500));}of(1, 2, 3).pipe( concatMap(id => makeRequest(id))).subscribe(console.log);// 输出: Result 1, Result 2, Result 3// 解释: 每个请求按顺序执行,前一个完成后才执行下一个实际应用场景:需要保证顺序的请求依赖前一个结果的后续请求防止服务器过载// 顺序上传文件示例files.pipe( concatMap(file => uploadFile(file))).subscribe(result => { // 文件按顺序上传 console.log('Uploaded:', result);});对比总结| 特性 | switchMap | mergeMap | concatMap ||------|-----------|----------|-----------|| 执行方式 | 取消旧的,只保留最新的 | 并行执行所有 | 顺序执行 || 结果顺序 | 只保留最新结果 | 不保证顺序 | 保证顺序 || 并发数 | 1(同时只有1个) | 无限制 | 1(同时只有1个) || 适用场景 | 搜索、自动完成 | 并行加载 | 顺序处理 || 性能 | 最快(取消旧请求) | 最快(并行) | 较慢(顺序) || 内存占用 | 低 | 高 | 低 |选择指南使用 switchMap 当:需要取消旧请求只关心最新结果搜索、自动完成等场景避免不必要的网络请求使用 mergeMap 当:需要并行处理请求之间没有依赖需要最大化性能不关心结果顺序使用 concatMap 当:需要保证顺序请求之间有依赖需要限制并发数避免服务器过载性能考虑// 性能对比示例const source = interval(100).pipe(take(10));// switchMap: 只处理最后一个source.pipe( switchMap(x => of(x).pipe(delay(1000)))).subscribe();// mergeMap: 并行处理所有(可能造成性能问题)source.pipe( mergeMap(x => of(x).pipe(delay(1000)))).subscribe();// concatMap: 顺序处理(可能较慢)source.pipe( concatMap(x => of(x).pipe(delay(1000)))).subscribe();实际项目中的选择// 1. 搜索功能 - 使用 switchMapsearchInput.pipe( debounceTime(300), distinctUntilChanged(), switchMap(query => searchAPI(query))).subscribe(results => displayResults(results));// 2. 批量加载 - 使用 mergeMapproductIds.pipe( mergeMap(id => getProductDetails(id))).subscribe(products => renderProducts(products));// 3. 顺序操作 - 使用 concatMapcommands.pipe( concatMap(command => executeCommand(command))).subscribe(result => logResult(result));注意事项内存泄漏: mergeMap 可能创建大量并发请求,需要注意内存使用取消逻辑: switchMap 会自动取消,但 concatMap 和 mergeMap 不会错误处理: 任何一个内部 Observable 出错都会导致整个流失败取消订阅: 始终记得取消订阅以避免内存泄漏最佳实践// 1. 结合错误处理searchInput.pipe( switchMap(query => searchAPI(query).pipe( catchError(error => of([])) ) )).subscribe(results => displayResults(results));// 2. 限制并发数(使用 mergeMap)import { mergeMap } from 'rxjs/operators';source.pipe( mergeMap(value => process(value), 3) // 限制并发数为3).subscribe();// 3. 添加重试机制source.pipe( switchMap(value => apiCall(value).pipe( retry(3), catchError(error => of(defaultValue)) ) )).subscribe();
阅读 0·2月21日 16:55

RxJS 中 Subject、BehaviorSubject、ReplaySubject 和 AsyncSubject 有什么区别?

Subject 的核心概念Subject 是 RxJS 中一种特殊的 Observable,它既是 Observable 又是 Observer。这意味着它可以:被多个观察者订阅主动推送新值给所有订阅者实现多播(multicast)功能Subject 类型1. Subject基础 Subject,每次有新订阅者时,不会回放之前的值const subject = new Subject();subject.next(1);subject.next(2);const subscription1 = subject.subscribe(value => console.log('订阅者1:', value));// 订阅者1: 3// 订阅者1: 4subject.next(3);subject.next(4);const subscription2 = subject.subscribe(value => console.log('订阅者2:', value));// 订阅者2: 5// 订阅者2: 6subject.next(5);subject.next(6);2. BehaviorSubjectBehaviorSubject 会记住最新的值,新订阅者会立即接收到当前值const behaviorSubject = new BehaviorSubject('初始值');behaviorSubject.subscribe(value => console.log('订阅者1:', value));// 订阅者1: 初始值behaviorSubject.next('值1');// 订阅者1: 值1behaviorSubject.subscribe(value => console.log('订阅者2:', value));// 订阅者2: 值1 - 立即收到最新值behaviorSubject.next('值2');// 订阅者1: 值2// 订阅者2: 值23. ReplaySubjectReplaySubject 可以回放指定数量的历史值const replaySubject = new ReplaySubject(2); // 回放最后2个值replaySubject.next('值1');replaySubject.next('值2');replaySubject.next('值3');replaySubject.subscribe(value => console.log('订阅者1:', value));// 订阅者1: 值2// 订阅者1: 值3replaySubject.next('值4');// 订阅者1: 值4replaySubject.subscribe(value => console.log('订阅者2:', value));// 订阅者2: 值3// 订阅者2: 值44. AsyncSubjectAsyncSubject 只在完成时发出最后一个值const asyncSubject = new AsyncSubject();asyncSubject.subscribe(value => console.log('订阅者1:', value));asyncSubject.next('值1');asyncSubject.next('值2');asyncSubject.next('值3');// 此时还没有输出asyncSubject.complete();// 订阅者1: 值3 - 只发出最后一个值asyncSubject.subscribe(value => console.log('订阅者2:', value));// 订阅者2: 值3 - 新订阅者也能收到最后一个值实际应用场景Subject 使用场景事件总线状态管理多个组件共享数据流BehaviorSubject 使用场景存储当前状态表单状态管理用户信息管理ReplaySubject 使用场景缓存历史数据重播操作日志错误重试机制AsyncSubject 使用场景HTTP 请求缓存只需要最终结果的计算异步操作的最后值与普通 Observable 的区别// 普通 Observable - 单播const observable = new Observable(subscriber => { subscriber.next(1); subscriber.next(2);});observable.subscribe(v => console.log('观察者1:', v));observable.subscribe(v => console.log('观察者2:', v));// 每个订阅者独立执行// Subject - 多播const subject = new Subject();const source = new Observable(subscriber => { subscriber.next(1); subscriber.next(2);});source.subscribe(subject);subject.subscribe(v => console.log('订阅者1:', v));subject.subscribe(v => console.log('订阅者2:', v));// 所有订阅者共享同一个数据流性能考虑Subject 在多播场景下可以提高性能,避免重复执行相同的异步操作。但需要注意内存泄漏问题,及时取消订阅。
阅读 0·2月21日 16:55

RxJS 中如何进行性能优化?

RxJS 性能优化的核心原则RxJS 性能优化主要关注以下几个方面:减少不必要的订阅和取消订阅优化操作符链的执行效率合理使用多播和缓存避免内存泄漏减少计算开销优化策略1. 使用 share 和 shareReplay避免重复执行相同的 Observable。import { of } from 'rxjs';import { share, shareReplay } from 'rxjs/operators';// ❌ 不好的做法:每次订阅都重新执行const data$ = http.get('/api/data');data$.subscribe(data => console.log('Subscriber 1:', data));data$.subscribe(data => console.log('Subscriber 2:', data));// 发起两次 HTTP 请求// ✅ 好的做法:共享 Observableconst shared$ = http.get('/api/data').pipe( share() // 或 shareReplay(1));shared$.subscribe(data => console.log('Subscriber 1:', data));shared$.subscribe(data => console.log('Subscriber 2:', data));// 只发起一次 HTTP 请求2. 使用 debounceTime 和 throttleTime减少高频事件的处理频率。import { fromEvent } from 'rxjs';import { debounceTime, throttleTime } from 'rxjs/operators';// ❌ 不好的做法:处理每个滚动事件fromEvent(window, 'scroll').subscribe(event => { handleScroll(event); // 可能每秒触发数百次});// ✅ 好的做法:节流处理fromEvent(window, 'scroll').pipe( throttleTime(200) // 每 200ms 最多处理一次).subscribe(event => { handleScroll(event);});// ✅ 好的做法:防抖处理fromEvent(searchInput, 'input').pipe( debounceTime(300) // 停止输入 300ms 后才处理).subscribe(event => { search(event.target.value);});3. 使用 distinctUntilChanged避免处理重复的值。import { fromEvent } from 'rxjs';import { debounceTime, distinctUntilChanged } from 'rxjs/operators';// ❌ 不好的做法:可能处理相同的搜索词fromEvent(searchInput, 'input').pipe( debounceTime(300)).subscribe(event => { search(event.target.value);});// ✅ 好的做法:只在值变化时处理fromEvent(searchInput, 'input').pipe( debounceTime(300), distinctUntilChanged() // 避免重复搜索).subscribe(event => { search(event.target.value);});4. 使用 take 和 takeWhile及时取消不再需要的订阅。import { interval } from 'rxjs';import { take, takeWhile } from 'rxjs/operators';// ❌ 不好的做法:无限订阅interval(1000).subscribe(value => { console.log(value); // 需要手动取消订阅});// ✅ 好的做法:自动取消订阅interval(1000).pipe( take(10) // 只取 10 个值).subscribe(value => { console.log(value);});// ✅ 好的做法:条件取消interval(1000).pipe( takeWhile(value => value < 10)).subscribe(value => { console.log(value);});5. 使用 switchMap 而不是 mergeMap在需要取消旧操作的场景中。import { fromEvent } from 'rxjs';import { switchMap, mergeMap } from 'rxjs/operators';// ❌ 不好的做法:所有请求都会完成fromEvent(searchInput, 'input').pipe( debounceTime(300), mergeMap(query => searchAPI(query)) // 所有请求都会完成).subscribe(results => { displayResults(results);});// ✅ 好的做法:取消旧请求fromEvent(searchInput, 'input').pipe( debounceTime(300), switchMap(query => searchAPI(query)) // 取消旧请求).subscribe(results => { displayResults(results);});6. 使用 buffer 和 bufferTime批量处理数据,减少处理次数。import { interval } from 'rxjs';import { bufferTime } from 'rxjs/operators';// ❌ 不好的做法:逐个处理interval(100).pipe( take(100)).subscribe(value => { processItem(value); // 处理 100 次});// ✅ 好的做法:批量处理interval(100).pipe( take(100), bufferTime(1000) // 每秒批量处理).subscribe(buffer => { processBatch(buffer); // 只处理 10 次});7. 使用 filter 提前过滤尽早过滤掉不需要的数据。import { of } from 'rxjs';import { map, filter } from 'rxjs/operators';// ❌ 不好的做法:先处理再过滤of(1, 2, 3, 4, 5).pipe( map(x => { console.log('Processing:', x); // 处理所有值 return x * 2; }), filter(x => x > 5)).subscribe(value => { console.log('Result:', value);});// ✅ 好的做法:先过滤再处理of(1, 2, 3, 4, 5).pipe( filter(x => x > 2), // 先过滤 map(x => { console.log('Processing:', x); // 只处理需要的值 return x * 2; })).subscribe(value => { console.log('Result:', value);});8. 使用 finalize 清理资源确保资源被正确释放。import { interval } from 'rxjs';import { take, finalize } from 'rxjs/operators';// ❌ 不好的做法:可能忘记清理const subscription = interval(1000).pipe( take(10)).subscribe(value => { console.log(value);});// 可能忘记取消订阅// ✅ 好的做法:自动清理interval(1000).pipe( take(10), finalize(() => { console.log('Cleaning up...'); cleanupResources(); })).subscribe(value => { console.log(value);});高级优化技巧1. 使用 combineLatest 而不是嵌套订阅// ❌ 不好的做法:嵌套订阅this.userService.getUser(userId).subscribe(user => { this.postsService.getPosts(user.id).subscribe(posts => { this.commentsService.getComments(posts[0].id).subscribe(comments => { // 处理数据 }); });});// ✅ 好的做法:使用 combineLatestcombineLatest([ this.userService.getUser(userId), this.postsService.getPosts(userId), this.commentsService.getComments(postId)]).pipe( map(([user, posts, comments]) => ({ user, posts, comments }))).subscribe(data => { // 处理数据});2. 使用 mergeMap 限制并发import { of } from 'rxjs';import { mergeMap } from 'rxjs/operators';// ❌ 不好的做法:可能同时发起大量请求const ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];from(ids).pipe( mergeMap(id => fetchData(id)) // 可能同时发起 10 个请求).subscribe(result => { console.log(result);});// ✅ 好的做法:限制并发数from(ids).pipe( mergeMap(id => fetchData(id), 3) // 最多同时 3 个请求).subscribe(result => { console.log(result);});3. 使用 ReplaySubject 缓存数据import { ReplaySubject } from 'rxjs';// ❌ 不好的做法:每次都重新获取class DataService { getData() { return http.get('/api/data'); }}// ✅ 好的做法:缓存数据class DataService { private cache$ = new ReplaySubject(1); getData() { if (!this.hasFetched) { http.get('/api/data').subscribe(data => { this.cache$.next(data); this.hasFetched = true; }); } return this.cache$.asObservable(); }}4. 使用 scan 而不是 reducescan 可以持续发出中间结果,而 reduce 只在完成时发出结果。import { of } from 'rxjs';import { scan, reduce } from 'rxjs/operators';// ❌ 不好的做法:只在完成时得到结果of(1, 2, 3, 4, 5).pipe( reduce((sum, value) => sum + value, 0)).subscribe(sum => { console.log('Final sum:', sum); // 只输出一次});// ✅ 好的做法:持续输出中间结果of(1, 2, 3, 4, 5).pipe( scan((sum, value) => sum + value, 0)).subscribe(sum => { console.log('Current sum:', sum); // 输出 5 次});5. 使用 tap 进行调试import { of } from 'rxjs';import { map, filter, tap } from 'rxjs/operators';// ✅ 好的做法:使用 tap 调试of(1, 2, 3, 4, 5).pipe( tap(value => console.log('Input:', value)), filter(x => x > 2), tap(value => console.log('After filter:', value)), map(x => x * 2), tap(value => console.log('After map:', value))).subscribe(value => { console.log('Output:', value);});性能监控1. 使用 performance.now() 测量性能import { of } from 'rxjs';import { map, tap } from 'rxjs/operators';const startTime = performance.now();of(1, 2, 3, 4, 5).pipe( map(x => x * 2), tap(() => { const elapsed = performance.now() - startTime; console.log(`Elapsed: ${elapsed.toFixed(2)}ms`); })).subscribe();2. 使用 count 统计数据量import { of } from 'rxjs';import { count, tap } from 'rxjs/operators';of(1, 2, 3, 4, 5).pipe( tap(value => console.log('Processing:', value)), count()).subscribe(count => { console.log('Total processed:', count);});常见性能问题1. 过多的订阅// ❌ 不好的做法:创建多个订阅const data$ = http.get('/api/data');data$.subscribe(data => updateUI1(data));data$.subscribe(data => updateUI2(data));data$.subscribe(data => updateUI3(data));// ✅ 好的做法:共享订阅const data$ = http.get('/api/data').pipe( share());data$.subscribe(data => updateUI1(data));data$.subscribe(data => updateUI2(data));data$.subscribe(data => updateUI3(data));2. 内存泄漏// ❌ 不好的做法:忘记取消订阅class MyComponent { ngOnInit() { this.dataService.getData().subscribe(data => { this.data = data; }); } // 组件销毁时订阅仍然存在}// ✅ 好的做法:正确取消订阅class MyComponent implements OnDestroy { private destroy$ = new Subject<void>(); ngOnInit() { this.dataService.getData().pipe( takeUntil(this.destroy$) ).subscribe(data => { this.data = data; }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }}3. 不必要的计算// ❌ 不好的做法:重复计算of(1, 2, 3).pipe( map(x => { const result = expensiveCalculation(x); return result; }), map(x => { const result = expensiveCalculation(x); // 重复计算 return result; })).subscribe();// ✅ 好的做法:避免重复计算of(1, 2, 3).pipe( map(x => { const result = expensiveCalculation(x); return { value: x, result }; })).subscribe(data => { console.log(data.value, data.result);});最佳实践1. 使用 AsyncPipe@Component({ template: ` <div *ngIf="data$ | async as data"> {{ data }} </div> `})export class MyComponent { data$ = this.service.getData(); // AsyncPipe 自动管理订阅}2. 使用 takeUntilexport class MyComponent implements OnDestroy { private destroy$ = new Subject<void>(); ngOnInit() { this.service.getData().pipe( takeUntil(this.destroy$) ).subscribe(data => { this.data = data; }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }}3. 使用 shareReplay@Injectable()export class DataService { private cache = new Map<string, Observable<any>>(); getData(key: string) { if (!this.cache.has(key)) { this.cache.set(key, http.get(`/api/data/${key}`).pipe( shareReplay(1) )); } return this.cache.get(key)!; }}总结RxJS 性能优化的关键点:共享订阅: 使用 share 和 shareReplay 避免重复执行减少处理频率: 使用 debounceTime 和 throttleTime过滤重复数据: 使用 distinctUntilChanged及时取消订阅: 使用 take 和 takeWhile选择合适的操作符: switchMap vs mergeMap vs concatMap批量处理: 使用 buffer 和 bufferTime提前过滤: 使用 filter 尽早过滤不需要的数据清理资源: 使用 finalize 确保资源释放避免嵌套订阅: 使用 combineLatest 和 forkJoin限制并发: 使用 mergeMap 的并发参数掌握这些优化技巧可以显著提升 RxJS 应用的性能。
阅读 0·2月21日 16:54