普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月21日掘金 前端
昨天 — 2026年1月20日掘金 前端

用 Intersection Observer 打造丝滑的级联滚动动画

作者 阿明Drift
2026年1月20日 17:49

无需任何动画库,仅用原生 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 事件,避免频繁计算
精准控制 通过 rootMarginthreshold 灵活调整触发时机
无障碍友好 尊重 prefers-reduced-motion 用户偏好
轻量可复用 无依赖,仅 50 行 JS + 简洁 CSS,适合嵌入任何项目
懒加载兼容 可扩展用于图片懒加载、广告曝光统计等场景

完整 Demo 已上传 CodePen:
👉 codepen.io/AMingDrift/…

如果你正在开发电商、博客、SaaS 产品页等内容密集型网站,不妨将这套方案集成进去,给用户带来更优雅的浏览体验!


学习优秀作品,是提升技术的最佳路径。本文既是我的学习笔记,也希望对你有所启发。

Bipes项目二次开发/扩展积木功能(八)

2026年1月20日 17:22

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>
        `,
  },
]

总结

扩展积木功能改动挺多的,功能也时不断的完善,讲解可能比较粗糙,也在尽量写注解,有需要可以看下提交日志,信息会比较全。

告别笨重的 Prometheus,这款 5 分钟部署的 Nginx 监控工具凭什么刷屏 GitHub?

2026年1月20日 16:49
前言 作为后端开发者,Nginx 几乎是我们每天都要打交道的“基础设施”。但说实话,Nginx 的运维体验一直很割裂: 原生监控太简陋:stub_status 只能看个连接数,想看接口响应耗时?想看

学术界最大的室内运动捕捉设施为世界领先的无人机研究提供支持

作者 爱迪斯通
2026年1月20日 16:42
亚利桑那州立大学跟踪体积为 230,000 立方英尺的无人机工作室是世界上学术机构中最大的室内无人机研究动捕设施。该设施前身是一个篮球馆,经过五年多的建造,由亚利桑那州立大学机器人研究员和副教授Pan

执行了 git commit 然后 git pull,现在想要回退到 pull 之前的状态

作者 新晨437
2026年1月20日 15:45

如果你已经执行了 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次)

具体操作步骤建议:

  1. 先查看当前状态
git status
git log --oneline
  1. 安全方案(保留更改)
# 先保存当前工作状态(如果有未提交的更改)
git stash

# 回退到 pull 之前的commit
git reset --hard HEAD~1

# 如果需要,恢复stash的更改
git stash pop
  1. 如果你只是想修改 commit 信息
# 修改最后一次commit
git commit --amend

注意事项:

  • 如果已经 push 到远程仓库,需要谨慎操作,可能需要 git push --force
  • 如果是多人协作的项目,最好先和团队沟通
  • 使用 git reset --hard 会丢失所有未提交的更改,请确保已备份

你具体遇到了什么情况?我可以提供更针对性的建议。

Vue组件变量值更新过程记录

作者 cj8140
2026年1月20日 15:45

从 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` 创建响应式

关键步骤

  • defineReactivea 创建 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 函数)

关键步骤详解

  1. setter 触发

    • 检查新值 1 是否与旧值不同(newVal !== oldVal)。
    • 若不同,调用 dep.notify() 通知所有依赖。
  2. dep.notify()

    • 遍历 dep.subs(存储所有订阅该属性的 Watcher)。
    • 调用每个 watcher.update()
  3. watcher.update()

    • 将 Watcher 加入异步队列(queueWatcher)。
    • 通过 nextTick 异步执行更新。
  4. flushSchedulerQueue

    • 遍历队列中的 Watcher,调用 watcher.run()
    • watcher.run()watcher.get() → 重新执行组件的 render 函数。
  5. 重新渲染

    • 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):

  1. 初始化:通过 reactive 创建响应式代理。
  2. 修改值:直接触发 Proxy.set 拦截器,后续流程类似(依赖收集、异步更新)。
  3. 核心差异
    • 无需 Object.defineProperty,支持动态属性。
    • 依赖收集通过 Track 操作,更新通过 Trigger 操作。

总结

阶段 核心操作 关键函数/类
初始化 a 创建响应式 getter/setter defineReactiveDep
修改值 触发 setter → 通知依赖 dep.notify()
依赖更新 异步队列合并更新 queueWatchernextTick
重新渲染 执行 render 函数 Watcher.run()

整个流程体现了 Vue 响应式系统的核心:依赖收集getter)和 派发更新setter),通过 异步队列 优化性能。

rxjs基本语法

作者 米诺zuo
2026年1月20日 15:38

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 跨组件通信
❌
❌