普通视图
Polyfill方式解决前端兼容性问题:core-js包结构与各种配置策略
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),通过 异步队列 优化性能。