阅读视图

发现新文章,点击刷新页面。

每日一题-交替位二进制数🟢

给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。

 

示例 1:

输入:n = 5
输出:true
解释:5 的二进制表示是:101

示例 2:

输入:n = 7
输出:false
解释:7 的二进制表示是:111.

示例 3:

输入:n = 11
输出:false
解释:11 的二进制表示是:1011.

 

提示:

  • 1 <= n <= 231 - 1

React 性能优化:图片懒加载

引言

在现代 Web 应用开发中,首屏加载速度(FCP)和最大内容绘制(LCP)是衡量用户体验的核心指标。随着富媒体内容的普及,图片资源往往占据了页面带宽的大部分。如果一次性加载页面上的所有图片,不仅会阻塞关键渲染路径,导致页面长时间处于“白屏”或不可交互状态,还会浪费用户的流量带宽。

图片懒加载(Lazy Loading)作为一种经典的性能优化策略,其核心思想是“按需加载”:即只有当图片出现在浏览器可视区域(Viewport)或即将进入可视区域时,才触发网络请求进行加载。这一策略能显著减少首屏 HTTP 请求数量,降低服务器压力,并提升页面的交互响应速度。

本文将基于 React 生态,从底层原理出发,深入探讨图片懒加载的多种实现方案,并重点分析如何解决布局偏移(CLS)等用户体验问题。

核心原理剖析

图片懒加载的本质是一个“可见性检测”问题。我们需要实时判断目标图片元素是否与浏览器的可视区域发生了交叉。在技术实现上,主要依赖以下两种依据:

  1. 几何计算:通过监听滚动事件,实时计算元素的位置坐标。核心公式通常涉及 scrollTop(滚动距离)、clientHeight(视口高度)与元素 offsetTop(偏移高度)的比较,或者使用 getBoundingClientRect() 获取元素相对于视口的位置。
  2. API 监测:利用浏览器提供的 IntersectionObserver API。这是一个异步观察目标元素与祖先元素或顶级文档视窗交叉状态的方法,它将复杂的几何计算交由浏览器底层处理,性能表现更优。

方案一:原生 HTML 属性(最简方案)

HTML5 标准为  标签引入了 loading 属性,这是实现懒加载最简单、成本最低的方式。

Jsx

const NativeLazyLoad = ({ src, alt }) => {
  return (
    <img 
      src={src} 
      alt={alt} 
      loading="lazy" 
      width="300" 
      height="200"
    />
  );
};

分析:

  • 优点:零 JavaScript 代码,完全依赖浏览器原生行为,不会阻塞主线程。

  • 缺点

    • 兼容性:虽然现代浏览器支持度已较高,但在部分旧版 Safari 或 IE 中无法工作。
    • 不可控:开发者无法精确控制加载的阈值(Threshold),也无法在加载失败或加载中注入自定义逻辑(如骨架屏切换)。
    • 功能单一:仅适用于 img 和 iframe 标签,无法用于 background-image。

方案二:传统 Scroll 事件监听(兼容方案)

在 IntersectionObserver 普及之前,监听 scroll 事件是主流做法。其原理是在 React 组件挂载后绑定滚动监听器,在回调中计算图片位置。

React 实现示例:

Jsx

import React, { useState, useEffect, useRef } from 'react';
import placeholder from './assets/placeholder.png';

// 简单的节流函数,生产环境建议使用 lodash.throttle
const throttle = (func, limit) => {
  let inThrottle;
  return function() {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  }
};

const ScrollLazyImage = ({ src, alt }) => {
  const [imageSrc, setImageSrc] = useState(placeholder);
  const imgRef = useRef(null);
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    const checkVisibility = () => {
      if (isLoaded || !imgRef.current) return;

      const rect = imgRef.current.getBoundingClientRect();
      const windowHeight = window.innerHeight || document.documentElement.clientHeight;

      // 设置 100px 的缓冲区,提前加载
      if (rect.top <= windowHeight + 100) {
        setImageSrc(src);
        setIsLoaded(true);
      }
    };

    // 必须使用节流,否则滚动时会频繁触发重排和重绘,导致性能灾难
    const throttledCheck = throttle(checkVisibility, 200);

    window.addEventListener('scroll', throttledCheck);
    window.addEventListener('resize', throttledCheck);
    
    // 初始化检查,防止首屏图片不加载
    checkVisibility();

    return () => {
      window.removeEventListener('scroll', throttledCheck);
      window.removeEventListener('resize', throttledCheck);
    };
  }, [src, isLoaded]);

  return <img ref={imgRef} src={imageSrc} alt={alt} />;
};

关键点分析:

  1. 节流(Throttle) :scroll 事件触发频率极高,若不加节流,每次滚动都会执行 DOM 查询和几何计算,占用大量主线程资源,导致页面掉帧。
  2. 回流(Reflow)风险:频繁调用 getBoundingClientRect() 或 offsetTop 会强制浏览器进行同步布局计算(Synchronous Layout),这是性能杀手。

方案三:IntersectionObserver API(现代标准方案)

这是目前最推荐的方案。IntersectionObserver 运行在独立线程中,不会阻塞主线程,且浏览器对其进行了内部优化。

React 实现示例:

我们可以将其封装为一个通用的组件 LazyImage。

Jsx

import React, { useState, useEffect, useRef } from 'react';
import './LazyImage.css'; // 假设包含样式

const LazyImage = ({ src, alt, placeholderSrc, width, height }) => {
  const [imageSrc, setImageSrc] = useState(placeholderSrc || '');
  const [isVisible, setIsVisible] = useState(false);
  const imgRef = useRef(null);

  useEffect(() => {
    let observer;
    
    if (imgRef.current) {
      observer = new IntersectionObserver((entries) => {
        const entry = entries[0];
        // 当元素进入视口
        if (entry.isIntersecting) {
          setImageSrc(src);
          setIsVisible(true);
          // 关键:图片加载触发后,立即停止观察,释放资源
          observer.unobserve(imgRef.current);
          observer.disconnect();
        }
      }, {
        rootMargin: '100px', // 提前 100px 加载
        threshold: 0.01
      });

      observer.observe(imgRef.current);
    }

    // 组件卸载时的清理逻辑
    return () => {
      if (observer) {
        observer.disconnect();
      }
    };
  }, [src]);

  return (
    <img
      ref={imgRef}
      src={imageSrc}
      alt={alt}
      width={width}
      height={height}
      className={`lazy-image ${isVisible ? 'loaded' : ''}`}
    />
  );
};

export default LazyImage;

优势分析:

  • 高性能:异步检测,无回流风险。
  • 资源管理:通过 unobserve 和 disconnect 及时释放观察者,避免内存泄漏。
  • 灵活性:rootMargin 允许我们轻松设置预加载缓冲区。

进阶:用户体验与 CLS 优化

仅仅实现“懒加载”是不够的。在工程实践中,如果处理不当,懒加载会导致严重的累积布局偏移(CLS, Cumulative Layout Shift) 。即图片加载前高度为 0,加载后撑开高度,导致页面内容跳动。这不仅体验极差,也是 Google Core Web Vitals 的扣分项。

1. 预留空间(Aspect Ratio)

必须在图片加载前确立其占据的空间。现代 CSS 提供了 aspect-ratio 属性,配合宽度即可自动计算高度。

CSS

/* LazyImage.css */
.img-wrapper {
  width: 100%;
  /* 假设图片比例为 16:9,或者由后端返回具体宽高计算 */
  aspect-ratio: 16 / 9; 
  background-color: #f0f0f0; /* 骨架屏背景色 */
  overflow: hidden;
  position: relative;
}

.lazy-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

.lazy-image.loaded {
  opacity: 1;
}

2. 结合数据的完整 React 组件

结合后端返回的元数据(如宽高、主色调),我们可以构建一个体验极佳的懒加载组件。

Jsx

const AdvancedLazyImage = ({ data }) => {
  // data 结构示例: { url: '...', width: 800, height: 600, basicColor: '#a44a00' }
  const imgRef = useRef(null);
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        const img = entry.target;
        // 使用 dataset 获取真实地址,或者直接操作 state
        img.src = img.dataset.src;
        
        img.onload = () => setIsLoaded(true);
        observer.unobserve(img);
      }
    });

    if (imgRef.current) observer.observe(imgRef.current);

    return () => observer.disconnect();
  }, []);

  return (
    <div 
      className="img-container"
      style={{
        // 核心:使用 aspect-ratio 防止 CLS
        aspectRatio: `${data.width} / ${data.height}`,
        // 核心:使用图片主色调作为占位背景,提供渐进式体验
        backgroundColor: data.basicColor 
      }}
    >
      <img
        ref={imgRef}
        data-src={data.url} // 暂存真实地址
        alt="Lazy load content"
        style={{
          opacity: isLoaded ? 1 : 0,
          transition: 'opacity 0.5s ease'
        }}
      />
    </div>
  );
};

方案对比与场景选择

方案 实现难度 性能 兼容性 适用场景
原生属性 (loading="lazy") 中 (现代浏览器) 简单的 CMS 内容页、对交互要求不高的场景。
Scroll 监听 低 (需节流) 高 (全兼容) 必须兼容 IE 等老旧浏览器,或有特殊的滚动容器逻辑。
IntersectionObserver 极高 高 (需 Polyfill) 现代 Web 应用、无限滚动列表、对性能和体验有高要求的场景。

结语

图片懒加载是前端性能优化的基石之一。从早期的 Scroll 事件监听,到如今标准化的 IntersectionObserver API,再到原生 HTML 属性的支持,技术在不断演进。

在 React 项目中落地懒加载时,我们不能仅满足于“功能实现”。作为架构师,更应关注性能损耗(如避免主线程阻塞)、资源管理(及时销毁 Observer)以及用户体验(防止 CLS、优雅的过渡动画)。通过合理利用 aspect-ratio 和占位策略,我们可以让懒加载不仅“快”,而且“稳”且“美”。

别让字体拖了后腿:FOIT/FOUT 深度解析与字体加载优化全攻略

🖋️ 别让字体拖了后腿:FOIT/FOUT 深度解析与字体加载优化全攻略

前端性能优化专栏 - 第九篇

在网页设计中,字体往往被视为“灵魂”。它不仅关乎品牌识别视觉统一,更能直接影响界面的质感专业感。好的字体能降低用户的认知成本,传递出产品想要营造的独特氛围。

然而,字体作为一种静态资源,必须经过下载和管理。如果处理不当,它就会变成性能的“累赘”,甚至引发一些让用户抓狂的“怪现象”。


⚠️ 字体加载中的两大“怪现象”

你是否遇到过这样的场景:页面打开了,但文字部分是一片空白,过了好几秒才突然蹦出来?或者文字先是以一种普通的系统字体显示,然后突然“闪”一下,变成了精美的设计字体?

这可不是浏览器的 Bug,而是浏览器默认的字体加载策略在作祟。

1. FOIT:不可见文本闪现 (Flash of Invisible Text)

这是 Chrome 等现代浏览器的典型行为。当自定义字体还没下载完时,浏览器会选择完全不渲染文本

  • 用户体验:网速慢时,用户看到的是大片空白,甚至会误以为页面挂了。
  • 本质:先保证样式统一,再显示内容。
  • 优点:避免了字体闪变,视觉风格高度一致。
  • 缺点:对弱网用户极其不友好,内容被阻塞。

2. FOUT:无样式文本闪现 (Flash of Unstyled Text)

这是 IE 等浏览器的传统行为。当自定义字体未加载完时,浏览器先用后备字体(系统自带字体)渲染。

  • 用户体验:页面一开始就能阅读,但字体样式会明显“跳”一下。
  • 本质:先保证内容可见,再追求样式正确。
  • 优点:文字优先可见,对内容型页面(如博客、资讯)非常友好。
  • 缺点:字体闪变会瞬间破坏设计感。

FOIT 与 FOUT 现象对比图


✨ 浏览器如何做权衡?

字体加载本质上是一个异步网络请求。在等待期间,浏览器必须决定:

  1. 是否渲染文本?
  2. 用什么字体渲染?
  3. 多久之后放弃等待?

为了不让浏览器“瞎猜”,我们需要一种方式显式地告诉它:“在这个项目里,你应该如何平衡内容、样式和性能。”


🔧 终极武器:font-display

@font-face 中使用 font-display 属性,可以精准控制字体在不同加载阶段的渲染策略。

@font-face {
  font-family: 'My Custom Font';
  src: url(/fonts/my-font.woff2) format('woff2');
  font-display: swap; /* 关键控制位 */
}

font-display 四种策略示意图

✅ 1. font-display: swap (强烈推荐)

这是目前最主流、最被推荐的策略。

  • 行为:立即使用后备字体渲染,等自定义字体准备好后再替换。
  • 特点:主动选择 FOUT,几乎没有文本阻塞时间。
  • 适用场景:新闻站、博客、社区等以内容为主的页面。

🚫2. font-display: block

  • 行为:设置约 3 秒的阻塞期。期间文本不可见(FOIT),超时后才回退到后备字体。
  • 特点:偏向 FOIT,强调视觉一致性。
  • 适用场景:品牌 Logo、视觉主标题等字体是核心识别部分的区域。切记不要对全站正文使用!

⚖️3. font-display: fallback

  • 行为:设置极短的阻塞期(约 100ms)。如果字体没到,立即用后备字体,且通常不再替换。
  • 特点:折中方案。快则用好字体,慢则全程用旧字体,不折腾,不闪变
  • 适用场景:希望有自定义字体,但不愿为此牺牲太多性能,也不希望看到闪变的场景。

⚡4. font-display: optional

  • 行为:阻塞期极短,且赋予浏览器更大的决策权。网络差时可能直接不加载字体。
  • 特点:性能优先级最高,字体属于“锦上添花”。
  • 适用场景:装饰性字体、非核心模块的氛围字体。

💡 总结

字体优化不是简单的“全都要”,而是一场关于内容可见性视觉一致性的博弈。

  • 如果你追求内容第一,请毫不犹豫地选择 swap
  • 如果你追求品牌至上,可以在局部使用 block
  • 如果你想要稳定体验fallbackoptional 是你的好伙伴。

合理利用 font-display,让你的网页在保持美感的同时,也能拥有丝滑的加载体验!


下一篇预告: 页面加载完了,但一滚动就发现元素在“乱跳”?这种让人头大的现象叫布局抖动(Layout Thrashing) 。下一篇我们将深入探讨如何识别并优化布局抖动,让你的页面稳如泰山!敬请期待!

【宫水三叶】位运算应用题

遍历

根据题意,对 $n$ 的每一位进行遍历检查。

代码:

###Java

class Solution {
    public boolean hasAlternatingBits(int n) {
        int cur = -1;
        while (n != 0) {
            int u = n & 1;
            if ((cur ^ u) == 0) return false;
            cur = u; n >>= 1;
        }
        return true;
    }
}
  • 时间复杂度:$O(\log{n})$
  • 空间复杂度:$O(1)$

位运算

另外一种更为巧妙的方式是利用交替位二进制数性质。

当给定值 $n$ 为交替位二进制数时,将 $n$ 右移一位得到的值 $m$ 仍为交替位二进制数,且与原数 $n$ 错开一位,两者异或能够得到形如 $0000...1111$ 的结果 $x$,此时对 $x$ 执行加法(进位操作)能够得到形如 $0000...10000$ 的结果,将该结果与 $x$ 执行按位与后能够得到全 $0$ 结果。

代码:

###Java

class Solution {
    public boolean hasAlternatingBits(int n) {
        int x = n ^ (n >> 1);
        return (x & (x + 1)) == 0;
    }
}
  • 时间复杂度:$O(1)$
  • 空间复杂度:$O(1)$

同类型加餐

今日份加餐:经典「状态压缩 + 位运算」入门题 🎉🎉

或是考虑加练如下「位运算」题目 🍭🍭🍭

题目 题解 难度 推荐指数
137. 只出现一次的数字 II LeetCode 题解链接 中等 🤩🤩🤩
190. 颠倒二进制位 LeetCode 题解链接 简单 🤩🤩🤩
191. 位1的个数 LeetCode 题解链接 简单 🤩🤩🤩
260. 只出现一次的数字 III LeetCode 题解链接 中等 🤩🤩🤩🤩
405. 数字转换为十六进制数 LeetCode 题解链接 简单 🤩🤩🤩🤩
461. 汉明距离 LeetCode 题解链接 简单 🤩🤩🤩🤩
477. 汉明距离总和 LeetCode 题解链接 简单 🤩🤩🤩🤩
526. 优美的排列 LeetCode 题解链接 中等 🤩🤩🤩

注:以上目录整理来自 wiki,任何形式的转载引用请保留出处。


最后

如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/

也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。

所有题解已经加入 刷题指南,欢迎 star 哦 ~

交替位二进制数

方法一:模拟

思路

从最低位至最高位,我们用对 $2$ 取模再除以 $2$ 的方法,依次求出输入的二进制表示的每一位,并与前一位进行比较。如果相同,则不符合条件;如果每次比较都不相同,则符合条件。

代码

###Python

class Solution:
    def hasAlternatingBits(self, n: int) -> bool:
        prev = 2
        while n:
            cur = n % 2
            if cur == prev:
                return False
            prev = cur
            n //= 2
        return True

###Java

class Solution {
    public boolean hasAlternatingBits(int n) {
        int prev = 2;
        while (n != 0) {
            int cur = n % 2;
            if (cur == prev) {
                return false;
            }
            prev = cur;
            n /= 2;
        }
        return true;
    }
}

###C#

public class Solution {
    public bool HasAlternatingBits(int n) {
        int prev = 2;
        while (n != 0) {
            int cur = n % 2;
            if (cur == prev) {
                return false;
            }
            prev = cur;
            n /= 2;
        }
        return true;
    }
}

###C++

class Solution {
public:
    bool hasAlternatingBits(int n) {
        int prev = 2;
        while (n != 0) {
            int cur = n % 2;
            if (cur == prev) {
                return false;
            }
            prev = cur;
            n /= 2;
        }
        return true;
    }
};

###C

bool hasAlternatingBits(int n) {
    int prev = 2;
    while (n != 0) {
        int cur = n % 2;
        if (cur == prev) {
            return false;
        }
        prev = cur;
        n /= 2;
    }
    return true;
} 

###go

func hasAlternatingBits(n int) bool {
    for pre := 2; n != 0; n /= 2 {
        cur := n % 2
        if cur == pre {
            return false
        }
        pre = cur
    }
    return true
}

###JavaScript

var hasAlternatingBits = function(n) {
    let prev = 2;
    while (n !== 0) {
        const cur = n % 2;
        if (cur === prev) {
            return false;
        }
        prev = cur;
        n = Math.floor(n / 2);
    }
    return true;
};

复杂度分析

  • 时间复杂度:$O(\log n)$。输入 $n$ 的二进制表示最多有 $O(\log n)$ 位。

  • 空间复杂度:$O(1)$。使用了常数空间来存储中间变量。

方法二:位运算

思路

对输入 $n$ 的二进制表示右移一位后,得到的数字再与 $n$ 按位异或得到 $a$。当且仅当输入 $n$ 为交替位二进制数时,$a$ 的二进制表示全为 $1$(不包括前导 $0$)。这里进行简单证明:当 $a$ 的某一位为 $1$ 时,当且仅当 $n$ 的对应位和其前一位相异。当 $a$ 的每一位为 $1$ 时,当且仅当 $n$ 的所有相邻位相异,即 $n$ 为交替位二进制数。

将 $a$ 与 $a + 1$ 按位与,当且仅当 $a$ 的二进制表示全为 $1$ 时,结果为 $0$。这里进行简单证明:当且仅当 $a$ 的二进制表示全为 $1$ 时,$a + 1$ 可以进位,并将原最高位置为 $0$,按位与的结果为 $0$。否则,不会产生进位,两个最高位都为 $1$,相与结果不为 $0$。

结合上述两步,可以判断输入是否为交替位二进制数。

代码

###Python

class Solution:
    def hasAlternatingBits(self, n: int) -> bool:
        a = n ^ (n >> 1)
        return a & (a + 1) == 0

###Java

class Solution {
    public boolean hasAlternatingBits(int n) {
        int a = n ^ (n >> 1);
        return (a & (a + 1)) == 0;
    }
}

###C#

public class Solution {
    public bool HasAlternatingBits(int n) {
        int a = n ^ (n >> 1);
        return (a & (a + 1)) == 0;
    }
}

###C++

class Solution {
public:
    bool hasAlternatingBits(int n) {
        long a = n ^ (n >> 1);
        return (a & (a + 1)) == 0;
    }
};

###C

bool hasAlternatingBits(int n) {
    long a = n ^ (n >> 1);
    return (a & (a + 1)) == 0;
}

###go

func hasAlternatingBits(n int) bool {
    a := n ^ n>>1
    return a&(a+1) == 0
}

###JavaScript

var hasAlternatingBits = function(n) {
    const a = n ^ (n >> 1);
    return (a & (a + 1)) === 0;
};

复杂度分析

  • 时间复杂度:$O(1)$。仅使用了常数时间来计算。

  • 空间复杂度:$O(1)$。使用了常数空间来存储中间变量。

后端跑路了怎么办?前端工程师用 Mock.js 自救实录

在现代 Web 应用的开发流程中,前后端分离已成为行业标准。然而,在实际协作中,前端工程师常常面临“后端接口未就绪、联调环境不稳定、异常场景难以复现”等痛点。这些问题导致前端开发进度被迫依赖后端,严重制约了交付效率。

Mock.js 作为一种数据模拟解决方案,不仅能解除这种依赖,还能通过工程化的方式提升代码的健壮性。本文将从架构视角出发,深入剖析 Mock.js 的核心价值、技术原理,并结合 Vite 生态展示如何在现代项目中落地最佳实践,同时客观分析其局限性与应对策略。

一、 核心价值:为何引入 Mock.js

在工程化体系中,Mock.js 不仅仅是一个生成随机数据的库,它是实现“并行开发”的关键基础设施。

  1. 解除依赖,并行开发
    传统模式下,前端需等待后端 API 开发完成并部署后才能进行数据交互逻辑的编写。引入 Mock.js 后,只要前后端约定好接口文档(API Contract),前端即可通过模拟数据独立完成 UI 渲染和交互逻辑,将开发流程从“串行”转变为“并行”。
  2. 高保真的数据仿真
    相比于手动硬编码的 test 或 123 等无意义数据,Mock.js 提供了丰富的数据模板定义(Schema)。它能生成具有语义化的数据,如随机生成的中文姓名、身份证号、布尔值、图片 URL、时间戳等。这使得前端在开发阶段就能发现因数据长度、类型或格式引发的 UI 适配问题。
  3. 边界条件与异常流测试
    真实后端环境往往难以稳定复现 500 服务器错误、404 资源丢失或超长网络延迟。Mock.js 允许开发者通过配置轻松模拟这些极端情况,验证前端在异常状态下的容错机制(如 Loading 状态、错误提示、重试逻辑)是否健壮。

二、 技术原理与现代实现方案

1. 原生拦截原理

Mock.js 的核心原理是重写浏览器原生的 XMLHttpRequest 对象。当代码发起请求时,Mock.js 会在浏览器端拦截该请求,判断 URL 是否匹配预定义的规则。如果匹配,则阻止网络请求的发出,并直接返回本地生成的模拟数据;如果不匹配,则放行请求。

2. 现代工程化方案:Vite + vite-plugin-mock

直接在业务代码(如 main.js)中引入 Mock.js 是一种侵入性较强的做法,且原生 Mock.js 拦截请求后,浏览器的 Network 面板无法抓取到请求记录,给调试带来不便。

在 Vite 生态中,推荐使用 vite-plugin-mock。该插件在开发环境(serve)下,通过 Node.js 中间件的形式拦截请求。这意味着请求真正从浏览器发出并到达了本地开发服务器,因此可以在 Network 面板清晰查看请求详情,体验与真实接口完全一致。

三、 实战演练:构建可分页的列表接口

以下将展示如何在 Vite + TypeScript 项目中集成 Mock.js,并实现一个包含逻辑处理(分页、切片)的模拟接口。

1. 项目目录结构

建议将 Mock 数据与业务代码分离,保持目录结构清晰:

Text

project-root/
├── src/
├── mock/
│   ├── index.ts        # Mock 服务配置
│   └── user.ts         # 用户模块接口
│   └── list.ts         # 列表模块接口(本例重点)
├── vite.config.ts      # Vite 配置
└── package.json

2. 环境配置 (vite.config.ts)

通过配置插件,确保 Mock 服务仅在开发模式下启动,生产构建时自动剔除。

TypeScript

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { viteMockServe } from 'vite-plugin-mock';

export default defineConfig(({ command }) => {
  return {
    plugins: [
      vue(),
      viteMockServe({
        // mock 文件存放目录
        mockPath: 'mock',
        // 仅在开发环境开启 mock
        localEnabled: command === 'serve',
        // 生产环境关闭,避免 mock 代码打包到生产包中
        prodEnabled: false, 
      }),
    ],
  };
});

3. 编写复杂分页接口 (mock/list.ts)

模拟接口不仅仅是返回死数据,更需要具备一定的逻辑处理能力。以下代码演示了如何利用 Mock.js 生成海量数据,并根据前端传入的 page 和 pageSize 参数进行数组切片,模拟真实的数据库查询行为。

TypeScript

import { MockMethod } from 'vite-plugin-mock';
import Mock from 'mockjs';

// 1. 生成模拟数据池
// 使用 Mock.js 模板语法生成 100 条具有语义的列表数据
const dataPool = Mock.mock({
  'list|100': [
    {
      'id|+1': 1, // ID 自增
      author: '@cname', // 随机中文名
      title: '@ctitle(10, 20)', // 10-20字的中文标题
      summary: '@cparagraph(2)', // 随机段落
      'tags|1-3': ['@string("lower", 5)'], // 随机标签数组
      publishDate: '@datetime', // 随机时间
      cover: '@image("200x100", "#50B347", "#FFF", "Mock.js")', // 占位图
      views: '@integer(100, 5000)', // 随机阅读量
    },
  ],
});

// 2. 定义接口逻辑
export default [
  {
    url: '/api/get-article-list',
    method: 'get',
    response: ({ query }) => {
      // 获取前端传递的分页参数,默认为第一页,每页10条
      const page = Number(query.page) || 1;
      const pageSize = Number(query.pageSize) || 10;

      const list = dataPool.list;
      const total = list.length;

      // 核心逻辑:计算分页切片
      const start = (page - 1) * pageSize;
      const end = start + pageSize;
      // 模拟数组切片,返回对应页的数据
      const pageData = list.slice(start, end);

      // 返回标准响应结构
      return {
        code: 200,
        message: 'success',
        data: {
          items: pageData,
          total: total,
          currentPage: page,
          pageSize: pageSize,
        },
      };
    },
  },
] as MockMethod[];

四、 Mock.js 的典型使用场景

  1. 项目原型与演示:在后端架构尚未搭建之前,前端可快速构建包含完整数据流的高保真原型,用于产品评审或客户演示。
  2. 单元测试与集成测试:在 Jest 等测试框架中,利用 Mock 屏蔽外部网络依赖,确保测试用例的运行速度和结果的确定性。
  3. 离线开发:在高铁、飞机等无网络环境下,通过本地 Mock 服务继续进行业务逻辑开发。
  4. 异常流复现:针对超时、空数据、字段缺失等后端难以配合构造的场景进行针对性开发。

五、 深度解析:局限性与弊端

尽管 Mock.js 极大提升了开发效率,但作为一名架构师,必须清晰认知其局限性,以避免在工程落地时产生负面影响。

1. Network 面板不可见问题

原生 Mock.js 通过重写 window.XMLHttpRequest 实现拦截。这种机制发生在浏览器脚本执行层面,请求并未真正进入网络层。因此,开发者在 Chrome DevTools 的 Network 面板中无法看到这些请求,导致调试困难(只能依赖 console.log)。

  • 解决方案:使用 vite-plugin-mock 或 webpack-dev-server 的中间件模式。这种模式在本地 Node 服务端拦截请求,浏览器感知到的是真实的 HTTP 请求,从而解决了 Network 面板不可见的问题。

2. Fetch API 兼容性

原生 Mock.js 仅拦截 XMLHttpRequest,而现代前端项目大量使用 fetch API。若直接使用原生 Mock.js,fetch 请求将无法被拦截,直接穿透到网络。

  • 解决方案:使用 mockjs-fetch 等补丁库,或者坚持使用基于 Node 中间件的拦截方案(如上述 Vite 插件方案),因为中间件方案对前端请求库(Axios/Fetch)是透明的。

3. 数据契约的一致性风险(联调火葬场)

这是 Mock.js 使用中最大的风险点。前端编写的 Mock 数据结构(字段名、类型、层级)完全依赖于开发者的主观定义或早期的接口文档。一旦后端在开发过程中修改了字段(例如将 userId 改为 uid,或将 money 类型由数字改为字符串),而前端 Mock 未及时同步,就会导致“本地开发一切正常,上线联调全面崩溃”的现象。

六、 最佳实践与总结

为了最大化 Mock.js 的收益并规避风险,建议团队遵循以下最佳实践:

  1. 严格的环境隔离:务必在构建配置中通过 Tree Shaking 或环境变量控制,确保 Mock 相关代码(包括 mockjs 库本身和 mock 数据文件)绝对不会被打入生产环境的包中,避免增加包体积或泄露开发逻辑。
  2. 统一接口契约:不要依赖口头约定。建议引入 Swagger (OpenAPI) 或 YAPI 等工具管理接口文档。理想情况下,应编写脚本根据 Swagger 文档自动生成 Mock 数据文件,保证 Mock 数据与后端接口定义的一致性。
  3. 适度模拟:Mock 的目的是打通前端逻辑,而非复刻后端业务。对于极度复杂的业务逻辑(如复杂的权限校验、支付流程),应尽早与后端联调,避免在 Mock 层投入过高成本。
  4. 规范化目录:将 Mock 文件视为项目源代码的一部分进行版本管理,保持清晰的模块化结构,便于团队成员协作和维护。

综上所述,Mock.js 是现代前端工程化中不可或缺的利器。通过合理的架构设计和工具选型,它能显著提升前后端协作效率,但开发者也需时刻警惕数据一致性问题,确保从模拟环境到真实环境的平滑过渡。

【翻译】用生成器实现可续充队列

原文链接:macarthur.me/posts/queue…

生成器执行完毕后便无法 “复活”,但借助 Promise,我们能打造出一个可续充的版本。接下来就动手试试吧。

作者:Alex MacArthur

自从深入研究并分享过生成器的相关内容后,JavaScript 生成器就成了我的 “万能工具”—— 只要有机会,我总会想方设法用上它。通常我会用它来分批处理有限的数据集,比如,遍历一系列闰年并执行相关操作:

function* generateYears(start = 1900) {
  const currentYear = new Date().getFullYear();
  
  for (let year = start + 1; year <= currentYear; year++) {
    if (isLeapYear(year)) {
      yield year;
    }
  }
}

for (const year of generateYears()) {
  console.log('下一个闰年是:', year);
}

又或者惰性处理一批文件:

const csvFiles = ["file1.csv", "file2.csv", "file3.csv"];

function *processFiles(files) {
  for (const file of files) {
    // 加载并处理文件
    yield `处理结果:${file}`;
  }
}

for(const result of processFiles(csvFiles)) {
  console.log(result);
}

这两个示例中,数据都会被一次性遍历完毕,且无法再补充新数据。for 循环执行结束后,迭代器返回的最后一个结果中会包含done: true,一切就此终止。

这种行为本就符合生成器的设计初衷 —— 它从一开始就不是为了执行完毕后能 “复活” 而设计的,其执行过程是一条单行道。但我至少有一次迫切希望它能支持续充,就在最近为 PicPerf 开发文件上传工具时。我当时铁了心要让生成器来实现一个可续充的先进先出(FIFO)队列,一番摸索后,最终的实现效果让我很满意。

可续充队列的设计思路

先明确一下,我所说的 “可续充” 具体是什么意思。生成器无法重启,但我们可以在队列数据耗尽时让它保持等待状态,而非直接终止,Promise 恰好能完美实现这个需求!

我们先从一个基础示例开始:实现一个队列,每隔 500 毫秒逐个处理队列中的圆点元素。

<html>
  <ul id="queue">
    <li class="item"></li>
    <li class="item"></li>
    <li class="item"></li>
  </ul>

  已处理总数:<span id="totalProcessed">0</span>
</html>

<script>
  async function* go() {
    // 初始化队列,包含页面中的初始元素
    const queue = Array.from(document.querySelectorAll("#queue .item"));

    for (const item of queue) {
      yield item;
    }
  }

  // 遍历队列,逐个处理并移除元素
  for await (const value of go()) {
    await new Promise((res) => setTimeout(res, 500));
    value.remove();

    totalProcessed.textContent = Number(totalProcessed.textContent) + 1;
  }
</script>

这就是一个典型的 “单行道” 队列:

如果我们加一个按钮,用于向队列添加新元素,若在生成器执行完毕后点击按钮,页面不会有任何反应 —— 因为生成器已经 “失效” 了。所以,我们需要对代码做一些重构。

实现队列的可续充功能

首先,我们用while(true)让循环无限执行,不再依赖队列初始的固定数据。

async function* go() {
  const queue = Array.from(document.querySelectorAll("#queue .item"));

  while (true) {
    if (!queue.length) {
      return;
    }

    yield queue.shift();
  }
}

现在只剩一个问题:代码中的return语句会让生成器在队列为空时直接终止。我们将其替换为一个 Promise,让循环在无数据可处理时暂停,直到有新数据加入:

let resolve = () => {};
const queue = Array.from(document.querySelectorAll('#queue .item'));
const queueElement = document.querySelector('#queue');
const addToQueueButton = document.querySelector('#addToQueueButton');

async function* go() {  
  while (true) {
    // 创建Promise,并为本次生成器迭代绑定resolve方法
    const promise = new Promise((res) => (resolve = res));

    // 队列为空时,等待Promise解析
    if (!queue.length) await promise;

    yield queue.shift();
  }
}

addToQueueButton.addEventListener("click", () => {
  const newElement = document.createElement("li");
  newElement.classList.add("item");
  queueElement.appendChild(newElement);

  // 添加新元素,唤醒队列
  queue.push(newElement);
  resolve();
});

// 后续处理代码不变
for await (const value of go()) {
  await new Promise((res) => setTimeout(res, 500));
  value.remove();
  totalProcessed.textContent = Number(totalProcessed.textContent) + 1;
}

这次的实现中,生成器的每次迭代都会创建一个新的 Promise。当队列为空时,代码会await这个 Promise 解析,而解析的时机就是我们点击按钮、向队列添加新元素的时刻。

最后,我们对代码做一层封装,打造一个更优雅的 API:

function buildQueue<T>(queue: T[] = []) {
  let resolve: VoidFunction = () => {};

  async function* go() {
    while (true) {
      const promise = new Promise((res) => (resolve = res));

      if (!queue.length) await promise;

      yield queue.shift();
    }
  }

  function push(items: T[]) {
    queue.push(...items);
    resolve();
  }

  return {
    go,
    push,
  };
}

这里补充一个小技巧:你并非一定要将队列中的元素逐个移除。如果希望保留所有元素,只需通过一个索引指针来遍历队列即可:

async function* go() {
  let currentIndex = 0;

  while (true) {
    const promise = new Promise((res) => (resolve = res));

    // 索引指向的位置无数据时,等待新数据
    if (!queue[currentIndex]) await promise;

    yield queue[currentIndex];
    currentIndex++;
  }
}

大功告成!接下来,我们将这个实现落地到实际开发场景中。

在 React 中落地可续充队列

正如前文所说,PicPerf 是一个图片优化、托管和缓存平台,支持用户上传多张图片进行处理。其界面采用了一个常见的交互模式:用户拖拽图片到指定区域,图片会按顺序逐步完成上传。 这正是可续充先进先出队列的适用场景:即便 “待上传” 的图片全部处理完毕,用户依然可以拖拽新的图片进来,上传流程会自动继续,队列会直接从新添加的文件开始处理。

React 中的基础实现方案

首先,我们尝试纯 React 的实现思路,充分利用 React 的状态与渲染生命周期,核心依赖两个状态:

  • files: UploadedFile[]:存储所有拖拽到界面的文件,每个文件自身维护一个状态:pending(待上传)、uploading(上传中)、completed(已完成)。
  • isUploading: boolean:标记当前是否正在上传文件,作为一个 “锁”,防止在已有上传任务执行时,启动新的上传循环。

这个组件的核心逻辑是监听files状态的变化,一旦有新文件加入,useEffect钩子就会触发上传流程;当一个文件上传完成后,将isUploading置为false,又会触发另一次useEffect执行,进而处理队列中的下一张图片。

以下是简化后的核心代码:

import { processUpload } from './wherever';

export default function MediaUpload() {
  const [files, setFiles] = useState([]);
  const [isUploading, setIsUploading] = useState(false);

  const updateFileStatus = useEffectEvent((id, status) => {
    setFiles((prev) =>
      prev.map((file) => (file.id === id ? { ...file, status } : file))
    );
  });

  useEffect(() => {
    // 已有上传任务执行时,直接返回
    if (isUploading) return;

    // 找到队列中第一个待上传的文件
    const nextPending = files.find((f) => f.status === 'pending');

    // 无待上传文件时,直接返回
    if (!nextPending) return;

    // 加锁,标记为上传中
    setIsUploading(true);
    updateFileStatus(nextPending.id, 'uploading');

    // 执行上传,完成后解锁并更新状态
    processUpload(nextPending).then(() => {
      updateFileStatus(nextPending.id, 'complete');
      setIsUploading(false);
    });
  }, [files, isUploading]);

  return <UploadComponent files={files} setFiles={setFiles} />;
}

在有文件正在上传时,用户依然可以添加新文件,新文件会被追加到队列末尾,等待后续逐个处理: 从 React 组件的设计角度来看,这种方案并非不可行,监听状态变化并做出相应响应也是很常见的实现方式。

但说实话,很难有人会觉得这个思路直观易懂。useEffect钩子的设计初衷是让组件与外部系统保持同步,而在这里,它却被用作了事件驱动的状态机调度工具,成了组件的核心行为逻辑,这显然偏离了其设计本意。

所以,我们不妨换掉这些useEffect钩子,用生成器实现的可续充队列来重构这个组件。

结合外部状态仓库实现

我们不再让 React 完全托管所有文件及其状态,而是将这些数据抽离到外部,从其他地方触发组件的重新渲染。这样一来,组件会变得更 “纯”,只专注于其核心职责 —— 渲染界面。

React 恰好提供了一个适配该场景的工具useSyncExternalStore。这个钩子能让组件监听外部管理的数据变化,组件的 “React 特性” 会适当让步,等待外部的指令,而非全权掌控所有状态。在本次实现中,这个 “外部状态仓库” 就是一个独立的模块,专门负责文件的处理逻辑。

useSyncExternalStore至少需要两个方法:一个用于监听数据变化(让 React 知道何时需要重新渲染组件),另一个用于返回数据的最新快照。以下是仓库的基础骨架:

// store.ts

let listeners: Function[] = [];
let files: UploadableFile[] = [];

// 必须返回一个取消监听的方法(供React内部使用)
export function subscribe(listener: Function) {
  listeners.push(listener);

  return () => {
    listeners = listeners.filter((l) => l !== listener);
  };
}

// 返回数据最新快照
export function getSnapshot() {
  return files;
}

接下来,我们补充实现所需的其他方法:

  • updateStatus():更新文件状态(待上传、上传中、已完成);
  • add():向队列中添加新文件;
  • process():启动并执行文件上传队列;
  • emitChange():通知 React 的监听器数据发生变化,触发组件重新渲染。

最终,状态仓库的完整代码如下:

// store.ts

import { buildQueue, processUpload } from './whatever';

let listeners: Function[] = [];
let files: any[] = [];
// 初始化可续充队列
const queue = buildQueue();

// 通知监听器,触发组件重渲染
function emitChange() {
  // 外部仓库的一个关键要点:数据变化时,必须返回新的引用
  files = [...queue.queue];

  for (let listener of listeners) {
    listener();
  }
}

// 更新文件状态
function updateStatus(file: any, status: string) {
  file.status = status;
  emitChange();
}

// 公共方法
export function getSnapshot() {
  return files;
}

export function subscribe(listener: Function) {
  listeners.push(listener);

  return () => {
    listeners = listeners.filter((l) => l !== listener);
  };
}

// 向队列添加新文件
export function add(newFiles: any[]) {
  queue.push(newFiles);
  emitChange();
}

// 执行文件上传流程
export async function process() {
  for await (const file of queue.go()) {
    updateStatus(file, 'uploading');
    await processUpload(file);
    updateStatus(file, 'complete');
  }
}

此时,我们的 React 组件会变得异常简洁:

import { 
  add,
  process, 
  subscribe,
  getSnapshot
} from './store';

export default function MediaUpload() {
  // 监听外部仓库的数据变化
  const files = useSyncExternalStore(subscribe, getSnapshot);

  // 组件挂载时启动上传队列
  useEffect(() => {
    process();
  }, []);

  // 将文件数据和添加方法传递给子组件
  return <UploadComponent files={files} setFiles={add} />;
}

现在只剩一个细节需要完善:合理的清理逻辑。当组件卸载时,我们不希望还有未完成的上传任务在后台执行。因此,我们为仓库添加一个abort方法,强制终止生成器,并在组件的useEffect中执行清理:

// store.ts

// 其他代码不变

let iterator = null;

export async function process() {
  // 保存生成器迭代器的引用
  iterator = queue.go();

  for await (const file of iterator) {
    updateStatus(file, 'uploading');
    await processUpload(file);
    updateStatus(file, 'complete');
  }

  iterator = null;
}

// 强制终止生成器
export function abort() {
  return iterator?.return();
}
function MediaUpload() {
  const files = useSyncExternalStore(subscribe, getSnapshot);

  useEffect(() => {
    process();
    // 组件卸载时执行清理,终止上传队列
    return () => abort();
  }, []);

  return <UploadComponent files={files} setFiles={add} />;
}

需要说明的是,为了简化代码,这里做了一些大胆的假设:上传过程永远不会失败、process方法同一时间只会被调用一次、该仓库只有一个使用者。请忽略这些细节以及其他可能的疏漏,重点来看这种实现方案带来的诸多优势:

  1. 组件的行为不再依赖useEffect的反复触发,逻辑更清晰;
  2. 所有文件上传的业务逻辑都被抽离到独立模块中,与 React 解耦,可单独维护和复用;
  3. 终于有机会实际使用useSyncExternalStore这个 React 钩子;
  4. 我们成功在 React 中用异步生成器实现了一个可续充队列。

对有些人来说,这种方案可能比最初的纯 React 方案复杂得多,我完全理解这种感受。但不妨换个角度想:现在把代码写得复杂一点,就能多拖延一点时间,避免 AI 工具完全取代我们的工作、毁掉我们的职业未来,甚至 “收割” 我们的价值。带着这个目标去写代码吧!

当然,说句正经的:要让 AI 辅助开发持续发挥价值,需要人类帮助 AI 理解底层技术原语的设计目的、取舍原则和发展前景。掌握这些底层知识,永远有其不可替代的价值。

Chmod Cheatsheet

Basic Syntax

Use these core command forms for chmod.

Command Description
chmod MODE FILE General chmod syntax
chmod 644 file.txt Set numeric permissions
chmod u+x script.sh Add execute for owner
chmod g-w file.txt Remove write for group
chmod o=r file.txt Set others to read-only

Numeric Modes

Common numeric permission combinations.

Mode Meaning
600 Owner read/write
644 Owner read/write, group+others read
640 Owner read/write, group read
700 Owner full access only
755 Owner full access, group+others read/execute
775 Owner+group full access, others read/execute
444 Read-only for everyone

Symbolic Modes

Change specific permissions without replacing all bits.

Command Description
chmod u+x file Add execute for owner
chmod g-w file Remove write for group
chmod o-rwx file Remove all permissions for others
chmod ug+rw file Add read/write for owner and group
chmod a+r file Add read for all users
chmod a-x file Remove execute for all users

Files and Directories

Typical permission patterns for files and directories.

Command Description
chmod 644 file.txt Standard file permissions
chmod 755 dir/ Standard executable directory permissions
chmod u=rw,go=r file.txt Symbolic equivalent of 644
chmod u=rwx,go=rx dir/ Symbolic equivalent of 755
chmod +x script.sh Make script executable

Recursive Changes

Apply permission updates to directory trees.

Command Description
chmod -R 755 project/ Recursively set mode for all entries
chmod -R u+rwX project/ Add read/write and smart execute recursively
find project -type f -exec chmod 644 {} + Set files to 644
find project -type d -exec chmod 755 {} + Set directories to 755
chmod -R g-w shared/ Remove group write recursively

Special Bits

Setuid, setgid, and sticky bit examples.

Command Description
chmod 4755 /usr/local/bin/tool Setuid on executable
chmod 2755 /srv/shared Setgid on directory
chmod 1777 /tmp/mytmp Sticky bit on world-writable directory
chmod u+s file Add setuid (symbolic)
chmod g+s dir Add setgid (symbolic)
chmod +t dir Add sticky bit (symbolic)

Safe Patterns

Use these patterns to avoid unsafe permission changes.

Command Description
chmod 600 ~/.ssh/id_ed25519 Secure SSH private key
chmod 700 ~/.ssh Secure SSH directory
chmod 644 ~/.ssh/id_ed25519.pub Public key permissions
chmod 750 /var/www/app Limit web root access
chmod 755 script.sh Safer than 777 for scripts

Common Errors

Quick checks when permission changes do not work.

Issue Check
Operation not permitted Check file ownership with ls -l and apply with the correct user or sudo
Permission still denied after chmod Parent directory may block access; check directory execute (x) bit
Cannot chmod symlink target as expected chmod applies to target file, not link metadata
Recursive mode broke app files Reset with separate file/dir modes using find ... -type f/-type d
Changes revert on mounted share Filesystem mount options/ACL may override mode bits

Related Guides

Use these guides for full permission and ownership workflows.

Guide Description
How to Change File Permissions in Linux (chmod command) Full chmod guide with examples
Chmod Recursive: Change File Permissions Recursively in Linux Recursive permission strategies
What Does chmod 777 Mean Security impact of 777
Chown Command in Linux (File Ownership) Change file and directory ownership
Umask Command in Linux Default permissions for new files
Understanding Linux File Permissions Permission model explained

【节点】[MainLightColor节点]原理解析与实际应用

【Unity Shader Graph 使用与特效实现】专栏-直达

在Unity URP渲染管线中,光照计算是创建逼真视觉效果的核心环节。Main Light Color节点作为Shader Graph中的重要组件,专门用于获取场景中主定向光源的颜色属性信息。这个节点为着色器艺术家和图形程序员提供了直接访问场景主要光源颜色数据的能力,使得材质能够对场景中最主要的光源做出精确响应。

Main Light Color节点在URP着色器开发中扮演着关键角色,它不仅仅返回简单的RGB颜色值,而是包含了完整的光照强度信息。这意味着开发者可以获取到经过Unity光照系统处理后的最终颜色结果,包括所有相关光照计算和后期处理效果的影响。这种直接访问方式大大简化了自定义光照模型的实现过程,使得即使是没有深厚图形编程背景的艺术家也能创建出专业级的光照响应材质。

在实时渲染中,主光源通常指场景中的主要定向光源,如太阳或月亮。Main Light Color节点正是针对这种关键光源设计的,它能够动态响应光照条件的变化,包括日夜循环、天气系统或游戏剧情驱动的光照变化。这种动态响应能力使得材质能够与游戏环境保持视觉一致性,创造出更加沉浸式的体验。

描述

Main Light Color节点是Shader Graph中专门用于获取场景主光源颜色信息的内置节点。该节点输出的颜色信息不仅包含基本的RGB色彩值,还整合了光源的亮度强度,形成了一个完整的颜色-强度组合数据。这种设计使得开发者可以直接使用该输出值参与光照计算,无需额外的强度调整或颜色处理。

从技术实现角度来看,Main Light Color节点在背后调用了URP渲染管线的内部函数,特别是GetMainLight()方法。这个方法会分析当前场景的光照设置,确定哪一个是主光源,并提取其所有相关属性。对于颜色信息,节点会综合考虑光源的基础颜色、强度值,以及任何可能影响最终输出的后期处理效果或光照修改组件。

在实际应用中,Main Light Color节点的输出值代表了主光源在当前渲染帧中对表面点可能产生的最大影响。这个值会根据光源的类型、设置和场景中的相对位置自动计算。对于定向光源,颜色和强度通常是恒定的(除非有动态修改),而对于其他类型的光源,可能会根据距离和角度有所不同。

该节点的一个关键特性是其输出的颜色值已经包含了亮度信息。这意味着一个强度为2的白色光源不会返回(1,1,1)的纯白色,而是会根据强度进行相应的亮度提升。这种设计决策使得节点输出可以直接用于光照计算,无需开发者手动将颜色与强度相乘,简化了着色器的构建过程。

Main Light Color节点在以下场景中特别有用:

  • 创建对动态光照条件响应的材质
  • 实现自定义的光照模型
  • 开发风格化的渲染效果
  • 构建与场景光照紧密交互的特效系统
  • 制作适应日夜循环的环境材质

技术实现细节

从底层实现来看,Main Light Color节点对应于HLSL代码中的_MainLightColor变量。在URP渲染管线中,这个变量在每帧开始时由渲染系统更新,确保着色器始终能够访问到最新的主光源信息。当场景中没有明确设置主光源时,系统会使用默认的光照设置,或者在某些情况下返回黑色(即无光照)。

节点的输出类型为Vector 3,分别对应颜色的R、G、B通道。每个通道的值范围通常是[0,∞),因为URP使用高动态范围光照计算。这意味着颜色值可以超过1,表示特别明亮的光源。在实际使用时,开发者可能需要根据具体需求对这些值进行适当的缩放或限制。

值得注意的是,Main Light Color节点获取的颜色已经考虑了光源的过滤器颜色(如果有的话)。例如,如果一个白色光源前面放置了红色的滤色片,那么节点返回的将是红色调的颜色值。这种完整性使得节点在各种复杂的照明场景中都能提供准确的结果。

性能考虑

Main Light Color节点是一个极其高效的操作,因为它只是读取一个已经计算好的全局着色器变量。与复杂的光照计算或纹理采样相比,它的性能开销可以忽略不计。这使得它非常适合用于移动平台或需要高性能的实时应用中。

在Shader Graph中使用该节点时,它不会增加额外的绘制调用或显著影响着色器的复杂度。然而,开发者应该注意,如果在一个着色器中多次使用该节点,最好将其输出存储在一个中间变量中,然后重复使用这个变量,而不是多次调用节点本身。这种优化实践有助于保持着色器的整洁和效率。

端口

Main Light Color节点的端口设计体现了其功能的专一性和高效性。作为一个输入输出结构简单的节点,它只包含一个输出端口,这种简约的设计反映了其单一职责原则——专注于提供主光源的颜色信息。

输出端口详解

Out - 输出方向 - Vector 3类型

Out端口是Main Light Color节点唯一的输出接口,负责传递主光源的完整颜色信息。这个Vector 3输出包含了以下关键信息:

  • R通道:红色分量,表示光源在红色频谱上的强度
  • G通道:绿色分量,表示光源在绿色频谱上的强度
  • B通道:蓝色分量,表示光源在蓝色频谱上的强度

重要的是,这些颜色分量已经包含了光源的亮度信息。这意味着一个强度为1的白色光源会返回近似(1,1,1)的值,而强度为2的白色光源会返回近似(2,2,2)的值。这种设计使得输出值可以直接用于光照计算,无需额外的强度乘法操作。

数据范围与特性

Main Light Color节点的输出值范围在理论上是无上限的,因为URP支持高动态范围渲染。在实际应用中,值的大小取决于光源的强度设置和颜色选择。以下是一些典型情况下的输出示例:

  • 默认白色定向光(强度1):约(1.0, 1.0, 1.0)
  • 明亮的白色太阳光(强度2):约(2.0, 2.0, 2.0)
  • 红色光源(强度1):约(1.0, 0.0, 0.0)
  • 蓝色光源(强度0.5):约(0.0, 0.0, 0.5)
  • 无主光源情况:约(0.0, 0.0, 0.0)

与其他节点的连接方式

Out端口的Vector 3输出可以与多种其他Shader Graph节点连接,实现复杂的光照效果:

与颜色操作节点连接

  • Multiply节点连接:调整光照颜色的强度或应用色调映射
  • Add节点连接:创建光照叠加效果
  • Lerp节点连接:在不同光照颜色间平滑过渡
  • Split节点连接:分离RGB通道进行独立处理

与光照计算节点结合

  • Dot Product节点结合:计算兰伯特光照
  • Normalize节点结合:准备光照方向计算
  • Power节点结合:实现更复杂的光照衰减

与纹理采样结合

  • Sample Texture 2D节点输出相乘:实现纹理受光照影响的效果
  • Texture Coordinates节点结合:创建基于光照的UV动画

实际应用示例

以下是一个基本的光照计算示例,展示如何使用Main Light Color节点的Out端口:

Main Light Color [Out] → Multiply [A]
Normal Vector → Dot Product [A]
Light Direction → Dot Product [B]
Dot Product [Out] → Multiply [B]

Multiply [Out] → Base Color [Base Map]

在这个示例中,Main Light Color的输出与兰伯特系数(通过法线与光方向的点积计算)相乘,最终结果用作基础颜色的调制因子。这种连接方式创建了基本的漫反射光照效果。

高级用法

对于更复杂的材质效果,开发者可以将Main Light Color的输出与其他高级节点结合:

镜面反射计算

Main Light Color → Multiply → Specular Output

自发光效果

Main Light Color → Add → Emission Input

阴影处理

Main Light Color → Multiply (with Shadow Attenuation) → Final Color

性能优化建议

虽然Main Light Color节点本身性能开销很小,但在复杂着色器中的使用方式会影响整体性能:

  • 尽量避免在着色器的多个位置重复调用Main Light Color节点,而是将其输出存储到变量中重复使用
  • 当只需要单通道信息时,考虑使用Split节点分离出所需通道,而不是处理完整的Vector 3
  • 在不需要HDR效果的场景中,可以使用Clamp节点将输出值限制在[0,1]范围内,这可能在某些硬件上提供轻微的性能提升

平台兼容性

Main Light Color节点的Out端口在所有支持URP的平台上都有相同的行为,包括:

  • Windows、MacOS、Linux
  • iOS和Android移动设备
  • 主流游戏主机平台
  • WebGL和XR设备

这种跨平台的一致性确保了使用Main Light Color节点的着色器可以在不同的目标平台上提供可预测的视觉效果,大大简化了多平台开发的复杂度。


【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

2026 春晚魔术大揭秘:作为程序员,分分钟复刻一个(附源码)

大家好,我是 Sunday。

昨晚的 2026 年春晚上的魔术【惊喜定格】大家看了吗?

说实话,作为一名资深的前端开发者,我对那些歌舞节目的兴趣一般,但每年的魔术环节我必看。不是为了看奇迹,而是为了:找 Bug 😂。

今年的魔术特别有意思:

魔术师拿出一个手机计算器,让全场观众参与,又是随机想数字,又是乱按屏幕,最后算出来的结果,竟然精准地命中了 当前的年、月、日、时、分

我老婆说:“哇哦,好厉害啊~”

不过我是越看越不对,这玩意怎么感觉像是个 写死的 JS 脚本啊?


其实,这个魔术并不是 2026 春晚的首创。

早在去年(2025年)底,武汉理工大学的迎新晚会 上就有这个魔术的雏形。

当时也是一样的套路:随机数字 + 观众乱按 = 预言时间。

而这个魔术的实现原理,就在魔术师手中的 计算器

魔术师手里那个所谓的“计算器”,压根就不是系统自带的。那是一个专门开发的 Web App 或者 Native App

所以,咱们今天大年初一不整虚的,直接打开 VS Code,从原理到代码,一比一复刻这个价值过亿流量的春晚魔术!

春晚魔术的实现原理

这个魔术的核心逻辑,可以拆解为两个部分:

  1. 数学逻辑(后端逻辑):逆向推导
  2. 交互逻辑(前端表现):输入幻觉

1. 数学逻辑:逆向推导

普通人的思维是:输入 A + 输入 B + 乱按的 C = 结果

但在代码里,逻辑是反过来的:目标结果(当前时间) - 输入 A - 输入 B = 必须填补的差值(Force Number)

比如:

  • 目标时间:2月16日 22点27分 -> 数字 2162227
  • 观众 A 输入1106
  • 观众 B 输入88396
  • 当前总和89502
  • 系统偷偷算的差值2162227 - 89502 = 2072725

接下来,魔术师要做的,就是让第三个观众,在以为自己是“随机乱按”的情况下,把 2072725 这个数字“按”出来。

2. 交互逻辑:输入幻觉

这是整个魔术最精彩,也是前端最能发挥的地方。

魔术师会说:“来,大家随便按计算器,越乱越好。”

观众以为按 9 屏幕就会显示 9,按 5 就会显示 5

大错特错!

在这个 App 进入“魔术模式”后,键盘事件已经被 e.preventDefault() 拦截了。无论你按哪个数字键,屏幕上只会依次显示程序预设好的那个 差值字符串

  • 差值是 2072725
  • 你按“9”,代码输出 2
  • 你按“1”,代码输出 0
  • 你按“任意键”,代码输出 7...

现在知道 为什么魔术师要把屏幕翻过来了吧。就是为了不让大家看到用户真实输入的是什么。

实现源码

原理讲通了,咱们直接上代码,

  • 第一步:界面布局(Tailwind 真的香)

作为一名前端,UI 的还原度决定了魔术的可信度。我用了 Tailwind CSS 来复刻 iOS/小米计算器的风格。

<div class="grid grid-cols-4 gap-4">
    <button @click="appendNum('7')" class="...">7</button>
    <button @click="appendNum('8')" class="...">8</button>
    <button @click="calculate" class="btn-orange ...">=</button>
</div>

  • 第二步:设计“触发机关”

魔术师不能直接说:“我要变魔术了”。他需要一个隐蔽的开关。在这个代码里,我设计了一个 “三连击触发器”:当连续点击 3 次 = 号时,激活魔术模式。(当然,你可以不用这个触发,也并不影响)

// 状态定义
const equalClickCount = ref(0); // 统计等号点击次数
const isMagicMode = ref(false); // 魔术模式开关
const magicSequence = ref('');  // 算好的差值(剧本)

const calculate = () => {
    // ... 正常计算逻辑 ...
    
    // 触发检测
    equalClickCount.value++;
    if (equalClickCount.value === 3) {
        // 1. 获取目标:当前时间 (比如 2162227)
        const target = getMagicTarget(); 
        // 2. 获取现状:屏幕上的数字
        const currentSum = parseFloat(currentVal.value);
        // 3. 计算剧本:差值
        const diff = target - currentSum;
        
        if (diff > 0) {
            // 激活魔术模式!
            magicSequence.value = String(diff);
            isMagicMode.value = true;
            // 控制台偷偷告诉我们一声
            console.log(`🔒 锁定!目标:${target}, 差值:${diff}`);
        }
    }
}

  • 第三步:核心“欺骗”逻辑

这是最关键的 appendNum 函数。它根据当前是否处于 魔术模式 来决定是“听你的”还是“听我的”。

const appendNum = (num) => {
    // >>> 魔术模式:虽然你按了键,但我只输出剧本里的数字
    if (isMagicMode.value) {
        // 第一次按键时,清空屏幕,开始表演
        if (isFirstMagicInput.value) {
            currentVal.value = ''; 
            isFirstMagicInput.value = false;
        }

        // 依次吐出 magicSequence 里的字符
        if (magicIndex.value < magicSequence.value.length) {
            currentVal.value += magicSequence.value[magicIndex.value];
            magicIndex.value++;
            
            // 加点震动反馈,增加真实感(手机端体验极佳)
            if (navigator.vibrate) navigator.vibrate(50); 
        }
        return; 
    }
    
    // >>> 正常模式:该咋算咋算
    // ... 原有逻辑
};

使用方式:

  • 随机输入一个四位数
  • 随机输入一个五位数
  • 然后相加

  • 然后是 重点,连续按下三次等号,激活 魔术模式

  • 然后随便输入,无论你输入的是什么,最终都会显示出咱们计算好的值

最终得出当前的时间点 2 月 17 日 11 点 32 分!

写在最后

可能有人会觉得:“Sunday,你一个做技术教育的,搞这些花里胡哨的干嘛?”

其实,这和我们做项目是相通的。

我在 前端 + AI 训练营 里经常跟同学们强调一点:前端工程师的价值,不仅仅在于画页面,而在于“交互逻辑的实现”和“用户体验的掌控”。

这个魔术的完整 HTML 代码,我已经打包好了,大家可以直接在公众号【程序员Sunday】中回复【魔术】来获取源码

数组查找与判断:find / some / every / includes 的正确用法

今天是2026年2月17日农历正月初一,在2026 愿大家:身体健康无病痛,收入翻番钱包鼓! 代码 0 Error 0 Warning,需求一次过,上线零回滚!策马扬鞭,从小白进阶专家,新年一路 “狂飙”!🧧🐎 给大家拜年啦~

前言

前端里权限判断、表单校验、勾选状态,几乎都要判断「数组里有没有某个值」或「是否全部满足条件」。很多人习惯用 for 循环 + if 一把梭,或者 indexOf 判断,写多了既啰嗦又容易漏边界情况。
find / some / every / includes 这四个方法,可以把「查找 → 判断 → 校验」写得更短、更语义化,也更好处理边界情况。本文用 10 个常见场景,把日常该怎么选、为什么这么选、容易踩的坑讲清楚。

适合读者:

  • 会写 JS,但对 find/some/every/includes 用哪个、什么时候用有点模糊
  • 刚学 JS,希望一开始就养成清晰的数组判断写法
  • 有经验的前端,想统一团队里的权限/校验/状态判断写法

一、先搞清楚:find / some / every / includes 在干什么

这四个方法都不是黑魔法,本质是:在不动原数组的前提下,用一次遍历完成「查找 / 判断是否存在 / 判断是否全部满足」

方法 在干什么 返回值 什么时候停
find 找第一个符合条件的元素 找到的元素,找不到返回 undefined 找到第一个就停
some 判断是否至少有一个满足条件 truefalse 找到第一个就停(短路)
every 判断是否全部满足条件 truefalse 遇到第一个不满足就停(短路)
includes 判断数组里是否包含某个值(严格相等) truefalse 遍历完或找到就停
// 传统 for:意图分散,还要自己管 break
let found = null;
for (let i = 0; i < users.length; i++) {
  if (users[i].id === targetId) {
    found = users[i];
    break;
  }
}

// find:一眼看出「找第一个 id 匹配的」
const found = users.find((u) => u.id === targetId);

记住一点:能用语义化方法就不用循环,用 find/some/every/includes 把「要查什么、要判断什么」写清楚,比「怎么循环、怎么 break」更重要。

二、数组查找与判断的 10 个常用场景

假设接口返回的数据类似:

const users = [
  { id: 1, name: '张三', role: 'admin', status: 'active' },
  { id: 2, name: '李四', role: 'user', status: 'active' },
  { id: 3, name: '王五', role: 'user', status: 'inactive' },
];

const permissions = ['read', 'write', 'delete'];
const selectedIds = [1, 2];

下面 10 个写法,覆盖权限判断、表单校验、勾选状态等真实场景。

场景 1:找第一个符合条件的对象(find

const admin = users.find((user) => user.role === 'admin');
// { id: 1, name: '张三', role: 'admin', status: 'active' }

// 找不到返回 undefined
const superAdmin = users.find((user) => user.role === 'superAdmin');
// undefined

适用: 默认选中第一项、取第一个有效配置、根据 id 找对象等。
注意: find 找不到返回 undefined,后续解构或访问属性要处理,用 ?? 给默认值。

场景 2:判断是否至少有一个满足条件(some

const hasAdmin = users.some((user) => user.role === 'admin');
// true

const hasInactive = users.some((user) => user.status === 'inactive');
// true

适用: 权限判断「是否有任一管理员」、表单校验「是否有错误项」、状态判断「是否有未完成项」等。
注意: 空数组时 some 返回 false,业务上要结合「空列表算通过还是不算」处理。

场景 3:判断是否全部满足条件(every

const allActive = users.every((user) => user.status === 'active');
// false(因为有王五是 inactive)

const allHaveId = users.every((user) => user.id != null);
// true

适用: 表单校验「是否全部勾选」、权限判断「是否全部有权限」、状态判断「是否全部完成」等。
注意: 空数组时 every 返回 true(空真),业务上要结合「空列表算通过还是不算」处理。

场景 4:判断数组是否包含某个值(includes

const hasRead = permissions.includes('read');
// true

const hasExecute = permissions.includes('execute');
// false

适用: 简单值数组的包含判断、权限列表判断、标签列表判断等。
注意: includes 底层用 严格相等=== 做比较,这对「简单值(string / number / boolean)」很友好,但对「对象 / 数组」这类引用类型完全不适用,因为===比较的是内存地址而非内容。

场景 5:权限判断:是否有某个权限(some + includes

const userPermissions = ['read', 'write'];
const requiredPermission = 'delete';

const hasPermission = userPermissions.includes(requiredPermission);
// false

// 或判断多个权限中是否有任一
const requiredPermissions = ['delete', 'admin'];
const hasAnyPermission = requiredPermissions.some((perm) => 
  userPermissions.includes(perm)
);
// false

适用: 按钮权限控制、路由权限控制、功能权限判断等。
推荐: 简单值用 includes,复杂条件用 some + 回调。

场景 6:表单校验:是否全部必填项已填(every

const formFields = [
  { name: 'username', value: '张三', required: true },
  { name: 'email', value: '', required: true },
  { name: 'phone', value: '13800138000', required: false },
];

const allRequiredFilled = formFields
  .filter((field) => field.required)
  .every((field) => field.value.trim() !== '');
// false(email 为空)

适用: 表单提交前校验、批量操作前校验、多步骤流程校验等。
推荐:filter 筛出必填项,再用 every 判断是否全部有值。

场景 7:勾选状态:是否全部选中(every

const checkboxes = [
  { id: 1, checked: true },
  { id: 2, checked: true },
  { id: 3, checked: false },
];

const allChecked = checkboxes.every((item) => item.checked);
// false

const hasChecked = checkboxes.some((item) => item.checked);
// true

适用: 全选/反选功能、批量操作按钮状态、表格多选状态等。
推荐: every 判断全选,some 判断是否有选中项。

场景 8:找第一个并给默认值(find+ ??

const defaultUser = users.find((user) => user.role === 'admin') ?? {
  id: 0,
  name: '默认用户',
  role: 'guest',
};

适用: 默认选中第一项、取第一个有效配置、兜底默认值等。
注意: find 找不到返回 undefined,用 ?? 可以统一成默认对象,避免后面解构报错。

场景 9:对象数组是否包含某个 id(some

const targetId = 2;
const exists = users.some((user) => user.id === targetId);
// true

// 或判断多个 id 中是否有任一存在
const targetIds = [2, 5];
const hasAny = targetIds.some((id) => users.some((user) => user.id === id));
// true(2 存在)

适用: 判断选中项是否在列表里、判断 id 是否已存在、去重前判断等。
注意: 对象数组不能用 includes,要用 some + 条件判断。

场景 10:组合判断:全部满足 A 且至少一个满足 B(every +some

const allActive = users.every((user) => user.status === 'active');
const hasAdmin = users.some((user) => user.role === 'admin');

// 业务逻辑:全部激活 且 有管理员
const canOperate = allActive && hasAdmin;
// false(因为有 inactive 的)

适用: 复杂业务规则判断、多条件组合校验、权限组合判断等。
推荐: 把每个条件拆成变量,用名字表达「这一步在判断什么」,可读性和调试都会好很多。

三、容易踩的坑

1. find 找不到返回 undefined,直接解构会报错

const user = users.find((u) => u.id === 999);
const { name } = user; // TypeError: Cannot read property 'name' of undefined

正确:?? 给默认值,或先判断再解构。

const user = users.find((u) => u.id === 999) ?? { name: '未知' };
// 或
const user = users.find((u) => u.id === 999);
if (user) {
  const { name } = user;
}

2. 空数组时 every 返回 truesome 返回 false

[].every((x) => x > 0); // true(空真)
[].some((x) => x > 0);  // false

业务上要结合「空列表算通过还是不算」处理。例如表单校验,空列表可能应该算「未填写」而不是「通过」。

const fields = [];
const allFilled = fields.length > 0 && fields.every((f) => f.value);
// 先判断长度,再 every

3. includes 只能判断简单值,对象数组要用 some

const users = [{ id: 1 }, { id: 2 }];
users.includes({ id: 1 }); // false(对象引用不同)

// 正确:用 some + 条件判断
users.some((user) => user.id === 1); // true

4. findfilter 的区别:find 只找第一个,filter 找全部

const firstAdmin = users.find((u) => u.role === 'admin');
// 返回第一个对象或 undefined

const allAdmins = users.filter((u) => u.role === 'admin');
// 返回数组,可能为空数组 []

要「第一个」用 find,要「全部」用 filter,别混用。

5. someevery 的短路特性:找到就停

const users = [
  { id: 1, role: 'admin' },
  { id: 2, role: 'user' },
  { id: 3, role: 'admin' },
];

// some:找到第一个 admin 就停,不会继续遍历
users.some((u) => {
  console.log(u.id); // 只打印 1
  return u.role === 'admin';
});

// every:遇到第一个不是 admin 就停
users.every((u) => {
  console.log(u.id); // 打印 1, 2(遇到 user 就停)
  return u.role === 'admin';
});

性能上这是好事,但如果有副作用(如打印、修改外部变量),要注意只执行到第一个匹配项。

四、实战推荐写法模板

权限判断(是否有某个权限):

const userPermissions = response?.data?.permissions ?? [];
const canDelete = userPermissions.includes('delete');

// 或判断多个权限中是否有任一
const canManage = ['delete', 'admin'].some((perm) => 
  userPermissions.includes(perm)
);

表单校验(是否全部必填项已填):

const fields = formData?.fields ?? [];
const isValid = fields
  .filter((field) => field.required)
  .every((field) => field.value?.trim() !== '');

// 或更严格的校验
const isValid = fields.length > 0 && 
  fields.filter((f) => f.required).every((f) => f.value?.trim() !== '');

勾选状态(全选/部分选中):

const items = tableData ?? [];
const allChecked = items.length > 0 && items.every((item) => item.checked);
const hasChecked = items.some((item) => item.checked);

// 全选按钮状态
const selectAllDisabled = items.length === 0;
const selectAllChecked = allChecked;

找第一个并给默认值:

const defaultItem = (response?.data?.list ?? []).find(
  (item) => item.isDefault
) ?? {
  id: 0,
  name: '默认选项',
  value: '',
};

对象数组是否包含某个 id:

const selectedIds = [1, 2, 3];
const targetId = 2;
const isSelected = selectedIds.includes(targetId);

// 对象数组
const users = response?.data?.users ?? [];
const targetId = 2;
const exists = users.some((user) => user.id === targetId);

五、小结

场景 推荐写法 返回值
找第一个符合条件的对象 list.find(item => ...) 对象或 undefined
判断是否至少有一个满足 list.some(item => ...) truefalse
判断是否全部满足 list.every(item => ...) truefalse
判断是否包含某个值(简单值) list.includes(value) truefalse
找第一个并给默认值 list.find(...) ?? 默认值 对象或默认值
对象数组是否包含某个 id list.some(item => item.id === id) truefalse
表单校验:全部必填已填 list.filter(...).every(...) truefalse
勾选状态:全部选中 list.every(item => item.checked) truefalse

记住:find 负责「找」,some 负责「至少一个」,every 负责「全部」,includes 负责「简单值包含」。日常写权限、校验、状态判断时,先想清楚是要找对象、判断存在、判断全部,还是简单值包含,再选方法,代码会干净很多,也少踩坑。

特别提醒:

  • find 找不到返回 undefined,记得用 ?? 给默认值
  • 空数组时 everytruesomefalse,业务上要结合长度判断
  • 对象数组不能用 includes,要用 some + 条件判断

文章到这里结束。如果你日常写权限判断、表单校验、勾选状态时经常纠结用哪个方法,希望这篇能帮你定个型。

以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

简单题,简单做(Python/Java/C++/Go)

枚举小时 $h=0,1,2,\ldots,11$ 以及分钟 $m=0,1,2,\ldots,59$。如果 $h$ 二进制中的 $1$ 的个数加上 $m$ 二进制中的 $1$ 的个数恰好等于 $\textit{turnedOn}$,那么把 $h:m$ 添加到答案中。

注意如果 $m$ 是个位数,需要添加一个前导零。

class Solution:
    def readBinaryWatch(self, turnedOn: int) -> List[str]:
        ans = []
        for h in range(12):
            for m in range(60):
                if h.bit_count() + m.bit_count() == turnedOn:
                    ans.append(f"{h}:{m:02d}")
        return ans
class Solution {
    public List<String> readBinaryWatch(int turnedOn) {
        List<String> ans = new ArrayList<>();
        for (int h = 0; h < 12; h++) {
            for (int m = 0; m < 60; m++) {
                if (Integer.bitCount(h) + Integer.bitCount(m) == turnedOn) {
                    ans.add(String.format("%d:%02d", h, m));
                }
            }
        }
        return ans;
    }
}
class Solution {
public:
    vector<string> readBinaryWatch(int turnedOn) {
        vector<string> ans;
        char s[6];
        for (uint8_t h = 0; h < 12; h++) {
            for (uint8_t m = 0; m < 60; m++) {
                if (popcount(h) + popcount(m) == turnedOn) {
                    sprintf(s, "%d:%02d", h, m);
                    ans.emplace_back(s);
                }
            }
        }
        return ans;
    }
};
func readBinaryWatch(turnedOn int) (ans []string) {
    for h := range 12 {
        for m := range 60 {
            if bits.OnesCount8(uint8(h))+bits.OnesCount8(uint8(m)) == turnedOn {
                ans = append(ans, fmt.Sprintf("%d:%02d", h, m))
            }
        }
    }
    return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(1)$。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Vue3 源码解析系列 1:从 Debugger 视角读 Vue

引言

直接把源码当黑盒,或者干巴巴从头读到尾,几乎读不下去。更高效的方式是把源码当作“正在运行的程序”,用断点一层层摸清主流程。

这一篇记录我用经典 markdown.html 示例,跟踪 createApp -> mount -> render 的阅读路径。

初期准备

git clone https://github.com/vuejs/core.git
cd core
pnpm install
pnpm run dev # 生成 dev 版本 Vue

# 用浏览器打开示例
open packages/vue/examples/classic/markdown.html

入口断点:createApp

createApp 开始是最稳定的入口,它是 app 创建的第一站。

createApp 断点

渲染对比:从“看见结果”到“找到入口”

先把渲染结果和源码入口对齐,这样断点才更有目标感。

渲染对比

mount 打断点

mount 是渲染真正开始的地方,后面会进入 renderpatch

mount 断点

主流程:props / data / computed / watch

这个阶段会完成 Options API 的初始化,包括 data 绑定、computed 计算、watch 监听等。

主流程

data 绑定:把 data 暴露到 ctx

data() 返回对象后,会被转成响应式,并在 dev 模式下挂到 ctx 以便访问。

data 绑定

for (const key in data) {
checkDuplicateProperties!(OptionTypes.DATA, key)
// expose data on ctx during dev
if (!isReservedPrefix(key[0])) {
Object.defineProperty(ctx, key, {
configurable: true,
enumerable: true,
get: () => data[key],
set: NOOP,
})
}
}

computed:依赖收集与访问触发

依赖收集断点

computed 依赖收集

访问触发断点

computed 访问触发

在模板里出现:

<div v-html="compiledMarkdown"></div>

就会在渲染时读取 compiledMarkdown,触发 computed 的 getter。完整流程可以拆成:

  1. 读取 computedOptions
  2. 为每个 computed 添加 getter / setter
  3. 模板渲染时访问 compiledMarkdown
  4. 触发 getter,开始依赖追踪

一句话总结:把一个 getter(或 get/set)包装成带缓存的响应式 ref,并在依赖变化时标记为脏。

export function computed(getterOrOptions, debugOptions, isSSR = false) {
// 1. 解析 getter / setter
let getter, setter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}

// 2. 创建 ComputedRefImpl 实例
const cRef = new ComputedRefImpl(getter, setter, isSSR)

// 3. dev 环境下注入调试钩子
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack
cRef.onTrigger = debugOptions.onTrigger
}

// 4. 返回一个 ref(带 value getter/setter)
return cRef
}

生命周期注册

生命周期函数在这里统一注册,方便后续统一触发。

lifecycle 注册

registerLifecycleHook(onBeforeMount, beforeMount)
registerLifecycleHook(onMounted, mounted)
registerLifecycleHook(onBeforeUpdate, beforeUpdate)
registerLifecycleHook(onUpdated, updated)
registerLifecycleHook(onActivated, activated)
registerLifecycleHook(onDeactivated, deactivated)
registerLifecycleHook(onErrorCaptured, errorCaptured)
registerLifecycleHook(onRenderTracked, renderTracked)
registerLifecycleHook(onRenderTriggered, renderTriggered)
registerLifecycleHook(onBeforeUnmount, beforeUnmount)
registerLifecycleHook(onUnmounted, unmounted)
registerLifecycleHook(onServerPrefetch, serverPrefetch)

小结

这一篇先把路径跑通:createApp -> mount -> render -> patch -> Options 初始化。后面再深入 patch 和响应式系统时,你会发现思路完全一致:

  1. 找入口
  2. 断点追踪
  3. 明确数据流与调用链

下一篇我会继续浏览更新渲染源码。

【2】 Zensical配置详解

zensical.toml 配置详解

全面了解 Zensical 的配置选项

image.png

配置文件格式

Zensical 项目通过 zensical.toml 文件进行配置。如果你使用 zensical new 命令创建项目,该文件会自动生成,并包含带注释的示例配置。

为什么选择 TOML?

TOML 文件格式 专门设计为易于扫描和理解。我们选择 TOML 而不是 YAML,因为它避免了 YAML 的一些问题:

  • YAML 使用缩进表示结构,这使得缩进错误特别容易出现且难以定位。在 TOML 中,空白主要是样式选择。
  • 在 YAML 中,值不需要转义,这可能导致歧义,例如 no 可能被解释为字符串或布尔值。TOML 要求所有字符串都要加引号。

从 MkDocs 过渡

为了便于从 Material for MkDocs 过渡,Zensical 可以原生读取 mkdocs.yml 配置文件。但是,我们建议新项目使用 zensical.toml 文件。

!!! info "配置文件支持" 由于 Zensical 是由 Material for MkDocs 的创建者构建的,我们支持通过 mkdocs.yml 文件进行配置,作为过渡机制,使现有项目能够平滑迁移到 Zensical。对 mkdocs.yml 的支持将始终保持,但最终会移出核心。

项目作用域

zensical.toml 配置以声明项目作用域的行开始:

[project]

目前,所有设置都包含在此作用域内。随着 Zensical 的发展,我们将引入额外的作用域,并在适当的地方将设置移出项目作用域。当然,我们会提供自动重构,因此无需手动迁移。

⚠️ 重要:配置顺序规则

在 TOML 配置文件中,配置顺序非常重要。必须遵循以下规则:

正确的配置顺序

  1. 先声明父表 [project]
  2. 然后配置所有直接属于 [project] 的键值对
    • site_name, site_url, site_description 等基本信息
    • repo_url, repo_name, edit_uri 等仓库配置
    • extra_javascript, extra_css 等额外资源
    • nav 导航配置
    • 其他所有直接属于 [project] 的配置
  3. 最后才声明子表
    • [project.theme] - 主题配置
    • [project.extra] - 额外配置
    • [project.plugins.xxx] - 插件配置
    • [project.markdown_extensions] - Markdown 扩展配置

为什么顺序很重要?

在 TOML 中,一旦声明了子表(如 [project.theme]),当前作用域就从 [project] 变成了 [project.theme]。之后的所有键值对都属于最后声明的表。

不能在声明子表后再回到父表添加键!

正确示例

[project]
# ✅ 所有父表的配置都在这里
site_name = "我的网站"
site_url = "https://example.com"
repo_url = "https://github.com/user/repo"
extra_javascript = ["script.js"]
extra_css = ["style.css"]
nav = [
    { "主页" = "index.md" },
]

# ✅ 父表配置完成后,才声明子表
[project.theme]
variant = "modern"
language = "zh"

[project.extra]
generator = true

[project.plugins.blog]
post_date_format = "full"

❌ 错误示例

[project]
site_name = "我的网站"

[project.theme]
variant = "modern"

# ❌ 错误!不能在子表之后回到父表添加键
site_url = "https://example.com"  # 这会导致解析错误!

常见错误

  1. 在子表后添加父表配置 - 会导致 TOML 解析错误
  2. 子表声明顺序混乱 - 虽然不会报错,但会让配置文件难以阅读和维护
  3. 忘记关闭父表配置 - 在声明子表前,确保所有父表配置都已完成

!!! warning "配置顺序错误会导致解析失败" 如果配置顺序不正确,Zensical 可能无法正确解析配置文件,导致构建失败。请务必遵循上述顺序规则。

主题变体

Zensical 提供两种主题变体:modern(现代)和 classic(经典),默认为 modern。classic 主题完全匹配 Material for MkDocs 的外观和感觉,而 modern 主题提供全新的设计。

如果你来自 Material for MkDocs 并希望保持其外观,或基于其外观自定义网站,可以切换到 classic 主题变体:

=== "zensical.toml"

```toml
[project.theme]
variant = "classic"
```

=== "mkdocs.yml"

```yaml
theme:
  variant: classic
```

!!! tip "自定义提示" Zensical 的 HTML 结构在两种主题变体中都与 Material for MkDocs 匹配。这意味着你现有的 CSS 和 JavaScript 自定义应该可以在任一主题变体中工作。

基础设置

让我们从最基础的配置开始,逐步构建一个完整的配置文件。

site_name

必需设置 - 提供网站名称,将显示在浏览器标签页、页面标题和导航栏中。

=== "zensical.toml"

```toml
[project]
site_name = "我的 Zensical 项目"
```

=== "mkdocs.yml"

```yaml
site_name: 我的 Zensical 项目
```

实际效果:

  • 浏览器标签页显示:我的 Zensical 项目
  • 页面标题显示:我的 Zensical 项目 - 页面名称
  • 导航栏左上角显示:我的 Zensical 项目

!!! note "关于 site_name" site_name 目前是必需的,因为 Zensical 替换的静态网站生成器 MkDocs 需要它。我们计划在未来版本中使此设置可选。

site_url

强烈推荐 - 网站的完整 URL,包括协议(http:// 或 https://)和域名。

=== "zensical.toml"

```toml
[project]
site_name = "我的 Zensical 项目"
site_url = "https://example.com"
```

=== "mkdocs.yml"

```yaml
site_name: 我的 Zensical 项目
site_url: https://example.com
```

为什么需要 site_url?

site_url 是以下功能的前提:

  1. 即时导航 - 需要知道网站的完整 URL 才能正确工作
  2. 即时预览 - 预览功能依赖正确的 URL
  3. 自定义错误页面 - 404 页面需要知道网站 URL
  4. RSS 订阅 - RSS 链接需要完整的 URL
  5. 社交分享 - 分享功能需要正确的 URL

!!! warning "重要" 如果使用即时导航功能,site_url必需的,否则即时导航将无法正常工作。

示例:

# 本地开发
site_url = "http://localhost:8000"

# GitHub Pages
site_url = "https://username.github.io"

# 自定义域名
site_url = "https://example.com"

site_description

可选 - 网站的描述,用于 SEO 和社交媒体分享。

=== "zensical.toml"

```toml
[project]
site_name = "我的 Zensical 项目"
site_url = "https://example.com"
site_description = "一个使用 Zensical 构建的文档网站"
```

=== "mkdocs.yml"

```yaml
site_name: 我的 Zensical 项目
site_url: https://example.com
site_description: 一个使用 Zensical 构建的文档网站
```

实际效果:

  • 在 HTML <meta name="description"> 标签中
  • 社交媒体分享时显示
  • 搜索引擎结果中可能显示

!!! tip "SEO 建议" 建议设置一个简洁、有吸引力的描述(50-160 个字符),有助于提高搜索引擎排名。

site_author

可选 - 网站作者名称。

=== "zensical.toml"

```toml
[project]
site_name = "我的 Zensical 项目"
site_author = "张三"
```

=== "mkdocs.yml"

```yaml
site_name: 我的 Zensical 项目
site_author: 张三
```

实际效果:

  • 在 HTML <meta name="author"> 标签中
  • 页脚可能显示作者信息(取决于主题配置)

copyright

可选 - 版权声明,显示在页面页脚。

=== "zensical.toml"

```toml
[project]
copyright = "Copyright &copy; 2025 张三"
```

=== "mkdocs.yml"

```yaml
copyright: "Copyright &copy; 2025 张三"
```

实际效果:

  • 显示在页面左下角页脚
  • 支持 HTML 标签(如 &copy; 显示为 ©)

示例:

# 纯文本
copyright = "Copyright 2025 张三"

# HTML 格式
copyright = "Copyright &copy; 2025 张三"

# 多行(使用多行字符串)
copyright = """
Copyright &copy; 2025 张三
All Rights Reserved
"""

docs_dir 和 site_dir

可选 - 文档目录和输出目录配置。

=== "zensical.toml"

```toml
[project]
docs_dir = "docs"      # 文档目录,默认:docs
site_dir = "site"      # 输出目录,默认:site
```

=== "mkdocs.yml"

```yaml
docs_dir: docs
site_dir: site
```

说明:

  • docs_dir:存放 Markdown 源文件的目录
  • site_dir:构建后生成的静态网站文件目录

!!! tip "目录结构示例" 项目根目录/ ├── docs/ # 源文件目录(docs_dir) │ ├── index.md │ └── blog/ ├── site/ # 输出目录(site_dir,运行 build 后生成) │ ├── index.html │ └── assets/ └── zensical.toml

完整的基础配置示例

以下是一个完整的基础配置示例,包含了所有推荐的基础设置:

[project]
# ===== 必需配置 =====
site_name = "我的 Zensical 项目"

# ===== 强烈推荐 =====
site_url = "https://example.com"  # 即时导航等功能需要

# ===== 推荐配置 =====
site_description = "一个使用 Zensical 构建的文档网站"
site_author = "张三"
copyright = "Copyright &copy; 2025 张三"

# ===== 目录配置(可选,有默认值)=====
docs_dir = "docs"      # 文档目录,默认:docs
site_dir = "site"      # 输出目录,默认:site
use_directory_urls = true  # 使用目录形式的 URL,默认:true

!!! tip "验证配置" 配置完成后,运行以下命令验证:

```bash
# 启动开发服务器
zensical serve

# 检查配置是否正确
zensical build
```

如果配置有误,会显示具体的错误信息。

extra

extra 配置选项用于存储模板使用的任意键值对。如果你覆盖模板,可以使用这些值来自定义行为。

=== "zensical.toml"

```toml
[project.extra]
key = "value"
analytics = "UA-XXXXXXXX-X"
```

=== "mkdocs.yml"

```yaml
extra:
  key: value
  analytics: UA-XXXXXXXX-X
```

use_directory_urls

控制文档网站的目录结构,从而控制用于链接到页面的 URL 格式。

=== "zensical.toml"

```toml
[project]
use_directory_urls = true  # 默认值
```

=== "mkdocs.yml"

```yaml
use_directory_urls: true
```

!!! info "离线使用" 在构建离线使用时,此选项会自动设置为 false,以便可以从本地文件系统浏览文档,而无需 Web 服务器。

主题配置

language

设置网站的语言。

=== "zensical.toml"

```toml
[project.theme]
language = "zh"  # 中文
```

=== "mkdocs.yml"

```yaml
theme:
  language: zh
```

features

启用或禁用主题功能。这是一个数组,可以同时启用多个功能。

配置示例:

[project.theme]
features = [
    # 导航相关
    "navigation.instant",           # 即时导航(推荐)
    "navigation.instant.prefetch",  # 预加载(推荐,提升性能)
    "navigation.tracking",          # 锚点跟踪
    "navigation.tabs",              # 导航标签
    "navigation.sections",          # 导航部分
    "navigation.top",               # 返回顶部按钮
    
    # 搜索相关
    "search.suggest",               # 搜索建议
    "search.highlight",             # 搜索高亮
    
    # 内容相关
    "content.code.copy",            # 代码复制按钮(推荐)
]

常用功能说明:

功能 说明 推荐
navigation.instant 即时导航,无需刷新页面 ✅ 强烈推荐
navigation.instant.prefetch 预加载链接,提升性能 ✅ 推荐
navigation.tracking URL 自动更新为当前锚点 ✅ 推荐
navigation.tabs 一级导航显示为顶部标签 ✅ 推荐
navigation.top 返回顶部按钮 ✅ 推荐
search.suggest 搜索时显示建议 ✅ 推荐
content.code.copy 代码块复制按钮 ✅ 强烈推荐

!!! warning "即时导航需要 site_url" 如果启用 navigation.instant,必须设置 site_url,否则即时导航将无法正常工作。

=== "zensical.toml"

```toml
[project.theme]
features = [
    "navigation.instant",
    "navigation.instant.prefetch",
    "navigation.tracking",
    "navigation.tabs",
    "navigation.top",
    "search.suggest",
    "content.code.copy",
]
```

=== "mkdocs.yml"

```yaml
theme:
  features:
    - navigation.instant
    - navigation.instant.prefetch
    - navigation.tracking
    - navigation.tabs
    - navigation.top
    - search.suggest
    - content.code.copy
```

palette

配置颜色主题,支持明暗模式切换。

基础配置示例:

# 日间模式
[[project.theme.palette]]
media = "(prefers-color-scheme: light)"
scheme = "default"
primary = "indigo"    # 主色调
accent = "indigo"     # 强调色

# 夜间模式
[[project.theme.palette]]
media = "(prefers-color-scheme: dark)"
scheme = "slate"
primary = "indigo"
accent = "indigo"

完整配置示例(包含自动模式):

# 自动模式(跟随系统)
[[project.theme.palette]]
media = "(prefers-color-scheme)"
toggle = { icon = "material/link", name = "关闭自动模式" }

# 日间模式
[[project.theme.palette]]
media = "(prefers-color-scheme: light)"
scheme = "default"
primary = "indigo"
accent = "indigo"
toggle = { icon = "material/toggle-switch", name = "切换至夜间模式" }

# 夜间模式
[[project.theme.palette]]
media = "(prefers-color-scheme: dark)"
scheme = "slate"
primary = "indigo"
accent = "indigo"
toggle = { icon = "material/toggle-switch-off-outline", name = "切换至日间模式" }

支持的主色调:

  • red, pink, purple, deep-purple
  • indigo(推荐), blue, light-blue, cyan
  • teal, green, light-green, lime
  • yellow, amber, orange, deep-orange
  • brown, grey, blue-grey, black, white

!!! tip "选择颜色" - indigoblue 是最常用的主色调 - primary 影响导航栏、链接等主要元素 - accent 影响按钮、高亮等强调元素

=== "zensical.toml"

```toml
[[project.theme.palette]]
media = "(prefers-color-scheme: light)"
scheme = "default"
primary = "indigo"
accent = "indigo"

[[project.theme.palette]]
media = "(prefers-color-scheme: dark)"
scheme = "slate"
primary = "indigo"
accent = "indigo"
```

=== "mkdocs.yml"

```yaml
theme:
  palette:
    - scheme: default
      primary: indigo
      accent: indigo
    - scheme: slate
      primary: indigo
      accent: indigo
```

font

配置字体。

=== "zensical.toml"

```toml
[project.theme.font]
text = "Roboto"
code = "Roboto Mono"
```

=== "mkdocs.yml"

```yaml
theme:
  font:
    text: Roboto
    code: Roboto Mono
```

logo 和 favicon

设置网站 logo 和 favicon。

=== "zensical.toml"

```toml
[project.theme]
logo = "assets/logo.png"
favicon = "assets/favicon.png"
```

=== "mkdocs.yml"

```yaml
theme:
  logo: assets/logo.png
  favicon: assets/favicon.png
```

插件配置

博客插件

=== "zensical.toml"

```toml
[project.plugins.blog]
post_date_format = "full"
post_url_format = "{date}/{slug}"
post_readtime = true
post_readtime_words_per_minute = 265
draft = true
```

=== "mkdocs.yml"

```yaml
plugins:
  - blog:
      enabled: true
      blog_dir: blog
      post_date_format: full
      post_url_format: "{date}/{slug}"
      post_readtime: true
      post_readtime_words_per_minute: 265
      draft: true
```

搜索插件

=== "zensical.toml"

```toml
[project.plugins.search]
lang = ["zh", "en"]
separator = '[\s\-\.]+'  # 中文优化:'[\s\u200b\-]'
```

=== "mkdocs.yml"

```yaml
plugins:
  - search:
      enabled: true
      lang:
        - zh
        - en
      separator: '[\s\-\.]+'
```

标签插件

=== "zensical.toml"

```toml
[project.plugins.tags]
tags_file = "tags.md"
```

=== "mkdocs.yml"

```yaml
plugins:
  - tags:
      enabled: true
      tags_file: tags.md
```

导航配置

nav

定义网站的导航结构。

基本用法

=== "zensical.toml"

```toml
[project]
nav = [    { "主页" = "index.md" },    { "快速开始" = "quick-start.md" },    { "配置" = "configuration.md" },]
```

=== "mkdocs.yml"

```yaml
nav:
  - 主页: index.md
  - 快速开始: quick-start.md
  - 配置: configuration.md
```

实际效果:

  • 导航栏显示三个顶级菜单项
  • 点击后跳转到对应页面
嵌套导航(导航分组)

创建多层级的导航结构,将相关页面组织在一起:

=== "zensical.toml"

```toml
[project]
nav = [    { "主页" = "index.md" },    { "快速开始" = [        { "5 分钟快速开始" = "getting-started/quick-start.md" },        { "从 MkDocs 迁移" = "getting-started/migration.md" },    ] },
    { "核心教程" = [        { "配置详解" = "tutorials/configuration.md" },        { "主题定制" = "tutorials/theme-customization.md" },    ] },
]
```

=== "mkdocs.yml"

```yaml
nav:
  - 主页: index.md
  - 快速开始:
      - 5 分钟快速开始: getting-started/quick-start.md
      - 从 MkDocs 迁移: getting-started/migration.md
  - 核心教程:
      - 配置详解: tutorials/configuration.md
      - 主题定制: tutorials/theme-customization.md
```

实际效果:

  • "快速开始" 和 "核心教程" 显示为可展开的分组
  • 点击分组名称展开子菜单
  • 子菜单项点击后跳转到对应页面
外部链接

导航项也可以指向外部 URL,任何无法解析为 Markdown 文件的字符串都会被当作 URL 处理:

=== "zensical.toml"

```toml
[project]
nav = [    { "主页" = "index.md" },    { "GitHub 仓库" = "https://github.com/zensical/zensical" },    { "个人博客" = "https://wcowin.work/" },]
```

=== "mkdocs.yml"

```yaml
nav:
  - 主页: index.md
  - GitHub 仓库: https://github.com/zensical/zensical
  - 个人博客: https://wcowin.work/
```

实际效果:

  • 外部链接在新标签页中打开
  • 可以混合使用内部页面和外部链接
完整配置示例

本教程实际使用的完整导航配置:

[project]
nav = [
    { "主页" = "index.md" },
    { "快速开始" = [
        { "5 分钟快速开始" = "getting-started/quick-start.md" },
        { "从 MkDocs 迁移" = "getting-started/migration.md" },
    ] },
    { "核心教程" = [
        { "zensical.toml 配置详解" = "tutorials/configuration.md" },
        { "主题定制指南" = "tutorials/theme-customization.md" },
        { "Markdown 扩展使用" = "tutorials/markdown-extensions.md" },
        { "Zensical 博客系统完全指南" = "tutorials/blog-tutorial.md" },
    ] },
    { "插件系统" = [
        { "博客插件详解" = "blog/plugins/blog.md" },
        { "搜索插件配置" = "blog/plugins/search.md" },
        { "标签插件使用" = "blog/plugins/tags.md" },
        { "RSS 插件配置" = "blog/plugins/rss.md" },
    ] },
    { "部署指南" = [
        { "GitHub Pages 部署(推荐)" = "blog/deployment/github-pages.md" },
        { "Netlify 部署" = "blog/deployment/netlify.md" },
        { "GitLab Pages 部署" = "blog/deployment/gitlab-pages.md" },
        { "自托管部署" = "blog/deployment/self-hosted.md" },
    ] },
    { "高级主题" = [
        { "性能优化" = "blog/advanced/performance.md" },
        { "SEO 优化" = "blog/advanced/seo.md" },
        { "多语言支持" = "blog/advanced/i18n.md" },
        { "自定义 404 页面" = "blog/advanced/custom-404.md" },
        { "自定义字体" = "blog/advanced/custom-fonts.md" },
        { "添加评论系统" = "blog/advanced/comment-system.md" },
    ] },
    { "常见问题" = "faq.md" },
    { "案例展示" = "showcase.md" },
    { "关于" = "about.md" },
    { "个人博客" = "https://wcowin.work/" },
]

!!! tip "导航配置技巧" - 路径相对于 docs_dir:所有文件路径都相对于 docs 目录 - 自动提取标题:如果不指定标题,Zensical 会自动从文件中提取 - 嵌套层级:支持多层嵌套,但建议不超过 3 层以保持导航清晰 - 外部链接:URL 会在新标签页中打开,内部链接在当前页面打开 - 数组格式:使用 nav = [...] 格式,结构清晰,易于维护

Markdown 扩展

Zensical 支持丰富的 Markdown 扩展,这些扩展基于官方推荐配置,提供了强大的文档编写能力。

官方推荐配置(完整版)

以下配置是 Zensical 官方推荐的完整 Markdown 扩展配置,包含了所有常用功能:

=== "zensical.toml"

```toml
# ===== 基础扩展 =====
[project.markdown_extensions.abbr]          # 缩写支持
[project.markdown_extensions.admonition]    # 警告框(!!! note)
[project.markdown_extensions.attr_list]     # 属性列表
[project.markdown_extensions.def_list]      # 定义列表
[project.markdown_extensions.footnotes]     # 脚注支持
[project.markdown_extensions.md_in_html]    # HTML 中使用 Markdown
[project.markdown_extensions.toc]           # 目录生成
toc_depth = 3                               # 目录深度
permalink = true                            # 标题锚点链接

# ===== 数学公式支持 =====
[project.markdown_extensions."pymdownx.arithmatex"]
generic = true  # 使用 MathJax 渲染数学公式

# ===== 文本增强 =====
[project.markdown_extensions."pymdownx.betterem"]
smart_enable = "all"  # 智能斜体/粗体

[project.markdown_extensions."pymdownx.caret"]      # 上标 (^text^)
[project.markdown_extensions."pymdownx.mark"]      # 标记文本 (==text==)
[project.markdown_extensions."pymdownx.tilde"]     # 删除线 (~~text~~)

# ===== 交互元素 =====
[project.markdown_extensions."pymdownx.details"]   # 可折叠详情框
[project.markdown_extensions."pymdownx.tabbed"]    # 标签页
alternate_style = true
[project.markdown_extensions."pymdownx.tasklist"]  # 任务列表
custom_checkbox = true

# ===== 代码相关 =====
[project.markdown_extensions."pymdownx.highlight"]     # 代码高亮
[project.markdown_extensions."pymdownx.inlinehilite"] # 行内代码高亮
[project.markdown_extensions."pymdownx.superfences"]  # 代码块和 Mermaid

# ===== 其他功能 =====
[project.markdown_extensions."pymdownx.keys"]         # 键盘按键 (++ctrl+alt+del++)
[project.markdown_extensions."pymdownx.smartsymbols"]  # 智能符号转换
[project.markdown_extensions."pymdownx.emoji"]        # Emoji 表情
emoji_generator = "zensical.extensions.emoji.to_svg"
emoji_index = "zensical.extensions.emoji.twemoji"
```

=== "mkdocs.yml"

```yaml
markdown_extensions:
  # 基础扩展
  - abbr
  - admonition
  - attr_list
  - def_list
  - footnotes
  - md_in_html
  - toc:
      permalink: true
      toc_depth: 3
  
  # PyMdown 扩展
  - pymdownx.arithmatex:
      generic: true
  - pymdownx.betterem:
      smart_enable: all
  - pymdownx.caret
  - pymdownx.details
  - pymdownx.emoji:
      emoji_generator: zensical.extensions.emoji.to_svg
      emoji_index: zensical.extensions.emoji.twemoji
  - pymdownx.highlight
  - pymdownx.inlinehilite
  - pymdownx.keys
  - pymdownx.mark
  - pymdownx.smartsymbols
  - pymdownx.superfences
  - pymdownx.tabbed:
      alternate_style: true
  - pymdownx.tasklist:
      custom_checkbox: true
  - pymdownx.tilde
```

扩展功能说明

基础扩展
扩展 功能 示例
abbr 缩写支持 <abbr title="HyperText Markup Language">HTML</abbr>
admonition 警告框 !!! note "提示"
attr_list 属性列表 {: .class-name }
def_list 定义列表 术语 : 定义
footnotes 脚注 [^1][^1]: 脚注内容
md_in_html HTML 中使用 Markdown <div markdown="1">**粗体**</div>
toc 自动生成目录 自动生成页面目录
PyMdown 扩展
扩展 功能 示例
pymdownx.arithmatex 数学公式 $E=mc^2$$$\int_0^\infty$$
pymdownx.betterem 智能斜体/粗体 自动处理 *text***text**
pymdownx.caret 上标 ^text^text
pymdownx.details 可折叠详情 ??? note "点击展开"
pymdownx.emoji Emoji 表情 :smile: → 😄
pymdownx.highlight 代码高亮 语法高亮的代码块
pymdownx.inlinehilite 行内代码高亮 `code`
pymdownx.keys 键盘按键 ++ctrl+alt+del++
pymdownx.mark 标记文本 ==text==text
pymdownx.smartsymbols 智能符号 (c) → ©, (tm) → ™
pymdownx.superfences 代码块和 Mermaid 支持代码块和流程图
pymdownx.tabbed 标签页 === "标签1"
pymdownx.tasklist 任务列表 - [ ] 未完成 / - [x] 已完成
pymdownx.tilde 删除线 ~~text~~text

常用配置示例

最小配置(仅基础功能)
[project.markdown_extensions]
admonition = {}           # 警告框
attr_list = {}            # 属性列表
md_in_html = {}           # HTML 中使用 Markdown
tables = {}               # 表格支持
推荐配置(常用功能)
[project.markdown_extensions]
admonition = {}
attr_list = {}
md_in_html = {}
toc = { permalink = true, toc_depth = 3 }

[project.markdown_extensions."pymdownx.highlight"]
[project.markdown_extensions."pymdownx.superfences"]
[project.markdown_extensions."pymdownx.tabbed"]
alternate_style = true
[project.markdown_extensions."pymdownx.tasklist"]
custom_checkbox = true
[project.markdown_extensions."pymdownx.emoji"]
emoji_generator = "zensical.extensions.emoji.to_svg"
emoji_index = "zensical.extensions.emoji.twemoji"

实际使用示例

警告框(Admonition)
!!! note "提示"
    这是一个提示框

!!! warning "警告"
    这是一个警告框

!!! tip "技巧"
    这是一个技巧提示
代码高亮(Highlight)
```python
def hello():
    print("Hello, Zensical!")
```
标签页(Tabbed)
=== "Python"
    ```python
    print("Hello")
    ```

=== "JavaScript"
    ```javascript
    console.log("Hello");
    ```
任务列表(Tasklist)
- [x] 已完成的任务
- [ ] 未完成的任务
数学公式(Arithmatex)
行内公式:$E=mc^2$

块级公式:
$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$
Emoji 表情
:smile: :heart: :rocket: :thumbsup:

!!! tip "更多示例" 详细的使用示例和说明请参考 Markdown 扩展使用指南

完整配置示例

以下是一个完整的、生产环境可用的配置示例,包含了所有常用配置:

[project]
# ===== 基本信息 =====
site_name = "我的 Zensical 项目"
site_url = "https://example.com"
site_description = "一个使用 Zensical 构建的文档网站"
site_author = "张三"
copyright = "Copyright &copy; 2025 张三"

# ===== 目录配置 =====
docs_dir = "docs"
site_dir = "site"
use_directory_urls = true

# ===== 仓库配置 =====
repo_url = "https://github.com/username/repo"
repo_name = "repo"
edit_uri = "edit/main/docs"

# ===== 额外资源 =====
extra_javascript = [
    "javascripts/extra.js",
]
extra_css = [
    "stylesheets/extra.css",
]

# ===== 导航配置 =====
nav = [
    { "主页" = "index.md" },
    { "快速开始" = [
        { "5 分钟快速开始" = "getting-started/quick-start.md" },
        { "从 MkDocs 迁移" = "getting-started/migration.md" },
    ] },
    { "核心教程" = [
        { "配置详解" = "tutorials/configuration.md" },
        { "主题定制" = "tutorials/theme-customization.md" },
        { "Markdown 扩展" = "tutorials/markdown-extensions.md" },
        { "博客系统指南" = "tutorials/blog-tutorial.md" },
    ] },
    { "常见问题" = "faq.md" },
    { "个人博客" = "https://wcowin.work/" },
]

# ===== 主题配置 =====
[project.theme]
variant = "modern"
language = "zh"
logo = "assets/logo.svg"
favicon = "assets/favicon.png"

features = [
    "navigation.instant",
    "navigation.instant.prefetch",
    "navigation.tracking",
    "navigation.tabs",
    "navigation.sections",
    "navigation.top",
    "search.suggest",
    "search.highlight",
    "content.code.copy",
]

# 日间模式
[[project.theme.palette]]
media = "(prefers-color-scheme: light)"
scheme = "default"
primary = "indigo"
accent = "indigo"

# 夜间模式
[[project.theme.palette]]
media = "(prefers-color-scheme: dark)"
scheme = "slate"
primary = "indigo"
accent = "indigo"

[project.theme.font]
text = "Roboto"
code = "Roboto Mono"

# ===== 插件配置 =====
[project.plugins.blog]
post_date_format = "full"
post_readtime = true
post_readtime_words_per_minute = 265
draft = true

[project.plugins.search]
lang = ["zh", "en"]
separator = '[\s\u200b\-]'

[project.plugins.tags]

# ===== Markdown 扩展配置 =====
[project.markdown_extensions.abbr]
[project.markdown_extensions.admonition]
[project.markdown_extensions.attr_list]
[project.markdown_extensions.def_list]
[project.markdown_extensions.footnotes]
[project.markdown_extensions.md_in_html]
[project.markdown_extensions.toc]
toc_depth = 3
permalink = true

[project.markdown_extensions."pymdownx.arithmatex"]
generic = true

[project.markdown_extensions."pymdownx.betterem"]
smart_enable = "all"

[project.markdown_extensions."pymdownx.caret"]
[project.markdown_extensions."pymdownx.details"]
[project.markdown_extensions."pymdownx.emoji"]
emoji_generator = "zensical.extensions.emoji.to_svg"
emoji_index = "zensical.extensions.emoji.twemoji"

[project.markdown_extensions."pymdownx.highlight"]
[project.markdown_extensions."pymdownx.inlinehilite"]
[project.markdown_extensions."pymdownx.keys"]
[project.markdown_extensions."pymdownx.mark"]
[project.markdown_extensions."pymdownx.smartsymbols"]
[project.markdown_extensions."pymdownx.superfences"]
[project.markdown_extensions."pymdownx.tabbed"]
alternate_style = true
[project.markdown_extensions."pymdownx.tasklist"]
custom_checkbox = true
[project.markdown_extensions."pymdownx.tilde"]

!!! tip "配置验证" 配置完成后,建议运行以下命令验证:

```bash
# 检查配置语法
zensical build

# 启动开发服务器查看效果
zensical serve
```

下一步


参考资料

Options API 与 Composition API 对照表

Options API 与 Composition API 对照表

学习目标

完成本章学习后,你将能够:

  • 理解Options API和Composition API的核心区别
  • 快速将Options API代码转换为Composition API
  • 根据项目需求选择合适的API风格
  • 理解两种API风格的优缺点和适用场景

前置知识

学习本章内容前,你需要掌握:

问题引入

实际场景

小李是一名前端开发者,公司的老项目使用Vue 2和Options API编写。现在公司决定将项目升级到Vue 3,并逐步迁移到Composition API。小李需要:

  1. 理解两种API的对应关系:如何将Options API的data、methods、computed等转换为Composition API?
  2. 保持功能一致性:迁移后的代码需要保持原有功能不变
  3. 利用新特性:在迁移过程中,如何利用Composition API的优势改进代码?
  4. 团队协作:如何让团队成员快速理解两种API的区别?

为什么需要这个对照表

Options API和Composition API是Vue提供的两种不同的组件编写方式:

  • Options API:Vue 2的传统写法,通过配置对象(data、methods、computed等)组织代码
  • Composition API:Vue 3引入的新写法,通过组合函数的方式组织代码,提供更好的逻辑复用和类型推导

这个对照表将帮助你:

  1. 快速找到Options API在Composition API中的对应写法
  2. 理解两种API风格的设计思想差异
  3. 顺利完成项目迁移和代码重构
  4. 在新项目中做出合适的技术选择

核心概念

概念1:API风格对比

Options API特点

Options API通过配置对象的方式组织代码,每个选项负责特定的功能:

export default {
  data() {
    return {
      // 响应式数据
    }
  },
  computed: {
    // 计算属性
  },
  methods: {
    // 方法
  },
  mounted() {
    // 生命周期钩子
  }
}

优点

  • 结构清晰,容易理解
  • 适合小型组件
  • 学习曲线平缓

缺点

  • 逻辑分散在不同选项中
  • 难以复用逻辑
  • TypeScript支持较弱
Composition API特点

Composition API通过组合函数的方式组织代码,相关逻辑可以放在一起:

<script setup>
import { ref, computed, onMounted } from 'vue';

// 所有逻辑都在setup中,可以按功能组织
const count = ref(0);
const doubled = computed(() => count.value * 2);

onMounted(() => {
  // 生命周期逻辑
});
</script>

优点

  • 逻辑复用更容易(组合式函数)
  • 更好的TypeScript支持
  • 更灵活的代码组织
  • 更好的tree-shaking

缺点

  • 学习曲线较陡
  • 需要理解ref和reactive的区别
  • 代码可能不如Options API结构化

概念2:核心API对照

下面是两种API风格的完整对照表:

完整对照表

1. 响应式数据

Options API Composition API 说明
data() ref() / reactive() 定义响应式数据

Options API示例:

<script>
export default {
  data() {
    return {
      count: 0,
      user: {
        name: '张三',
        age: 25
      }
    }
  }
}
</script>

Composition API示例:

<script setup>
import { ref, reactive } from 'vue';

// 使用ref定义基本类型
const count = ref(0);

// 使用reactive定义对象
const user = reactive({
  name: '张三',
  age: 25
});

// 注意:访问ref需要.value,reactive不需要
console.log(count.value); // 0
console.log(user.name);   // '张三'
</script>

2. 计算属性

Options API Composition API 说明
computed computed() 定义计算属性

Options API示例:

<script>
export default {
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    // 只读计算属性
    fullName() {
      return this.firstName + this.lastName;
    },
    // 可写计算属性
    reversedName: {
      get() {
        return this.lastName + this.firstName;
      },
      set(value) {
        this.lastName = value[0];
        this.firstName = value.slice(1);
      }
    }
  }
}
</script>

Composition API示例:

<script setup>
import { ref, computed } from 'vue';

const firstName = ref('张');
const lastName = ref('三');

// 只读计算属性
const fullName = computed(() => {
  return firstName.value + lastName.value;
});

// 可写计算属性
const reversedName = computed({
  get() {
    return lastName.value + firstName.value;
  },
  set(value) {
    lastName.value = value[0];
    firstName.value = value.slice(1);
  }
});
</script>

3. 方法

Options API Composition API 说明
methods 普通函数 定义方法

Options API示例:

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
    reset() {
      this.count = 0;
    }
  }
}
</script>

Composition API示例:

<script setup>
import { ref } from 'vue';

const count = ref(0);

// 直接定义函数,不需要methods选项
const increment = () => {
  count.value++;
};

const decrement = () => {
  count.value--;
};

const reset = () => {
  count.value = 0;
};
</script>

4. 侦听器

Options API Composition API 说明
watch watch() / watchEffect() 侦听数据变化

Options API示例:

<script>
export default {
  data() {
    return {
      question: '',
      answer: '请输入问题'
    }
  },
  watch: {
    // 简单侦听
    question(newValue, oldValue) {
      console.log(`问题从 "${oldValue}" 变为 "${newValue}"`);
      this.getAnswer();
    },
    // 深度侦听
    user: {
      handler(newValue, oldValue) {
        console.log('用户信息变化');
      },
      deep: true,
      immediate: true
    }
  },
  methods: {
    getAnswer() {
      this.answer = '正在思考...';
    }
  }
}
</script>

Composition API示例:

<script setup>
import { ref, watch, watchEffect } from 'vue';

const question = ref('');
const answer = ref('请输入问题');
const user = ref({ name: '张三', age: 25 });

// 使用watch侦听特定数据源
watch(question, (newValue, oldValue) => {
  console.log(`问题从 "${oldValue}" 变为 "${newValue}"`);
  getAnswer();
});

// 深度侦听对象
watch(user, (newValue, oldValue) => {
  console.log('用户信息变化');
}, {
  deep: true,
  immediate: true
});

// 使用watchEffect自动追踪依赖
watchEffect(() => {
  // 自动追踪question的变化
  console.log(`当前问题:${question.value}`);
});

const getAnswer = () => {
  answer.value = '正在思考...';
};
</script>

5. 生命周期钩子

Options API Composition API 说明
beforeCreate - 使用setup()替代
created - 使用setup()替代
beforeMount onBeforeMount() 挂载前
mounted onMounted() 挂载后
beforeUpdate onBeforeUpdate() 更新前
updated onUpdated() 更新后
beforeUnmount onBeforeUnmount() 卸载前
unmounted onUnmounted() 卸载后
errorCaptured onErrorCaptured() 错误捕获
activated onActivated() keep-alive激活
deactivated onDeactivated() keep-alive停用

Options API示例:

<script>
export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  beforeCreate() {
    console.log('beforeCreate: 实例初始化之后');
  },
  created() {
    console.log('created: 实例创建完成');
    // 可以访问this.message
  },
  beforeMount() {
    console.log('beforeMount: 挂载开始之前');
  },
  mounted() {
    console.log('mounted: 挂载完成');
    // 可以访问DOM
  },
  beforeUpdate() {
    console.log('beforeUpdate: 数据更新前');
  },
  updated() {
    console.log('updated: 数据更新后');
  },
  beforeUnmount() {
    console.log('beforeUnmount: 卸载前');
  },
  unmounted() {
    console.log('unmounted: 卸载完成');
    // 清理定时器、事件监听等
  }
}
</script>

Composition API示例:

<script setup>
import { 
  ref, 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue';

const message = ref('Hello');

// setup()本身就相当于beforeCreate和created
console.log('setup执行,相当于created');

onBeforeMount(() => {
  console.log('onBeforeMount: 挂载开始之前');
});

onMounted(() => {
  console.log('onMounted: 挂载完成');
  // 可以访问DOM
});

onBeforeUpdate(() => {
  console.log('onBeforeUpdate: 数据更新前');
});

onUpdated(() => {
  console.log('onUpdated: 数据更新后');
});

onBeforeUnmount(() => {
  console.log('onBeforeUnmount: 卸载前');
});

onUnmounted(() => {
  console.log('onUnmounted: 卸载完成');
  // 清理定时器、事件监听等
});
</script>

6. Props

Options API Composition API 说明
props defineProps() 定义组件属性

Options API示例:

<script>
export default {
  props: {
    // 简单声明
    title: String,
    // 详细声明
    count: {
      type: Number,
      required: true,
      default: 0,
      validator(value) {
        return value >= 0;
      }
    },
    user: {
      type: Object,
      default: () => ({ name: '匿名' })
    }
  },
  mounted() {
    // 通过this访问props
    console.log(this.title);
    console.log(this.count);
  }
}
</script>

Composition API示例:

<script setup>
import { computed } from 'vue';

// 简单声明
// const props = defineProps(['title', 'count']);

// 详细声明(推荐)
const props = defineProps({
  title: String,
  count: {
    type: Number,
    required: true,
    default: 0,
    validator(value) {
      return value >= 0;
    }
  },
  user: {
    type: Object,
    default: () => ({ name: '匿名' })
  }
});

// TypeScript类型声明(更推荐)
// const props = defineProps<{
//   title?: string;
//   count: number;
//   user?: { name: string };
// }>();

// 直接访问props,不需要this
console.log(props.title);
console.log(props.count);

// props是响应式的,可以在computed中使用
const doubledCount = computed(() => props.count * 2);
</script>

7. Emits(事件)

Options API Composition API 说明
emits + $emit defineEmits() 定义和触发事件

Options API示例:

<script>
export default {
  emits: ['update', 'delete'],
  // 或者详细声明
  emits: {
    update: (value) => {
      // 验证事件参数
      return typeof value === 'string';
    },
    delete: null
  },
  methods: {
    handleClick() {
      // 触发事件
      this.$emit('update', 'new value');
    },
    handleDelete() {
      this.$emit('delete');
    }
  }
}
</script>

Composition API示例:

<script setup>
// 简单声明
// const emit = defineEmits(['update', 'delete']);

// 详细声明(推荐)
const emit = defineEmits({
  update: (value) => {
    // 验证事件参数
    return typeof value === 'string';
  },
  delete: null
});

// TypeScript类型声明(更推荐)
// const emit = defineEmits<{
//   update: [value: string];
//   delete: [];
// }>();

const handleClick = () => {
  // 触发事件
  emit('update', 'new value');
};

const handleDelete = () => {
  emit('delete');
};
</script>

8. 插槽

Options API Composition API 说明
$slots useSlots() 访问插槽

Options API示例:

<script>
export default {
  mounted() {
    // 检查插槽是否存在
    if (this.$slots.default) {
      console.log('有默认插槽内容');
    }
    if (this.$slots.header) {
      console.log('有header插槽内容');
    }
  }
}
</script>

<template>
  <div>
    <header v-if="$slots.header">
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
  </div>
</template>

Composition API示例:

<script setup>
import { useSlots, onMounted } from 'vue';

// 获取插槽对象
const slots = useSlots();

onMounted(() => {
  // 检查插槽是否存在
  if (slots.default) {
    console.log('有默认插槽内容');
  }
  if (slots.header) {
    console.log('有header插槽内容');
  }
});
</script>

<template>
  <div>
    <header v-if="slots.header">
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
  </div>
</template>

9. Refs(模板引用)

Options API Composition API 说明
$refs ref() 访问DOM或组件实例

Options API示例:

<script>
export default {
  mounted() {
    // 访问DOM元素
    console.log(this.$refs.input);
    this.$refs.input.focus();
    
    // 访问子组件实例
    console.log(this.$refs.child);
    this.$refs.child.someMethod();
  }
}
</script>

<template>
  <div>
    <input ref="input" />
    <ChildComponent ref="child" />
  </div>
</template>

Composition API示例:

<script setup>
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';

// 创建ref,变量名必须与模板中的ref属性值相同
const input = ref(null);
const child = ref(null);

onMounted(() => {
  // 访问DOM元素
  console.log(input.value);
  input.value.focus();
  
  // 访问子组件实例
  console.log(child.value);
  child.value.someMethod();
});
</script>

<template>
  <div>
    <input ref="input" />
    <ChildComponent ref="child" />
  </div>
</template>

10. 暴露组件方法

Options API Composition API 说明
自动暴露 defineExpose() 暴露组件内部方法给父组件

Options API示例:

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    },
    getCount() {
      return this.count;
    }
  }
  // Options API中,所有methods和data都会自动暴露给父组件
}
</script>

Composition API示例:

<script setup>
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
  count.value++;
};

const getCount = () => {
  return count.value;
};

// Composition API中,默认不暴露任何内容
// 需要使用defineExpose显式暴露
defineExpose({
  increment,
  getCount
  // 注意:通常不暴露响应式数据本身
});
</script>

11. Provide / Inject(依赖注入)

Options API Composition API 说明
provide / inject provide() / inject() 跨层级组件通信

Options API示例:

<!-- 祖先组件 -->
<script>
export default {
  data() {
    return {
      theme: 'dark'
    }
  },
  provide() {
    return {
      theme: this.theme,
      // 注意:这样提供的值不是响应式的
      updateTheme: this.updateTheme
    }
  },
  methods: {
    updateTheme(newTheme) {
      this.theme = newTheme;
    }
  }
}
</script>

<!-- 后代组件 -->
<script>
export default {
  inject: ['theme', 'updateTheme'],
  mounted() {
    console.log(this.theme); // 'dark'
    this.updateTheme('light');
  }
}
</script>

Composition API示例:

<!-- 祖先组件 -->
<script setup>
import { ref, provide } from 'vue';

const theme = ref('dark');

const updateTheme = (newTheme) => {
  theme.value = newTheme;
};

// 提供响应式数据
provide('theme', theme);
provide('updateTheme', updateTheme);
</script>

<!-- 后代组件 -->
<script setup>
import { inject, onMounted } from 'vue';

// 注入数据,可以提供默认值
const theme = inject('theme', 'light');
const updateTheme = inject('updateTheme');

onMounted(() => {
  console.log(theme.value); // 'dark'
  updateTheme('light');
});
</script>

12. Mixins(混入)

Options API Composition API 说明
mixins 组合式函数 逻辑复用

Options API示例:

// mixins/logger.js
export const loggerMixin = {
  data() {
    return {
      logCount: 0
    }
  },
  methods: {
    log(message) {
      console.log(message);
      this.logCount++;
    }
  },
  mounted() {
    console.log('Logger mixin mounted');
  }
};

// 使用mixin
export default {
  mixins: [loggerMixin],
  mounted() {
    this.log('组件已挂载');
    console.log(`日志次数:${this.logCount}`);
  }
}

Composition API示例:

// composables/useLogger.js
import { ref, onMounted } from 'vue';

export function useLogger() {
  const logCount = ref(0);
  
  const log = (message) => {
    console.log(message);
    logCount.value++;
  };
  
  onMounted(() => {
    console.log('Logger composable mounted');
  });
  
  return {
    logCount,
    log
  };
}

// 使用组合式函数
import { useLogger } from './composables/useLogger';

const { logCount, log } = useLogger();

onMounted(() => {
  log('组件已挂载');
  console.log(`日志次数:${logCount.value}`);
});

最佳实践

企业级应用场景

场景1:大型组件迁移策略

在企业级项目中,通常不会一次性将所有组件从Options API迁移到Composition API。推荐采用渐进式迁移策略:

<!-- 步骤1:保持Options API,先熟悉Composition API -->
<script>
export default {
  // 保持原有Options API代码
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}
</script>

<!-- 步骤2:混合使用,逐步迁移 -->
<script>
import { ref, computed } from 'vue';

export default {
  // 新功能使用Composition API
  setup() {
    const newFeature = ref('');
    const processedFeature = computed(() => newFeature.value.toUpperCase());
    
    return {
      newFeature,
      processedFeature
    };
  },
  // 旧功能保持Options API
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}
</script>

<!-- 步骤3:完全迁移到Composition API -->
<script setup>
import { ref, computed } from 'vue';

// 所有逻辑都使用Composition API
const count = ref(0);
const message = ref('Hello');
const newFeature = ref('');

const processedFeature = computed(() => newFeature.value.toUpperCase());

const increment = () => {
  count.value++;
};
</script>

迁移建议

  1. 从小型、独立的组件开始迁移
  2. 优先迁移新功能和新组件
  3. 对于复杂组件,可以先混合使用
  4. 充分测试迁移后的功能
  5. 团队成员需要先学习Composition API基础
场景2:逻辑复用最佳实践

Composition API的最大优势是逻辑复用。下面是一个完整的企业级示例:

// composables/useUserManagement.js
/**
 * 用户管理组合式函数
 * 封装用户相关的所有逻辑,包括获取、更新、删除等操作
 */
import { ref, computed, onMounted } from 'vue';
import { userApi } from '@/api/user';

export function useUserManagement() {
  // 响应式状态
  const users = ref([]);
  const loading = ref(false);
  const error = ref(null);
  const currentPage = ref(1);
  const pageSize = ref(10);
  
  // 计算属性
  const totalPages = computed(() => {
    return Math.ceil(users.value.length / pageSize.value);
  });
  
  const paginatedUsers = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value;
    const end = start + pageSize.value;
    return users.value.slice(start, end);
  });
  
  // 方法
  const fetchUsers = async () => {
    loading.value = true;
    error.value = null;
    try {
      const response = await userApi.getUsers();
      users.value = response.data;
    } catch (err) {
      error.value = err.message;
      console.error('获取用户列表失败:', err);
    } finally {
      loading.value = false;
    }
  };
  
  const deleteUser = async (userId) => {
    try {
      await userApi.deleteUser(userId);
      // 从列表中移除已删除的用户
      users.value = users.value.filter(user => user.id !== userId);
    } catch (err) {
      error.value = err.message;
      throw err;
    }
  };
  
  const updateUser = async (userId, userData) => {
    try {
      const response = await userApi.updateUser(userId, userData);
      // 更新列表中的用户数据
      const index = users.value.findIndex(user => user.id === userId);
      if (index !== -1) {
        users.value[index] = response.data;
      }
    } catch (err) {
      error.value = err.message;
      throw err;
    }
  };
  
  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page;
    }
  };
  
  // 生命周期
  onMounted(() => {
    fetchUsers();
  });
  
  // 返回需要暴露的状态和方法
  return {
    // 状态
    users,
    loading,
    error,
    currentPage,
    pageSize,
    // 计算属性
    totalPages,
    paginatedUsers,
    // 方法
    fetchUsers,
    deleteUser,
    updateUser,
    goToPage
  };
}

在组件中使用:

<script setup>
import { useUserManagement } from '@/composables/useUserManagement';
import { ElMessage } from 'element-plus';

// 使用组合式函数,获取所有用户管理相关的功能
const {
  paginatedUsers,
  loading,
  error,
  currentPage,
  totalPages,
  deleteUser,
  goToPage
} = useUserManagement();

// 处理删除操作
const handleDelete = async (userId) => {
  try {
    await deleteUser(userId);
    ElMessage.success('删除成功');
  } catch (err) {
    ElMessage.error('删除失败');
  }
};
</script>

<template>
  <div class="user-management">
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>
    
    <!-- 错误提示 -->
    <div v-if="error" class="error">{{ error }}</div>
    
    <!-- 用户列表 -->
    <div v-else class="user-list">
      <div v-for="user in paginatedUsers" :key="user.id" class="user-item">
        <span>{{ user.name }}</span>
        <button @click="handleDelete(user.id)">删除</button>
      </div>
    </div>
    
    <!-- 分页 -->
    <div class="pagination">
      <button 
        @click="goToPage(currentPage - 1)" 
        :disabled="currentPage === 1"
      >
        上一页
      </button>
      <span>{{ currentPage }} / {{ totalPages }}</span>
      <button 
        @click="goToPage(currentPage + 1)" 
        :disabled="currentPage === totalPages"
      >
        下一页
      </button>
    </div>
  </div>
</template>

优势分析

  1. 逻辑集中:所有用户管理相关的逻辑都在一个文件中
  2. 易于复用:多个组件可以使用同一个组合式函数
  3. 易于测试:可以单独测试组合式函数
  4. 类型安全:配合TypeScript可以获得完整的类型提示
  5. 按需引入:只引入需要的功能,减少组件代码量

常见陷阱

陷阱1:忘记.value

错误示例:

<script setup>
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
  // ❌ 错误:忘记使用.value
  count++;  // 这不会触发响应式更新
};
</script>

正确做法:

<script setup>
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
  // ✅ 正确:使用.value访问和修改ref的值
  count.value++;
};
</script>

原因分析

  • ref()返回的是一个响应式引用对象,不是原始值
  • 在JavaScript中访问和修改ref需要使用.value
  • 在模板中Vue会自动解包,不需要.value
陷阱2:reactive对象的解构

错误示例:

<script setup>
import { reactive } from 'vue';

const state = reactive({
  count: 0,
  message: 'Hello'
});

// ❌ 错误:直接解构会失去响应性
const { count, message } = state;

const increment = () => {
  count++;  // 这不会触发响应式更新
};
</script>

正确做法:

<script setup>
import { reactive, toRefs } from 'vue';

const state = reactive({
  count: 0,
  message: 'Hello'
});

// ✅ 正确:使用toRefs保持响应性
const { count, message } = toRefs(state);

const increment = () => {
  count.value++;  // 现在可以正常工作
};

// 或者不解构,直接使用state
const increment2 = () => {
  state.count++;  // 这也可以正常工作
};
</script>

原因分析

  • 直接解构reactive对象会失去响应性
  • toRefs()reactive对象的每个属性转换为ref
  • 转换后的ref保持与原对象的响应式连接
陷阱3:watch的immediate选项

错误示例:

<script setup>
import { ref, watch } from 'vue';

const userId = ref(null);
const userData = ref(null);

// ❌ 问题:只有userId变化时才会执行
watch(userId, async (newId) => {
  if (newId) {
    const response = await fetchUser(newId);
    userData.value = response.data;
  }
});

// 如果userId初始值不是null,watch不会立即执行
// 需要手动调用一次fetchUser
</script>

正确做法:

<script setup>
import { ref, watch } from 'vue';

const userId = ref(123);  // 初始值不是null
const userData = ref(null);

// ✅ 正确:使用immediate选项立即执行一次
watch(userId, async (newId) => {
  if (newId) {
    const response = await fetchUser(newId);
    userData.value = response.data;
  }
}, {
  immediate: true  // 组件挂载时立即执行一次
});
</script>

原因分析

  • 默认情况下,watch只在数据变化时执行
  • 使用immediate: true可以在组件挂载时立即执行一次
  • 这对于需要根据初始值加载数据的场景非常有用

性能优化建议

建议1:合理选择ref和reactive
<script setup>
import { ref, reactive } from 'vue';

// ✅ 推荐:基本类型使用ref
const count = ref(0);
const message = ref('Hello');
const isActive = ref(false);

// ✅ 推荐:对象使用reactive(如果不需要整体替换)
const user = reactive({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com'
});

// ❌ 不推荐:对象使用ref(除非需要整体替换)
const user2 = ref({
  name: '李四',
  age: 30
});
// 访问属性需要user2.value.name,比较繁琐

// ✅ 但如果需要整体替换对象,ref更合适
const config = ref({ theme: 'dark' });
// 可以整体替换
config.value = { theme: 'light', fontSize: 14 };
</script>
建议2:使用computed缓存计算结果
<script setup>
import { ref, computed } from 'vue';

const items = ref([
  { id: 1, name: '商品A', price: 100, quantity: 2 },
  { id: 2, name: '商品B', price: 200, quantity: 1 },
  { id: 3, name: '商品C', price: 150, quantity: 3 }
]);

// ✅ 推荐:使用computed缓存计算结果
const totalPrice = computed(() => {
  console.log('计算总价');  // 只在items变化时执行
  return items.value.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0);
});

// ❌ 不推荐:使用方法每次都重新计算
const getTotalPrice = () => {
  console.log('计算总价');  // 每次调用都执行
  return items.value.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0);
};
</script>

<template>
  <div>
    <!-- computed会缓存结果,多次使用不会重复计算 -->
    <p>总价:{{ totalPrice }}</p>
    <p>总价(含税):{{ totalPrice * 1.1 }}</p>
    
    <!-- 方法每次都会重新计算 -->
    <p>总价:{{ getTotalPrice() }}</p>
    <p>总价(含税):{{ getTotalPrice() * 1.1 }}</p>
  </div>
</template>
建议3:避免在模板中使用复杂表达式
<script setup>
import { ref, computed } from 'vue';

const users = ref([
  { id: 1, name: '张三', age: 25, status: 'active' },
  { id: 2, name: '李四', age: 30, status: 'inactive' },
  { id: 3, name: '王五', age: 28, status: 'active' }
]);

// ❌ 不推荐:在模板中使用复杂表达式
// <div v-for="user in users.filter(u => u.status === 'active').sort((a, b) => a.age - b.age)">

// ✅ 推荐:使用computed处理复杂逻辑
const activeUsers = computed(() => {
  return users.value
    .filter(user => user.status === 'active')
    .sort((a, b) => a.age - b.age);
});
</script>

<template>
  <div>
    <!-- 模板更简洁,逻辑更清晰 -->
    <div v-for="user in activeUsers" :key="user.id">
      {{ user.name }} - {{ user.age }}岁
    </div>
  </div>
</template>

实践练习

练习1:Options API转Composition API(难度:简单)

需求描述

将下面的Options API组件转换为Composition API:

<script>
export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`;
    }
  },
  methods: {
    updateFirstName(value) {
      this.firstName = value;
    },
    updateLastName(value) {
      this.lastName = value;
    }
  },
  mounted() {
    console.log('组件已挂载');
  }
}
</script>

<template>
  <div>
    <input :value="firstName" @input="updateFirstName($event.target.value)" />
    <input :value="lastName" @input="updateLastName($event.target.value)" />
    <p>全名:{{ fullName }}</p>
  </div>
</template>

实现提示

  1. 使用ref()定义响应式数据
  2. 使用computed()定义计算属性
  3. 直接定义函数替代methods
  4. 使用onMounted()替代mounted钩子
  5. 记得在访问ref时使用.value

参考答案

<script setup>
import { ref, computed, onMounted } from 'vue';

// 使用ref定义响应式数据
const firstName = ref('');
const lastName = ref('');

// 使用computed定义计算属性
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

// 直接定义函数
const updateFirstName = (value) => {
  firstName.value = value;
};

const updateLastName = (value) => {
  lastName.value = value;
};

// 使用onMounted替代mounted钩子
onMounted(() => {
  console.log('组件已挂载');
});
</script>

<template>
  <div>
    <!-- 模板部分保持不变 -->
    <input :value="firstName" @input="updateFirstName($event.target.value)" />
    <input :value="lastName" @input="updateLastName($event.target.value)" />
    <p>全名:{{ fullName }}</p>
  </div>
</template>

答案解析

  1. 响应式数据:使用ref()替代data(),因为firstName和lastName是基本类型
  2. 计算属性computed()的用法与Options API类似,但需要使用.value访问ref
  3. 方法:直接定义函数,不需要methods选项
  4. 生命周期:使用onMounted()替代mounted()钩子
  5. 模板:模板部分不需要修改,Vue会自动解包ref

练习2:创建可复用的组合式函数(难度:中等)

需求描述

创建一个useCounter组合式函数,实现以下功能:

  1. 维护一个计数器状态
  2. 提供增加、减少、重置方法
  3. 提供一个计算属性显示计数器是否为偶数
  4. 支持设置初始值和步长
  5. 在两个不同的组件中使用这个组合式函数

实现提示

  1. 创建一个独立的文件存放组合式函数
  2. 使用ref定义响应式状态
  3. 使用computed定义计算属性
  4. 函数接受配置参数(初始值、步长)
  5. 返回需要暴露的状态和方法

参考答案

// composables/useCounter.js
/**
 * 计数器组合式函数
 * @param {number} initialValue - 初始值,默认为0
 * @param {number} step - 步长,默认为1
 * @returns {Object} 计数器状态和方法
 */
import { ref, computed } from 'vue';

export function useCounter(initialValue = 0, step = 1) {
  // 响应式状态
  const count = ref(initialValue);
  
  // 计算属性:判断是否为偶数
  const isEven = computed(() => {
    return count.value % 2 === 0;
  });
  
  // 方法:增加
  const increment = () => {
    count.value += step;
  };
  
  // 方法:减少
  const decrement = () => {
    count.value -= step;
  };
  
  // 方法:重置
  const reset = () => {
    count.value = initialValue;
  };
  
  // 方法:设置为指定值
  const setValue = (value) => {
    count.value = value;
  };
  
  // 返回需要暴露的内容
  return {
    count,
    isEven,
    increment,
    decrement,
    reset,
    setValue
  };
}

组件A:基础计数器

<script setup>
import { useCounter } from '@/composables/useCounter';

// 使用默认配置
const { count, isEven, increment, decrement, reset } = useCounter();
</script>

<template>
  <div class="counter">
    <h2>基础计数器</h2>
    <p>当前值:{{ count }}</p>
    <p>是否为偶数:{{ isEven ? '是' : '否' }}</p>
    <div class="buttons">
      <button @click="decrement">-1</button>
      <button @click="reset">重置</button>
      <button @click="increment">+1</button>
    </div>
  </div>
</template>

组件B:自定义步长计数器

<script setup>
import { useCounter } from '@/composables/useCounter';

// 使用自定义配置:初始值100,步长10
const { count, isEven, increment, decrement, reset, setValue } = useCounter(100, 10);

// 可以创建多个独立的计数器实例
const counter2 = useCounter(0, 5);
</script>

<template>
  <div class="counter">
    <h2>自定义步长计数器</h2>
    <p>当前值:{{ count }}</p>
    <p>是否为偶数:{{ isEven ? '是' : '否' }}</p>
    <div class="buttons">
      <button @click="decrement">-10</button>
      <button @click="reset">重置到100</button>
      <button @click="increment">+10</button>
      <button @click="setValue(0)">设置为0</button>
    </div>
    
    <hr />
    
    <h2>第二个计数器(步长5)</h2>
    <p>当前值:{{ counter2.count }}</p>
    <div class="buttons">
      <button @click="counter2.decrement">-5</button>
      <button @click="counter2.increment">+5</button>
    </div>
  </div>
</template>

答案解析

  1. 组合式函数设计

    • 接受配置参数,提供灵活性
    • 封装所有相关逻辑,包括状态、计算属性和方法
    • 返回对象,方便按需解构
  2. 逻辑复用

    • 同一个组合式函数可以在多个组件中使用
    • 每次调用都创建独立的实例,互不影响
    • 可以在同一个组件中创建多个实例
  3. 优势体现

    • 代码复用:避免重复编写相同逻辑
    • 逻辑集中:所有计数器相关逻辑都在一个文件中
    • 易于测试:可以单独测试组合式函数
    • 类型安全:配合TypeScript可以获得完整的类型提示
  4. 与Mixin对比

    • 组合式函数的来源清晰(显式导入)
    • 不会有命名冲突问题
    • 可以传递参数配置
    • 更好的TypeScript支持

进阶阅读

大规模监控数据下的 JSON 优化:从 OOM 崩溃到极致吞吐的进阶之路

一、 内存架构优化:破解“内存翻倍”魔咒

在处理大规模监控 JSON 时,最隐形的杀手是 “对象实例化开销”

1. 为什么全量解析会崩掉内存?

当你执行 JSON.parse 处理一个 100MB 的字符串时,内存占用并不是增加 100MB。

  • 字符串拷贝:V8 需要一份原始字符串的内存。
  • 对象图谱(Object Graph) :解析出的每个 Key 和 Value 都是一个独立的 JS 对象,会有额外的指针和隐藏类(Hidden Class)开销。
  • 最终结果:100MB 的原始数据可能在内存中膨胀到 300MB-500MB,直接诱发频繁的 Full GC

2. 流式解析(Streaming Parser)的深度实践

在监控后端分析场景,应引入 状态机解析

  • 技术实现:使用 JSONStream。它不会一次性把整个 JSON 加载进内存,而是像吃拉面一样,一根一根(一个节点一个节点)地处理。
  • 实战案例:在解析上亿条埋点组成的 JSON 数组时,通过流式监听 rows.* 路径,处理完一个对象后立即交给聚合引擎并释放内存,将内存波动控制在恒定范围内。

二、 序列化压榨:绕过 V8 的通用检查

Node.js 原生的 JSON.stringify 为了通用性,在每次调用时都会进行复杂的类型探测和属性遍历。

1. Schema 预编译:快到飞起的秘密

如果你上报的监控埋点格式是固定的(例如:{ event: string, duration: number }),那么预编译序列化是最佳选择。

  • fast-json-stringify:它会预先生成一段高度优化的 JS 函数,直接拼接字符串,跳过所有的逻辑判断。
  • 性能增益:在 Benchmark 测试中,针对固定结构的监控数据,其速度比原生方法快 200% 到 500%

2. 避免属性检索:隐藏类(Hidden Classes)的复用

在生成大型监控报告时,确保你构建的对象具有一致的形状

  • 技巧:始终以相同的顺序给对象属性赋值。这能让 V8 引擎复用隐藏类,极大地提升后续 stringify 时的查找效率。

三、 传输层的“降维打击”:从文本到二进制

你应该意识到 JSON 的文本格式在大规模传输中是极度低效的(冗余的引号、重复的 Key、Base64 编码后的体积膨胀)。

1. 字段映射压缩(Field Mapping)

在监控 SDK 上报阶段,通过字典映射减少 Payload:

  • 原始数据{"errorMessage": "timeout", "errorCode": 504}
  • 压缩后{"m": "timeout", "c": 504}
  • 效果:仅此一项,在每秒万级请求下,就能为数据网关节省 TB 级的月带宽流量。

2. 跨越 JSON:Protobuf 与 MessagePack

当 JSON 的解析 CPU 占用率超过 30% 时,必须考虑协议升级:

  • Protobuf(Protocol Buffers) :通过预定义的 ID 映射字段名,不传输任何 Key 文本。解析速度极快,因为它几乎就是内存数据的直接二进制映射。
  • MessagePack:如果你需要保留动态性(不需要提前定义 Schema),MessagePack 提供了比 JSON 更小的体积和更快的编解码速度,非常适合在 BFF 内部服务之间传递监控中间件。

你真的懂 JSON 吗?那些被忽略的底层边界与性能陷阱

一、 语法边界:JSON 并不是 JavaScript 的子集

这是一个常见的误区。虽然 JSON 源于 JS,但它的规范(RFC 8259)比 JS 严格且局限得多。

1. 那些被“吞掉”的类型

在执行 JSON.stringify(obj) 时,JS 引擎会进行一套复杂的类型转换,而这些转换往往是非对称的:

  • undefined、函数、Symbol

    • 作为对象属性时:会被直接忽略(Key 都会消失)。
    • 作为数组元素时:会被转化为 null
    • 独立值时:返回 undefined
  • 不可枚举属性:默认会被完全忽略。

  • BigInt:会直接抛出 TypeError,因为 JSON 规范中没有对应的大数表示协议。

2. 数值的精度丢失

JSON 的数值遵循 IEEE 754 双精度浮点数。如果你在处理前端监控中的高精纳秒级时间戳,直接序列化可能会导致精度被截断。


二、 序列化的高级操纵:Replacer 与 toJSON 的深度应用

当你需要处理复杂的业务对象(比如含有循环引用或敏感数据)时,基础的 JSON.stringify 就不够用了。

1. toJSON:对象的自白

如果一个对象拥有 toJSON 方法,序列化时会优先调用它。这在处理复杂类实例(Class)时非常有用:

JavaScript

class User {
  constructor(name, pwd) { this.name = name; this.pwd = pwd; }
  toJSON() { return { name: this.name }; } // 自动屏蔽敏感字段
}

2. Replacer 过滤器:解决循环引用

面对嵌套极深的监控数据,循环引用会导致进程崩溃。我们可以利用 Replacer 的第二个参数(函数或数组)来进行“外科手术”:

JavaScript

const seen = new WeakSet();
const safeJson = JSON.stringify(data, (key, value) => {
  if (typeof value === "object" && value !== null) {
    if (seen.has(value)) return "[Circular]"; // 标记循环引用而非报错
    seen.add(value);
  }
  return value;
});

三、 性能深水区:V8 引擎是如何“吃”掉 JSON 的?

在 Node.js 服务端,大规模的 JSON 处理往往是 CPU 的头号杀手。

1. 为什么 JSON.parse 比 JS 字面量快?

这是一个反直觉的结论:解析一段字符串 JSON.parse('{"a":1}') 通常比 JS 引擎解析代码 {a:1} 快。

  • 原因:JS 解析器需要进行复杂的词法和语法分析(考虑到变量提升、作用域等),而 JSON 解析器是单向、无状态的。
  • 优化建议:对于大型静态配置,直接以字符串形式存放并用 JSON.parse 载入,能有效缩短代码冷启动的解析时间(Parse Time)。

2. 阻塞与内存的“双重打击”

  • 同步阻塞JSON.stringify 在处理 10MB 以上的对象时,会阻塞 Event Loop 几十毫秒。在高并发环境下,这足以导致后续请求全部超时。
  • 内存膨胀:序列化时,V8 会先生成一个完整的巨大字符串放入堆内存中。如果你的对象接近 1GB,序列化过程可能会瞬间触发 OOM (Out of Memory)

3. 安全陷阱:JSON 劫持与注入

  • __proto__ 注入:不安全的 JSON.parse(特别是在某些旧库中)可能被恶意构造的字符串攻击,通过原型链污染篡改全局逻辑。

每日一题-二进制手表🟢

二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。

  • 例如,下面的二进制手表读取 "4:51"

给你一个整数 turnedOn ,表示当前亮着的 LED 的数量,返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。

小时不会以零开头:

  • 例如,"01:00" 是无效的时间,正确的写法应该是 "1:00"

分钟必须由两位数组成,可能会以零开头:

  • 例如,"10:2" 是无效的时间,正确的写法应该是 "10:02"

 

示例 1:

输入:turnedOn = 1
输出:["0:01","0:02","0:04","0:08","0:16","0:32","1:00","2:00","4:00","8:00"]

示例 2:

输入:turnedOn = 9
输出:[]

 

提示:

  • 0 <= turnedOn <= 10
❌