每日一题-交替位二进制数🟢
给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。
示例 1:
输入:n = 5 输出:true 解释:5 的二进制表示是:101
示例 2:
输入:n = 7 输出:false 解释:7 的二进制表示是:111.
示例 3:
输入:n = 11 输出:false 解释:11 的二进制表示是:1011.
提示:
1 <= n <= 231 - 1
给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。
示例 1:
输入:n = 5 输出:true 解释:5 的二进制表示是:101
示例 2:
输入:n = 7 输出:false 解释:7 的二进制表示是:111.
示例 3:
输入:n = 11 输出:false 解释:11 的二进制表示是:1011.
提示:
1 <= n <= 231 - 1在现代 Web 应用开发中,首屏加载速度(FCP)和最大内容绘制(LCP)是衡量用户体验的核心指标。随着富媒体内容的普及,图片资源往往占据了页面带宽的大部分。如果一次性加载页面上的所有图片,不仅会阻塞关键渲染路径,导致页面长时间处于“白屏”或不可交互状态,还会浪费用户的流量带宽。
图片懒加载(Lazy Loading)作为一种经典的性能优化策略,其核心思想是“按需加载”:即只有当图片出现在浏览器可视区域(Viewport)或即将进入可视区域时,才触发网络请求进行加载。这一策略能显著减少首屏 HTTP 请求数量,降低服务器压力,并提升页面的交互响应速度。
本文将基于 React 生态,从底层原理出发,深入探讨图片懒加载的多种实现方案,并重点分析如何解决布局偏移(CLS)等用户体验问题。
图片懒加载的本质是一个“可见性检测”问题。我们需要实时判断目标图片元素是否与浏览器的可视区域发生了交叉。在技术实现上,主要依赖以下两种依据:
HTML5 标准为
标签引入了 loading 属性,这是实现懒加载最简单、成本最低的方式。
Jsx
const NativeLazyLoad = ({ src, alt }) => {
return (
<img
src={src}
alt={alt}
loading="lazy"
width="300"
height="200"
/>
);
};
分析:
优点:零 JavaScript 代码,完全依赖浏览器原生行为,不会阻塞主线程。
缺点:
在 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} />;
};
关键点分析:
这是目前最推荐的方案。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;
优势分析:
仅仅实现“懒加载”是不够的。在工程实践中,如果处理不当,懒加载会导致严重的累积布局偏移(CLS, Cumulative Layout Shift) 。即图片加载前高度为 0,加载后撑开高度,导致页面内容跳动。这不仅体验极差,也是 Google Core Web Vitals 的扣分项。
必须在图片加载前确立其占据的空间。现代 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;
}
结合后端返回的元数据(如宽高、主色调),我们可以构建一个体验极佳的懒加载组件。
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 和占位策略,我们可以让懒加载不仅“快”,而且“稳”且“美”。
前端性能优化专栏 - 第九篇
在网页设计中,字体往往被视为“灵魂”。它不仅关乎品牌识别和视觉统一,更能直接影响界面的质感与专业感。好的字体能降低用户的认知成本,传递出产品想要营造的独特氛围。
然而,字体作为一种静态资源,必须经过下载和管理。如果处理不当,它就会变成性能的“累赘”,甚至引发一些让用户抓狂的“怪现象”。
你是否遇到过这样的场景:页面打开了,但文字部分是一片空白,过了好几秒才突然蹦出来?或者文字先是以一种普通的系统字体显示,然后突然“闪”一下,变成了精美的设计字体?
这可不是浏览器的 Bug,而是浏览器默认的字体加载策略在作祟。
这是 Chrome 等现代浏览器的典型行为。当自定义字体还没下载完时,浏览器会选择完全不渲染文本。
这是 IE 等浏览器的传统行为。当自定义字体未加载完时,浏览器先用后备字体(系统自带字体)渲染。
![]()
字体加载本质上是一个异步网络请求。在等待期间,浏览器必须决定:
为了不让浏览器“瞎猜”,我们需要一种方式显式地告诉它:“在这个项目里,你应该如何平衡内容、样式和性能。”
在 @font-face 中使用 font-display 属性,可以精准控制字体在不同加载阶段的渲染策略。
@font-face {
font-family: 'My Custom Font';
src: url(/fonts/my-font.woff2) format('woff2');
font-display: swap; /* 关键控制位 */
}
![]()
这是目前最主流、最被推荐的策略。
字体优化不是简单的“全都要”,而是一场关于内容可见性与视觉一致性的博弈。
swap。block。fallback 或 optional 是你的好伙伴。合理利用 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;
}
}
另外一种更为巧妙的方式是利用交替位二进制数性质。
当给定值 $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;
}
}
今日份加餐:经典「状态压缩 + 位运算」入门题 🎉🎉
或是考虑加练如下「位运算」题目 🍭🍭🍭
| 题目 | 题解 | 难度 | 推荐指数 |
|---|---|---|---|
| 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)$。使用了常数空间来存储中间变量。
将n右移一位异或n,检查结果是否全为1。
class Solution:
def hasAlternatingBits(self, n: int) -> bool:
tmp = n^(n>>1)
return tmp&(tmp+1) == 0
在现代 Web 应用的开发流程中,前后端分离已成为行业标准。然而,在实际协作中,前端工程师常常面临“后端接口未就绪、联调环境不稳定、异常场景难以复现”等痛点。这些问题导致前端开发进度被迫依赖后端,严重制约了交付效率。
Mock.js 作为一种数据模拟解决方案,不仅能解除这种依赖,还能通过工程化的方式提升代码的健壮性。本文将从架构视角出发,深入剖析 Mock.js 的核心价值、技术原理,并结合 Vite 生态展示如何在现代项目中落地最佳实践,同时客观分析其局限性与应对策略。
在工程化体系中,Mock.js 不仅仅是一个生成随机数据的库,它是实现“并行开发”的关键基础设施。
Mock.js 的核心原理是重写浏览器原生的 XMLHttpRequest 对象。当代码发起请求时,Mock.js 会在浏览器端拦截该请求,判断 URL 是否匹配预定义的规则。如果匹配,则阻止网络请求的发出,并直接返回本地生成的模拟数据;如果不匹配,则放行请求。
直接在业务代码(如 main.js)中引入 Mock.js 是一种侵入性较强的做法,且原生 Mock.js 拦截请求后,浏览器的 Network 面板无法抓取到请求记录,给调试带来不便。
在 Vite 生态中,推荐使用 vite-plugin-mock。该插件在开发环境(serve)下,通过 Node.js 中间件的形式拦截请求。这意味着请求真正从浏览器发出并到达了本地开发服务器,因此可以在 Network 面板清晰查看请求详情,体验与真实接口完全一致。
以下将展示如何在 Vite + TypeScript 项目中集成 Mock.js,并实现一个包含逻辑处理(分页、切片)的模拟接口。
建议将 Mock 数据与业务代码分离,保持目录结构清晰:
Text
project-root/
├── src/
├── mock/
│ ├── index.ts # Mock 服务配置
│ └── user.ts # 用户模块接口
│ └── list.ts # 列表模块接口(本例重点)
├── vite.config.ts # Vite 配置
└── package.json
通过配置插件,确保 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,
}),
],
};
});
模拟接口不仅仅是返回死数据,更需要具备一定的逻辑处理能力。以下代码演示了如何利用 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 极大提升了开发效率,但作为一名架构师,必须清晰认知其局限性,以避免在工程落地时产生负面影响。
原生 Mock.js 通过重写 window.XMLHttpRequest 实现拦截。这种机制发生在浏览器脚本执行层面,请求并未真正进入网络层。因此,开发者在 Chrome DevTools 的 Network 面板中无法看到这些请求,导致调试困难(只能依赖 console.log)。
原生 Mock.js 仅拦截 XMLHttpRequest,而现代前端项目大量使用 fetch API。若直接使用原生 Mock.js,fetch 请求将无法被拦截,直接穿透到网络。
这是 Mock.js 使用中最大的风险点。前端编写的 Mock 数据结构(字段名、类型、层级)完全依赖于开发者的主观定义或早期的接口文档。一旦后端在开发过程中修改了字段(例如将 userId 改为 uid,或将 money 类型由数字改为字符串),而前端 Mock 未及时同步,就会导致“本地开发一切正常,上线联调全面崩溃”的现象。
为了最大化 Mock.js 的收益并规避风险,建议团队遵循以下最佳实践:
综上所述,Mock.js 是现代前端工程化中不可或缺的利器。通过合理的架构设计和工具选型,它能显著提升前后端协作效率,但开发者也需时刻警惕数据一致性问题,确保从模拟环境到真实环境的平滑过渡。
原文链接:macarthur.me/posts/queue…
生成器执行完毕后便无法 “复活”,但借助 Promise,我们能打造出一个可续充的版本。接下来就动手试试吧。
自从深入研究并分享过生成器的相关内容后,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++;
}
}
大功告成!接下来,我们将这个实现落地到实际开发场景中。
正如前文所说,PicPerf 是一个图片优化、托管和缓存平台,支持用户上传多张图片进行处理。其界面采用了一个常见的交互模式:用户拖拽图片到指定区域,图片会按顺序逐步完成上传。
这正是可续充先进先出队列的适用场景:即便 “待上传” 的图片全部处理完毕,用户依然可以拖拽新的图片进来,上传流程会自动继续,队列会直接从新添加的文件开始处理。
首先,我们尝试纯 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方法同一时间只会被调用一次、该仓库只有一个使用者。请忽略这些细节以及其他可能的疏漏,重点来看这种实现方案带来的诸多优势:
useEffect的反复触发,逻辑更清晰;useSyncExternalStore这个 React 钩子;对有些人来说,这种方案可能比最初的纯 React 方案复杂得多,我完全理解这种感受。但不妨换个角度想:现在把代码写得复杂一点,就能多拖延一点时间,避免 AI 工具完全取代我们的工作、毁掉我们的职业未来,甚至 “收割” 我们的价值。带着这个目标去写代码吧!
当然,说句正经的:要让 AI 辅助开发持续发挥价值,需要人类帮助 AI 理解底层技术原语的设计目的、取舍原则和发展前景。掌握这些底层知识,永远有其不可替代的价值。
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 |
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 |
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 |
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 |
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 |
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) |
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 |
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 |
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 |
在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输出包含了以下关键信息:
重要的是,这些颜色分量已经包含了光源的亮度信息。这意味着一个强度为1的白色光源会返回近似(1,1,1)的值,而强度为2的白色光源会返回近似(2,2,2)的值。这种设计使得输出值可以直接用于光照计算,无需额外的强度乘法操作。
Main Light Color节点的输出值范围在理论上是无上限的,因为URP支持高动态范围渲染。在实际应用中,值的大小取决于光源的强度设置和颜色选择。以下是一些典型情况下的输出示例:
Out端口的Vector 3输出可以与多种其他Shader Graph节点连接,实现复杂的光照效果:
与颜色操作节点连接
与光照计算节点结合
与纹理采样结合
以下是一个基本的光照计算示例,展示如何使用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节点的Out端口在所有支持URP的平台上都有相同的行为,包括:
这种跨平台的一致性确保了使用Main Light Color节点的着色器可以在不同的目标平台上提供可预测的视觉效果,大大简化了多平台开发的复杂度。
【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)
大家好,我是 Sunday。
昨晚的 2026 年春晚上的魔术【惊喜定格】大家看了吗?
![]()
说实话,作为一名资深的前端开发者,我对那些歌舞节目的兴趣一般,但每年的魔术环节我必看。不是为了看奇迹,而是为了:找 Bug 😂。
今年的魔术特别有意思:
魔术师拿出一个手机计算器,让全场观众参与,又是随机想数字,又是乱按屏幕,最后算出来的结果,竟然精准地命中了 当前的年、月、日、时、分。
我老婆说:“哇哦,好厉害啊~”
不过我是越看越不对,这玩意怎么感觉像是个 写死的 JS 脚本啊?
其实,这个魔术并不是 2026 春晚的首创。
早在去年(2025年)底,武汉理工大学的迎新晚会 上就有这个魔术的雏形。
![]()
当时也是一样的套路:随机数字 + 观众乱按 = 预言时间。
而这个魔术的实现原理,就在魔术师手中的 计算器 上
魔术师手里那个所谓的“计算器”,压根就不是系统自带的。那是一个专门开发的 Web App 或者 Native App。
所以,咱们今天大年初一不整虚的,直接打开 VS Code,从原理到代码,一比一复刻这个价值过亿流量的春晚魔术!
这个魔术的核心逻辑,可以拆解为两个部分:
普通人的思维是:输入 A + 输入 B + 乱按的 C = 结果。
但在代码里,逻辑是反过来的:目标结果(当前时间) - 输入 A - 输入 B = 必须填补的差值(Force Number)。
比如:
2162227。1106。88396。89502。2162227 - 89502 = 2072725。接下来,魔术师要做的,就是让第三个观众,在以为自己是“随机乱按”的情况下,把 2072725 这个数字“按”出来。
这是整个魔术最精彩,也是前端最能发挥的地方。
魔术师会说:“来,大家随便按计算器,越乱越好。”
观众以为按 9 屏幕就会显示 9,按 5 就会显示 5。
大错特错!
在这个 App 进入“魔术模式”后,键盘事件已经被 e.preventDefault() 拦截了。无论你按哪个数字键,屏幕上只会依次显示程序预设好的那个 差值字符串。
2072725。2。0。7...现在知道 为什么魔术师要把屏幕翻过来了吧。就是为了不让大家看到用户真实输入的是什么。
原理讲通了,咱们直接上代码,
作为一名前端,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】中回复【魔术】来获取源码
今天是2026年2月17日农历正月初一,在2026 愿大家:身体健康无病痛,收入翻番钱包鼓! 代码 0 Error 0 Warning,需求一次过,上线零回滚!策马扬鞭,从小白进阶专家,新年一路 “狂飙”!🧧🐎 给大家拜年啦~
前端里权限判断、表单校验、勾选状态,几乎都要判断「数组里有没有某个值」或「是否全部满足条件」。很多人习惯用 for 循环 + if 一把梭,或者 indexOf 判断,写多了既啰嗦又容易漏边界情况。
用 find / some / every / includes 这四个方法,可以把「查找 → 判断 → 校验」写得更短、更语义化,也更好处理边界情况。本文用 10 个常见场景,把日常该怎么选、为什么这么选、容易踩的坑讲清楚。
适合读者:
find/some/every/includes 用哪个、什么时候用有点模糊find / some / every / includes 在干什么这四个方法都不是黑魔法,本质是:在不动原数组的前提下,用一次遍历完成「查找 / 判断是否存在 / 判断是否全部满足」。
| 方法 | 在干什么 | 返回值 | 什么时候停 |
|---|---|---|---|
find |
找第一个符合条件的元素 | 找到的元素,找不到返回 undefined
|
找到第一个就停 |
some |
判断是否至少有一个满足条件 |
true 或 false
|
找到第一个就停(短路) |
every |
判断是否全部满足条件 |
true 或 false
|
遇到第一个不满足就停(短路) |
includes |
判断数组里是否包含某个值(严格相等) |
true 或 false
|
遍历完或找到就停 |
// 传统 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」更重要。
假设接口返回的数据类似:
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 个写法,覆盖权限判断、表单校验、勾选状态等真实场景。
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,后续解构或访问属性要处理,用 ?? 给默认值。
some)const hasAdmin = users.some((user) => user.role === 'admin');
// true
const hasInactive = users.some((user) => user.status === 'inactive');
// true
适用: 权限判断「是否有任一管理员」、表单校验「是否有错误项」、状态判断「是否有未完成项」等。
注意: 空数组时 some 返回 false,业务上要结合「空列表算通过还是不算」处理。
every)const allActive = users.every((user) => user.status === 'active');
// false(因为有王五是 inactive)
const allHaveId = users.every((user) => user.id != null);
// true
适用: 表单校验「是否全部勾选」、权限判断「是否全部有权限」、状态判断「是否全部完成」等。
注意: 空数组时 every 返回 true(空真),业务上要结合「空列表算通过还是不算」处理。
includes)const hasRead = permissions.includes('read');
// true
const hasExecute = permissions.includes('execute');
// false
适用: 简单值数组的包含判断、权限列表判断、标签列表判断等。
注意: includes 底层用 严格相等=== 做比较,这对「简单值(string / number / boolean)」很友好,但对「对象 / 数组」这类引用类型完全不适用,因为===比较的是内存地址而非内容。
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 + 回调。
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 判断是否全部有值。
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 判断是否有选中项。
find+ ??)const defaultUser = users.find((user) => user.role === 'admin') ?? {
id: 0,
name: '默认用户',
role: 'guest',
};
适用: 默认选中第一项、取第一个有效配置、兜底默认值等。
注意: find 找不到返回 undefined,用 ?? 可以统一成默认对象,避免后面解构报错。
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 + 条件判断。
every +some)const allActive = users.every((user) => user.status === 'active');
const hasAdmin = users.some((user) => user.role === 'admin');
// 业务逻辑:全部激活 且 有管理员
const canOperate = allActive && hasAdmin;
// false(因为有 inactive 的)
适用: 复杂业务规则判断、多条件组合校验、权限组合判断等。
推荐: 把每个条件拆成变量,用名字表达「这一步在判断什么」,可读性和调试都会好很多。
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;
}
every 返回 true,some 返回 false
[].every((x) => x > 0); // true(空真)
[].some((x) => x > 0); // false
业务上要结合「空列表算通过还是不算」处理。例如表单校验,空列表可能应该算「未填写」而不是「通过」。
const fields = [];
const allFilled = fields.length > 0 && fields.every((f) => f.value);
// 先判断长度,再 every
includes 只能判断简单值,对象数组要用 some
const users = [{ id: 1 }, { id: 2 }];
users.includes({ id: 1 }); // false(对象引用不同)
// 正确:用 some + 条件判断
users.some((user) => user.id === 1); // true
find 和 filter 的区别:find 只找第一个,filter 找全部const firstAdmin = users.find((u) => u.role === 'admin');
// 返回第一个对象或 undefined
const allAdmins = users.filter((u) => u.role === 'admin');
// 返回数组,可能为空数组 []
要「第一个」用 find,要「全部」用 filter,别混用。
some 和 every 的短路特性:找到就停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 => ...) |
true 或 false
|
| 判断是否全部满足 | list.every(item => ...) |
true 或 false
|
| 判断是否包含某个值(简单值) | list.includes(value) |
true 或 false
|
| 找第一个并给默认值 | list.find(...) ?? 默认值 |
对象或默认值 |
| 对象数组是否包含某个 id | list.some(item => item.id === id) |
true 或 false
|
| 表单校验:全部必填已填 | list.filter(...).every(...) |
true 或 false
|
| 勾选状态:全部选中 | list.every(item => item.checked) |
true 或 false
|
记住:find 负责「找」,some 负责「至少一个」,every 负责「全部」,includes 负责「简单值包含」。日常写权限、校验、状态判断时,先想清楚是要找对象、判断存在、判断全部,还是简单值包含,再选方法,代码会干净很多,也少踩坑。
特别提醒:
find 找不到返回 undefined,记得用 ?? 给默认值every 为 true,some 为 false,业务上要结合长度判断includes,要用 some + 条件判断文章到这里结束。如果你日常写权限判断、表单校验、勾选状态时经常纠结用哪个方法,希望这篇能帮你定个型。
以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。
我是 Eugene,你的电子学友。
如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~
枚举小时 $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
}
欢迎关注 B站@灵茶山艾府
直接把源码当黑盒,或者干巴巴从头读到尾,几乎读不下去。更高效的方式是把源码当作“正在运行的程序”,用断点一层层摸清主流程。
这一篇记录我用经典 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 开始是最稳定的入口,它是 app 创建的第一站。
![]()
先把渲染结果和源码入口对齐,这样断点才更有目标感。
![]()
mount 是渲染真正开始的地方,后面会进入 render 与 patch。
![]()
这个阶段会完成 Options API 的初始化,包括 data 绑定、computed 计算、watch 监听等。
![]()
当 data() 返回对象后,会被转成响应式,并在 dev 模式下挂到 ctx 以便访问。
![]()
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,
})
}
}
![]()
![]()
在模板里出现:
<div v-html="compiledMarkdown"></div>
就会在渲染时读取 compiledMarkdown,触发 computed 的 getter。完整流程可以拆成:
computedOptions
compiledMarkdown
一句话总结:把一个 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
}
生命周期函数在这里统一注册,方便后续统一触发。
![]()
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 和响应式系统时,你会发现思路完全一致:
下一篇我会继续浏览更新渲染源码。
全面了解 Zensical 的配置选项
![]()
Zensical 项目通过 zensical.toml 文件进行配置。如果你使用 zensical new 命令创建项目,该文件会自动生成,并包含带注释的示例配置。
TOML 文件格式 专门设计为易于扫描和理解。我们选择 TOML 而不是 YAML,因为它避免了 YAML 的一些问题:
no 可能被解释为字符串或布尔值。TOML 要求所有字符串都要加引号。为了便于从 Material for MkDocs 过渡,Zensical 可以原生读取 mkdocs.yml 配置文件。但是,我们建议新项目使用 zensical.toml 文件。
!!! info "配置文件支持"
由于 Zensical 是由 Material for MkDocs 的创建者构建的,我们支持通过 mkdocs.yml 文件进行配置,作为过渡机制,使现有项目能够平滑迁移到 Zensical。对 mkdocs.yml 的支持将始终保持,但最终会移出核心。
zensical.toml 配置以声明项目作用域的行开始:
[project]
目前,所有设置都包含在此作用域内。随着 Zensical 的发展,我们将引入额外的作用域,并在适当的地方将设置移出项目作用域。当然,我们会提供自动重构,因此无需手动迁移。
在 TOML 配置文件中,配置顺序非常重要。必须遵循以下规则:
[project]
[project] 的键值对
site_name, site_url, site_description 等基本信息repo_url, repo_name, edit_uri 等仓库配置extra_javascript, extra_css 等额外资源nav 导航配置[project] 的配置[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" # 这会导致解析错误!
!!! 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 自定义应该可以在任一主题变体中工作。
让我们从最基础的配置开始,逐步构建一个完整的配置文件。
必需设置 - 提供网站名称,将显示在浏览器标签页、页面标题和导航栏中。
=== "zensical.toml"
```toml
[project]
site_name = "我的 Zensical 项目"
```
=== "mkdocs.yml"
```yaml
site_name: 我的 Zensical 项目
```
实际效果:
我的 Zensical 项目
我的 Zensical 项目 - 页面名称
我的 Zensical 项目
!!! note "关于 site_name"
site_name 目前是必需的,因为 Zensical 替换的静态网站生成器 MkDocs 需要它。我们计划在未来版本中使此设置可选。
强烈推荐 - 网站的完整 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 是以下功能的前提:
!!! warning "重要"
如果使用即时导航功能,site_url 是必需的,否则即时导航将无法正常工作。
示例:
# 本地开发
site_url = "http://localhost:8000"
# GitHub Pages
site_url = "https://username.github.io"
# 自定义域名
site_url = "https://example.com"
可选 - 网站的描述,用于 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 构建的文档网站
```
实际效果:
<meta name="description"> 标签中!!! tip "SEO 建议" 建议设置一个简洁、有吸引力的描述(50-160 个字符),有助于提高搜索引擎排名。
可选 - 网站作者名称。
=== "zensical.toml"
```toml
[project]
site_name = "我的 Zensical 项目"
site_author = "张三"
```
=== "mkdocs.yml"
```yaml
site_name: 我的 Zensical 项目
site_author: 张三
```
实际效果:
<meta name="author"> 标签中可选 - 版权声明,显示在页面页脚。
=== "zensical.toml"
```toml
[project]
copyright = "Copyright © 2025 张三"
```
=== "mkdocs.yml"
```yaml
copyright: "Copyright © 2025 张三"
```
实际效果:
© 显示为 ©)示例:
# 纯文本
copyright = "Copyright 2025 张三"
# HTML 格式
copyright = "Copyright © 2025 张三"
# 多行(使用多行字符串)
copyright = """
Copyright © 2025 张三
All Rights Reserved
"""
可选 - 文档目录和输出目录配置。
=== "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 © 2025 张三"
# ===== 目录配置(可选,有默认值)=====
docs_dir = "docs" # 文档目录,默认:docs
site_dir = "site" # 输出目录,默认:site
use_directory_urls = true # 使用目录形式的 URL,默认:true
!!! tip "验证配置" 配置完成后,运行以下命令验证:
```bash
# 启动开发服务器
zensical serve
# 检查配置是否正确
zensical build
```
如果配置有误,会显示具体的错误信息。
extra 配置选项用于存储模板使用的任意键值对。如果你覆盖模板,可以使用这些值来自定义行为。
=== "zensical.toml"
```toml
[project.extra]
key = "value"
analytics = "UA-XXXXXXXX-X"
```
=== "mkdocs.yml"
```yaml
extra:
key: value
analytics: UA-XXXXXXXX-X
```
控制文档网站的目录结构,从而控制用于链接到页面的 URL 格式。
=== "zensical.toml"
```toml
[project]
use_directory_urls = true # 默认值
```
=== "mkdocs.yml"
```yaml
use_directory_urls: true
```
!!! info "离线使用"
在构建离线使用时,此选项会自动设置为 false,以便可以从本地文件系统浏览文档,而无需 Web 服务器。
设置网站的语言。
=== "zensical.toml"
```toml
[project.theme]
language = "zh" # 中文
```
=== "mkdocs.yml"
```yaml
theme:
language: zh
```
启用或禁用主题功能。这是一个数组,可以同时启用多个功能。
配置示例:
[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
```
配置颜色主题,支持明暗模式切换。
基础配置示例:
# 日间模式
[[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 "选择颜色"
- indigo 和 blue 是最常用的主色调
- 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
```
配置字体。
=== "zensical.toml"
```toml
[project.theme.font]
text = "Roboto"
code = "Roboto Mono"
```
=== "mkdocs.yml"
```yaml
theme:
font:
text: Roboto
code: Roboto Mono
```
设置网站 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
```
定义网站的导航结构。
=== "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 = [...] 格式,结构清晰,易于维护
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 |
自动生成目录 | 自动生成页面目录 |
| 扩展 | 功能 | 示例 |
|---|---|---|
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~~ → |
[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"
!!! note "提示"
这是一个提示框
!!! warning "警告"
这是一个警告框
!!! tip "技巧"
这是一个技巧提示
```python
def hello():
print("Hello, Zensical!")
```
=== "Python"
```python
print("Hello")
```
=== "JavaScript"
```javascript
console.log("Hello");
```
- [x] 已完成的任务
- [ ] 未完成的任务
行内公式:$E=mc^2$
块级公式:
$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$
:smile: :heart: :rocket: :thumbsup:
!!! tip "更多示例" 详细的使用示例和说明请参考 Markdown 扩展使用指南。
以下是一个完整的、生产环境可用的配置示例,包含了所有常用配置:
[project]
# ===== 基本信息 =====
site_name = "我的 Zensical 项目"
site_url = "https://example.com"
site_description = "一个使用 Zensical 构建的文档网站"
site_author = "张三"
copyright = "Copyright © 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
```
参考资料:
完成本章学习后,你将能够:
学习本章内容前,你需要掌握:
小李是一名前端开发者,公司的老项目使用Vue 2和Options API编写。现在公司决定将项目升级到Vue 3,并逐步迁移到Composition API。小李需要:
Options API和Composition API是Vue提供的两种不同的组件编写方式:
这个对照表将帮助你:
Options API通过配置对象的方式组织代码,每个选项负责特定的功能:
export default {
data() {
return {
// 响应式数据
}
},
computed: {
// 计算属性
},
methods: {
// 方法
},
mounted() {
// 生命周期钩子
}
}
优点:
缺点:
Composition API通过组合函数的方式组织代码,相关逻辑可以放在一起:
<script setup>
import { ref, computed, onMounted } from 'vue';
// 所有逻辑都在setup中,可以按功能组织
const count = ref(0);
const doubled = computed(() => count.value * 2);
onMounted(() => {
// 生命周期逻辑
});
</script>
优点:
缺点:
下面是两种API风格的完整对照表:
| 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>
| 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>
| 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>
| 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>
| 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>
| 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>
| 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>
| 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>
| 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>
| 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>
| 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>
| 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}`);
});
在企业级项目中,通常不会一次性将所有组件从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>
迁移建议:
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>
优势分析:
错误示例:
<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()返回的是一个响应式引用对象,不是原始值.value
.value
错误示例:
<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
错误示例:
<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可以在组件挂载时立即执行一次<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>
<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>
<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>
需求描述
将下面的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>
实现提示
ref()定义响应式数据computed()定义计算属性onMounted()替代mounted钩子.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>
答案解析
ref()替代data(),因为firstName和lastName是基本类型computed()的用法与Options API类似,但需要使用.value访问refonMounted()替代mounted()钩子需求描述
创建一个useCounter组合式函数,实现以下功能:
实现提示
参考答案
// 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>
答案解析
组合式函数设计:
逻辑复用:
优势体现:
与Mixin对比:
在处理大规模监控 JSON 时,最隐形的杀手是 “对象实例化开销” 。
当你执行 JSON.parse 处理一个 100MB 的字符串时,内存占用并不是增加 100MB。
在监控后端分析场景,应引入 状态机解析。
JSONStream。它不会一次性把整个 JSON 加载进内存,而是像吃拉面一样,一根一根(一个节点一个节点)地处理。rows.* 路径,处理完一个对象后立即交给聚合引擎并释放内存,将内存波动控制在恒定范围内。Node.js 原生的 JSON.stringify 为了通用性,在每次调用时都会进行复杂的类型探测和属性遍历。
如果你上报的监控埋点格式是固定的(例如:{ event: string, duration: number }),那么预编译序列化是最佳选择。
在生成大型监控报告时,确保你构建的对象具有一致的形状。
stringify 时的查找效率。你应该意识到 JSON 的文本格式在大规模传输中是极度低效的(冗余的引号、重复的 Key、Base64 编码后的体积膨胀)。
在监控 SDK 上报阶段,通过字典映射减少 Payload:
{"errorMessage": "timeout", "errorCode": 504}
{"m": "timeout", "c": 504}
当 JSON 的解析 CPU 占用率超过 30% 时,必须考虑协议升级:
这是一个常见的误区。虽然 JSON 源于 JS,但它的规范(RFC 8259)比 JS 严格且局限得多。
在执行 JSON.stringify(obj) 时,JS 引擎会进行一套复杂的类型转换,而这些转换往往是非对称的:
undefined、函数、Symbol:
null。undefined。不可枚举属性:默认会被完全忽略。
BigInt:会直接抛出 TypeError,因为 JSON 规范中没有对应的大数表示协议。
JSON 的数值遵循 IEEE 754 双精度浮点数。如果你在处理前端监控中的高精纳秒级时间戳,直接序列化可能会导致精度被截断。
当你需要处理复杂的业务对象(比如含有循环引用或敏感数据)时,基础的 JSON.stringify 就不够用了。
toJSON:对象的自白如果一个对象拥有 toJSON 方法,序列化时会优先调用它。这在处理复杂类实例(Class)时非常有用:
JavaScript
class User {
constructor(name, pwd) { this.name = name; this.pwd = pwd; }
toJSON() { return { name: this.name }; } // 自动屏蔽敏感字段
}
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;
});
在 Node.js 服务端,大规模的 JSON 处理往往是 CPU 的头号杀手。
JSON.parse 比 JS 字面量快?这是一个反直觉的结论:解析一段字符串 JSON.parse('{"a":1}') 通常比 JS 引擎解析代码 {a:1} 快。
JSON.parse 载入,能有效缩短代码冷启动的解析时间(Parse Time)。JSON.stringify 在处理 10MB 以上的对象时,会阻塞 Event Loop 几十毫秒。在高并发环境下,这足以导致后续请求全部超时。__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