普通视图
Three.js 实战:使用 DOM/CSS 打造高性能 3D 文字
2026 年,值得前端全栈尝试的 NestJS 技术栈组合 😍😍😍
让 AI 学会"问一嘴":assistant-ui 前端工具的人机交互实践
回首 jQuery 20 年:从辉煌到没落
MCP、Agent、大模型应用架构解读
用 Intersection Observer 打造丝滑的级联滚动动画
无需任何动画库,仅用原生 Web API 实现滚动时丝滑的淡入滑入效果,兼顾性能与体验。
你是否见过这样的交互动效:
- 用户滚动页面时,一组卡片像被“唤醒”一样,依次从下方滑入并淡入;
![]()
- 如果这些元素在页面加载时已在视口内,它们也会自动按顺序浮现。
![]()
这种效果不仅视觉流畅,还能有效引导用户注意力,提升内容层次感。更重要的是——它不依赖 GSAP、AOS 等第三方库,仅靠 Intersection Observer + CSS 动画 + 少量 JavaScript,就能实现高性能、可访问、且高度可控的滚动触发型级联动画。
今天,我们就来一步步拆解这个经典动效,并给出一套可直接复用的轻量级方案。
🔧 核心原理概览
整个动画系统依赖三个关键技术点:
| 技术 | 作用 |
|---|---|
IntersectionObserver |
监听元素是否进入视口,避免频繁 scroll 事件 |
CSS @keyframes
|
定义滑入 + 淡入动画 |
--animation-order 自定义属性 |
通过 calc() 动态设置 animation-delay,实现“逐个延迟”的级联感 |
最关键的设计哲学是:动画只在用户能看到它的时候才执行,既节省性能,又避免“闪现”。
🧱 HTML 结构(简化版)
为便于理解,我们剥离业务逻辑,只保留动效核心:
<div class="container">
<ul class="card-list">
<li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 1;"
>Card 1</li
>
<li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 2;"
>Card 2</li
>
<li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 3;"
>Card 3</li
>
<!-- 更多卡片... -->
</ul>
</div>
💡 类名与属性说明
-
.scroll-trigger:表示该元素需要被滚动监听; -
.animate--slide-in:启用滑入动画; -
data-cascade:JS 识别“需设置动画顺序”的标志; -
--animation-order:CSS 自定义属性,用于计算延迟时间(如第 2 个元素延迟 150ms)。
🎨 CSS 动画定义
:root {
--duration-extra-long: 600ms;
--ease-out-slow: cubic-bezier(0, 0, 0.3, 1);
}
/* 仅在用户未开启“减少运动”时启用动画(晕动症用户友好) */
@media (prefers-reduced-motion: no-preference) {
.scroll-trigger:not(.scroll-trigger--offscreen).animate--slide-in {
animation: slideIn var(--duration-extra-long) var(--ease-out-slow) forwards;
animation-delay: calc(var(--animation-order) * 75ms);
}
@keyframes slideIn {
from {
transform: translateY(2rem);
opacity: 0.01;
}
to {
transform: translateY(0);
opacity: 1;
}
}
}
✨ 参数说明
| 属性 | 值 | 作用 |
|---|---|---|
transform |
translateY(2rem) → 0 |
由下往上滑入 |
opacity |
0.01 → 1 |
淡入(避免完全透明导致布局跳动) |
animation-delay |
n × 75ms |
第1个延迟75ms,第2个150ms……形成级联 |
animation-fill-mode |
forwards |
动画结束后保持最终状态 |
✅ 无障碍提示:通过
@media (prefers-reduced-motion)尊重用户偏好,对晕动症用户更友好。
🕵️ JavaScript:Intersection Observer 监听逻辑
为什么不用 scroll 事件?
传统方式:
// ❌ 性能差,频繁触发
window.addEventListener('scroll', checkVisibility);
现代方案:
// ✅ 高性能,浏览器底层优化
const observer = new IntersectionObserver(callback, options);
完整监听逻辑
const SCROLL_ANIMATION_TRIGGER_CLASSNAME = 'scroll-trigger';
const SCROLL_ANIMATION_OFFSCREEN_CLASSNAME = 'scroll-trigger--offscreen';
function onIntersection(entries, observer) {
entries.forEach((entry, index) => {
const el = entry.target;
if (entry.isIntersecting) {
// 进入视口:移除 offscreen 类,允许动画播放
el.classList.remove(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);
// 若为级联元素,动态设置顺序(兜底)
if (el.hasAttribute('data-cascade')) {
el.style.setProperty('--animation-order', index + 1);
}
// 只触发一次,停止监听
observer.unobserve(el);
} else {
// 离开视口:加上 offscreen 类,禁用动画
el.classList.add(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);
}
});
}
function initScrollAnimations(root = document) {
const triggers = root.querySelectorAll(`.${SCROLL_ANIMATION_TRIGGER_CLASSNAME}`);
if (!triggers.length) return;
const observer = new IntersectionObserver(onIntersection, {
rootMargin: '0px 0px -50px 0px', // 元素进入视口 50px 后才触发
threshold: [0, 0.25, 0.5, 0.75, 1.0],
});
triggers.forEach((el) => observer.observe(el));
}
// 页面加载完成后启动
document.addEventListener('DOMContentLoaded', () => {
initScrollAnimations();
});
🎯 关键设计细节
-
rootMargin: '0px 0px -50px 0px':确保元素完全进入用户视野后再触发动画,避免“刚看到就结束”; - 初始所有
.scroll-trigger元素默认带有.scroll-trigger--offscreen类,阻止 CSS 动画生效; -
unobserve:动画只播放一次,避免重复触发,节省资源。
📊 两种场景下的行为对比
| 场景 | 初始状态 | 触发时机 | 动画表现 |
|---|---|---|---|
| 卡片已在视口内 | 无 --offscreen 类 |
页面加载后立即 | 依次淡入(基于 --animation-order) |
| 卡片在视口外 | 有 --offscreen 类 |
滚动到视口(超过 50px) | 滚动时依次淡入 |
这正是你感受到的“丝滑感”来源:无论用户如何进入页面,动画总是在最合适的时机出现。
💡 总结:这套方案的优势
| 能力 | 说明 |
|---|---|
| ✅ 高性能 | 使用 IntersectionObserver 替代 scroll 事件,避免频繁计算 |
| ✅ 精准控制 | 通过 rootMargin 和 threshold 灵活调整触发时机 |
| ✅ 无障碍友好 | 尊重 prefers-reduced-motion 用户偏好 |
| ✅ 轻量可复用 | 无依赖,仅 50 行 JS + 简洁 CSS,适合嵌入任何项目 |
| ✅ 懒加载兼容 | 可扩展用于图片懒加载、广告曝光统计等场景 |
附
完整 Demo 已上传 CodePen:
👉 codepen.io/AMingDrift/…
如果你正在开发电商、博客、SaaS 产品页等内容密集型网站,不妨将这套方案集成进去,给用户带来更优雅的浏览体验!
学习优秀作品,是提升技术的最佳路径。本文既是我的学习笔记,也希望对你有所启发。
CSS 动效进阶:从“能动就行”到“性能优化”,一个呼吸球背后的 3 个思考
Bipes项目二次开发/扩展积木功能(八)
Bipes项目二次开发/扩展积木功能(八)
新年第一篇文章,这一篇开发扩展积木功能。先看一段VCR。 广告:需要二开Bipes,Scratch,blockly可以找我。 项目地址:maxuecan.github.io/Bipes/index…
VCR
[video(video-CjWu9kdf-1768899636737)(type-csdn)(url-live.csdn.net/v/embed/510…)]
第一:模式选择
在三种模式中,暂时对海龟编程加了扩展积木功能,点击选择海龟编程,就可以看到积木列表多了个添加按钮。其它模式下不会显示。
第二:积木扩展
![]()
点击扩展按钮,会弹窗一个扩展积木弹窗,接着点击卡片,会显示确认添加按钮,最后点击确认添加,就能动态添加扩展积木。
第三:代码解析
ui/components/extensions-btn.js(扩展积木按钮)
import EventEmitterController from '../utils/event-emitter-controller'
import { resetPostion } from '../utils/utils'
export default class extensionsBtn {
constructor(props) {
this.settings = props.settings
this.resetPostion = resetPostion
if (document.getElementById('content_blocks')) {
$('#content_blocks').append(this.render())
this.initEvent()
}
// 根据模式,控制扩展按钮的显示
setTimeout(() => {
let { mode } = this.settings
resetPostion()
$('#extensions-btn').css('display', mode === 'turtle' ? 'block' : 'none')
}, 1000);
}
// 初始化事件
initEvent() {
window.addEventListener('resize', (e) => {
this.resetPostion()
})
$('#extensions-btn').on('click', () => {
EventEmitterController.emit('open-extensions-dialog')
})
}
render() {
return `
<div id="extensions-btn">
<div class="extensions-add"></div>
</div>
`
}
}
ui/components/extensions-dialog.js(扩展积木弹窗)
import ExtensionsList from '../config/extensions-blocks.js'
import { resetPostion } from '../utils/utils'
export default class extensionsDialog {
constructor() {
this._xml = undefined
this._show = false
this.list = ExtensionsList
this.use = []
this.after_extensions = [] // 记录已经添加过的扩展积木
}
// 初始化事件
initEvent() {
$('.extensions-modal-close').on('click', this.close.bind(this))
$('.extensions-modal-confirm').on('click', this.confirm.bind(this))
$('.extensions-modal-list').on('click', this.select.bind(this))
}
// 销毁事件
removeEvent() {
$('.extensions-modal-close').off('click', this.close.bind(this))
$('.extensions-modal-confirm').off('click', this.confirm.bind(this))
$('.extensions-modal-list').off('click', this.select.bind(this))
}
// 显示隐藏弹窗
show() {
if (this._show) {
$('.extensions-dialog').remove()
this.removeEvent()
} else {
$('body').append(this.render())
this.initEvent()
this.createList()
}
this._show = !this._show
}
// 创建扩展列表
createList() {
$('.extensions-list').empty()
for (let i in this.list) {
let li = $('<li>')
.attr('key', this.list[i]['type'])
.css({
background: `url(${this.list[i]['image']}) center/cover no-repeat`,
})
let box = $('<div>')
.addClass('extensions-list-image')
.attr('key', this.list[i]['type'])
let detail = $('<div>')
.addClass('extensions-list-detail')
.attr('key', this.list[i]['type'])
let name = $('<h4>').text(this.list[i]['name']).attr('key', this.list[i]['type'])
let remark = $('<span>').text(this.list[i]['remark']).attr('key', this.list[i]['type'])
detail.append(name).append(remark)
$('.extensions-modal-list').append(li.append(box).append(detail))
}
}
// 选择列表
select(e) {
let key = e.target.getAttribute('key')
if (key !== null) {
let index = this.use.indexOf(key)
let type = undefined
if (index !== -1) {
this.use.splice(index, 1)
type = 'delete'
} else {
this.use.push(key)
type = 'add'
}
this.highlightList(type, key)
this.showConfirm()
}
}
// 高亮列表项
highlightList(action, key) {
$('.extensions-modal-list li').each(function(index) {
let c_key = $(this).attr('key')
if (key === c_key) {
if (action === 'add') {
$(this).addClass('extensions-modal-list-act')
} else if (action === 'delete') {
$(this).removeClass('extensions-modal-list-act')
}
}
})
}
// 显示确认按钮
showConfirm() {
if (this.use.length > 0) {
$('.extensions-modal-footer').css('display', 'block')
} else {
$('.extensions-modal-footer').css('display', 'none')
}
}
// 关闭
close() {
this.show()
}
// 确认操作
confirm() {
let str = ''
this.use.forEach(item => {
let index = this.after_extensions.indexOf(item)
if (index === -1) {
this.after_extensions.push(item)
str += this.getExtendsionsXML(item)
}
})
if (str) {
if (!this._xml) this._xml = window._xml.cloneNode(true)
let toolbox = this._xml
toolbox.children[0].innerHTML += str
Code.reloadToolbox(toolbox)
}
this.show()
resetPostion()
}
/* 获取扩展积木的XML */
getExtendsionsXML(type) {
let item = ExtensionsList.filter(itm => itm.type === type)
return item[0].xml
}
// 重置toolbox
resetToolbox() {
return new Promise((resolve) => {
this._xml = window._xml.cloneNode(true)
Code.reloadToolbox(this._xml)
this.use = []
this.after_extensions = []
setTimeout(resolve(true), 200)
})
}
render() {
return `
<div class="extensions-dialog">
<div class="extensions-modal">
<div class="extensions-modal-header">
<h4></h4>
<ul class="extensions-modal-nav">
<li class="extensions-modal-nav-act" key="basic">
<span key="basic">扩展积木</span>
</li>
</ul>
<div class="extensions-modal-close"></div>
</div>
<div class="extensions-modal-content">
<ul class="extensions-modal-list"></ul>
</div>
<div class="extensions-modal-footer">
<button class="extensions-modal-confirm">确认添加</button>
</div>
</div>
</div>
`
}
}
ui/config/extensions-blocks.js(扩展积木配置)
let turtle = require('./turtle.png')
module.exports = [
{
type: 'turtle',
name: '海龟函数',
image: turtle,
remark: '可以调用海龟编辑器中对应Python函数。',
xml: `
<category name="海龟" colour="%{BKY_TURTLE_HUE}">
<block type="variables_set" id="fg004w+XJ=maCm$V7?3T" x="238" y="138">
<field name="VAR" id="dfa$SFe(HK(10)Y+T-bS">海龟</field>
<value name="VALUE">
<block type="turtle_create" id="Hv^2jr?;yxhA=%oCs1=d"></block>
</value>
</block>
<block type="turtle_create"></block>
<block type="turtle_move">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="distance">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="turtle_rotate">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="angle">
<shadow type="math_number">
<field name="NUM">90</field>
</shadow>
</value>
</block>
<block type="turtle_move_xy">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="x">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="y">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="turtle_set_position">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="position">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="turtle_draw_circle">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="radius">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="extent">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="steps">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="turtle_draw_polygon">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="num_sides">
<shadow type="math_number">
<field name="NUM">5</field>
</shadow>
</value>
<value name="radius">
<shadow type="math_number">
<field name="NUM">30</field>
</shadow>
</value>
</block>
<block type="turtle_draw_point">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="diameter">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
</block>
<block type="turtle_write">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="text">
<shadow type="text">
<field name="TEXT">Hello</field>
</shadow>
</value>
</block>
<block type="turtle_set_heading">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="angle">
<shadow type="math_number">
<field name="NUM">90</field>
</shadow>
</value>
</block>
<block type="turtle_pendown">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
</block>
<block type="turtle_set_pensize">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="size">
<shadow type="math_number">
<field name="NUM">5</field>
</shadow>
</value>
</block>
<block type="turtle_set_speed">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="speed">
<shadow type="math_number">
<field name="NUM">5</field>
</shadow>
</value>
</block>
<block type="turtle_get_position">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
</block>
<block type="turtle_show_hide">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
</block>
<block type="turtle_clear">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
</block>
<block type="turtle_stop">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
</block>
<block type="turtle_set_bgcolor">
<value name="COLOUR">
<block type="colour_picker"></block>
</value>
</block>
<block type="turtle_set_pencolor">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="COLOUR">
<block type="colour_picker"></block>
</value>
</block>
<block type="turtle_set_fillcolor">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="COLOUR">
<block type="colour_picker"></block>
</value>
</block>
<block type="turtle_set_colormode">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="COLOUR">
<shadow type="math_number">
<field name="NUM">255</field>
</shadow>
</value>
</block>
<block type="turtle_set_fill">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
</block>
<block type="turtle_set_color">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">{turtleVariable}</field>
</block>
</value>
<value name="COLOUR">
<block type="colour_picker"></block>
</value>
</block>
</category>
`,
},
]
总结
扩展积木功能改动挺多的,功能也时不断的完善,讲解可能比较粗糙,也在尽量写注解,有需要可以看下提交日志,信息会比较全。
详解TypedArray的内存机制——从backing store到 Native Heap 与 JS Heap
runtime chunk 到底是什么?
告别笨重的 Prometheus,这款 5 分钟部署的 Nginx 监控工具凭什么刷屏 GitHub?
Nuxt 3 vs Next.js:新手选型指南与项目实战对比
学术界最大的室内运动捕捉设施为世界领先的无人机研究提供支持
Element Plus SCSS 变量覆盖用法
大白话解释Vue响应式三要素
执行了 git commit 然后 git pull,现在想要回退到 pull 之前的状态
如果你已经执行了 git commit 然后 git pull,现在想要回退到 pull 之前的状态,这里有几个解决方案:
解决方案
1. 使用 git reset(最常用)
# 查看提交历史,找到你想要回退到的commit
git log --oneline
# 回退到指定的commit(保留工作区更改)
git reset --soft <commit-hash>
# 或者回退并丢弃更改
git reset --hard <commit-hash>
2. 回退到 pull 之前的状态
# 使用 reflog 查看操作历史
git reflog
# 找到 pull 之前的 HEAD 位置,然后重置
git reset --hard HEAD@{n} # n 是 reflog 中的编号
3. 如果你想要完全撤销 pull 和 commit
# 查看最近的操作,找到 pull 之前的提交
git log --oneline
# 重置到 pull 之前的提交
git reset --hard HEAD~2 # 回退2个提交(假设你commit了1次,pull了1次)
具体操作步骤建议:
- 先查看当前状态:
git status
git log --oneline
- 安全方案(保留更改):
# 先保存当前工作状态(如果有未提交的更改)
git stash
# 回退到 pull 之前的commit
git reset --hard HEAD~1
# 如果需要,恢复stash的更改
git stash pop
- 如果你只是想修改 commit 信息:
# 修改最后一次commit
git commit --amend
注意事项:
- 如果已经 push 到远程仓库,需要谨慎操作,可能需要
git push --force - 如果是多人协作的项目,最好先和团队沟通
- 使用
git reset --hard会丢失所有未提交的更改,请确保已备份
你具体遇到了什么情况?我可以提供更针对性的建议。
Vue组件变量值更新过程记录
从 Vue 2.x 源码角度分析将组件变量 a 从空值修改为 1 的完整调用栈如下:
1. 组件初始化阶段
在组件创建时,Vue 会初始化响应式数据:
// 调用栈:
Vue.prototype._init (init.js)
└── initState (state.js)
└── initData (state.js)
└── observe (observer/index.js)
└── new Observer (observer/index.js)
└── walk (observer/index.js)
└── defineReactive (observer/index.js) // 为属性 `a` 创建响应式
关键步骤:
-
defineReactive为a创建getter/setter:- 初始化
dep实例(依赖收集器)。 - 通过
Object.defineProperty重写a的访问器:Object.defineProperty(obj, key, { get() { /* 依赖收集 */ }, set(newVal) { /* 触发更新 */ } })
- 初始化
2. 修改 a 的值
执行 this.a = 1 时触发 setter:
// 调用栈:
this.a = 1
└── a 的 setter (defineReactive 内部)
└── dep.notify() (observer/dep.js)
└── subs[i].update() (observer/watcher.js)
└── queueWatcher (scheduler.js)
└── nextTick (scheduler.js)
└── flushSchedulerQueue (scheduler.js)
└── watcher.run (observer/watcher.js)
└── watcher.get (observer/watcher.js)
└── 组件重新渲染 (render 函数)
关键步骤详解:
-
setter触发:- 检查新值
1是否与旧值不同(newVal !== oldVal)。 - 若不同,调用
dep.notify()通知所有依赖。
- 检查新值
-
dep.notify():- 遍历
dep.subs(存储所有订阅该属性的 Watcher)。 - 调用每个
watcher.update()。
- 遍历
-
watcher.update():- 将 Watcher 加入异步队列(
queueWatcher)。 - 通过
nextTick异步执行更新。
- 将 Watcher 加入异步队列(
-
flushSchedulerQueue:- 遍历队列中的 Watcher,调用
watcher.run()。 -
watcher.run()→watcher.get()→ 重新执行组件的render函数。
- 遍历队列中的 Watcher,调用
-
重新渲染:
-
render函数执行时访问a,触发getter重新收集依赖。 - 生成新的虚拟 DOM,对比差异后更新真实 DOM。
-
3. 依赖收集机制
在首次渲染和后续更新时,getter 负责收集依赖:
// getter 调用栈:
组件访问 a (render 函数)
└── a 的 getter (defineReactive 内部)
└── Dep.target (全局唯一 Watcher)
└── dep.depend() (observer/dep.js)
└── 将当前 Watcher 添加到 dep.subs
关键点:
-
Dep.target:全局唯一变量,指向当前正在执行的 Watcher(如渲染 Watcher)。 -
dep.depend():将当前 Watcher 加入dep.subs,建立属性 → Watcher的依赖关系。
4. 异步更新队列
Vue 使用异步队列合并更新:
// nextTick 流程:
queueWatcher (scheduler.js)
└── nextTick (util/next-tick.js)
└── 异步任务 (Promise/MutationObserver/setTimeout)
└── flushSchedulerQueue (scheduler.js)
优化逻辑:
- 多次修改
a会被合并为一次更新(避免重复渲染)。 - 通过
nextTick确保在 DOM 更新后执行回调。
Vue 3 Proxy 版本的差异
若使用 Vue 3(基于 Proxy):
-
初始化:通过
reactive创建响应式代理。 -
修改值:直接触发
Proxy.set拦截器,后续流程类似(依赖收集、异步更新)。 -
核心差异:
- 无需
Object.defineProperty,支持动态属性。 - 依赖收集通过
Track操作,更新通过Trigger操作。
- 无需
总结
| 阶段 | 核心操作 | 关键函数/类 |
|---|---|---|
| 初始化 | 为 a 创建响应式 getter/setter
|
defineReactive、Dep
|
| 修改值 | 触发 setter → 通知依赖 |
dep.notify() |
| 依赖更新 | 异步队列合并更新 |
queueWatcher、nextTick
|
| 重新渲染 | 执行 render 函数 |
Watcher.run() |
整个流程体现了 Vue 响应式系统的核心:依赖收集(getter)和 派发更新(setter),通过 异步队列 优化性能。
rxjs基本语法
RxJS (Reactive Extensions for JavaScript) 是 Angular 中处理异步编程的核心库。 它通过使用 Observable(可观察对象) 序列来编写异步和基于回调的代码。
一、 核心概念
在 RxJS 中,一切基于数据流。
- Observable (被观察者): 数据的源头,发出数据。
- Observer (观察者): 数据的消费者,接收数据。
- Subscription (订阅): 连接 Observable 和 Observer 的桥梁。注意:必须取消订阅,否则会内存泄漏。
- Operators (操作符): 纯函数,用来处理、转换数据流(如 map, filter)。
- Subject (主题): 既是 Observable 又是 Observer,可以多播数据(常用于组件通信)。
二、 基础写法
1. 创建 Observable 和 订阅
import { Observable } from 'rxjs';
// 1. 创建 Observable
const observable$ = new Observable(subscriber => {
subscriber.next(1); // 发出数据
subscriber.next(2);
subscriber.next(3);
subscriber.complete(); // 结束
// subscriber.error('出错了'); // 抛出异常
});
// 2. 订阅
const subscription = observable$.subscribe({
next: (x) => console.log('收到数据:', x),
error: (err) => console.error('错误:', err),
complete: () => console.log('流结束')
});
// 3. 取消订阅 (非常重要)
subscription.unsubscribe();
2. 简写订阅 (只关心 next)
observable$.subscribe(data => console.log(data));
三、 常用创建操作符
用于生成数据流。
import { of, from, interval, fromEvent, throwError } from 'rxjs';
// 1. of: 依次发出参数
of(1, 2, 3).subscribe(console.log); // 输出: 1, 2, 3
// 2. from: 将数组/Promise 转为 Observable
from([10, 20, 30]).subscribe(console.log); // 输出: 10, 20, 30
// 3. interval: 周期性发出数字 (每1秒发一个)
interval(1000).subscribe(n => console.log(n)); // 0, 1, 2...
// 4. fromEvent: 监听 DOM 事件
fromEvent(document.querySelector('button')!, 'click')
.subscribe(() => console.log('按钮被点击'));
// 5. throwError: 创建一个只报错的流
// throwError(() => new Error('哎呀出错了')).subscribe();
四、 常用转换操作符
这是 RxJS 最强大的部分,管道 语法是 Angular 18+ 的标准写法。
import { map, filter, pluck } from 'rxjs/operators';
of(1, 2, 3, 4, 5).pipe(
// 1. map: 转换数据 (类似数组的 map)
map(x => x * 10),
// 2. filter: 过滤数据 (只有 true 才会通过)
filter(x => x > 20)
).subscribe(console.log);
// 输出: 30, 40, 50
// 3. pluck: 提取对象属性 (已废弃,推荐用 map)
// 旧写法: source$.pipe(pluck('user', 'name'))
// 新写法:
interface User { name: string; age: number; }
const user$: Observable<User> = of({ name: 'Tom', age: 18 });
user$.pipe(map(user => user.name)).subscribe(console.log);
五、 工具操作符 (面试高频)
用于处理流的逻辑,如限流、防抖、错误处理。
import { delay, tap, catchError, takeUntil, debounceTime } from 'rxjs/operators';
import { of, Subject, throwError } from 'rxjs';
// 1. tap: 副作用操作 (不修改数据,通常用于打印日志、存 LocalStorage)
of('Hello').pipe(
tap(val => console.log('处理前:', val)),
delay(1000) // 延迟1秒发射
).subscribe(val => console.log('处理后:', val));
// 2. catchError: 错误捕获 (让流不中断)
throwError(() => new Error('网络错误')).pipe(
catchError(err => {
console.error(err);
// 捕获错误后,返回一个新的 Observable 给下游,防止程序崩溃
return of('默认数据');
})
).subscribe(console.log); // 输出: 默认数据
// 3. debounceTime: 防抖 (用户停止输入 300ms 后才发送请求)
fromEvent(document.querySelector('input')!, 'input').pipe(
debounceTime(300)
).subscribe((event: any) => console.log(event.target.value));
// 4. takeUntil: 立即取消订阅 (在 Angular 组件销毁时最常用)
const destroy$ = new Subject<void>();
interval(1000).pipe(
takeUntil(destroy$) // 当 destroy$ 发出值时,上面的流自动停止
).subscribe(console.log);
// 模拟组件销毁
setTimeout(() => {
destroy$.next(); // 停止上面的 interval
destroy$.complete();
}, 5000);
六、 高阶操作符 (处理嵌套流)
当一个 Observable 发出的数据还是一个 Observable 时使用。
import { mergeMap, switchMap, concatMap, exhaustMap } from 'rxjs/operators';
// 场景:点击按钮 -> 发送 HTTP 请求
// 假设 click$ 是点击事件流, getData(id) 返回 Observable
// 1. mergeMap (并行): 点击一次发一次请求,不管上一个有没有完成。
// 适用:并发上传,互不干扰。
click$.pipe(
mergeMap(() => this.http.get('/api/data'))
).subscribe();
// 2. switchMap (切换): **面试必考**。如果有新请求,取消旧请求。
// 适用:搜索框输入。
searchInput$.pipe(
switchMap(keyword => this.http.search(keyword))
).subscribe();
// 3. concatMap (串行): 等前一个请求完成,再发下一个。
// 适用:必须按顺序执行的任务。
// 4. exhaustMap (排他): 如果有请求正在进行,忽略新的点击。
// 适用:防止重复提交表单。
submitBtn$.pipe(
exhaustMap(() => this.http.submit())
).subscribe();
七、 Subject (多播)
普通的 Observable 是单播的;Subject 可以让多个订阅者共享同一个数据源。
import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs';
// 1. Subject: 只有订阅后发出的数据才会收到。
const subject = new Subject<number>();
subject.subscribe(n => console.log('A:', n));
subject.next(1); // A 收到 1
subject.subscribe(n => console.log('B:', n));
subject.next(2); // A 收到 2, B 收到 2 (B 错过了 1)
// 2. BehaviorSubject: 必须有初始值,新订阅者会立即收到**最新**的值。
const bs = new BehaviorSubject<number>(0); // 初始值 0
bs.subscribe(n => console.log('C:', n)); // C 立即收到 0
bs.next(100);
// 3. ReplaySubject: 可以缓存最近的 N 个值,新订阅者会收到缓存的历史记录。
const rs = new ReplaySubject(2); // 缓存最近 2 个
rs.next(1);
rs.next(2);
rs.next(3);
rs.subscribe(n => console.log('D:', n)); // D 收到 2 和 3
八、 Angular 实战:AsyncPipe (语法糖)
在 Angular 中,你甚至不需要手动调用 .subscribe()。
// 组件 TS
export class MyComponent {
// 自动处理订阅、取消订阅、变化检测
data$ = of([{ name: 'Tom' }, { name: 'Jerry' }]);
}
// 组件 HTML
<div *ngFor="let item of data$ | async">
{{ item.name }}
</div>
注意: 如果你需要拿到数据后在 TS 逻辑里做复杂处理,还是需要手动 subscribe 并配合 takeUntil 使用。
总结速查表
| 类别 | 操作符 | 作用 |
|---|---|---|
| 创建 |
of, from, interval
|
造数据 |
| 转换 |
map, filter
|
改数据 |
| 工具 |
tap, delay, debounceTime
|
辅助/拦截 |
| 组合 |
switchMap, mergeMap
|
处理嵌套流 (HTTP) |
| 生命周期 |
takeUntil, first, take
|
管理订阅 |
| 错误 |
catchError, retry
|
异常处理 |
| 多播 |
Subject, BehaviorSubject
|
跨组件通信 |