前端杂记202607
前端杂记202607
setTimeout 与 requestAnimationFrame 的区别
| 特性 | setTimeout | requestAnimationFrame |
|---|---|---|
| 调度机制 | 基于时间,指定多少毫秒后执行 | 基于屏幕刷新率,在下次重绘前执行 |
| 最小延迟 | 嵌套超过一定层级后至少 4ms,后台标签页可能被节流到 1s | 通常 16.7ms(60Hz),自动与刷新率对齐 |
| 是否与渲染同步 | 不同步,可能在两次渲染之间执行,导致丢帧或多余计算 | 完全同步,保证回调在渲染帧开始时运行,适合视觉更新 |
| 后台标签页行为 | 延迟会被浏览器大幅延长,降低功耗但可能造成卡顿感 | 自动暂停,标签页不可见时不执行,恢复后继续 |
| 适用场景 | 延迟执行、非视觉逻辑、精确时间控制 | 动画、滚动/选区变化的实时 UI 更新、节流高频事件 |
简单总结:
- 如果你想等待用户停止操作后处理(如搜索、保存状态),用
setTimeout防抖。 - 如果你要在屏幕上实时响应选中变化(如浮动工具栏跟随选中文字),用
requestAnimationFrame节流,能避免布局抖动且更省电。
window.addEventListener 和 document.addEventListener 区别
window.addEventListener 和 document.addEventListener 在语法上完全一样(都继承自 EventTarget),但它们的监听对象和事件触发时机有本质区别。在日常开发中,如果混用,最容易踩的坑是滚动事件不生效和事件触发顺序不符合预期。
以下是核心区别和实战避坑指南:
1. 事件触发的“物理”路径(最关键)
document:代表 HTML 文档对象(DOM 树根节点)。window:代表包含文档的浏览器窗口(视口)。
当一个事件(如点击)发生时,传播路径如下(冒泡阶段):
目标元素 → <body> → <html> → document → window
因此,document 先于 window 收到冒泡事件。如果在捕获阶段(useCapture: true),则顺序完全相反(window 先于 document)。
2. 各自“专属”的事件类型(80% 的选错原因)
不同的对象负责不同的职责,以下事件只能在特定对象上稳定监听:
| 对象 | 专属/推荐监听的事件 | 备注 |
|---|---|---|
window | resize(窗口大小变化)、scroll(滚动条滚动)、hashchange(URL哈希变化)、online/offline(网络状态) | 注意:页面滚动条属于窗口视口,强烈推荐用 window 监听滚动,在 document 上监听滚动在移动端或某些浏览器(如Safari)会失效。 |
document | DOMContentLoaded(DOM解析完成)、visibilitychange(页面可见性变化)、keydown/keyup(全局键盘事件) | DOMContentLoaded 是 document 的专有事件,window 上监听不到。 |
3. this 指向(即 event.currentTarget)
- 监听器中的
this指向调用者:window监听器中的this指向Window对象;document监听器中的this指向Document对象。 - 建议:在回调中尽量使用
event.currentTarget而不是this,避免箭头函数改变上下文时产生混淆。
4. 加载时机的微妙区别
window.onload(或window.addEventListener('load')):必须等所有资源(图片、样式、脚本、字体)完全加载完毕后触发。document.addEventListener('DOMContentLoaded'):只需 DOM 树构建完成即可触发,无需等待图片等外部资源。如果你的 JS 需要操作 DOM,用这个速度更快,用户体验更好。
实战避坑:监听页面滚动的正确姿势
很多新手会写成 document.addEventListener('scroll', fn),结果在 iPhone 的 Safari 上毫无反应。
✅ 正确写法(兼容全平台):
// 必须绑定在 window 上
window.addEventListener('scroll', function(e) {
// 获取滚动高度用 document.documentElement.scrollTop
console.log('滚动位置:', document.documentElement.scrollTop);
});如果同时监听同一个事件(如 click),谁先触发?
假设在捕获阶段(第三个参数为 true):
window先触发(从外向内捕获)。
假设在冒泡阶段(第三个参数为 false 或默认):
document先触发,然后冒泡到window。
日常经验法则:
- 操作浏览器窗口本身(大小、滚动、标签页切换)→ 用
window。 - 操作页面 DOM 结构(节点加载、全局点击委托)→ 用
document。 - 除非特殊需求,不要把
click同时挂在两者上,容易造成事件重复处理或逻辑混乱。如果必须用,确保调用e.stopPropagation()阻断传播。
强制同步布局(Forced Synchronous Layout)
2026-07-22
by deepseek-v4-pro
1. 为什么不是“等所有操作结束”才布局?
浏览器确实会尽量把重排(reflow)延迟到下一帧,但有一个著名的例外:
强制同步布局(Forced Synchronous Layout)
当你读取某些会依赖最新布局信息的属性时(例如 getBoundingClientRect()、offsetTop、scrollHeight 等),浏览器必须立即停下当前 JS,立刻计算布局,否则它没法给你正确的数值。
你的代码顺序正是触发了这一点:
input_p.insertBefore(mirror, input); // mirror 现在是 static,占据一行,把 input 向下挤
const textareaRect = input.getBoundingClientRect(); // 🚨 强制同步布局!此时 input 还在被挤开的位置
mirror.style.position = 'fixed'; // 然后才脱离文档流,input 回到原位于是 textareaRect.top 变成了 107。后面再读 getBoundingClientRect() 时,因为 position: fixed 已经生效,布局再次被强制计算,top 恢复到 66。这就是你看到两个值不同的根本原因。
2. “先把样式弄好再插入” 是最佳实践吗?
是的,而且是解决这个问题的根本方法。
如果你在插入之前就把所有影响几何的样式(特别是 position、display 等)设置好,元素进入文档的那一刻就已经是脱离流的,就不会对兄弟元素造成任何布局抖动。
更好:插入前设置,或插入后立刻设置 fixed 再读布局
// ✅ 方案 A:先设置样式,再插入(推荐)
const mirror = document.createElement('div');
// ... 复制所有样式 ...
mirror.style.position = 'fixed'; // 先设置 fixed
mirror.style.visibility = 'hidden';
// ... 其他样式 ...
input_p.insertBefore(mirror, input); // 再插入,此时已经脱离流,不影响 input 位置
const textareaRect = input.getBoundingClientRect(); // 此时得到的 rect 是正确值// ✅ 方案 B:插入后立刻设置 fixed,不要中间读布局
input_p.insertBefore(mirror, input);
mirror.style.position = 'fixed'; // 马上改定位
// 然后再执行可能触发强制布局的操作(包括 getBoundingClientRect)
const textareaRect = input.getBoundingClientRect(); // 此时 fixed 已生效两种方式都能避免 static 镜像造成的位移。实际项目中,方案 A 更为“原子”,没有一瞬间的文档流污染。
总结
getBoundingClientRect 会立即触发重排,所以如果在此之前 DOM 发生了会影响布局的变更,它返回的就是变更后的中间态。让元素在进入 DOM 前就具备脱离流的定位,是避免这类“瞬间抖动”的最可靠方式。
getBoundingClientRect() 返回的永远是 border-box 几何数据
无论目标元素的 box-sizing 是 content-box、border-box 还是其他
仅闭源 Core 模块
背景
我需要修改一个 git 和 workflow 流程。
我有一个私有仓库 A,他将自动同步到公开仓库 B。
同步过程中,
(1) 首先需要将 Core 模块文件夹 tsc 编译并 publish npm。对应配置:
// tsconfig.json
{
"compilerOptions": {
"types": ["node"],
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
},
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": false,
"allowSyntheticDefaultImports": true, // 导入markdown-it/plantuml-encoder等cjs模块要加这个
"isolatedModules": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"removeComments": true, // 去掉注释
"lib": [
"DOM",
"ES5",
"ES6",
"ES7",
"ES2021",
"ES2022",
"dom.iterable"
],
"declaration": true,
"rootDir": "./",
"outDir": "./dist",
},
"include": [
"**/*.ts"
],
"exclude": [
"dist", "node_modules"
]
}(2) 然后不同步 Core 模块,除该模块外其他都同步
可参考工作流:
// sync_obsidian.yml
# 避免自动审查时,审查 Obsidian 插件用不上的部分
name: Sync Obsidian Branch
on: # 触发器,定义何时运行此工作流
push:
branches: [main] # 默认分支名!
workflow_dispatch: # 手动执行
jobs:
sync-branch:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Remove unused directories
run: |
# 删除不需要参与 Obsidian 审查的目录
rm -rf src/App src/Tauri
# 覆盖说明文件
mv docs/dev/only-obsidian.md README.md
# 覆盖 package.json 等
mv src/Obsidian/obsidian-only/* src/Obsidian/
- name: Push to only-obsidian branch
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: only-obsidian # 目标分支
folder: .
commit-message: "chore: sync obsidian branch"
sync-repo:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Remove unused directories
run: |
# 删除不需要同步到新仓库的目录
rm -rf src/App src/Tauri
# 覆盖说明文件
mv docs/dev/only-obsidian.md README.md
# 覆盖 package.json 等
mv src/Obsidian/obsidian-only/* src/Obsidian/
- name: Push to external repository
uses: JamesIves/github-pages-deploy-action@v4
with:
repository-name: any-menu/obsidian-any-menu # 替换为你的目标仓库 (需提前在 GitHub 创建好)
branch: main # 目标仓库的分支
folder: .
token: ${{ secrets.AnyMenu }} # 跨仓库推送必须使用 PAT
commit-message: "chore: sync from monorepo"(3) 更改被同步仓库的构建方式
该仓库在 build 时需要额外 install 上传的 Core 模块,然后用 copyfile 等方式将模块从 node_module 中移动回目标位置。
然后才可正常进行编译
(4) 补充
这里的目的是不太想开源 Core 的源代码,但是又想允许大家可以基于 Core 模块来进行扩展、失陪、迁移等。故只公开对应的混淆 js 部分。
不确定怎么优化流程比较好。前面的做法只是我自己琢磨的一个可能有效的攻略流。不一定是优解,如果还有其他更好的方法也可以说说