普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月23日掘金 前端

做一个简单的圆脸心情鼠标跟踪效果动态图

2025年5月23日 23:31

image.png

完整的代码如下:


<!DOCTYPE html>
<html>
<head>
    <title>互动表情</title>
    <style>
        /* 基础页面样式 */
        body {
            margin: 0;
            height: 100vh; /* 全屏高度 */
            display: flex;
            justify-content: center; /* 水平居中 */
            align-items: center; /* 垂直居中 */
            background: #f5f5f5; /* 浅灰色背景 */
            overflow: hidden; /* 隐藏溢出内容 */
            cursor: default; /* 默认鼠标指针 */
        }
        
        /* 表情容器 */
        .face-container {
            position: relative;
            width: 400px;
            height: 500px;
        }
        
        /* 脸部基础样式 */
        .face {
            position: relative;
            width: 400px;
            height: 400px;
            background: #FFD700; /* 金色背景 */
            border-radius: 50%; /* 圆形 */
            box-shadow: 0 0 40px rgba(0,0,0,0.15); /* 柔和阴影 */
            overflow: hidden; /* 隐藏溢出内容 */
        }
        
        /* 眼睛基础样式 */
        .eye {
            position: absolute;
            width: 70px;
            height: 70px;
            background: white; /* 白色眼白 */
            border-radius: 50%; /* 圆形 */
            top: 80px; /* 垂直位置 */
            z-index: 10; /* 确保在脸部上方 */
            box-shadow: 0 5px 15px rgba(0,0,0,0.2); /* 立体阴影 */
            transition: transform 0.2s ease-out; /* 平滑变换 */
            perspective: 1000px; /* 3D透视效果 */
        }
        
        /* 左眼定位 */
        .eye.left {
            left: 60px;
        }
        
        /* 右眼定位 */
        .eye.right {
            right: 60px;
        }
        
        /* 瞳孔样式 */
        .pupil {
            position: absolute;
            width: 30px;
            height: 30px;
            background: #222; /* 深灰色瞳孔 */
            border-radius: 50%; /* 圆形 */
            top: 20px;
            left: 20px;
            transition: all 0.1s ease; /* 平滑移动 */
            transform-style: preserve-3d; /* 保持3D变换 */
        }
        
        /* 瞳孔3D效果 */
        .pupil::before {
            content: '';
            position: absolute;
            width: 100%;
            height: 100%;
            background: #111; /* 更深的瞳孔颜色 */
            border-radius: 50%;
            transform: translateZ(-15px); /* 3D深度效果 */
        }
        
        /* 嘴巴容器 */
        .mouth-container {
            position: absolute;
            width: 200px;
            height: 100px;
            bottom: 80px; /* 底部位置 */
            left: 50%;
            transform: translateX(-50%); /* 水平居中 */
            overflow: hidden; /* 隐藏溢出内容 */
        }
        
        /* 嘴巴样式 */
        .mouth {
            position: absolute;
            width: 100%;
            height: 100%;
            background: #FF6B6B; /* 红色嘴巴 */
            border-radius: 0 0 100px 100px; /* 半圆形底部 */
            transition: all 0.8s cubic-bezier(0.25, 0.1, 0.25, 1); /* 弹性动画 */
        }
        
        /* 按钮容器 */
        .btn-container {
            position: absolute;
            width: 100%;
            bottom: 10px; /* 底部位置 */
            display: flex;
            justify-content: space-between; /* 按钮左右分布 */
            padding: 0 30px;
            box-sizing: border-box;
        }
        
        /* 基础按钮样式 */
        .btn {
            padding: 12px 24px;
            border: none;
            border-radius: 30px; /* 圆角按钮 */
            background: linear-gradient(145deg, #4CAF50, #81C784); /* 绿色渐变 */
            color: white;
            font-size: 18px;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 6px 12px rgba(0,0,0,0.2); /* 按钮阴影 */
            transition: all 0.3s; /* 悬停动画 */
        }
        
        /* 按钮悬停效果 */
        .btn:hover {
            transform: scale(1.1); /* 放大效果 */
            box-shadow: 0 8px 16px rgba(0,0,0,0.3); /* 更强的阴影 */
        }
        
        /* 否定按钮样式 */
        .btn-no {
            background: linear-gradient(145deg, #F44336, #E57373); /* 红色渐变 */
        }
        
        /* 彩花效果 */
        .confetti {
            position: absolute;
            width: 12px;
            height: 12px;
            animation: fall 3s ease-in forwards; /* 下落动画 */
            z-index: 5; /* 在脸部下方 */
            transform-origin: center bottom; /* 旋转中心 */
            opacity: 0.8; /* 半透明 */
            background-size: contain;
            background-repeat: no-repeat;
        }
        
        /* 彩花下落动画 */
        @keyframes fall {
            0% {
                transform: translateY(-100px) rotate(0deg) scale(0.8);
                opacity: 1;
            }
            100% {
                transform: translateY(600px) rotate(720deg) scale(1.2);
                opacity: 0;
            }
        }
        
        /* 脸颊样式 */
        .cheek {
            position: absolute;
            width: 50px;
            height: 30px;
            background: rgba(255,192,203,0.4); /* 粉色半透明 */
            border-radius: 50%; /* 圆形 */
            bottom: 90px; /* 嘴巴上方 */
            transition: all 0.5s ease; /* 平滑变化 */
        }
        
        /* 左脸颊定位 */
        .cheek.left {
            left: 50px;
        }
        
        /* 右脸颊定位 */
        .cheek.right {
            right: 50px;
        }
    </style>
</head>
<body>
    <!-- 主容器 -->
    <div class="face-container">
        <!-- 脸部 -->
        <div class="face">
            <!-- 左眼 -->
            <div class="eye left">
                <div class="pupil"></div>
            </div>
            <!-- 右眼 -->
            <div class="eye right">
                <div class="pupil"></div>
            </div>
            <!-- 左脸颊 -->
            <div class="cheek left"></div>
            <!-- 右脸颊 -->
            <div class="cheek right"></div>
            <!-- 嘴巴容器 -->
            <div class="mouth-container">
                <div class="mouth"></div>
            </div>
        </div>
        <!-- 按钮容器 -->
        <div class="btn-container">
            <button class="btn btn-no">不行</button>
            <button class="btn btn-yes">可以</button>
        </div>
    </div>

    <script>
        // 页面加载完成后初始化嘴巴形状
        document.addEventListener('DOMContentLoaded', () => {
            const mouth = document.querySelector('.mouth');
            mouth.style.borderRadius = "0 0 100px 100px";
        });

        // 鼠标移动时更新表情
        document.addEventListener('mousemove', (e) => {
            const face = document.querySelector('.face');
            const pupils = document.querySelectorAll('.pupil');
            const mouth = document.querySelector('.mouth');
            const cheeks = document.querySelectorAll('.cheek');
            
            // 获取脸部中心坐标
            const faceRect = face.getBoundingClientRect();
            const faceCenterX = faceRect.left + faceRect.width / 2;
            const faceCenterY = faceRect.top + faceRect.height / 2;
            
            // 鼠标坐标
            const mouseX = e.clientX;
            const mouseY = e.clientY;
            
            // 计算相对位置
            const relX = (mouseX - faceCenterX) / (faceRect.width / 2);
            const relY = (mouseY - faceCenterY) / (faceRect.height / 2);
            
            // 更新瞳孔位置
            pupils.forEach(pupil => {
                const eye = pupil.parentElement;
                const maxMove = 20; // 最大移动距离
                const moveX = relX * maxMove;
                const moveY = relY * maxMove * 0.5; // 垂直移动幅度较小
                
                // 应用3D变换
                pupil.style.transform = `translate(${moveX}px, ${moveY}px) translateZ(10px)`;
                
                // 根据鼠标位置缩放眼睛
                const scale = 1 + Math.abs(relX) * 0.3;
                eye.style.transform = `scale(${scale})`;
                eye.style.boxShadow = `0 ${5 + Math.abs(relX) * 5}px ${15 + Math.abs(relX) * 10}px rgba(0,0,0,0.3)`;
            });
            
            // 根据鼠标位置更新表情
            if (relX > 0) {
                // 右侧 - 开心表情
                const happiness = Math.min(1, relX);
                const curve = 100 + happiness * 80;
                mouth.style.borderRadius = `0 0 ${curve}px ${curve}px`;
                mouth.style.transform = `translateY(${happiness * -15}px) scale(${1 + happiness * 0.1})`;
                cheeks.forEach(cheek => {
                    cheek.style.transform = `scale(${1 + happiness * 0.2})`;
                    cheek.style.background = `rgba(255,192,203,${0.4 + happiness * 0.3})`;
                });
            } else {
                // 左侧 - 悲伤表情
                const sadness = Math.min(1, -relX);
                const curve = 100 + sadness * 80;
                mouth.style.borderRadius = `${curve}px ${curve}px 0 0`;
                mouth.style.transform = `translateY(${sadness * 40}px) scale(${1 - sadness * 0.05})`;
                cheeks.forEach(cheek => {
                    cheek.style.transform = `scale(${1 - sadness * 0.1})`;
                    cheek.style.background = `rgba(255,192,203,${0.4 - sadness * 0.2})`;
                });
            }
        });

        // "可以"按钮点击事件 - 触发彩花效果
        document.querySelector('.btn-yes').addEventListener('click', function() {
            this.blur(); // 移除焦点
            const faceContainer = document.querySelector('.face-container');
            const confettiTypes = [
                '💰', '🎉', '🎁', '✨', '❤️', '💎', '🏆', '⭐', '🍀', '🎈'
            ];
            
            // 创建单个彩花
            function createConfetti() {
                const confetti = document.createElement('div');
                confetti.className = 'confetti';
                // 随机选择彩花类型
                confetti.textContent = confettiTypes[Math.floor(Math.random() * confettiTypes.length)];
                confetti.style.fontSize = `${12 + Math.random() * 16}px`; // 随机大小
                confetti.style.color = `hsl(${Math.random() * 360}, 100%, 50%)`; // 随机颜色
                
                // 随机位置
                confetti.style.left = `${Math.random() * faceContainer.offsetWidth}px`;
                confetti.style.top = `-30px`;
                confetti.style.transform = `rotate(${Math.random() * 360}deg)`;
                confetti.style.opacity = `${0.7 + Math.random() * 0.3}`;
                confetti.style.animationDuration = `${2 + Math.random() * 2}s`;
                
                faceContainer.appendChild(confetti);
                
                // 3秒后移除彩花
                setTimeout(() => {
                    confetti.remove();
                }, 3000);
            }
            
            // 创建100个彩花,分批出现
            for (let i = 0; i < 100; i++) {
                setTimeout(createConfetti, i * 50);
            }
        });
    </script>
</body>
</html>

具体效果小伙伴们可以自行尝试下。

有兴趣的小伙伴可以优化下眼睛移动时的变化,同时也可以优化下嘴角的变化过渡

祝大家周末玩的开心。

Python匿名函数详解:lambda真的如此神秘?

作者 烛阴
2025年5月23日 23:30

一、匿名函数是什么?

在Python中,通常我们这样定义函数:

def add(x, y):
    return x + y

print(add(3, 5))  # 输出:8

但有时候,为了临时、简单的功能,写一个完整的函数未免有点冗长。因此,Python引入了匿名函数,也称lambda函数。

匿名函数最大的特点是:没有名字、适合简单的逻辑。

二、lambda语法及基础用法

匿名函数主要有以下特点:

  • lambda 只是一个表达式,函数体比 def 简单很多。
  • lambda 的主体是一个表达式,而不是一个代码块。仅仅能在 lambda 表达式中封装有限的逻辑进去。
  • lambda 函数拥有自己的命名空间,且不能访问自有参数列表之外或全局命名空间里的参数。

基本语法:

lambda 参数1, 参数2, ... : 表达式

示例:

add = lambda x, y: x + y
print(add(3, 5))  # 输出:8

三、高阶函数与匿名函数的组合

匿名函数通常与 mapfiltersortedreduce 等函数配合使用。

1. map函数

nums = [1, 2, 3, 4]
squared = list(map(lambda x: x ** 2, nums))
print(squared)  # 输出:[1, 4, 9, 16]

2. filter函数

过滤出所有的偶数:

nums = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)  # 输出:[2, 4, 6]

3. sorted函数自定义排序

按元组第二个元素排序:

arr = [(1, 2), (3, 1), (5, 0)]
print(sorted(arr, key=lambda x: x[1]))  # 输出:[(5, 0), (3, 1), (1, 2)]

四、lambda的进阶用法和注意点

  1. 可以作为任何需要函数对象的地方:
def apply_func(func, x, y):
    return func(x, y)

result = apply_func(lambda a, b: a * b, 2, 3)
print(result)  # 输出:6
  1. lambda表达式只能写一条表达式,不能写多条语句
# 正确
lambda x: x + 1

# 错误
# lambda x: x + 1; print(x)  # 不能有多条语句
  1. lambda是表达式而非语句,因此可内嵌于一些结构中,提高代码简洁度。

例如给列表按长度排序:

words = ['Python', 'Java', 'C']
print(sorted(words, key=lambda s: len(s)))  # ['C', 'Java', 'Python']

五、lambda的使用场景及最佳实践

1. 适合场景

  • 用于实现小型、一次性用途的函数;
  • 传递到高阶函数(如map、filter、sorted等)的简单逻辑;

2. 不适合场景

  • 逻辑较复杂时,推荐用普通def函数增强可读性;
  • 需要多语句处理,lambda无法胜任;

3. 语法糖与代码可读性

善用lambda可让Python代码简洁高效,但要适度使用,避免“过度匿名”导致的可读性下降。

六、扩展:lambda和函数式编程

Python虽然不是纯函数式语言,但lambda的加入,让列表推导式、更高阶的函数式编程得以实现。例如:

from functools import reduce

nums = [1, 2, 3, 4]
sum_all = reduce(lambda x, y: x + y, nums)
print(sum_all)  # 输出:10

如果你喜欢这篇文章,欢迎点赞、收藏和转发,更多Python干货内容敬请关注!

详解DNS是如何为域名找到对应的 IP 地址的。

2025年5月23日 21:59

这是一个经典的问题,话不多说,直接开干

1.浏览器DNS缓存

浏览器地址栏输入域名后,浏览器首先会去其本身的 DNS 缓存中查找是否有对应域名的记录,有就直接返回,没有就继续往下找

1.1 浏览器DNS缓存在哪里看呢?

在浏览器地址栏上输入这个 chrome://net-internals/#dns就可以看到

image.png 假设要看www.baidu.com的, 输入之后可以发现存在缓存记录

image.png

可以看到存在记录,看看是否使用缓存

image.png

勾选上停用缓存,可以看到查找dns的时间变长了。 image.png

2.系统DNS缓存

系统DNS缓存其实指的是host文件,位于C:/windows/system32/drivers/etc/hosts,在公司里,前端和后端同事联调的时候经常使用到它

127.0.0.1 www.baidu.com
192.168.1.1 www.google.com

如果在这个文件找到了就返回,没有就继续往下找

3.本地DNS服务器

如果在系统 DNS 也找不到域名的记录,那么浏览器就会去本地的 DNS 服务器(LDNS)上寻找该域名的记录。在这里的 LDNS 一般是指本机所设置的 DNS 服务器地址。

3.1 本地dns服务器指的是什么?在哪里?

本地域名服务器(Local DNS Server)一般指用户设备配置的 DNS 服务器地址,可能是路由器、ISP 提供或公共 DNS。它在 DNS 解析流程中承担递归查询任务,是用户访问互联网的关键中间角色。常见情况如下:

  默认网络配置中的 DNS 服务器

  • 来源:由网络自动分配(如通过 DHCP)或用户手动设置。

  • 作用:负责接收设备发起的 DNS 查询请求,并递归/迭代查询其他 DNS 服务器以获取结果。

  • 典型示例

家庭/企业路由器:如  192.168.1.1 (局域网内默认网关通常兼任 DNS 转发器)。

ISP(互联网服务商)提供的 DNS:如中国电信的  114.114.114.114 。

公共 DNS:如 Google 的  8.8.8.8  或 Cloudflare 的  1.1.1.1 。

3.2 怎么查看本地dns服务器?

命令提示符操作

ipconfig /all | findstr "DNS Servers"

 可以看到

image.png

4.根DNS服务器

当 LDNS 服务器还是找不到域名对应的记录时,那么其就会去根 DNS 服务器去寻找域名对应的记录。根 DNS 服务器全球只有 13 台,所以在 LDNS 服务器建立的时候,其会写死一个根 DNS 服务器的 IP 地址列表。

但实际上根 DNS 服务器并不存储所有域名的信息,其只存储顶级域名的解析记录。即对于我们访问的域名 www.baidu.com 来说,根 DNS 服务器并不能告诉我们 www.baidu.com 这个域名对应的 IP 是多少,它只能告诉我们 .com 这个顶级域名应该找哪个 DNS 服务器解析,然后把对应顶级 DNS 服务器的 IP 地址告诉我们。

如图所示:可以理解为.com. 右边的.就是根域,只不过可以隐藏了。 image.png

image.png

5.顶级DNS服务器

根 DNS 服务器告诉我们可以找对应的 .com 顶级 DNS 服务器解析。但实际上顶级 DNS 服务器只能解析二级域名的地址,即它只能告诉你:baidu.com 这个域名应该找哪台 DNS 服务器去解析。

这里我们从顶级 DNS 服务器解析 baidu.com 这个域名得到的 DNS 服务器,其实就是你买域名时用于管理解析的服务器。而且也是你设置域名 NS 记录时,设置的 DNS 服务器的地址。

5.1 NS记录是什么

NS 记录(Name Server Record) 是域名系统(DNS)中的一种关键记录类型,用于指定哪些服务器是该域名的权威DNS服务器(即负责管理该域名的解析)。简单来说,NS 记录告诉互联网:“查询这个域名时,请找以下指定的 DNS 服务器获取解析结果”。 image.png

5.2 NS记录需要自己配置吗

默认情况:无需自行配置

如果您的域名直接使用注册商或托管平台提供的DNS服务,NS记录通常由服务商自动配置,无需手动干预。
示例

  • 在GoDaddy注册域名,默认NS记录指向其服务器(如ns1.godaddy.com)。
  • 在阿里云购买域名,默认使用阿里云DNS(如dns9.hichina.com)。

需要自行配置的场景

使用第三方DNS服务(如Cloudflare、AWS Route 53)

步骤

  1. 在第三方DNS服务商处创建域名托管区域。
  2. 获取其提供的NS服务器地址(如Cloudflare的kate.ns.cloudflare.com)。
  3. 返回域名注册商控制台,将NS记录修改为第三方提供的地址。

6. 次级域名服务器

上一步我们知道 .com 这个顶级域名告诉应该找哪台 DNS 服务器去解析(这台服务器我们称为次级域名服务器),其实这台 DNS 服务器就是我们购买域名时设置的用于解析的 DNS 服务器。在这台 DNS 服务器上,我们通过设置一条 A 记录(A 记录就是域名到 IP 地址的映射)就可以让某个域名与 IP 联系起来。

6.1 什么是A记录呢?


A记录(Address Record) 是DNS系统中最基础的记录类型之一,用于将域名直接映射到IPv4地址。

如下图所示,我设置的是一条针对 hello.csyimg.com 域名的 A 记录,其指向了 192.168.1.1 这个 IP。如果最终找不到 A 记录,那浏览器就会报 DNS 解析错误。

image.png

参考文献

DNS自述:我是如何为域名找到家的-腾讯云开发者社区-腾讯云

一文讲清什么是A记录,CNAME记录,NS记录?

Three.js 完全学习指南(十二)数据可视化

作者 鲫小鱼
2025年5月23日 18:33

数据可视化

在 Three.js 中,我们可以创建各种形式的数据可视化效果。本章将介绍如何使用 Three.js 创建 3D 数据可视化,包括基础图表、动态数据更新和交互控制等。

基础图表系统

1. 柱状图

3D 柱状图示例

图 12.1: 3D 柱状图示例

// 柱状图类
class BarChart {
    constructor(scene, data, options = {}) {
        this.scene = scene;
        this.data = data;
        this.options = {
            width: options.width || 10,
            height: options.height || 5,
            depth: options.depth || 0.5,
            spacing: options.spacing || 0.2,
            colors: options.colors || [0x2194ce, 0x32cd32, 0xffd700],
            ...options
        };

        this.bars = [];
        this.createBars();
    }

    createBars() {
        const { width, height, depth, spacing, colors } = this.options;
        const barWidth = (width - (this.data.length - 1) * spacing) / this.data.length;

        this.data.forEach((value, index) => {
            const barHeight = (value / Math.max(...this.data)) * height;
            const geometry = new THREE.BoxGeometry(barWidth, barHeight, depth);
            const material = new THREE.MeshPhongMaterial({
                color: colors[index % colors.length],
                shininess: 30
            });

            const bar = new THREE.Mesh(geometry, material);
            bar.position.x = (index * (barWidth + spacing)) - (width / 2) + (barWidth / 2);
            bar.position.y = barHeight / 2;

            this.bars.push(bar);
            this.scene.add(bar);
        });
    }

    updateData(newData) {
        const { height } = this.options;
        const maxValue = Math.max(...newData);

        this.data = newData;
        this.bars.forEach((bar, index) => {
            const newHeight = (newData[index] / maxValue) * height;
            bar.scale.y = newHeight / bar.geometry.parameters.height;
            bar.position.y = newHeight / 2;
        });
    }
}

2. 折线图

// 折线图类
class LineChart {
    constructor(scene, data, options = {}) {
        this.scene = scene;
        this.data = data;
        this.options = {
            width: options.width || 10,
            height: options.height || 5,
            lineWidth: options.lineWidth || 0.05,
            color: options.color || 0x2194ce,
            ...options
        };

        this.line = null;
        this.points = [];
        this.createLine();
    }

    createLine() {
        const { width, height, lineWidth, color } = this.options;
        const points = [];

        this.data.forEach((value, index) => {
            const x = (index / (this.data.length - 1) - 0.5) * width;
            const y = (value / Math.max(...this.data) - 0.5) * height;
            points.push(new THREE.Vector3(x, y, 0));
        });

        const curve = new THREE.CatmullRomCurve3(points);
        const geometry = new THREE.TubeGeometry(curve, 64, lineWidth, 8, false);
        const material = new THREE.MeshPhongMaterial({ color });

        this.line = new THREE.Mesh(geometry, material);
        this.scene.add(this.line);

        // 创建数据点
        points.forEach(point => {
            const sphereGeometry = new THREE.SphereGeometry(lineWidth * 1.5, 16, 16);
            const sphereMaterial = new THREE.MeshPhongMaterial({ color });
            const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
            sphere.position.copy(point);
            this.points.push(sphere);
            this.scene.add(sphere);
        });
    }

    updateData(newData) {
        const { width, height } = this.options;
        const maxValue = Math.max(...newData);
        const points = [];

        newData.forEach((value, index) => {
            const x = (index / (newData.length - 1) - 0.5) * width;
            const y = (value / maxValue - 0.5) * height;
            points.push(new THREE.Vector3(x, y, 0));
        });

        // 更新线条
        const curve = new THREE.CatmullRomCurve3(points);
        const newGeometry = new THREE.TubeGeometry(curve, 64, this.options.lineWidth, 8, false);
        this.line.geometry.dispose();
        this.line.geometry = newGeometry;

        // 更新数据点
        points.forEach((point, index) => {
            this.points[index].position.copy(point);
        });
    }
}

动态数据系统

1. 数据管理器

// 数据管理器
class DataManager {
    constructor() {
        this.data = new Map();
        this.subscribers = new Map();
    }

    setData(key, value) {
        this.data.set(key, value);
        this.notifySubscribers(key);
    }

    getData(key) {
        return this.data.get(key);
    }

    subscribe(key, callback) {
        if (!this.subscribers.has(key)) {
            this.subscribers.set(key, new Set());
        }
        this.subscribers.get(key).add(callback);
    }

    unsubscribe(key, callback) {
        if (this.subscribers.has(key)) {
            this.subscribers.get(key).delete(callback);
        }
    }

    notifySubscribers(key) {
        if (this.subscribers.has(key)) {
            const value = this.data.get(key);
            this.subscribers.get(key).forEach(callback => callback(value));
        }
    }
}

2. 动画控制器

// 动画控制器
class AnimationController {
    constructor(duration = 1000) {
        this.duration = duration;
        this.animations = new Map();
    }

    animate(startValue, endValue, onUpdate, onComplete) {
        const startTime = performance.now();
        const animation = {
            startValue,
            endValue,
            onUpdate,
            onComplete
        };

        const id = Math.random().toString(36).substr(2, 9);
        this.animations.set(id, animation);

        return id;
    }

    update() {
        const currentTime = performance.now();

        this.animations.forEach((animation, id) => {
            const elapsed = currentTime - animation.startTime;
            const progress = Math.min(elapsed / this.duration, 1);

            if (progress < 1) {
                const currentValue = this.interpolate(
                    animation.startValue,
                    animation.endValue,
                    progress
                );
                animation.onUpdate(currentValue);
            } else {
                animation.onUpdate(animation.endValue);
                animation.onComplete?.();
                this.animations.delete(id);
            }
        });
    }

    interpolate(start, end, progress) {
        if (typeof start === 'number') {
            return start + (end - start) * progress;
        } else if (start instanceof THREE.Vector3) {
            return new THREE.Vector3(
                this.interpolate(start.x, end.x, progress),
                this.interpolate(start.y, end.y, progress),
                this.interpolate(start.z, end.z, progress)
            );
        }
        return end;
    }
}

交互系统

1. 图表交互器

// 图表交互器
class ChartInteractor {
    constructor(camera, renderer) {
        this.camera = camera;
        this.renderer = renderer;
        this.raycaster = new THREE.Raycaster();
        this.mouse = new THREE.Vector2();
        this.hoveredObject = null;
        this.setupEventListeners();
    }

    setupEventListeners() {
        this.renderer.domElement.addEventListener('mousemove', (event) => {
            this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
        });

        this.renderer.domElement.addEventListener('click', (event) => {
            this.raycaster.setFromCamera(this.mouse, this.camera);
            const intersects = this.raycaster.intersectObjects(scene.children, true);

            if (intersects.length > 0) {
                this.onObjectClick(intersects[0].object);
            }
        });
    }

    update(scene) {
        this.raycaster.setFromCamera(this.mouse, this.camera);
        const intersects = this.raycaster.intersectObjects(scene.children, true);

        if (intersects.length > 0) {
            const object = intersects[0].object;
            if (this.hoveredObject !== object) {
                this.onObjectHover(object);
            }
            this.hoveredObject = object;
        } else if (this.hoveredObject) {
            this.onObjectUnhover(this.hoveredObject);
            this.hoveredObject = null;
        }
    }

    onObjectHover(object) {
        if (object.material) {
            object.material.emissive.setHex(0x333333);
        }
    }

    onObjectUnhover(object) {
        if (object.material) {
            object.material.emissive.setHex(0x000000);
        }
    }

    onObjectClick(object) {
        console.log('Clicked object:', object);
    }
}

2. 相机控制器

// 相机控制器
class CameraController {
    constructor(camera, renderer) {
        this.camera = camera;
        this.renderer = renderer;
        this.controls = new THREE.OrbitControls(camera, renderer.domElement);
        this.setupControls();
    }

    setupControls() {
        this.controls.enableDamping = true;
        this.controls.dampingFactor = 0.05;
        this.controls.screenSpacePanning = false;
        this.controls.minDistance = 5;
        this.controls.maxDistance = 20;
        this.controls.maxPolarAngle = Math.PI / 2;
    }

    update() {
        this.controls.update();
    }

    focusOnObject(object) {
        const box = new THREE.Box3().setFromObject(object);
        const center = box.getCenter(new THREE.Vector3());
        const size = box.getSize(new THREE.Vector3());

        const maxDim = Math.max(size.x, size.y, size.z);
        const fov = this.camera.fov * (Math.PI / 180);
        let cameraZ = Math.abs(maxDim / Math.sin(fov / 2));

        const cameraPosition = new THREE.Vector3(
            center.x,
            center.y,
            center.z + cameraZ
        );

        this.camera.position.copy(cameraPosition);
        this.controls.target.copy(center);
    }
}

实战:创建数据可视化场景

让我们创建一个完整的数据可视化场景:

// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);

// 创建相机
const camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);
camera.position.set(0, 5, 10);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

// 创建光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
directionalLight.castShadow = true;
scene.add(directionalLight);

// 创建数据管理器
const dataManager = new DataManager();

// 创建图表
const barChart = new BarChart(scene, [10, 20, 15, 25, 30]);
const lineChart = new LineChart(scene, [5, 15, 10, 20, 25]);

// 创建控制器
const cameraController = new CameraController(camera, renderer);
const chartInteractor = new ChartInteractor(camera, renderer);
const animationController = new AnimationController();

// 订阅数据更新
dataManager.subscribe('chartData', (newData) => {
    animationController.animate(
        barChart.data,
        newData,
        (value) => barChart.updateData(value),
        () => console.log('Animation complete')
    );
});

// 动画循环
function animate() {
    requestAnimationFrame(animate);

    // 更新控制器
    cameraController.update();
    chartInteractor.update(scene);
    animationController.update();

    // 渲染场景
    renderer.render(scene, camera);
}

animate();

// 窗口大小改变时更新
window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

// 模拟数据更新
setInterval(() => {
    const newData = Array.from({ length: 5 }, () => Math.random() * 30);
    dataManager.setData('chartData', newData);
}, 3000);

练习

  1. 创建一个基础的 3D 柱状图
  2. 实现折线图的数据更新动画
  3. 添加图表交互功能
  4. 实现相机聚焦效果

下一步学习

在下一章中,我们将学习:

  • 游戏开发基础
  • 物理引擎集成
  • 碰撞检测
  • 游戏逻辑实现

借V8发动机之钥,开启Chrome V8引擎认知大门

作者 夏天1995
2025年5月23日 17:59

car.jpg

❓ 什么是v8引擎

汽车 V8发动机:

具有8个气缸的V型发动机配置。这里的"V"代表的是气缸排列的形状,即气缸成V字形排列。在V8引擎中,气缸通常分为两组,每组4个气缸,两组气缸之间以一定的角度(通常是90度)相对排列,形成一个V字形。

Chrome V8定义:

使用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome与Node.js等。 是一个接收JavaScript代码,编译JavaScript代码然后执行的C++程序,编译后的代码能在多种操作系统、多种处理器上运行。


🏠 V8:强大的JavaScript引擎

在为数不多JavaScript引擎中,V8无疑是最流行的,Chrome与Node.js都使用了V8引擎,Chrome的市场占有率高达66%,而Node.js是JS后端编程的事实标准。国内的众多浏览器,其实都是基于Chromium浏览器开发,而Chromium相当于开源版本的Chrome,自然也是基于V8引擎的。神奇的是,就连浏览器界的独树一帜的Microsoft也投靠了Chromium阵营。另外,Electron也是基于Node.js与Chromium来开发桌面应用,也是基于V8的。

chrome.jpg

🕵️ V8核心模块

v8架构.png

核心模块对照表

汽车部件 V8引擎组件 核心作用
燃油喷射 Parser解析器 Parser将源代码转换为可执行燃料(AST),类似燃油喷射系统供给能量原料
点火系统 Ignition解释器 Ignition生成紧凑字节码,也能执行字节码,是代码执行的启动器,负责"点燃"字节码的执行流程,符合点火系统功能特征
涡轮增压 TurboFan编译器 对热点代码进行JIT优化,提升执行效率,类似增压提升动力
ECU控制单元 内存管理 精准控制内存分配与回收,如堆空间和栈空间管理
排气系统 Orinoco垃圾回收器GC 清理无用内存(如标记-清除算法),保持引擎高效运转

🔍 旧版本V8执行全流程(2017年前)

2017 v8.png

旧版缺陷:
  1. 内存占用高:直接生成机器码导致内存消耗大。
  2. 优化不灵活:Crankshaft无法处理新语法(如async/await)。
  3. 启动速度慢:全量编译拖慢首次执行效率。

🔍 现代V8执行全流程(2025)

1. 全链路流程

v8.png

关键阶段:

  1. 解析阶段:Parser将代码转换为AST,延迟解析未调用函数以节省内存。
  2. 字节码生成:Ignition生成紧凑字节码(内存占用仅为机器码的25%-50%)。
  3. 反馈收集:Ignition在解释执行过程中收集热点函数以及类型推导信息。
  4. JIT优化:TurboFan根据类型反馈,将字节码(byteCode)生成优化机器码(Optimized Machine Code)。
  5. 去优化阶段:机器码逆向还原为字节码。原因是Ignition收集的信息可能是错误的,比如add函数的参数之前是整数,后来又变成了字符串。生成的Optimized Machine Code已经假定add函数的参数是整数,那当然是错误的,于是需要进行Deoptimization。
Deoptimization示例
function add(x, y) {
    return x + y;
}
 
add(1, 2); // Ignition解释器执行过程推导add函数接收的参数类型为number型,TurboFan编译为Machine Code
add("1", "2"); // 后续某次执行,参数却成了string类型,导致去优化(Deoptimization)产生.

🎯 V8 TurboFan编译器的优化触发机制

核心触发逻辑

TurboFan编译器的优化并非仅依赖固定执行次数,而是基于动态热点检测的综合判断:

  • 热点代码识别:通过两种计数器实现

    • 方法调用计数器:统计函数被调用的频率
    • 回边计数器:统计循环体的执行次数(如forwhile循环)
  • 类型反馈收集:在Ignition解释执行期间,记录变量类型、分支预测等运行时信息,形成优化假设

  • 阈值动态调整:实际触发优化的阈值由V8内部算法动态决定,通常根据代码复杂度和执行频率综合评估,可能从数千次到数十万次不等

🎭 v8 新老架构对比

特性 2017年前(Crankshaft) 2017年后(TurboFan + Ignition)
编译层级 直接生成机器码 字节码 → TurboFan优化
内存占用 高(机器码体积大) 低(字节码紧凑)
启动速度 慢(需完整编译) 快(惰性解析 + 预编译)

💡 性能编码建议

对象操作优化

1.隐藏类一致性
// ❌ 错误:动态增删属性导致隐藏类变更
const obj = {};
obj.x = 1;      // 隐藏类 A
obj.y = 2;      // 隐藏类 B
delete obj.x;   // 隐藏类 C(性能杀手)

// ✅ 正确:一次性初始化属性
class Point {
  constructor(x, y) {
    this.x = x;  // 固定隐藏类
    this.y = y;
  }
}
2. 避免多态对象
// ❌ 错误:相同属性不同顺序导致多态
const obj1 = { x: 1, y: 2 };
const obj2 = { y: 2, x: 1 }; // 隐藏类不同!

// ✅ 正确:统一属性顺序
function createObj(x, y) {
  return { x, y }; // 保证所有实例隐藏类一致
}

类型稳定性优化

1. 数值类型稳定

// ❌ 错误:类型震荡触发去优化
function sum(a, b) {
  return a + b; // 可能处理 number/string
}
sum(1, 2);     // 优化为整数加法
sum("1", "2"); // 去优化回字节码解释

// ✅ 正确:强制类型约束
function typedSum(a: number, b: number) {
  return a + b; // 类型稳定
}
2. 数组类型优化
// ❌ 错误:稀疏数组拖慢访问
const arr = [];
arr[1000000] = 1; // 创建空洞数组

// ✅ 正确:预分配或使用类型化数组
const buffer = new Float64Array(1024); // 直接内存访问
for (let i = 0; i < buffer.length; i++) {
  buffer[i] = i * 0.1; // 比普通数组快 5-10 倍
}

函数设计优化

1. 函数内联阈值
// ❌ 错误:过大函数无法内联
function bigFunc() {
  // 超过 600 字节码长度(约 50 行代码)
}

// ✅ 正确:拆分为小函数
function optimized() {
  step1();
  step2(); // 每个函数可被单独优化
}
2. 参数处理优化
// ❌ 错误:arguments 导致去优化
function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i]; // 破坏类型推断
  }
  return total;
}

// ✅ 正确:使用剩余参数
function safeSum(...nums: number[]) {
  return nums.reduce((a, b) => a + b, 0);
}

内存管理优化

1. 及时解除引用
// 大对象及时回收
function processData() {
  const bigData = new ArrayBuffer(1024 * 1024 * 100); // 100MB
  // ...处理数据
  bigData = null; // 主动触发 GC
}
2. 避免闭包滥用
// ❌ 错误:闭包捕获外部变量
function createCounter() {
  let count = 0; // 被闭包捕获,无法栈分配
  return () => count++;
}

// ✅ 正确:使用类封装
class Counter {
  constructor() { this.count = 0; }
  increment() { this.count++; }
}

异步与并发优化

1. 任务分片
// 长任务拆分为微任务队列
function processChunk(data, chunkSize) {
  let index = 0;
  function next() {
    const chunk = data.slice(index, index + chunkSize);
    // 处理当前分片
    if (index < data.length) {
      Promise.resolve().then(next); // 避免阻塞主线程
    }
  }
  next();
}
2. Worker 线程
// 将计算密集型任务分流
const worker = new Worker('compute.js');
worker.postMessage(largeData);
worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

V8 优化检测工具

# 查看优化日志
node --trace-opt myScript.js

# 使用node命令(node版本为12.6.0)的`--print-bytecode`选项,打印出Ignition生成的Bytecode:
node --print-bytecode myScript.js

# 使用node命令的`--print-code`以及`--print-opt-code`选项,打印出TurboFan生成的汇编代码:
node --print-code --print-opt-code myScript.js

# 监控去优化事件
node --trace-deopt myScript.js

# 生成优化流程图
node --turbo-profiling myScript.js

🚨 性能优化黄金法则

  1. 类型即速度:保持变量类型稳定
  2. 结构即性能:固定对象/数组结构
  3. 内存即时间:主动管理内存生命周期
  4. 工具即眼睛:善用 V8 诊断工具

通过将代码特性与 V8 的优化路径对齐,可使 JavaScript 执行效率提升 3-10 倍。建议结合 Chrome DevTools 的 Performance 面板和 Node.js 的 --prof 参数进行深度性能剖析。

JavaScript设计模式 / 高阶函数 / 柯里化函数 - 汇总

作者 千度麒麟
2025年5月23日 17:51

§ 创建型模式

1. 工厂模式 (Factory Pattern)

// 工厂函数创建不同类型的产品
class ProductA { 
    getName() { 
        return 'Product A'; 
    }
}
class ProductB {
    getName() {
        return 'Product B';
    }
}
function createProduct(type) {
    if (type === 'A') {
        return new ProductA();
    } else if (type === 'B') {
        return new ProductB();
    }
}
const product1 = createProduct('A');
console.log(product1.getName()); // 输出: Product A

特性:通过工厂函数创建对象,隐藏具体实现细节,客户端只需知道接口。

应用场景:动态创建不同类型对象,如表单控件生成、日志记录器。

  • 优点:解耦客户端与具体产品类,符合单一职责原则

  • 缺点:添加新产品需要修改工厂类,违反开闭原则

1. Q: 工厂模式与直接使用new创建对象有什么区别?

A: 工厂模式将对象创建逻辑集中管理,客户端无需知道具体类名,符合"开闭原则"(对扩展开放,对修改关闭),当需要添加新产品时只需扩展工厂类而不用修改客户端代码。

2. Q: 什么时候应该使用简单工厂而不是抽象工厂?

A: 当产品种类较少且不会频繁变化时使用简单工厂;当产品族需要一起创建或有复杂层次结构时使用抽象工厂。

2. 单例模式 (Singleton Pattern)

const Singleton = (function() {
  let instance;
  function createInstance() {
    return {
      name: 'Singleton Instance',
      getName() {
        return this.name;
      }
    };
  }
  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // 输出: true

特性:确保类只有一个实例,并提供全局访问点。

应用场景:全局状态管理、配置对象、数据库连接池。

  • 优点:严格控制实例访问,节省内存

  • 缺点:难以测试(全局状态),违反单一职责原则

3. 建造者模式(Builder Pattern)

class CarBuilder {
  constructor() {
    this.car = {};
  }
 
  setModel(model) {
    this.car.model = model;
    return this;
  }
 
  setColor(color) {
    this.car.color = color;
    return this;
  }
 
  build() {
    return this.car;
  }
}
 
const car = new CarBuilder()
  .setModel('Tesla')
  .setColor('Red')
  .build();
 
console.log(car); // 输出: { model: 'Tesla', color: 'Red' }

特性:分步骤构建复杂对象,允许客户端灵活配置。

应用场景:分步创建复杂对象,如文件上传配置、UI 组件构建。

4. 原型模式(Prototype Pattern)

const userPrototype = {
  name: 'Default User',
  sayHello() {
    console.log(`Hello, I'm ${this.name}`);
  },
  clone() {
    return Object.create(this);
  }
};
 
const user1 = userPrototype.clone();
user1.name = 'Alice';
user1.sayHello(); // 输出: Hello, I'm Alice
 
const user2 = userPrototype.clone();
user2.name = 'Bob';
user2.sayHello(); // 输出: Hello, I'm Bob

特性:通过克隆现有对象创建新实例,避免重复初始化。

应用场景:对象初始化成本高时,如复杂 DOM 节点、游戏角色。

§ 结构型模式

1. 适配器模式(Adapter Pattern)

// 旧接口
class OldMap {
  display() {
    console.log('Displaying old map');
  }
}
 
// 新接口
class NewMap {
  show() {
    console.log('Displaying new map');
  }
}
 
// 适配器
class MapAdapter {
  constructor(map) {
    this.map = map;
  }
 
  display() {
    if (this.map.show) {
      this.map.show();
    }
  }
}
 
const oldMap = new OldMap();
const newMap = new NewMap();
const adaptedMap = new MapAdapter(newMap);
 
oldMap.display(); // 输出: Displaying old map
adaptedMap.display(); // 输出: Displaying new map

特性:使不兼容接口能够一起工作

应用场景:集成第三方库、新旧接口兼容

2. 装饰器模式(Decorator Pattern)

class Coffee {
  cost() {
    return 5;
  }
}
 
class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
 
  cost() {
    return this.coffee.cost();
  }
}
 
class MilkDecorator extends CoffeeDecorator {
  cost() {
    return super.cost() + 2;
  }
}
 
class SugarDecorator extends CoffeeDecorator {
  cost() {
    return super.cost() + 1;
  }
}
 
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 输出: 8

特性:动态为对象添加功能,不修改原对象。

应用场景:扩展对象功能,如日志记录、权限校验。

  • 优点:比继承更灵活,符合开闭原则

  • 缺点:多层装饰会增加复杂性,调试困难

3. 代理模式(Proxy Pattern)

// 真实对象
class RealSubject {
  request() {
    console.log('RealSubject: Handling request.');
    return 'real-data';
  }
}
 
// 代理对象
class ProxySubject {
  constructor(realSubject) {
    this.realSubject = realSubject;
  }
 
  request() {
    if (this.checkAccess()) {
      console.log('Proxy: Pre-processing request.');
      const result = this.realSubject.request();
      console.log('Proxy: Post-processing request.');
      return result;
    }
  }
 
  checkAccess() {
    console.log('Proxy: Checking access permissions.');
    return true;
  }
}
 
const realSubject = new RealSubject();
const proxy = new ProxySubject(realSubject);
console.log(proxy.request());
/* 输出:
Proxy: Checking access permissions.
Proxy: Pre-processing request.
RealSubject: Handling request.
Proxy: Post-processing request.
real-data
*/

特性:为对象提供代理,控制访问或延迟初始化。

应用场景:延迟加载、访问控制。

1. Q: 装饰器模式与代理模式有什么区别?

A: 关键区别在于目的:

装饰器:动态添加功能(透明增强)

代理:控制访问(可能限制功能)

装饰器通常透明地增强对象,而代理可能完全改变行为

4. 外观模式(Facade Pattern)

class SubsystemA {
  operationA() {
    console.log('Subsystem A operation');
  }
}
 
class SubsystemB {
  operationB() {
    console.log('Subsystem B operation');
  }
}
 
class Facade {
  constructor() {
    this.subsystemA = new SubsystemA();
    this.subsystemB = new SubsystemB();
  }
 
  operation() {
    this.subsystemA.operationA();
    this.subsystemB.operationB();
  }
}
 
const facade = new Facade();
facade.operation();
/* 输出:
Subsystem A operation
Subsystem B operation
*/

特性:为复杂子系统提供简化接口。

应用场景:简化复杂库或模块的调用。

§ 行为型模式

1. 观察者模式(Observer Pattern)

class Weather {
  constructor() {
    this.observers = [];
  }
 
  attach(observer) {
    this.observers.push(observer);
  }
 
  notify(temp) {
    this.observers.forEach(observer => observer.update(temp));
  }
 
  setTemp(temp) {
    this.notify(temp);
  }
}
 
class Observer {
  constructor(name) {
    this.name = name;
  }
 
  update(temp) {
    console.log(`${this.name} received temperature: ${temp}°C`);
  }
}
 
const weather = new Weather();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
 
weather.attach(observer1);
weather.attach(observer2);
weather.setTemp(25);
/* 输出:
Observer 1 received temperature: 25°C
Observer 2 received temperature: 25°C
*/

特性:定义一对多依赖关系,对象状态变化时通知所有观察者。

应用场景:事件处理、数据绑定。

2. 策略模式(Strategy Pattern)

class SortContext {
  constructor(strategy) {
    this.strategy = strategy;
  }
 
  setStrategy(strategy) {
    this.strategy = strategy;
  }
 
  execute(data) {
    return this.strategy.sort(data);
  }
}
 
class AscendingSort {
  sort(data) {
    return [...data].sort((a, b) => a - b);
  }
}
 
class DescendingSort {
  sort(data) {
    return [...data].sort((a, b) => b - a);
  }
}
 
const data = [3, 1, 4, 1, 5, 9, 2, 6];
const ascendingSort = new AscendingSort();
const descendingSort = new DescendingSort();
 
const context = new SortContext(ascendingSort);
console.log(context.execute(data)); // 输出: [1, 1, 2, 3, 4, 5, 6, 9]
 
context.setStrategy(descendingSort);
console.log(context.execute(data)); // 输出: [9, 6, 5, 4, 3, 2, 1, 1]

特性:封装一系列算法,客户端可动态切换。

应用场景:动态选择算法,如排序、表单验证规则。

3. 命令模式(Command Pattern)

class Light {
  turnOn() {
    console.log('Light is on');
  }
 
  turnOff() {
    console.log('Light is off');
  }
}
 
class Command {
  constructor(receiver) {
    this.receiver = receiver;
  }
 
  execute() {}
}
 
class TurnOnCommand extends Command {
  execute() {
    this.receiver.turnOn();
  }
}
 
class TurnOffCommand extends Command {
  execute() {
    this.receiver.turnOff();
  }
}
 
class RemoteControl {
  constructor() {
    this.command = null;
  }
 
  setCommand(command) {
    this.command = command;
  }
 
  pressButton() {
    this.command.execute();
  }
}
 
const light = new Light();
const turnOnCommand = new TurnOnCommand(light);
const turnOffCommand = new TurnOffCommand(light);
const remote = new RemoteControl();
 
remote.setCommand(turnOnCommand);
remote.pressButton(); // 输出: Light is on
 
remote.setCommand(turnOffCommand);
remote.pressButton(); // 输出: Light is off

特性:将请求封装为对象,支持撤销、重做或队列操作。

应用场景:实现撤销/重做功能、任务队列。

4. 状态模式(State Pattern)

class Order {
  constructor() {
    this.state = new PendingState(this);
  }
 
  setState(state) {
    this.state = state;
  }
 
  request() {
    this.state.handle();
  }
}
 
class PendingState {
  constructor(order) {
    this.order = order;
  }
 
  handle() {
    console.log('Order is pending');
    this.order.setState(new ProcessingState(this.order));
  }
}
 
class ProcessingState {
  constructor(order) {
    this.order = order;
  }
 
  handle() {
    console.log('Order is processing');
    this.order.setState(new ShippedState(this.order));
  }
}
 
class ShippedState {
  constructor(order) {
    this.order = order;
  }
 
  handle() {
    console.log('Order is shipped');
  }
}
 
const order = new Order();
order.request(); // 输出: Order is pending
order.request(); // 输出: Order is processing
order.request(); // 输出: Order is shipped

特性:允许对象根据内部状态改变行为。

应用场景:状态机,如订单状态管理、游戏角色状态切换。

§ 其他常见模式

1. 迭代器模式(Iterator Pattern)

class Iterator {
  constructor(data) {
    this.data = data;
    this.index = 0;
  }
 
  next() {
    if (this.index < this.data.length) {
      return { value: this.data[this.index++], done: false };
    } else {
      return { done: true };
    }
  }
}
 
const data = [1, 2, 3];
const iterator = new Iterator(data);
 
let result = iterator.next();
while (!result.done) {
  console.log(result.value);
  result = iterator.next();
}
/* 输出:
1
2
3
*/

特性:提供统一接口遍历集合元素。

应用场景:遍历数组、链表等数据结构。

2. 发布-订阅模式(Pub/Sub Pattern)

class EventBus {
  constructor() {
    this.events = {};
  }
 
  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }
 
  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
}
 
const eventBus = new EventBus();
 
eventBus.subscribe('event1', data => {
  console.log('Subscriber 1 received:', data);
});
 
eventBus.subscribe('event1', data => {
  console.log('Subscriber 2 received:', data);
});
 
eventBus.publish('event1', 'Hello World');
/* 输出:
Subscriber 1 received: Hello World
Subscriber 2 received: Hello World
*/

特性:解耦发布者与订阅者,支持一对多通信。

应用场景:事件总线、消息队列。

3. 模块模式(Module Pattern)

const CounterModule = (function() {
  let count = 0;
 
  function increment() {
    count++;
  }
 
  function getCount() {
    return count;
  }
 
  return {
    increment,
    getCount
  };
})();
 
CounterModule.increment();
console.log(CounterModule.getCount()); // 输出: 1
console.log(CounterModule.count); // 输出: undefined (私有变量不可访问)

特性:封装私有变量和方法,暴露公共接口。

应用场景:代码组织、避免全局污染。

§ 高阶函数

1. 什么是高阶函数?

高阶函数(Higher-Order Function)是指满足以下任一条件的函数:

1. 接受一个或多个函数作为参数

2. 返回一个函数作为结果

在编程中,高阶函数是将函数视为"一等公民"(first-class citizen)的语言特性体现,意味着函数可以像其他数据类型一样被传递和使用。

2. 高阶函数的特点

1. 函数作为参数:能够接收其他函数作为输入

2. 函数作为返回值:可以生成并返回新的函数

3. 抽象行为:能够抽象和封装常见的行为模式

4. 组合性:可以通过组合简单函数构建复杂功能

5. 延迟执行:返回的函数可以在需要时才执行

3. 高阶函数的用途

1. 抽象与代码复用

// 不使用高阶函数
const arr1 = [1, 2, 3].map(x => x * 2);
const arr2 = [1, 2, 3].map(x => x * 3);

// 使用高阶函数
function multiplyBy(factor) {
  return x => x * factor;
}
const arr1 = [1, 2, 3].map(multiplyBy(2));
const arr2 = [1, 2, 3].map(multiplyBy(3));

2. 回调机制

// 事件处理
button.addEventListener('click', () => {
  console.log('Button clicked!');
});

// 异步操作
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data));

3. 函数组合与管道

// 组合两个函数
const compose = (f, g) => x => f(g(x));
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);

console.log(shout('hello')); // 输出: "HELLO!"

4. 创建装饰器/中间件

// 日志装饰器
function withLogging(fn) {
  return (...args) => {
    console.log(`Calling with args: ${args}`);
    const result = fn(...args);
    console.log(`Result: ${result}`);
    return result;
  };
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3); // 输出调用和结果信息

5. 柯里化与部分应用

// 柯里化函数
const curry = fn => {
  const arity = fn.length;
  return function $curry(...args) {
    if (args.length < arity) {
      return $curry.bind(null, ...args);
    }
    return fn.apply(null, args);
  };
};

const add = curry((a, b) => a + b);
const add2 = add(2);
console.log(add2(3)); // 5

4. 常见的高阶函数示例

数组方法mapfilterreducesort

const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8]
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]

函数绑定bind

const greet = function(greeting, name) {
  return `${greeting}, ${name}!`;
};
const sayHello = greet.bind(null, 'Hello');
sayHello('Alice'); // "Hello, Alice!"

延迟执行

function lazyEvaluate(fn) {
  return function() {
    return fn.apply(this, arguments);
  };
}

5. 高阶函数的优势

1. 提高代码可读性:通过命名操作意图而非具体实现

2. 减少重复代码:抽象通用模式

3. 增强灵活性:行为可以通过参数动态配置

4. 支持函数式编程范式:如纯函数、不可变性等

5. 便于测试和维护:小的、可组合的函数单元

高阶函数是现代编程语言中强大的工具,尤其在JavaScript、Python、Haskell等语言中广泛应用,是函数式编程的核心概念之一。

§ 柯里化函数

1. 什么是柯里化函数?

柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术。它得名于数学家Haskell Curry,核心思想是:一个接收多个参数的函数可以转换为接收单一参数的函数序列,每次调用返回一个新函数,直到所有参数都被提供,最终返回结果。

// 普通函数
function add(a, b, c) {
  return a + b + c;
}

// 柯里化版本
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

// 使用方式
add(1, 2, 3); // 6
curriedAdd(1)(2)(3); // 6

2. 柯里化函数的特点

1. 参数分解:将多参数函数分解为单参数函数链

2. 延迟执行:在收集到足够参数前不会执行,返回新函数

3. 函数组合:便于创建可组合的函数管道

4. 闭包利用:每个返回的函数都闭包保存了之前传入的参数

5. 部分应用:可以提前固定部分参数,生成更专用的函数

3. 柯里化函数的特性

1. 参数记忆性

const add = a => b => a + b;
const add5 = add(5); // 记住第一个参数5
add5(3); // 8

2. 动态生成函数

const greet = greeting => name => `${greeting}, ${name}!`;
const sayHello = greet('Hello');
const sayHi = greet('Hi');

sayHello('Alice'); // "Hello, Alice!"
sayHi('Bob'); // "Hi, Bob!"

3. 自动柯里化

可以通过工具函数实现自动柯里化:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

const curriedAdd = curry((a, b, c) => a + b + c);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6

4. 柯里化函数的用途

1. 参数复用

// 创建通用的URL构建器
const buildUrl = protocol => domain => path => `${protocol}://${domain}/${path}`;

const buildHttpsUrl = buildUrl('https');
const githubUrl = buildHttpsUrl('github.com');
githubUrl('username/repo'); // "https://github.com/username/repo"

2. 延迟执行/按需计算

// 日志函数
const logger = level => message => timestamp => 
  `[${timestamp}] [${level}] ${message}`;

const errorLogger = logger('ERROR');
const errorMessage = errorLogger('Database connection failed');
errorMessage(new Date().toISOString());

3. 函数组合

const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

const toUpper = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const shout = compose(exclaim, toUpper);

shout('hello'); // "HELLO!"

4. 事件处理

// 事件监听器工厂
const createEventListener = eventType => element => handler => {
  element.addEventListener(eventType, handler);
  return () => element.removeEventListener(eventType, handler);
};

const createClickHandler = createEventListener('click');
const createButtonClick = createClickHandler(document.getElementById('myBtn'));
const unsubscribe = createButtonClick(e => console.log('Clicked!'));

5. 配置预设

// API请求构建器
const createRequest = baseUrl => headers => method => endpoint => data => {
  return fetch(`${baseUrl}/${endpoint}`, {
    method,
    headers,
    body: JSON.stringify(data)
  });
};

const apiRequest = createRequest('https://api.example.com')({ 'Content-Type': 'application/json' });
const getUsers = apiRequest('GET')('users');
const createUser = apiRequest('POST')('users');

5. 柯里化的优势

1. 代码复用:通过部分应用减少重复代码

2. 关注点分离:将参数获取与函数执行分离

3. 灵活性:动态生成具有预设参数的函数

4. 可读性:函数调用链更清晰地表达数据处理流程

5. 函数纯度:更容易保持函数纯净(无副作用)

6. 柯里化的局限性

1. 性能开销:嵌套函数调用可能带来轻微性能损失

2. 调试难度:调用栈可能变得更深更复杂

3. 学习曲线:对初学者可能不太直观

4. 过度使用:不是所有场景都适合柯里化

7. 实际应用场景

1. 函数式编程库:Lodash/fp、Ramda等

2. React高阶组件:参数化组件生成器

3. Redux中间件:如redux-thunk的action creators

4. Express中间件:配置预设的处理函数

5. 测试工具:创建特定的测试用例生成器

假如使用AxureMaps的地图服务

2025年5月23日 17:26

前言

由于AzureMaps的文档写的过于简洁。这个简洁可不是我说的,是ChatGpt说的

Azure Maps 的官方文档 确实偏概括、不够细致,特别是对 azure-maps-drawing-tools 插件的说明 基本就只有示例代码,没有系统性的 API 文档或参数详解,这对开发者尤其是使用插件时调试是比较困难的。

所以汇总下常规用法

创建Web应用

先决条件

  • 创建 Azure Maps 帐户获取key

这里的第三部获取key需要自己去操作,因为本人调研的时候直接拿到的key故无从得知后台界面操作。

身份验证

AzureMaps提供了四种身份验证类型

aad AAD 隐式授予机制。 建议用于受登录保护的页面。 默认情况下,当映射控件初始化时,页面将被重定向到 AAD 登录名。 在 中AuthenticationOptions指定登录PublicClientApplication,以便更好地控制用户登录时间/方式。 文本值 "aad"
anonymous 匿名身份验证机制。 建议用于公共页面。 允许提供负责获取身份验证令牌的回调。 文本值 "anonymous"
sas 共享访问签名身份验证机制。 允许在请求中提供负责获取令牌的回调。 文本值 "sas"
subscriptionKey 订阅密钥身份验证机制。 文本值 "subscriptionKey"

一般我们会使用anonymous或者subscriptionKey的方式来初始化地图

anonymous身份验证

如果想要更加安全的访问,官方推荐使用anonymous的方式来取得身份验证。如果使用这种方式来获取身份验证你在初始化地图的时候需要在authOptions配置authTypeanonymous。根据示例代码

 map = new atlas.Map("map", {
                 center: [-97.55859, 39.63953],
                 zoom: 3,
                 pitch: 60,
                 style: "grayscale_dark",
                 antialias: true,
                 authOptions: {
                     //Use Microsoft Entra ID authentication.
                     authType: 'anonymous',
                     clientId: 'e6b6ab59-eb5d-4d25-aa57-581135b927f0', // 这个客户端id从AzureMap后台拿
                     getToken: function (resolve, reject, map) {
                         //URL to your authentication service that retrieves an Microsoft Entra ID Token.
                         var tokenServiceUrl = 'https://samples.azuremaps.com/api/GetAzureMapsToken';
 
                         fetch(tokenServiceUrl).then(r => r.text()).then(token => resolve(token));
                     }
 
                     //从AzureMaps后台获取key来加载地图
                     //authType: 'subscriptionKey',
                     //subscriptionKey: '[YOUR_AZURE_MAPS_KEY]' 这里输入你申请的key
                 }
             });
 

subscriptionKey方式验证

subscriptionKey方式验证身份就跟我们国内的如高德地图一样。高德地图的初始化地图代码输入在高德地图后台申请的key来启动服务。

 <script type="text/javascript">
   window._AMapSecurityConfig = {
     securityJsCode: "「你申请的安全密钥」",
   };
 </script>
 <script src="https://webapi.amap.com/loader.js"></script>
 <script type="text/javascript">
   AMapLoader.load({
     key: "「你申请的应用Key」", //申请好的Web端开发者 Key,调用 load 时必填
     version: "2.0", //指定要加载的 JS API 的版本,缺省时默认为 1.4.15
   })
     .then((AMap) => {
       const map = new AMap.Map("container");
     })
     .catch((e) => {
       console.error(e); //加载错误提示
     });
 </script>

使用AzureMaps初始化的时候如果使用这种方式,anonymous身份验证代码块内注释有介绍。

在项目中使用

官方文档

有两种方式引入地图包

如何使用azure-maps-control npm包来创建地图

如果你有现成的项目需要引入该包则需要直接安装

 npm install azure-maps-control

还需要嵌入 CSS 样式表才能正确显示各种控件。 如果使用的是 JavaScript 捆绑程序来捆绑依赖项并打包代码,请参考所使用捆绑程序的文档,了解如何完成操作。 如果使用的是 webpack,则通常通过将 style-loadercss-loaderstyle-loader 提供的文档相结合来完成。

 npm install --save-dev style-loader css-loader

在源文件中,导入 atlas.min.css

JavaScript

 import "azure-maps-control/dist/atlas.min.css";

然后将加载程序添加到 webpack 配置的模块规则部分:

 module.exports = {
   module: {
     rules: [
       {
         test: /.css$/i,
         use: ["style-loader", "css-loader"]
       }
     ]
   }
 };

完成以上配置之后可以直接使用如下代码来初始化

 import * as atlas from "azure-maps-control";
 import "azure-maps-control/dist/atlas.min.css";
 
 const onload = () => {
   // Initialize a map instance.
   const map = new atlas.Map("map", {
     view: "Auto",
     // Add authentication details for connecting to Azure Maps.
     authOptions: {
       authType: "subscriptionKey",
       subscriptionKey: "<Your Azure Maps Key>"
     }
   });
 };
 
 document.body.onload = onload;

那么正常来说你页面上地图应该已经渲染了,地图渲染在页面上一个id为map的元素内。

错误

什么?你的地图没有渲染且页面上报了如下的错误?

 ERROR
 Failed to fetch
 TypeError: Failed to fetch
     at e.eval (webpack://azuremap/./node_modules/azure-maps-control/dist/atlas-esm.min.js?:170:1274720)
     at Object.eval [as next] (webpack://azuremap/./node_modules/azure-maps-control/dist/atlas-esm.min.js?:170:1260844)
     at eval (webpack://azuremap/./node_modules/azure-maps-control/dist/atlas-esm.min.js?:170:1259849)
     at new Promise (<anonymous>)
     at __awaiter$1 (webpack://azuremap/./node_modules/azure-maps-control/dist/atlas-esm.min.js?:170:1259600)
     at e._request (webpack://azuremap/./node_modules/azure-maps-control/dist/atlas-esm.min.js?:170:1274118)
     at eval (webpack://azuremap/./node_modules/azure-maps-control/dist/atlas-esm.min.js?:170:1266552)

两种解决方案

  • 挂梯子
  • 截止我博客发时可以生效的就是本地host添加一行
 13.107.42.21 atlas.microsoft.com 

🎯 《Flex布局奇妙历险:从菜鸟到面试王者!》

作者 MrSkye
2025年5月23日 17:22

前言

上一期我们学习了关于float的相关知识,其实本来这期要做position的,但是感觉没什么东西,我就直接把position放在了上一期的最后一部分(偷偷更新了一下),如果没有看,那么下面是传送门↓↓↓:

浮动:让元素学会“轻功”,让他们飞起来!!!(Chapter 2)

ok,话不多说,我们开始今天的弹性布局,这一期先给大家做一些基本的介绍,下一期将会带来弹性布局的实际应用

🌟 「弹性布局」揭秘:为什么这能拯救你的CSS人生?

——从“挤到怀疑人生”到“优雅排版”的魔法之路

如果你是 CSS 新手,是否经历过这些崩溃瞬间?👇

  • 想让几个 div 水平排列?结果它们要么叠罗汉,要么撑爆容器
  • 试图垂直居中一个按钮?写了 10 行代码,结果它在 IE 里离家出走
  • 用 float 做两栏布局?稍不留神,下一部分内容就莫名消失

面试官最喜欢问的问题:如何使一个元素居中?

ok,这是我们常见的一个问题,能很好的说明我们为什么要学习flex布局。

常规做法:

如果不利用flex布局实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .container {
  position: relative;      /* 父容器相对定位 */
  width: 300px;
  height: 200px;
  border: 2px dashed #666;
}
.box {
  position: absolute;      /* 子元素绝对定位 */
  top: 50%;                /* 顶部定位到50% */
  left: 50%;               /* 左侧定位到50% */
  transform: translate(-50%, -50%); /* 往回挪自身50% */
  width: 100px;
  height: 50px;
  background: coral;
  
}

    </style>
</head>
<body>
    <div class="container">
  <div class="box">我要居中!</div>
</div>

</body>
</html>

效果如下:

image.png

(当然里面的文字不需要在意,我们的目的是使得中间橙色的box居中)

不使用flex布局,则我们选用position的relative和absolute,再利用top和left子元素左上角定位于距离父元素左上角的50%距离处,最后利用transform:translate(-50%,-50%) 将元素自身向左向上移动自身宽高的50%,达成了居中效果。

看到这你可能会说:主播主播!你的position定位实在是太吃操作了,有没有更简单更好的方法呢?

我只能回答:有的兄弟,有的!我们现在是有bear来!

微信图片_20250430194704.jpg

接下来请看flex布局做法:

Flex布局做法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .container {
  display: flex;      
  width: 300px;
  height: 200px;
  border: 2px dashed #666;
  justify-content: center;
  align-items: center;
}
.box {
 
  width: 100px;
  height: 50px;
  background: coral;
  
}

    </style>
</head>
<body>
    <div class="container">
  <div class="box">我要居中!</div>
</div>

</body>
</html>

OK,除去没用的宽高等一系列东西,我们实现元素居中只需要三行代码:

.container{
 display:flex;
 justify-content: center;
 align-items: center;
 }

And.......Boom!!!!!!!!!!!!!

image.png 效果是不是和之前那一大堆一样了~~~

下面让我们来开始认识一下flex布局吧!

Flex布局是个什么东西?

其为CSS3的一种新的布局方式,弹性盒子主要由弹性容器(Flex-container)弹性子元素(Flex-item) 组成。

容器通过设置display属性的值为flex将其定义为弹性容器

默认在弹性容器中的内容都横向摆放,这也是我们利用Flex布局的主要目的!!!

<style>
.container{
display:flex;  /*这就是把container设置成了弹性容器
}
</style>

<div class='container'>

<div class='box'>

</div>

<div>

image-20250511110110761.png

注意:我们一定要分清楚容器属性元素属性,容器属性是在容器上使用的,元素属性是在元素上用的。

容器属性

总览:

1.flex-direction

2.flex-wrap

3.flex-flow (1,2的简写形式)

4.justify-content

5.align-items

6.align-content

1.flex-direction

flex-direction:规定容器内子元素的排列方向

image.png

2.flex-wrap

flex-wrap:决定元素放不下了是否换行

空间被挤压前: image.png空间被挤压后:

image.png 注意:wrap-reverse的挤压是会令元素逐个向上,空间缩小,先挤压 7,再是6,5,4.....依次向上挤压换行。

3.flex-flow

其用法非常简单

.container{
display:flex;
flex-flow: column wrap;
/* 第一个变量为排列方向,第二个变量为换行方式
}

4.justify-content

justify-content:定义了内部元素在主轴(main-axis) 上的对齐方式

image.png

image.png justify-content还有好多属性,这里就不一一列举了,因为常用的属性基本就这么多了。

5.align-items

align-items:定义属性项目在交叉轴(Cross axis)上如何对齐

(交叉轴与主轴垂直)

image.png 关于stretch:

交叉轴为y轴时,如果项目的高度没有设置,那么项目就会竖直拉伸铺满交叉轴。(如上)

交叉轴为x轴时,如果项目的宽度没有设置,那么项目就会水平拉伸铺满交叉轴。(如下)

image.png


image.png

💥 前方核能警告:Flexbox 主轴/交叉轴大逃杀!新手必看!

🚨 Alert! Alert! 🚨

你以为 justify-content 永远只管“横着排”,align-items 只管“竖着排”?Too young, too simple!

Flexbox 其实是个“魔法转盘”,它的主轴方向会被 flex-direction 一键反转!如果你搞不清主次,你的布局就会像没放盐的泡面——毫无味道,还很难受!

如果你仔细观察一下,你会发现我对于justify-content和align-items的描述是主轴和交叉轴,而不是x轴和y轴,这是因为不同的排列方式会造成主轴和交叉轴的不同,也就是说flex-direction会改变主轴和交叉轴,使得justify-content和align-items效果不同。

🧠 生存指南:1 秒搞懂主轴 vs. 交叉轴

设置 flex-direction 主轴方向 justify-content 管谁? align-items 管谁?
row(默认) → 横的 水平排列 垂直对齐
column ↓ 竖的 垂直排列 水平对齐

🎮 终极试炼(测测你的 Flexbox 求生技能)

.container {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: stretch;
}

问题: 这些container中的子元素会怎么排?

  • A. 水平拉伸 + 垂直贴底 ✅(恭喜,你活下来了!)
  • B. 垂直拉伸 + 水平贴右 ❌(抱歉,你已被 Flexbox 吞噬…)

6.align-content

align-content属性定义了浏览器如何沿着Flexbox容器的交叉轴分配多行Flex项目之间的空间。该属性在多行Flex容器(设置了flex-wrap: wrap)中才会生效。

image.png

image.png

image.png

image.png

image.png 总结一下,这个属性就是沿着交叉轴来的,下面是Html的代码,大家可以自行去CV观察一下具体的效果。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flexbox align-content属性详解</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f9f9f9;
        }
        
        h1, h2 {
            color: #2c3e50;
        }
        
        h1 {
            border-bottom: 2px solid #3498db;
            padding-bottom: 10px;
        }
        
        .property-card {
            background: white;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            padding: 20px;
            margin-bottom: 30px;
        }
        
        .demo-container {
            border: 2px dashed #7f8c8d;
            height: 200px;
            display: flex;
            flex-wrap: wrap;
            margin: 20px 0;
            background-color: #f1f1f1;
        }
        
        .demo-item {
            width: 80px;
            height: 50px;
            background-color: #3498db;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 5px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        
        .code {
            background-color: #f8f8f8;
            padding: 15px;
            border-radius: 4px;
            font-family: 'Courier New', Courier, monospace;
            overflow-x: auto;
            margin: 10px 0;
        }
        
        .note {
            background-color: #fffde7;
            padding: 15px;
            border-left: 4px solid #ffd600;
            margin: 15px 0;
        }
        
        .warning {
            background-color: #ffebee;
            padding: 15px;
            border-left: 4px solid #f44336;
            margin: 15px 0;
        }
        
        .property-description {
            margin-bottom: 20px;
        }
        
        .property-values {
            margin-top: 20px;
        }
        
        .value-item {
            margin-bottom: 40px;
        }
        
        .flex-direction-info {
            background-color: #e3f2fd;
            padding: 15px;
            border-left: 4px solid #2196f3;
            margin: 15px 0;
        }
    </style>
</head>
<body>
    <h1>Flexbox中的align-content属性详解</h1>
    
    <div class="property-card">
        <h2>align-content属性介绍</h2>
        
        <div class="property-description">
            <p><code>align-content</code>属性定义了浏览器如何沿着Flexbox容器的交叉轴分配多行Flex项目之间的空间。该属性在多行Flex容器(设置了<code>flex-wrap: wrap</code>)中才会生效。</p>
            
            <div class="code">
                .container {<br>
                &nbsp;&nbsp;display: flex;<br>
                &nbsp;&nbsp;flex-wrap: wrap;<br>
                &nbsp;&nbsp;align-content: stretch; /* 默认值 */<br>
                }
            </div>
            
            <div class="note">
                <strong>注意:</strong> <code>align-content</code><code>align-items</code>不同:
                <ul>
                    <li><code>align-items</code>控制单行内项目在交叉轴上的对齐</li>
                    <li><code>align-content</code>控制多行之间在交叉轴上的空间分布</li>
                </ul>
            </div>
            
            <div class="flex-direction-info">
                <p><strong>主轴与交叉轴的关系:</strong></p>
                <ul>
                    <li><code>flex-direction: row</code>(默认)时,主轴是水平的,交叉轴是垂直的</li>
                    <li><code>flex-direction: column</code>时,主轴是垂直的,交叉轴是水平的</li>
                </ul>
                <p>这意味着<code>align-content</code>的效果会根据Flexbox的方向而变化!</p>
            </div>
        </div>
        
        <div class="property-values">
            <h3>align-content的可取值</h3>
            
            <!-- stretch -->
            <div class="value-item">
                <h4>1. stretch (默认值)</h4>
                <p>项目被拉伸以填满容器。没有指定交叉轴尺寸的项目将被拉伸以占据剩余空间。</p>
                <div class="code">align-content: stretch;</div>
                <div class="demo-container" style="align-content: stretch;">
                    <div class="demo-item">1</div>
                    <div class="demo-item">2</div>
                    <div class="demo-item">3</div>
                    <div class="demo-item">4</div>
                    <div class="demo-item">5</div>
                    <div class="demo-item">6</div>
                </div>
                <div class="warning">
                    <strong>注意:</strong> 如果项目设置了交叉轴尺寸(如height或width,取决于主轴方向),stretch将不生效。
                </div>
            </div>
            
            <!-- flex-start -->
            <div class="value-item">
                <h4>2. flex-start</h4>
                <p>项目在交叉轴起点对齐。第一行紧贴容器边缘,后续每行在前一行下方排列。</p>
                <div class="code">align-content: flex-start;</div>
                <div class="demo-container" style="align-content: flex-start;">
                    <div class="demo-item">1</div>
                    <div class="demo-item">2</div>
                    <div class="demo-item">3</div>
                    <div class="demo-item">4</div>
                    <div class="demo-item">5</div>
                    <div class="demo-item">6</div>
                </div>
                <p>这种对齐方式在垂直方向上不会有多行之间额外间距的分配。</p>
            </div>
            
            <!-- flex-end -->
            <div class="value-item">
                <h4>3. flex-end</h4>
                <p>项目在交叉轴终点对齐。第一行放在容器底部,其他行在上面排列。</p>
                <div class="code">align-content: flex-end;</div>
                <div class="demo-container" style="align-content: flex-end;">
                    <div class="demo-item">1</div>
                    <div class="demo-item">2</div>
                    <div class="demo-item">3</div>
                    <div class="demo-item">4</div>
                    <div class="demo-item">5</div>
                    <div class="demo-item">6</div>
                </div>
                <p>适用于需要内容在底部对齐的情况。</p>
            </div>
            
            <!-- center -->
            <div class="value-item">
                <h4>4. center</h4>
                <p>项目在交叉轴中间对齐。所有行作为一组居中,容器会在上方和下方有相同的间距。</p>
                <div class="code">align-content: center;</div>
                <div class="demo-container" style="align-content: center;">
                    <div class="demo-item">1</div>
                    <div class="demo-item">2</div>
                    <div class="demo-item">3</div>
                    <div class="demo-item">4</div>
                    <div class="demo-item">5</div>
                    <div class="demo-item">6</div>
                </div>
                <p>这是实现垂直居中的有效方法。</p>
            </div>
            
            <!-- space-between -->
            <div class="value-item">
                <h4>5. space-between</h4>
                <p>项目在交叉轴上均匀分布,第一行在起点,最后一行在终点,其余行之间距离相等。</p>
                <div class="code">align-content: space-between;</div>
                <div class="demo-container" style="align-content: space-between;">
                    <div class="demo-item">1</div>
                    <div class="demo-item">2</div>
                    <div class="demo-item">3</div>
                    <div class="demo-item">4</div>
                    <div class="demo-item">5</div>
                    <div class="demo-item">6</div>
                </div>
                <div class="note">
                    <strong>注意:</strong> 如果只有一行,效果和flex-start相同。
                </div>
            </div>
            
            <!-- space-around -->
            <div class="value-item">
                <h4>6. space-around</h4>
                <p>项目在交叉轴上均匀分布,每行周围分配相等的空间,因此行与行之间的空间是行与边缘空间的两倍。</p>
                <div class="code">align-content: space-around;</div>
                <div class="demo-container" style="align-content: space-around;">
                    <div class="demo-item">1</div>
                    <div class="demo-item">2</div>
                    <div class="demo-item">3</div>
                    <div class="demo-item">4</div>
                    <div class="demo-item">5</div>
                    <div class="demo-item">6</div>
                </div>
                <p>适合需要均衡空间分布的情况。</p>
            </div>
            
            <!-- space-evenly -->
            <div class="value-item">
                <h4>7. space-evenly</h4>
                <p>项目在交叉轴上均匀分布,所有空间(包括边缘)的大小相等。</p>
                <div class="code">align-content: space-evenly;</div>
                <div class="demo-container" style="align-content: space-evenly;">
                    <div class="demo-item">1</div>
                    <div class="demo-item">2</div>
                    <div class="demo-item">3</div>
                    <div class="demo-item">4</div>
                    <div class="demo-item">5</div>
                    <div class="demo-item">6</div>
                </div>
                <p>创建完全对称的空间分布,各间距完全一致。</p>
            </div>
        </div>
        
        <div class="note">
            <h3>浏览器支持情况</h3>
            <p><code>align-content</code>在所有现代浏览器中都有很好的支持:</p>
            <ul>
                <li>Chrome 57+ (完全支持)</li>
                <li>Firefox 52+ (完全支持)</li>
                <li>Safari 10.1+ (完全支持)</li>
                <li>Edge 16+ (完全支持)</li>
                <li>Opera 44+ (完全支持)</li>
            </ul>
            <p>对于旧版浏览器,可能需要添加浏览器前缀。</p>
        </div>
        
        <div class="warning">
            <h3>常见错误</h3>
            <p>使用<code>align-content</code>时常犯的错误:</p>
            <ol>
                <li>忘记设置<code>flex-wrap: wrap</code> - 没有多行时<code>align-content</code>不会生效</li>
                <li>没有设置容器高度 - 交叉轴方向没有限制时,效果不明显</li>
                <li>混淆<code>align-content</code><code>align-items</code></li>
            </ol>
        </div>
        
        <div class="note">
            <h3>最佳实践</h3>
            <ul>
                <li>明确设置<code>flex-wrap</code>属性</li>
                <li>为容器设置明确的交叉轴尺寸(高度或宽度,取决于方向)</li>
                <li>考虑使用<code>gap</code>属性来增加行与行之间的间距(现代浏览器支持)</li>
                <li>在需要特殊对齐效果时才使用<code>align-content</code>,默认值(stretch)通常就有良好的表现</li>
            </ul>
        </div>
    </div>
</body>
</html>

元素属性

1.order

2.flex-grow

3.flex-shrink

4.flex-basis

5.flex(2,3,4的简写形式)

6.align-self

image-20250511110110761.png 注意哦!我们这是元素属性!是写在 item上的,不是container 哈~

1.order

order决定了容器内部项目的排列顺序,数值越小,排列越靠前,默认为0 正常情况:

image.png 赋值后:

image.png 可以看到order=-1的box在最前面,后面依次按照数值排列。

使用场景:  当需要改变项目的视觉顺序而不修改DOM结构时非常有用。例如响应式布局中,不同屏幕尺寸下可能需要不同的项目顺序。

注意: 虽然order可以改变视觉顺序,但不会影响DOM顺序,因此对于屏幕阅读器和键盘导航的顺序不会改变,可能会导致可访问性问题。

关于DOM,我们会在以后的JavaScript中讲解~ 埋一个小坑啦(๑•̀ㅂ•́)و✧

2.flex-grow

flex-grow:定义了容器内部项目放大的能力(按剩余的空间比例分配),默认值为0,即如果存在剩余空间也不会放大

image.png 如果分别给这3个box设置flex-grow:0,则这3个box都不会放大,

如果分别给这几个box设置不同的数字,比如: flex-grow:1 , flex-grow:2, flex-grow:3,

那么这3个box将会根据剩余空间大小,按照1:2:3的比例来分配空间。

3.flex-shrink

flex-shrink:定义了项目的缩小能力,即在空间不足时该如何缩小

image.png 与上面的flex-grow同理,混合使用按比例缩小。

注意:  flex-shrink: 0的项目可能会超出容器,导致溢出,需要配合overflow属性使用。 比如利用overflow:hidden来隐藏溢出的部分~

4.flex-basis

flex-basis :属性定义了在分配多余空间之前的大小,和width的表现基本一致。

image.png

注意:

 1.如果同时使用widthflex-basis,则flex-basis的优先级更高一些

 2.flex-basis会覆盖width属性(如果主轴是水平的)。设置min-width或max-width可   以限制最终的弹性尺寸。

5.flex

flexflex-growflex-shrinkflex-basis的简写属性,推荐使用这个属性而不是单独写三个分离的属性。

image.png

6.align-self

align-self允许单独的对某个Flex项目进行交叉轴(纵轴)方向上的对齐方式设置,会覆盖align-items的属性, 默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch

image.png

结语

今天我们通过面试官最最最最喜欢的元素居中的例子(其实一共有五种解法),引入了我们今天要讲的flex布局和flex弹性盒子模型,一起解开了Flex的魔法卷轴,利用

.container { 
display: flex; 
justify-content: center;
align-items: center; 
}

秒杀了一道经典面试题。现在猜猜我们下一章节要干嘛呢?

dk2.jpg 下一章我们将:

  • 💻 把设计图变成会呼吸的网页
  • 🧩 用Flex搭建俄罗斯方块般的组件
  • 🌈 让布局像乐高一样自由拼接
  • 📱 施展响应式魔法:让页面在手机和平板间自由变形!

我们将会手把手利用flex布局做出以下页面:

image.png 相信我,绝对是婴儿向教程,包教包会~

虚拟滚动白屏问题的处理方案

作者 大明88
2025年5月23日 17:18

背景

在开发大型数据列表时,虚拟滚动(Virtual Scrolling)是一种常见的优化手段,它通过只渲染可见区域内的元素来减少 DOM 节点的数量,从而提升性能。然而,虚拟滚动在实际应用中可能会遇到“白屏”问题,即用户在快速滚动时,页面出现短暂的空白区域。本文将介绍如何通过引入代理滚动条来解决这一问题。

问题分析

虚拟滚动的核心思想是根据滚动位置动态计算并渲染可见区域内的元素。然而,当用户快速滚动时,浏览器可能无法及时渲染新的元素,导致页面出现白屏。这种现象在数据量较大或滚动速度较快时尤为明显。

解决方案

为了解决白屏问题,我们可以引入一个代理滚动条(Proxy Scrollbar)。代理滚动条的作用是监听用户的滚动行为,并将滚动事件同步到主滚动条上。通过这种方式,我们可以确保主滚动条的滚动行为更加平滑,减少白屏现象的发生。

  1. 使用浏览器限制下CPU性能,方便测试

image.png

  1. 左侧为虚拟列表本身的滚动条,滚动过程中出现空白区域。右侧为代理滚动条,滚动正常。

scroll.gif

后续优化

  1. 使用滚动滚动时也会出现白屏现象。这时候可以隐藏原本的滚动条,使用wheel事件替代原本的滚动事件。
  2. 使用节流函数避免频繁触发滚动条同步。

源码

急!华为手机-鸿蒙系统兼容性问题,快检查你的项目

作者 蓝色海岛
2025年5月23日 17:14

一、背景:pdf预览

我司预览pdf时,pc端使用的是微软的在线预览,样式如下:

image.png

或者使用pdfjs预览:

image.png

这些预览插件在pc端表现不错,但是在移动端表现不尽如人意,尤其是在遇到不同的型号的浏览器时,兼容性问题层出不穷

为了统一解决pdf在h5端的预览,我找了个插件:pdfh5,它可以很轻松的预览pdf,不挑机型,都显示这样

image.png

二、判断设备类型

由于pc和h5使用的插件不一样,那么肯定要判断设备类型,众所周知,navigator.userAgent就是专门用来判断设备的,它可以识别出pc还是h5,识别出浏览器类型

谷歌:

image.png 火狐:

image.png QQ浏览器:

image.png

我的vivo手机自带浏览器(可以看到Android、Mobile、VivoBrowser等关键信息):

image.png

于是我们可以封装一个函数,用来判断设备:

function getDeviceType(userAgent) {
  userAgent = userAgent || navigator.userAgent
  // 检查是否为移动设备
  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)
  // 检查是否为平板设备
  const isTablet =
    /Tablet|iPad|PlayBook|Nexus 7|Nexus 10|Kindle|Silk|GT-P1000|GT-P1010|GT-P7500|GT-P7510|SCH-I800M|SHW-M180S|SM-T320|SM-T350|SM-T520|SM-T550|SM-T560|SM-T800|SM-T805|SM-T807|SM-T820|SM-T830|SM-T850|SM-T900|SM-T905|SM-T920|SM-T950|SM-T960|SM-T970|SM-T975|SM-T980|SM-T985|SM-T990|SM-T995|SM-T999|SM-T810|SM-T815|SM-T817|SM-T825|SM-T835|SM-T855|SM-T860|SM-T870|SM-T880|SM-T890|SM-T895|SM-T900|SM-T905|SM-T920|SM-T950|SM-T960|SM-T970|SM-T975|SM-T980|SM-T985|SM-T990|SM-T995|SM-T999/i.test(
      userAgent
    )
  // 如果是移动设备但不是平板设备,则为手机
  if ((isMobile || isHarmonyPhone) && !isTablet) {
    return "Mobile"
  } else if (isTablet) {
    return "Tablet"
  } else {
    return "PC"
  }
}

三、问题点

  1. 以上判断设置机型的方法,在绝大多数情况下,都是适用的;或者说在鸿蒙系统之前,在h5端都没问题

image.png

但是在华为的鸿蒙系统出来后,判断设备时,它有自己的一些特殊标识,需要将鸿蒙系统考虑进去

  1. 此外,在老版的鸿蒙系统中,自带的浏览器打印出来的userAgent竟然和pc端一样

image.png

询问AI,这是什么骚操作

image.png

所以我们封装的方法在这个浏览器上无法区分出来是pc还是h5,好在最新版的鸿蒙已不存在此问题,只需要在识别设备时将鸿蒙考虑进去即可

最终的方法(参考):

function getDeviceType(userAgent) {
  userAgent = userAgent || navigator.userAgent
  // 检查是否为移动设备
  const isMobile =
    /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
      userAgent
    )
  // 检查是否为鸿蒙手机端
  const isHarmonyPhone = /Harmony/i.test(navigator.userAgent) && /Mobile|Phone/i.test(navigator.userAgent)
  // 检查是否为平板设备
  const isTablet =
    /Tablet|iPad|PlayBook|Nexus 7|Nexus 10|Kindle|Silk|GT-P1000|GT-P1010|GT-P7500|GT-P7510|SCH-I800M|SHW-M180S|SM-T320|SM-T350|SM-T520|SM-T550|SM-T560|SM-T800|SM-T805|SM-T807|SM-T820|SM-T830|SM-T850|SM-T900|SM-T905|SM-T920|SM-T950|SM-T960|SM-T970|SM-T975|SM-T980|SM-T985|SM-T990|SM-T995|SM-T999|SM-T810|SM-T815|SM-T817|SM-T825|SM-T835|SM-T855|SM-T860|SM-T870|SM-T880|SM-T890|SM-T895|SM-T900|SM-T905|SM-T920|SM-T950|SM-T960|SM-T970|SM-T975|SM-T980|SM-T985|SM-T990|SM-T995|SM-T999/i.test(
      userAgent
    )
  // 如果是移动设备但不是平板设备,则为手机
  if ((isMobile || isHarmonyPhone) && !isTablet) {
    return "Mobile"
  } else if (isTablet) {
    return "Tablet"
  } else {
    const isSmallScreen = 'ontouchstart' in window || navigator.maxTouchPoints >= 2 || window.screen.width <= 960
    return isSmallScreen ? "Mobile": "PC"
  }
}

如果你的项目比较早,鸿蒙系统发布后,用户使用鸿蒙设备访问,可能会有个线上bug哦,自检

四、扩展

pdfh5自带的download方法进行下载,在手机端各个浏览器表现不一致

image.png

例如:

  1. 谷歌:支持设置pdf文件名,但文件超过8M时,下载不了
  2. vivo浏览器:不支持设置pdf文件名
  3. QQ浏览器:不支持设置pdf文件名 ...

通过创建a元素实现下载,实测几个浏览器都没问题,苹果手机也支持,鸿蒙也支持

function downloadUrl(url,fileName){
  let title = fileName;
    let urlName = url.split('/'),
       urlNameLen  = urlName.length-1;

    if(!title){
      title = urlName[urlNameLen]
    }
    fetch(url)
    // 获取 blob 对象
    .then(res=>res.blob())
    .then(blob=>{
      var link = document.createElement('a');
      link.style.display = 'none';
      document.body.appendChild(link);
      var url = window.URL.createObjectURL(blob);
      link.href = url;
      link.download = title;
      link.click();
      //下载完成后清理URL对象和a标签
      window.URL.revokeObjectURL(url);
      document.body.removeChild(link);
    })

}

Android Framework核心服务联动

2025年5月23日 17:14

一、系统启动与服务初始化全链路

1. Zygote进程到SystemServer的跃迁

frameworks/base/cmds/app_process/app_main.cpp

`// app_main.cpp (Zygote进程)
int main(int argc, char** argv) {
    
    // 创建Binder线程池
    sp<ProcessState> proc(ProcessState::self());
    // 启动SystemServer进程
    if (startSystemServer) {
        pid_t pid = fork();
        if (pid == 0) {
            // 设置实时优先级(关键参数)
            setpriority(PRIO_PROCESS, 0, -20);
            // 跳转到SystemServer主函数
            SystemMain();
        }
    }
}`

值得注意的是:

Binder线程池初始化ProcessState::self()->startThreadPool()创建Binder线程池

ade5bfde-e6f1-4d16-9ce7-d09d45801e04.png

如上代码将流程直接推到了ZygoInit.java

//frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

` /**
     * Performs Zygote process initialization. Loads and initializes commonly used classes.
     *
     * Most classes only cause a few hundred bytes to be allocated, but a few will allocate a dozen
     * Kbytes (in one case, 500+K).
     */
    private static void preloadClasses() {
        final VMRuntime runtime = VMRuntime.getRuntime();

        InputStream is;
        try {
            is = new FileInputStream(PRELOADED_CLASSES);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
            return;
        }

        Log.i(TAG, "Preloading classes...");
        long startTime = SystemClock.uptimeMillis();

        // Drop root perms while running static initializers.
        final int reuid = Os.getuid();
        final int regid = Os.getgid();

        // We need to drop root perms only if we're already root. In the case of "wrapped"
        // processes (see WrapperInit), this function is called from an unprivileged uid
        // and gid.
        boolean droppedPriviliges = false;
        if (reuid == ROOT_UID && regid == ROOT_GID) {
            try {
                Os.setregid(ROOT_GID, UNPRIVILEGED_GID);
                Os.setreuid(ROOT_UID, UNPRIVILEGED_UID);
            } catch (ErrnoException ex) {
                throw new RuntimeException("Failed to drop root", ex);
            }

            droppedPriviliges = true;
        }

        try {
            BufferedReader br =
                    new BufferedReader(new InputStreamReader(is), Zygote.SOCKET_BUFFER_SIZE);

            int count = 0;
            int missingLambdaCount = 0;
            String line;
            while ((line = br.readLine()) != null) {
                // Skip comments and blank lines.
                line = line.trim();
                if (line.startsWith("#") || line.equals("")) {
                    continue;
                }

                Trace.traceBegin(Trace.TRACE_TAG_DALVIK, line);
                try {
                    // Load and explicitly initialize the given class. Use
                    // Class.forName(String, boolean, ClassLoader) to avoid repeated stack lookups
                    // (to derive the caller's class-loader). Use true to force initialization, and
                    // null for the boot classpath class-loader (could as well cache the
                    // class-loader of this class in a variable).
                    Class.forName(line, true, null);
                    count++;
                } catch (ClassNotFoundException e) {
                    if (line.contains("$$Lambda$")) {
                        if (LOGGING_DEBUG) {
                            missingLambdaCount++;
                        }
                    } else {
                        Log.w(TAG, "Class not found for preloading: " + line);
                    }
                } catch (UnsatisfiedLinkError e) {
                    Log.w(TAG, "Problem preloading " + line + ": " + e);
                } catch (Throwable t) {
                    Log.e(TAG, "Error preloading " + line + ".", t);
                    if (t instanceof Error) {
                        throw (Error) t;
                    } else if (t instanceof RuntimeException) {
                        throw (RuntimeException) t;
                    } else {
                        throw new RuntimeException(t);
                    }
                }
                Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
            }

            Log.i(TAG, "...preloaded " + count + " classes in "
                    + (SystemClock.uptimeMillis() - startTime) + "ms.");
            if (LOGGING_DEBUG && missingLambdaCount != 0) {
                Log.i(TAG, "Unresolved lambda preloads: " + missingLambdaCount);
            }
        } catch (IOException e) {
            Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
        } finally {
            IoUtils.closeQuietly(is);

            // Fill in dex caches with classes, fields, and methods brought in by preloading.
            Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadDexCaches");
            runtime.preloadDexCaches();
            Trace.traceEnd(Trace.TRACE_TAG_DALVIK);

            // If we are profiling the boot image, reset the Jit counters after preloading the
            // classes. We want to preload for performance, and we can use method counters to
            // infer what clases are used after calling resetJitCounters, for profile purposes.
            if (shouldProfileBootClasspath()) {
                Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ResetJitCounters");
                VMRuntime.resetJitCounters();
                Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
            }

            // Bring back root. We'll need it later if we're in the zygote.
            if (droppedPriviliges) {
                try {
                    Os.setreuid(ROOT_UID, ROOT_UID);
                    Os.setregid(ROOT_GID, ROOT_GID);
                } catch (ErrnoException ex) {
                    throw new RuntimeException("Failed to restore root", ex);
                }
            }
        }
    }`
    
2. SystemServer服务启动时序

进入SystemServer之后,执行run方法

private void run() {
    TimingsTraceAndSlog t = new TimingsTraceAndSlog();
    try {
        t.traceBegin("InitBeforeStartServices");

        // Record the process start information in sys props.
        SystemProperties.set(SYSPROP_START_COUNT, String.valueOf(mStartCount));
        SystemProperties.set(SYSPROP_START_ELAPSED, String.valueOf(mRuntimeStartElapsedTime));
        SystemProperties.set(SYSPROP_START_UPTIME, String.valueOf(mRuntimeStartUptime));

        EventLog.writeEvent(EventLogTags.SYSTEM_SERVER_START,
                mStartCount, mRuntimeStartUptime, mRuntimeStartElapsedTime);

        // Set the device's time zone (a system property) if it is not set or is invalid.
        SystemTimeZone.initializeTimeZoneSettingsIfRequired();

        // If the system has "persist.sys.language" and friends set, replace them with
        // "persist.sys.locale". Note that the default locale at this point is calculated
        // using the "-Duser.locale" command line flag. That flag is usually populated by
        // AndroidRuntime using the same set of system properties, but only the system_server
        // and system apps are allowed to set them.
        //
        // NOTE: Most changes made here will need an equivalent change to
        // core/jni/AndroidRuntime.cpp
        if (!SystemProperties.get("persist.sys.language").isEmpty()) {
            final String languageTag = Locale.getDefault().toLanguageTag();

            SystemProperties.set("persist.sys.locale", languageTag);
            SystemProperties.set("persist.sys.language", "");
            SystemProperties.set("persist.sys.country", "");
            SystemProperties.set("persist.sys.localevar", "");
        }

        // The system server should never make non-oneway calls
        Binder.setWarnOnBlocking(true);
        // The system server should always load safe labels
        PackageItemInfo.forceSafeLabels();

        // Default to FULL within the system server.
        SQLiteGlobal.sDefaultSyncMode = SQLiteGlobal.SYNC_MODE_FULL;

        // Deactivate SQLiteCompatibilityWalFlags until settings provider is initialized
        SQLiteCompatibilityWalFlags.init(null);

        // Here we go!
        Slog.i(TAG, "Entered the Android system server!");
        final long uptimeMillis = SystemClock.elapsedRealtime();
        EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_SYSTEM_RUN, uptimeMillis);
        if (!mRuntimeRestart) {
            FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
                    FrameworkStatsLog
                            .BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SYSTEM_SERVER_INIT_START,
                    uptimeMillis);
        }

        // In case the runtime switched since last boot (such as when
        // the old runtime was removed in an OTA), set the system
        // property so that it is in sync. We can't do this in
        // libnativehelper's JniInvocation::Init code where we already
        // had to fallback to a different runtime because it is
        // running as root and we need to be the system user to set
        // the property. http://b/11463182
        SystemProperties.set("persist.sys.dalvik.vm.lib.2", VMRuntime.getRuntime().vmLibrary());

        // Mmmmmm... more memory!
        VMRuntime.getRuntime().clearGrowthLimit();

        // Some devices rely on runtime fingerprint generation, so make sure
        // we've defined it before booting further.
        Build.ensureFingerprintProperty();

        // Within the system server, it is an error to access Environment paths without
        // explicitly specifying a user.
        Environment.setUserRequired(true);

        // Within the system server, any incoming Bundles should be defused
        // to avoid throwing BadParcelableException.
        BaseBundle.setShouldDefuse(true);

        // Within the system server, when parceling exceptions, include the stack trace
        Parcel.setStackTraceParceling(true);

        // Ensure binder calls into the system always run at foreground priority.
        BinderInternal.disableBackgroundScheduling(true);

        // Increase the number of binder threads in system_server
        BinderInternal.setMaxThreads(sMaxBinderThreads);

        // Prepare the main looper thread (this thread).
        android.os.Process.setThreadPriority(
                android.os.Process.THREAD_PRIORITY_FOREGROUND);
        android.os.Process.setCanSelfBackground(false);
        Looper.prepareMainLooper();
        Looper.getMainLooper().setSlowLogThresholdMs(
                SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS);

        SystemServiceRegistry.sEnableServiceNotFoundWtf = true;

        // Initialize native services.
        System.loadLibrary("android_servers");

        // Allow heap / perf profiling.
        initZygoteChildHeapProfiling();

        // Debug builds - spawn a thread to monitor for fd leaks.
        if (Build.IS_DEBUGGABLE) {
            spawnFdLeakCheckThread();
        }

        // Check whether we failed to shut down last time we tried.
        // This call may not return.
        performPendingShutdown();

        // Initialize the system context.
        createSystemContext();

        // Call per-process mainline module initialization.
        ActivityThread.initializeMainlineModules();

        // Sets the dumper service
        ServiceManager.addService("system_server_dumper", mDumper);
        mDumper.addDumpable(this);

        // Create the system service manager.
        mSystemServiceManager = new SystemServiceManager(mSystemContext);
        mSystemServiceManager.setStartInfo(mRuntimeRestart,
                mRuntimeStartElapsedTime, mRuntimeStartUptime);
        mDumper.addDumpable(mSystemServiceManager);

        LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);
        // Prepare the thread pool for init tasks that can be parallelized
        SystemServerInitThreadPool tp = SystemServerInitThreadPool.start();
        mDumper.addDumpable(tp);

        // Lazily load the pre-installed system font map in SystemServer only if we're not doing
        // the optimized font loading in the FontManagerService.
        if (!com.android.text.flags.Flags.useOptimizedBoottimeFontLoading()
                && Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) {
            Slog.i(TAG, "Loading pre-installed system font map.");
            Typeface.loadPreinstalledSystemFontMap();
        }

        // Attach JVMTI agent if this is a debuggable build and the system property is set.
        if (Build.IS_DEBUGGABLE) {
            // Property is of the form "library_path=parameters".
            String jvmtiAgent = SystemProperties.get("persist.sys.dalvik.jvmtiagent");
            if (!jvmtiAgent.isEmpty()) {
                int equalIndex = jvmtiAgent.indexOf('=');
                String libraryPath = jvmtiAgent.substring(0, equalIndex);
                String parameterList =
                        jvmtiAgent.substring(equalIndex + 1, jvmtiAgent.length());
                // Attach the agent.
                try {
                    Debug.attachJvmtiAgent(libraryPath, parameterList, null);
                } catch (Exception e) {
                    Slog.e("System", "*************************************************");
                    Slog.e("System", "********** Failed to load jvmti plugin: " + jvmtiAgent);
                }
            }
        }
    } finally {
        t.traceEnd();  // InitBeforeStartServices
    }

    // Setup the default WTF handler
    RuntimeInit.setDefaultApplicationWtfHandler(SystemServer::handleEarlySystemWtf);

    // Start services.
    try {
        t.traceBegin("StartServices");
        startBootstrapServices(t);
        startCoreServices(t);
        startOtherServices(t);
        startApexServices(t);
        // Only update the timeout after starting all the services so that we use
        // the default timeout to start system server.
        updateWatchdogTimeout(t);
        CriticalEventLog.getInstance().logSystemServerStarted();
    } catch (Throwable ex) {
        Slog.e("System", "******************************************");
        Slog.e("System", "************ Failure starting system services", ex);
        throw ex;
    } finally {
        t.traceEnd(); // StartServices
    }

    StrictMode.initVmDefaults(null);

    if (!mRuntimeRestart && !isFirstBootOrUpgrade()) {
        final long uptimeMillis = SystemClock.elapsedRealtime();
        FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
                FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SYSTEM_SERVER_READY,
                uptimeMillis);
        final long maxUptimeMillis = 60 * 1000;
        if (uptimeMillis > maxUptimeMillis) {
            Slog.wtf(SYSTEM_SERVER_TIMING_TAG,
                    "SystemServer init took too long. uptimeMillis=" + uptimeMillis);
        }
    }

    // Set binder transaction callback after starting system services
    Binder.setTransactionCallback(new IBinderCallback() {
        @Override
        public void onTransactionError(int pid, int code, int flags, int err) {
            mActivityManagerService.frozenBinderTransactionDetected(pid, code, flags, err);
        }
    });

    // Loop forever.
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

如上代码,可以持续优化,所有的核心服务开始在此处启动,其中包括AMS,WMS等服务,其中WMS和AMS依赖关系如下:

  • WMS初始化需AMS注册窗口管理接口(ServiceManager.addService("window", wms)
  • PMS需等待Installer服务就绪(mInstaller = mSystemServiceManager.startService(Installer.class)

接下来,进入AMS,WMS,PMS等交互全链路

二、AMS-WMS-PMS交互全链路

1. Activity启动触发窗口创建
  • 调用链路
    ActivityThread.handleLaunchActivity()ActivityManagerService.attachApplication()

ActivityThread:frameworks/base/core/java/android/app/ActivityThread.java

/**
 * Extended implementation of activity launch. Used when server requests a launch or relaunch.
 */
@Override
public Activity handleLaunchActivity(ActivityClientRecord r,
        PendingTransactionActions pendingActions, int deviceId, Intent customIntent) {
    // If we are getting ready to gc after going to the background, well
    // we are back active so skip it.
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;

    if (r.profilerInfo != null) {
        mProfiler.setProfiler(r.profilerInfo);
        mProfiler.startProfiling();
    }

    // Make sure we are running with the most recent config and resource paths.
    applyPendingApplicationInfoChanges(r.activityInfo.packageName);
    mConfigurationController.handleConfigurationChanged(null, null);
    updateDeviceIdForNonUIContexts(deviceId);

    if (localLOGV) Slog.v(
        TAG, "Handling launch of " + r);

    // Initialize before creating the activity
    if (ThreadedRenderer.sRendererEnabled
            && (r.activityInfo.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
        HardwareRenderer.preload();
    }
    WindowManagerGlobal.initialize();

    // Hint the GraphicsEnvironment that an activity is launching on the process.
    GraphicsEnvironment.hintActivityLaunch();

    final Activity a = performLaunchActivity(r, customIntent);

    if (a != null) {
        r.createdConfig = new Configuration(mConfigurationController.getConfiguration());
        reportSizeConfigurations(r);
        if (!r.activity.mFinished && pendingActions != null) {
            pendingActions.setOldState(r.state);
            pendingActions.setRestoreInstanceState(true);
            pendingActions.setCallOnPostCreate(true);
        }
    } else {
        // If there was an error, for any reason, tell the activity manager to stop us.
        ActivityClient.getInstance().finishActivity(r.token, Activity.RESULT_CANCELED,
                null /* resultData */, Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
    }

    return a;
}

AMS:

image.png

2. WMS窗口绑定

WMS启动整体流程:

1.SystemServer: mWindowManagerService = WindowManagerService.main( mContext, mDisplayManager, mPackageManagerService, mPowerManagerService); ServiceManager.addService(Context.WINDOW_SERVICE, mWindowManagerService);

2.创建DisplayContent:初始化物理/虚拟屏幕的显示区域(DisplayContent.java)

3.注册窗口策略:PhoneWindowManager设置系统窗口规则(如状态栏位置)

4.绑定SurfaceFlinger:通过SurfaceComposerClient建立与图形子系统的连接

5 `// WindowManagerGlobal.java public void addView(View view, LayoutParams params) { // 1. 创建WindowState WindowState win = new WindowState(...); // 2. 调用WMS接口 mWindowManager.addView(win, params); }

// WindowManagerService.java (JNI层)
static void android_view_WindowManager_addView(JNIEnv* env, jobject clazz, jobject view, jobject attrs) {
    sp<IWindowManager> wms = IWindowManager::asBinder(getService("window"));
    wms->addWindow(...); // Binder调用WMS
}`

6.

`  
// WindowManagerService.cpp
status_t WindowManagerService::addWindow(...) {
    sp<WindowState> win = new WindowState(...);
    mWindows.add(win); // 添加到窗口列表
    mLayoutNeeded = true; // 触发布局更新
} `

7. `

3. 图形合成机制
```cpp
// SurfaceFlinger.cpp
void SurfaceFlinger::composeSurfaces() {
    for (auto& layer : mLayers) {
        if (layer->isVisible()) {
            layer->latchBuffer(); // 获取GraphicBuffer
            mHwc->setLayerBuffer(layer->getHandle(), buffer); // 提交给HWC
        }
    }
    mHwc->commit(); // 提交合成指令
} `

WMS角色:通过SurfaceControl.setLayer()调整窗口Z-order,触发SurfaceFlinger重新排序

三,PMS全流程解析

1.PMS启动流程:

` // SystemServer.java
private void startBootstrapServices() {
    mPackageManagerService = PackageManagerService.main(
        mContext, mInstaller, mFactoryTestMode != FactoryTest.OFF, mOnlyCore);
    ServiceManager.addService("package", mPackageManagerService);
}

// PackageManagerService.java
public static PackageManagerService main(...) {
    PackageManagerService pms = new PackageManagerService(...);
    pms.mInstaller = mSystemServiceManager.startService(Installer.class); // 依赖Installer
    pms.scanPackagesLI(); // 扫描已安装应用
    return pms;
} `

2. APK解析与安装

    `// PackageParser.java
    public Package parsePackage(File apkFile, int flags) {
        // 1. 解析AndroidManifest.xml
        XmlPullParser parser = Xml.resolvePullParser(manifestFile);
        mPackage = new Package(...);
        // 2. 提取权限声明
        mPackage.mPermissions = parsePermissions(parser);
        // 3. 生成应用信息
        mPackage.applicationInfo = generateApplicationInfo();
    }`
    

3.权限校验流程

` // PackageManagerService.java
int checkUidPermission(String permName, int uid) {
    synchronized (mLock) {
        return mSettings.getPermissionsState().checkUidPermission(permName, uid);
    }
}

// 权限状态检查(PermissionsState.java)
int checkUidPermission(String permName, int uid) {
    UidState uidState = mUidStates.get(uid);
    return uidState != null ? uidState.checkPermission(permName) : PERMISSION_DENIED;
} `

关键数据结构:

    `// PackageInfo.java
    public static class Package {
        String packageName;          // 包名
        int versionCode;             // 版本号
        Signature[] signatures;      // 签名信息
        Permission[] permissions;    // 声明权限
    }`

联动示例图:

image.png

联动的基础是IPC的通信,这里就必须介绍一下Binder

Binder通信详细介绍在这里

说到这里,关于WMS有一个很重要的组成部分(专题)

SurfaceFlinger合成引擎

Three.js 渲染玻璃幕墙大楼

2025年5月23日 17:07

微信截图_20250523170501.png

Three.js 真实感渲染:从环境到材质深度解析

我们将一起探索如何运用 Three.js 来创建更逼真的三维场景,特别是理解环境设置的重要性,以及如何调校PBR(Physically Based Rendering,基于物理的渲染)材质,让建筑模型拥有更真实的玻璃感、金属感和墙体质感。

项目地址:github.com/xiaxiangfen…

一、真实感渲染的基石:PBR 简介

在计算机图形学中,PBR 是一套旨在模拟真实世界光照和材质交互方式的理论和着色技术。它的核心思想是,与其凭经验和"魔法数字"去凑效果,不如遵循物理光学原理来定义材质属性和光照行为。这样不仅能得到更可信的视觉效果,而且材质在不同光照环境下的表现也会更加一致和自然。

项目中的 MeshStandardMaterialMeshPhysicalMaterial 就是 Three.js 中实现 PBR 的主力。

二、核心图形学概念:为真实感铺路

在我们深入代码之前,先了解几个关键的图形学概念:

1. 色彩空间 (Color Spaces) 与线性工作流 (Linear Workflow)

想象一下,光在真实世界中是以"线性"方式叠加的(例如,两个光源的亮度直接相加)。但我们的显示器(以及很多图像文件格式,如 JPG、PNG)通常使用"非线性"的色彩空间,最常见的是 sRGB。sRGB 会对颜色值进行一种叫做"伽马校正 (Gamma Correction)"的非线性变换,这让人眼在屏幕上感知到的亮度更自然。

为什么重要? 如果我们在进行光照计算(混合颜色、计算反射等)时不考虑这种差异,直接用 sRGB 空间下的颜色值进行线性运算,结果就会不准确,通常会导致场景偏暗,高光部分丢失细节。

线性工作流: 核心思想是在光照计算时,将所有颜色输入(如纹理颜色、材质颜色)转换到线性空间,进行物理上正确的计算,然后在渲染的最后阶段,将结果转换回 sRGB 空间(或其他显示设备的目标空间)以正确显示。

在代码中:

renderer.outputColorSpace = THREE.SRGBColorSpace;

这行代码告诉 Three.js 渲染器的最终输出颜色应该被处理为 sRGB 色彩空间。Three.js 的 PBR 材质和加载器(如 GLTFLoader)通常会自动处理纹理和颜色的输入,确保它们在线性空间中参与计算。

2. 色调映射 (Tone Mapping)

真实世界的光照强度范围(动态范围)远超普通显示器所能展示的范围。例如,正午阳光下的亮度可能是室内灯光的几千甚至几万倍。HDR(高动态范围)图像可以捕捉这种宽广的亮度信息。

色调映射就是将场景中计算出来的高动态范围颜色值"压缩"或"映射"到显示器能够表现的低动态范围(LDR)的过程。好的色调映射算法能在保留场景亮部和暗部细节的同时,产生视觉上愉悦和自然的图像。

在代码中:

renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
  • THREE.ACESFilmicToneMapping: Academy Color Encoding System (ACES) 是一种电影工业常用的色彩管理系统,其 Filmic 色调映射能提供一种接近电影胶片质感的柔和过渡和漂亮的色彩表现,尤其在高光区域处理得很好。
  • toneMappingExposure: 控制最终图像的整体亮度,类似于相机中的曝光补偿。增加此值会使场景变亮。

三、环境设置:构筑光影世界

环境是决定渲染效果好坏的重中之重。它不仅包括光源,还包括天空和周围能反射光线的物体。

1. 基础场景组件

  • THREE.Scene: 容纳所有模型、光源、相机等对象的容器。
  • THREE.PerspectiveCamera: 模拟人眼或相机看到的透视效果。参数包括视野角度 (FOV)、宽高比 (aspect)、近裁剪面 (near) 和远裁剪面 (far)。
  • THREE.WebGLRenderer: 将场景渲染到 HTML canvas 上的"画笔"。

2. 光源 (Lighting)

场景中使用了两种主要光源:

  • 环境光 (AmbientLight):

    ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);
    

    环境光是一种无方向的光,它会均匀地照亮场景中的所有物体,无论其朝向如何。可以把它想象成光线在环境中多次反弹后形成的漫射背景光。

    • 0xffffff: 光的颜色(白色)。
    • 0.4: 光的强度。 环境光本身不能产生阴影,它主要用于提升场景的整体亮度和减少纯黑区域,使暗部不至于死黑一片。
  • 平行光 (DirectionalLight):

    directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
    directionalLight.position.set(10, 20, 10);
    directionalLight.castShadow = true;
    // ... shadow properties ...
    scene.add(directionalLight);
    

    平行光模拟来自非常遥远光源(如太阳)的光线,所有光线都是平行的。

    • position: 非常重要,平行光的方向是由其位置指向场景原点(或其目标对象)决定的。所以 directionalLight.position.set(10, 20, 10) 意味着光从 (10, 20, 10) 点照向原点。
    • castShadow = true: 使此光源能够投射阴影。
    • 阴影属性:
      • directionalLight.shadow.mapSize.width = 2048;
      • directionalLight.shadow.mapSize.height = 2048; 阴影贴图的分辨率。值越大,阴影边缘越清晰,但消耗的性能也越高。
      • directionalLight.shadow.camera.near = 0.5;
      • directionalLight.shadow.camera.far = 50; 定义了光源"视锥体"的范围,只有在这个范围内的物体才会产生阴影。调整这个范围以紧密包裹场景中需要投射阴影的物体,可以提高阴影质量。
      • directionalLight.shadow.bias = -0.0005; 阴影偏移量。用于解决"阴影痤疮"(shadow acne,物体自身投射在自己表面的斑驳阴影)或"彼得潘现象"(peter panning,物体与阴影分离)的问题。这是一个需要细调的值。

3. 动态天空与太阳模拟 (Dynamic Sky & Sun)

代码使用 THREE.Sky 对象来程序化地生成一个动态天空背景,并且这个天空的状态(如浑浊度、太阳位置)会影响场景光照。

// 初始化天空
sky = new Sky();
sky.scale.setScalar(450000); // 天空穹顶的大小
scene.add(sky);

sun = new THREE.Vector3(); // 用于存储太阳方向向量
pmremGenerator = new THREE.PMREMGenerator(renderer); // 后续用于生成环境贴图

// 天空着色器uniforms (参数)
const skyUniforms = sky.material.uniforms;
skyUniforms['turbidity'].value = 10;    // 浑浊度
skyUniforms['rayleigh'].value = 3;      // 瑞利散射系数
skyUniforms['mieCoefficient'].value = 0.005; // 米氏散射系数
skyUniforms['mieDirectionalG'].value = 0.7;  // 米氏散射方向性

updateSkyAndSun(); // 根据参数更新天空和太阳
  • THREE.Sky 原理: 它内部是一个基于大气散射物理模型的着色器。主要模拟两种散射:

    • 瑞利散射 (Rayleigh Scattering): 由尺寸远小于光波长的空气分子引起(如氮气、氧气)。这种散射对短波长的蓝光比长波长的红光更强烈,所以天空是蓝色的,日落是红色的。skyUniforms['rayleigh'] 控制其强度。
    • 米氏散射 (Mie Scattering): 由尺寸与光波长相当的较大颗粒物引起(如水蒸气、尘埃、烟雾)。这种散射对所有波长的光影响相近,使天空呈现灰白色,并形成太阳周围的光晕。skyUniforms['turbidity'] (浑浊度)、skyUniforms['mieCoefficient']skyUniforms['mieDirectionalG'] 控制这部分效果。
  • 更新太阳位置 (updateSkyAndSun 函数):

    const phi = THREE.MathUtils.degToRad(90 - elevation);
    const theta = THREE.MathUtils.degToRad(azimuth);
    sun.setFromSphericalCoords(1, phi, theta);
    sky.material.uniforms['sunPosition'].value.copy(sun);
    
    • elevation (太阳高度角) 和 azimuth (太阳方位角) 是通过UI控制的。
    • THREE.MathUtils.degToRad(): 将角度从度转换为弧度,因为三角函数通常使用弧度。
    • 球坐标 (Spherical Coordinates): sun.setFromSphericalCoords(radius, phi, theta)
      • radius: 到原点的距离(这里设为1,因为我们主要关心方向)。
      • phi: "极角"或"倾斜角",是从正Y轴(天顶)向下到太阳方向的夹角。代码中 90 - elevation 是因为 elevation 通常指从地平线向上的角度。
      • theta: "方位角",是从正Z轴(或X轴,取决于约定)在XZ平面上旋转到太阳方向投影的夹角。
    • sky.material.uniforms['sunPosition'].value.copy(sun): 将计算出的太阳方向向量传递给天空着色器。
  • 太阳与平行光的联动:

    directionalLight.position.copy(sun.clone().multiplyScalar(sunDistance));
    // ... 根据太阳高度调整光照颜色和强度 ...
    directionalLight.color = sunColor;
    

    平行光的位置(从而决定其方向)被设置为与天空着色器中的太阳方向一致。并且,根据太阳高度,平行光的颜色和强度也会动态调整,模拟不同时间段(如日出、正午、日落)的光照变化。

  • PMREMGenerator 与天空的环境光:

    const renderTarget = pmremGenerator.fromScene(sky);
    scene.environment = renderTarget.texture;
    scene.background = sky; // 将天空本身设置为背景
    
    • pmremGenerator.fromScene(sky): 这行代码非常重要。PMREMGenerator (Pre-filtered Mipmapped Radiance Environment Map Generator) 会将当前天空的视觉效果(包括太阳的亮度和颜色)渲染到一个特殊处理过的纹理中(环境贴图)。
    • 这个纹理不仅包含了天空的图像,还预先计算了不同粗糙度级别下的反射光照信息,PBR材质可以直接使用它来进行高效的基于图像的照明 (Image-Based Lighting, IBL) 和反射。
    • scene.environment = renderTarget.texture;: 将这个生成的天空环境贴图设置为场景的全局环境贴图。PBR材质会自动拾取这个贴图用于计算反射和间接光照。
    • 这意味着,即使没有HDR文件,动态天空本身也能为场景提供基础的环境光照和反射源。

4. HDR 环境贴图 (HDR Environment Mapping)

虽然动态天空能提供环境光,但高质量的HDR(高动态范围)图像通常能提供更丰富、更真实的光照细节和反射信息。

const hdrEnvironmentURL = './kloofendal_48d_partly_cloudy_puresky_4k.hdr';
// ...
new RGBELoader()
    .setPath('')
    .load(hdrEnvironmentURL, (hdrEquirect) => {
        const envMapFromHDR = pmremGenerator.fromEquirectangular(hdrEquirect).texture;
        envMap = envMapFromHDR; // 全局变量,供材质使用
        hdrEquirect.dispose();
        console.log("HDR environment map loaded for reflections.");
        loadModel(); // HDR加载成功后加载模型
    }, null, (error) => {
        console.error('Failed to load HDR environment map:', error);
        envMap = scene.environment; // Fallback: 如果HDR加载失败,使用天空生成的环境贴图
        loadModel();
    });
  • HDR 图像: .hdr 文件通常存储的是高动态范围的图像,它能记录比普通图像(如JPG)更广阔的亮度级别。例如,太阳本身在HDR中可能具有非常高的亮度值,而阴影区域的亮度值则较低,这种对比在普通图像中会被裁剪掉。
  • RGBELoader: 用于加载 Radiance HDR (.hdr) 文件格式。
  • Equirectangular (等距柱状投影) 格式: HDR环境贴图常以这种全景图格式存储。
  • pmremGenerator.fromEquirectangular(hdrEquirect).texture: 这是关键步骤。PMREMGenerator 再次登场,这次它将2D的等距柱状投影HDR图像转换成GPU更喜欢使用的格式——通常是带有预过滤Mipmap的立方体贴图 (Cubemap)。
    • 预过滤 (Pre-filtering): 对环境贴图进行不同程度的模糊处理,并存储在Mipmap层级中。当PBR材质的粗糙度较高时,它会采样模糊的环境贴图版本,得到柔和的反射;粗糙度低时,采样清晰的版本,得到锐利的反射。
    • envMap = envMapFromHDR;: 将处理后的HDR纹理存储在全局变量 envMap 中。
  • 材质中的应用:
    // 在金属和玻璃材质中
    envMap: envMap || scene.environment, // 优先使用加载的HDR,否则用天空的
    
    PBR材质的 envMap 属性被设置为这个处理过的HDR纹理。这使得物体能够反射出HDR环境中的细节,并且场景光照也会受到这个HDR环境的影响(IBL)。
  • 注意: 代码中,scene.environment 是先由 sky 生成的,然后如果HDR加载成功,envMap 变量会指向HDR纹理,并在材质中优先使用 envMap。如果HDR加载失败,材质会回退到使用 scene.environment(即天空生成的环境图)。这是一个很好的容错处理。

四、PBR 材质详解:赋予物体灵魂

现在我们来看看项目中定义的几种PBR材质。PBR材质的核心在于用一组直观且符合物理意义的参数来描述表面特性。

通用PBR属性

  • color (颜色): 物体的基础颜色。对于非金属,这是光线散射出物体表面时呈现的颜色。对于金属,它更像是反射光线的色调。
  • metalness (金属度): 0 到 1 的值。
    • 0: 非金属 (Dielectric)。光线一部分被表面反射(高光通常是白色的),一部分进入物体内部发生散射后再射出(这部分带有物体颜色)。
    • 1: 金属 (Metallic)。光线几乎完全在表面被反射,并且反射光会带上金属本身的颜色(例如金是黄色的反射,铜是红褐色的反射)。
    • 介于 0 和 1 之间的值可以模拟脏污的金属或带有金属薄层的非金属。
  • roughness (粗糙度): 0 到 1 的值。描述表面的微观平整度。
    • 0: 完美光滑(如镜子、平静水面),反射非常清晰锐利。
    • 1: 非常粗糙(如粉笔、毛毡),反射非常模糊,接近漫反射。
  • envMap (环境贴图): 指定一个纹理(通常是预处理过的Cubemap,如我们之前生成的 envMapFromHDRpmremGenerator.fromScene(sky).texture)作为反射和环境光照的来源。这是PBR材质产生真实反射效果的关键。
  • envMapIntensity (环境贴图强度): 控制环境贴图对材质影响的强度。

1. 金属窗框材质 (metalFrameMaterial)

const metalFrameMaterial = new THREE.MeshPhysicalMaterial({
    color: 0x777777,       // 基础颜色,对于金属,通常较暗或中性,因为主要颜色来自反射
    metalness: 1.0,        // 纯金属
    roughness: 0.1,        // 较低的粗糙度,使其具有光泽和清晰反射 (之前是0.2,调整为0.1效果更好)
    envMap: envMap || scene.environment,
    envMapIntensity: 1.5   // 环境反射强度
});
  • 原理: 金属的视觉特征主要来自于其对环境的强烈反射。
    • metalness: 1.0 告诉渲染器按照金属的光学模型处理光线。
    • roughness: 0.1 (较低值) 意味着表面相对光滑,能够形成较为清晰的环境反射。如果这个值很高(比如0.8),金属看起来会像磨砂的,反射会非常模糊。
    • envMap 提供了反射的内容。没有 envMap,金属看起来会很平淡,失去了金属光泽。
    • color: 0x777777 (灰色) 作为基础色,有色的环境光反射叠加上后会形成最终的金属色感。对于纯净金属,有时也会使用更亮的基色(如白色),让反射的颜色完全主导。

2. 玻璃幕墙材质 (glassCurtainWallMaterial)

const glassCurtainWallMaterial = new THREE.MeshPhysicalMaterial({
    color: 0xadd8e6,        // 淡蓝色玻璃的颜色 (透射色)
    metalness: 0.0,         // 玻璃是非金属
    roughness: 0.05,        // 非常光滑的玻璃,产生清晰反射
    transmission: 0.9,      // 透射率,90%的光线可以穿透玻璃
    ior: 1.52,              // 折射率 (Index of Refraction),普通玻璃的典型值
    envMap: envMap || scene.environment,
    envMapIntensity: 1.8,   // 环境反射强度 (之前是1.0,调高以增强反射)
    transparent: true,      // 必须开启,以启用透明效果和透射
    opacity: 0.85,          // 不透明度 (之前是0.8)。注意:transmission 和 opacity 会相互影响
    thickness: 0.1          // 玻璃的厚度,影响透射光的衰减和色散(如果启用了相关高级特性)
});

玻璃是PBR中比较复杂的材质之一,MeshPhysicalMaterial 提供了更多控制参数:

  • metalness: 0.0: 玻璃是非金属。
  • roughness: 0.05: 高度抛光的玻璃表面非常光滑,所以粗糙度很低。
  • transmission: 0.9: 这是实现"透光"效果的核心属性。它表示有多少比例的光线能够穿透材质。值越高,玻璃越透明。
  • ior: 1.52: 折射率。当光线从一种介质(如空气)进入另一种介质(如玻璃)时会发生弯曲,弯曲的程度由两种介质的折射率决定(基于斯涅尔定律 Snell's Law: (n_1 \sin\theta_1 = n_2 \sin\theta_2))。ior 值越高,光线弯曲越明显,透过玻璃看到的物体变形也越大。1.52 是普通玻璃的常见折射率。水大约是1.33,钻石大约是2.42。
  • transparent: true: 为了让 transmission 生效,或者使用 opacity 实现传统alpha混合透明,这个属性需要设为 true
  • opacity: 0.85: 这是传统的Alpha透明度。对于 MeshPhysicalMaterial 的透射效果,opacity 的影响比较微妙。当 transmission 启用时,opacity 更多地像一个整体的"可见性"因子。如果希望玻璃有轻微的磨砂感或者整体偏暗淡,可以调整 opacity。但主要的透明感应由 transmission 控制。
  • envMapenvMapIntensity: 玻璃表面也会反射环境,这是玻璃质感的重要组成部分。想象一下窗户上反射出的天空和周围景物。
  • thickness: 0.1: 对于具有 transmission 的材质,thickness 可以影响透射光的颜色衰减。如果玻璃有颜色(color 属性),光线穿过越厚的玻璃,颜色会显得越深。对于薄玻璃,这个值影响可能不显著,但对于有色厚玻璃或模拟液体等效果时很重要。

让玻璃更"玻璃"的关键

  1. transmission (例如 0.80.95)。
  2. roughness (例如 0.00.1) 以获得清晰的反射和透射。
  3. 合适的 ior (通常 1.41.6 之间是各种玻璃的范围)。
  4. 清晰且细节丰富的 envMap,以及适当的 envMapIntensity
  5. transparent: true

3. 混凝土与石材墙体材质 (concreteMaterial, stoneMaterial)

// 混凝土
const concreteMaterial = new THREE.MeshStandardMaterial({
    color: 0xd0d0d0,        // 浅灰色混凝土
    metalness: 0.1,         // 通常混凝土是非金属,这里0.1可能想表达非常轻微的金属杂质感或湿润感
    roughness: 0.8,         // 混凝土表面粗糙,反射模糊
    envMap: envMap || scene.environment,
    envMapIntensity: 0.3,
    // bumpScale: 0.005 // (配合bumpMap使用)
});

// 石材/花岗岩
const stoneMaterial = new THREE.MeshStandardMaterial({
    color: 0xb8b8b8,
    metalness: 0.05,        // 类似混凝土,极低的金属度
    roughness: 0.9,         // 石材通常更粗糙
    envMap: envMap || scene.environment,
    envMapIntensity: 0.2
});
  • 原理: 墙体这类材质通常是非金属的、表面粗糙的。
    • metalness: 通常为 0。代码中的 0.10.05 是一个非常低的值,可能是为了给材质增加一点点微弱的、不易察觉的光泽变化,或者模拟表面某些微小颗粒的反射特性。对于纯粹的混凝土或干燥石材,设为 0 更符合物理。
    • roughness: 较高的值 (如 0.8, 0.9)。这使得光线在表面发生广泛的漫反射,形成哑光的外观,反射的环境影像会非常模糊,几乎看不清具体内容,但仍能贡献整体的颜色和亮度。
    • envMapIntensity: 即使是粗糙表面,环境光照依然重要,它能提供间接光照,使得暗部不死黑,并影响物体整体色调。但强度通常比金属或玻璃低。
  • 提升真实感: 对于这类材质,除了基础的PBR参数,使用纹理贴图是提升真实感的关键:
    • map (颜色贴图/漫反射贴图): 提供表面详细的颜色和图案。
    • normalMap (法线贴图): 模拟表面细微的凹凸细节(如砖缝、石材质地),极大地增强立体感,而无需增加模型的多边形数量。法线贴图存储的是表面法线方向的扰动信息。
    • roughnessMap (粗糙度贴图): 允许表面不同区域有不同的粗糙度。例如,混凝土上湿润的部分可能比干燥的部分更光滑。
    • metalnessMap (金属度贴图): 如果表面有金属和非金属混合的部分(如生锈的铁皮)。
    • aoMap (环境光遮蔽贴图): 预计算的阴影信息,用于表现缝隙、角落等难以接收到直接光照的区域,增加深度感。 代码中提到了 bumpScale,它通常与 bumpMap (凹凸贴图) 配合使用。凹凸贴图是一种简单的灰度图,通过模拟表面高度变化来改变法线,也能增加细节,但效果通常不如法线贴图精确。

五、模型集成与后续调整

1. GLTF 模型加载与材质应用

loader.load(modelUrl, function(gltf) {
    model = gltf.scene;
    scene.add(model);
    
    model.traverse((child) => { // 遍历模型的所有子对象
        if (child.isMesh) {     // 如果是网格模型
            child.castShadow = true;
            child.receiveShadow = true;
            console.log('Found mesh in GLB:', child.name);

            // 根据网格名称应用材质
            if (child.name === "窗框") {
                child.material = metalFrameMaterial;
            } else if (child.name === "墙体_1") {
                child.material = concreteMaterial;
            } // ... 其他材质分配
        }
    });
    // ... 模型居中和缩放 ...
});
  • GLTFLoader: Three.js 推荐的现代三维模型格式加载器。
  • model.traverse(): 这是一个非常实用的方法,可以递归地访问模型及其所有子节点。通过检查 child.isMeshchild.name,您可以为导入模型中的特定部分指定在代码中创建的自定义材质。这是替换或增强glTF内部材质的常用方法。
  • 模型居中与缩放: 代码中通过 THREE.Box3 计算模型的边界框,然后将其平移到场景中心并缩放到合适的大小,这对于保证模型在场景中正确显示非常重要。

2. 调试与优化建议

  • 控制台日志: 您在代码中加入了很多 console.log,这对于调试模型加载、材质应用等非常有帮助。
  • Dat.GUI 或类似UI: 对于PBR材质参数的调整,使用一个实时调试UI(如 Dat.GUI)会非常高效。您可以直接在浏览器中拖动滑块改变参数值,立即看到效果,而无需反复修改代码和刷新页面。项目已经实现了一个控制面板,这是非常棒的实践!
  • 检查HDR加载: 确保HDR环境贴图 (kloofendal_48d_partly_cloudy_puresky_4k.hdr) 能够被正确加载。如果网络问题或路径问题导致加载失败,材质的反射效果会大打折扣(虽然有天空作为后备,但HDR细节更丰富)。注意浏览器控制台中的网络请求和错误信息。
  • 光照强度: 环境光、平行光的强度,以及 toneMappingExposure,这些都会显著影响材质的最终观感。金属和玻璃对光线变化尤为敏感。
  • 循序渐进: 调整材质时,可以先从一个基本的光照环境开始,单独调整一种材质,观察其在不同视角下的变化。例如,先确保金属的反射清晰,再调整玻璃的透明度和反射,最后是墙体的质感。

希望这篇详细的解析能帮助您更深入地理解项目中的渲染技术和PBR材质原理,并为您的后续创作提供启发!

cesium中如何创建图片点位和点击Billboard更改其图片

作者 木子李i
2025年5月23日 17:07

大屏可视化中,我们会经常遇到一个需求就是在地图上打点,为了好看一般点位都是图片的形式。并且点击的时候会有切换图片,告诉用户当前点击的是哪个点。

在cesium中如何创建图片点位,并且绘制大量点的同时保证性能

cesium中提供了类new Cesium.Billboard中文翻译就是广告牌的意思new Cesium.Billboard官方文档 不废话直接看我的代码:

export function addPoints(points: Array<{ lon: number; lat: number }>) {
  const primitives = new Cesium.PrimitiveCollection();
  const billboards = new Cesium.BillboardCollection();
  primitives.add(billboards);
  points.forEach((point) => {
    if (point.lon && point.lat) {
      billboards.add({
        image: cameraPoint,
        position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat),
        width: 52,
        height: 81,
        id: 'point-' + point.lon + '-' + point.lat,
      });
    }
  });
  return { primitives, billboards };
}
 const { primitives } = addPoints([
      {
        lon: 119,
        lat: 32,
      },
    ]);
viewer.scene.primitives.add(primitives)

看到我的代码是不是发现也没有用Cesium.Billboard啊。现在我来一点点解读这段代码到底在做什么

new Cesium.PrimitiveCollection
new Cesium.BillboardCollection
其实就是创建一个集合,可以理解是一个容器,目的就是为了方便管理
比如:billboards.destroy()就可以销毁
primitives同理

billboards.add(options)参数options就是new Cesium.Billboard中的options

然后就是primitives,这个就是能让绘制大量点的时候保持性能

其实还有个原始的方法就是创建Cesium.Entity也是可以创建点位的,但是这个第一不好管理,第二性能不好,点少的倒是无所谓的。

创建点击事件,给点位切换图片

直接看代码:

export function setupClickEvent(viewer) {
  const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);

  handler.setInputAction((movement) => {
    const pickedObject = viewer.scene.pick(movement.position);

    if (pickedObject && pickedObject.collection && pickedObject.primitive && pickedObject.collection.contains(pickedObject.primitive)) {
      // 修改点击到的 billboard 的图片
      console.log('点击到了 billboard', pickedObject.primitive);
      pickedObject.primitive.image = soundPoint;
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}

创建事件没有好说的,网上都有 核心代码就是:

  if (pickedObject && pickedObject.collection && pickedObject.primitive && pickedObject.collection.contains(pickedObject.primitive)) {
      // 修改点击到的 billboard 的图片
      console.log('点击到了 billboard', pickedObject.primitive);
      pickedObject.primitive.image = soundPoint;
    }

pickedObject.collection其实就是属于应该是属于Cesium.PrimitiveCollection或者Cesium.BillboardCollection类中的,这两个类中都有一个方法contains,主要是用来判断你点击的点是不是属于创建的里面的。

其实我发现 只要pickedObject.primitive存不存在就可以了,以防万一嘛。

然后将新的图片地址赋值给 pickedObject.primitive.image即可

【滑动窗口算法实战】最小子串和最小覆盖子串问题(JS + Python 双解)

作者 前端付豪
2025年5月23日 17:06

【滑动窗口算法实战】最小子串和最小覆盖子串问题(JS + Python 双解)

🧠 引言

你是否遇到过这些场景?

  • 在字符串中找出“最短连续子串”,满足某些条件
  • 判断一个数组内是否存在“某长度内的目标和”
  • 滑动窗口模板写了又忘,记不住!

别怕,本篇将通过两个经典问题:

  1. 最小和子数组 ≥ target
  2. 最小覆盖子串(LeetCode 困难题)

来系统讲解滑动窗口的思维模型与实战技巧,并用 JS + Python 双解写出高性能代码!


🧱 一、什么是滑动窗口?

滑动窗口是一种通过维护区间范围(如 [left, right])动态调整子串或子数组范围的技巧,常用于查找最优区间问题。


🧪 Part 1:最短子数组长度(中等)

❓题目描述(LeetCode 209)

给定正整数数组 nums 和目标值 target,找出和 ≥ target 的最短子数组长度,返回其长度。如果不存在则返回 0。

示例:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:[4,3] 是最短的子数组

✅ 解法思路(滑动窗口)

  1. 用两个指针 startend 表示窗口
  2. 向右扩展 end,累加窗口和
  3. 当窗口和 ≥ target,尝试缩小 start 来更新最短长度

💻 JavaScript 实现

function minSubArrayLen(target, nums) {
  let left = 0, sum = 0, minLen = Infinity;

  for (let right = 0; right < nums.length; right++) {
    sum += nums[right];

    while (sum >= target) {
      minLen = Math.min(minLen, right - left + 1);
      sum -= nums[left++];
    }
  }

  return minLen === Infinity ? 0 : minLen;
}

🐍 Python 实现

def min_sub_array_len(target, nums):
    left, total = 0, 0
    min_len = float('inf')

    for right in range(len(nums)):
        total += nums[right]
        while total >= target:
            min_len = min(min_len, right - left + 1)
            total -= nums[left]
            left += 1

    return 0 if min_len == float('inf') else min_len

🧪 Part 2:最小覆盖子串(困难)

❓题目描述(LeetCode 76)

给定字符串 st,返回 s 中包含 t 所有字符的最小子串。若无解则返回空串。

输入: s = "ADOBECODEBANC", t = "ABC"
输出: "BANC"

✅ 解法思路(滑动窗口 + 计数器)

  1. 用两个指针维护窗口
  2. 用字典统计 t 中需要的字符频率
  3. 移动 right 扩大窗口,直到包含所有字符
  4. 再移动 left 尝试缩小窗口

💻 JavaScript 实现

function minWindow(s, t) {
  const need = {};
  for (const c of t) need[c] = (need[c] || 0) + 1;

  let left = 0, count = 0;
  let minLen = Infinity, res = '';
  const window = {};

  for (let right = 0; right < s.length; right++) {
    const c = s[right];
    window[c] = (window[c] || 0) + 1;

    if (need[c] && window[c] === need[c]) count++;

    while (count === Object.keys(need).length) {
      if (right - left + 1 < minLen) {
        minLen = right - left + 1;
        res = s.slice(left, right + 1);
      }

      const d = s[left++];
      if (need[d]) {
        if (window[d] === need[d]) count--;
        window[d]--;
      }
    }
  }

  return res;
}

🐍 Python 实现

from collections import Counter

def min_window(s, t):
    need = Counter(t)
    window = {}
    left = 0
    valid = 0
    start, length = 0, float('inf')

    for right, c in enumerate(s):
        window[c] = window.get(c, 0) + 1
        if c in need and window[c] == need[c]:
            valid += 1

        while valid == len(need):
            if right - left + 1 < length:
                start, length = left, right - left + 1

            d = s[left]
            left += 1
            if d in need:
                if window[d] == need[d]:
                    valid -= 1
                window[d] -= 1

    return "" if length == float('inf') else s[start:start + length]

⚠️ 易错点分析

场景 正确做法
窗口字符多但不满足需要 只在匹配字符频次后计数有效
不判断 minLen === Infinity 输出时需返回空串或 0
漏判断窗口收缩时更新 valid 及时递减匹配字符计数

🧩 拓展任务

  • 改写为找“最长”符合条件的子串
  • 支持“重复字符 + 区分大小写”情形
  • 用 Trie 替代哈希结构处理大规模字符集

📚 总结一句话

滑动窗口是一种处理区间类问题的通用模型,掌握它,就能轻松搞定数组和字符串中的“最小/最大/最短”问题!


📘 下一篇预告:

第12篇:【用双指针解决有序数组问题】JS 与 Python 解“两数之和 II”和“移动零”!

ECharts-for-React 使用攻略:从入门到进阶

作者 五号厂房
2025年5月22日 17:23

ECharts-for-React 使用攻略:从入门到进阶

ECharts-for-React 是 Apache ECharts 的官方 React 封装组件,它通过声明式语法和 React 生命周期管理,极大简化了 ECharts 在 React 项目中的集成流程。本文将系统讲解其核心功能与使用技巧,助你快速实现数据可视化。


一、快速入门

1. 安装依赖

npm install echarts-for-react echarts
# 或
yarn add echarts-for-react echarts

注意:echarts-for-react 需与 echarts@5.x 以上版本兼容

2. 基础柱状图示例

import React from 'react';
import ReactECharts from 'echarts-for-react';

const BarChart = () => {
  const option = {
    title: { text: '销量统计' },
    xAxis: { type: 'category', data: ['衬衫', '羊毛衫', '雪纺衫'] },
    yAxis: {},
    series: [{ data: [5, 20, 36], type: 'bar' }]
  };

  return <ReactECharts option={option} style={{ height: 400 }} />;
};

通过 option 属性传递 ECharts 配置项,支持所有原生 ECharts 配置


二、核心功能详解

1. 图表配置

  • 动态更新:通过函数返回配置项实现数据动态加载

    const getOption = () => ({ /* 动态配置 */ });
    <ReactECharts option={getOption()} />
    
  • 主题定制:支持内置主题与自定义主题

    <ReactECharts option={option} theme="dark" />
    

    预置主题:light, dark, infographic

2. 样式控制

属性 说明 示例
style 容器尺寸 style={{ width: '100%' }}
className 自定义 CSS 类名 className="chart-style"
opts 初始化参数 opts={{ renderer: 'svg' }}

3. 事件处理

绑定原生 ECharts 事件:

<ReactECharts
  onEvents={{
    click: (params) => console.log('点击事件:', params),
    legendselectchanged: (params) => console.log('图例切换:', params)
  }}
/>

支持所有 ECharts 事件类型


三、进阶技巧

1. 性能优化

  • 按需加载:减少打包体积

    import { BarChart } from 'echarts/charts';
    echarts.use([BarChart, TooltipComponent]);
    
  • 智能更新:避免重复渲染

    <ReactECharts
      notMerge={true}    // 不合并旧配置
      lazyUpdate={true}  // 延迟更新
    />
    

    适用于大数据量场景

2. 高级配置

  • 响应式布局:自动适配容器尺寸

    <ReactECharts autoResize={true} />
    
  • 图表实例操作:通过 ref 获取实例

    const chartRef = useRef();
    
    <ReactECharts ref={chartRef} />
    
    // 获取实例
    const echartsInstance = chartRef.current.getEchartsInstance();
    

四、最佳实践

  1. 数据更新策略

    const [data, setData] = useState([]);
    
    useEffect(() => {
      fetchData().then(res => setData(res));
    }, []);
    
    const option = useMemo(() => ({
      series: [{ data }]
    }), [data]);
    

    使用 useMemo 避免不必要的配置重建

  2. 复杂图表实现

    • 组合图表:多 series 配置
    • 自定义渲染:使用 graphic 组件
    • 动画控制:通过 animation 属性调节

五、常见问题

  1. 内存泄漏
    组件卸载时需手动销毁实例:

    useEffect(() => {
      return () => {
        if (chartInstance) chartInstance.dispose();
      };
    }, []);
    
  2. 样式冲突
    建议通过 CSS 模块化方案隔离样式,避免全局污染。


总结

ECharts-for-React 通过以下优势显著提升开发效率:

  • 📦 开箱即用:封装初始化/销毁逻辑
  • 🎨 无缝集成:完美适配 React 生态
  • 性能卓越:智能更新机制

建议结合 ECharts 官方文档官方文档 深入掌握高级功能,如地理坐标系、3D 图表等复杂场景的实现。

🚀🚀🚀 Node.js 24.1.0 新版本功能详解

2025年5月22日 11:00

前言

凌晨,Node.js 官方发布了 24.1.0 版本,快来跟我看看更新了哪些内容吧!

往期精彩推荐

正文

Node.js 24.1.0 版本引入了一系列新功能和改进,涵盖 文件系统HTTP/2调试工具REPL 等多个模块!

1. 文件系统(fs)模块增强

Node.js 24.1.0 对文件系统模块进行了两项重要改进:

  • 显式资源管理支持:为 Dir 类添加了显式资源管理支持(标记为 SEMVER-MINOR)。这一改进通过优化目录资源管理,可能减少资源泄漏问题并提升性能,尤其在处理大量文件操作时效果显著。
  • URL 支持:为 fs.globcwd 选项添加了对 URL 的支持。开发者现在可以使用 URL 格式指定工作目录,例如:
    import { glob } from 'fs/promises';
    await glob('*.js', { cwd: new URL('file:///path/to/dir/') });
    
    这增加了文件操作的灵活性,特别是在跨平台开发中。
2. HTTP/2 模块

Node.js 24.1.0HTTP/2 模块新增了两个诊断通道:

  • http2.client.stream.start:用于监控 HTTP/2 客户端流的启动事件,帮助开发者跟踪流的初始化过程。
  • http2.client.stream.created:用于监控 HTTP/2 客户端流的创建事件,提供流的创建时间和状态信息。 这些通道为调试高性能网络应用提供了更细粒度的工具。例如,开发者可以通过监听这些事件来分析 HTTP/2 流的性能瓶颈。
3. Inspector 模块
  • Worker 检查支持Node.js 24.1.0Chrome DevTools 中支持 worker 线程的检查。通过使用 --experimental-worker-inspection 标志,开发者可以调试多线程 Node.js 应用程序。例如:
    node --inspect-brk --experimental-worker-inspection index.js
    
    这一功能利用了 Chrome DevTools ProtocolTarget 域,通过 attachedToTarget 事件管理每个 worker 线程的调试会话。这对于开发复杂的并发应用(如多线程数据处理)尤为重要。
4. REPL 模块

REPL(交互式解释器)模块在用户体验方面得到了显著改进:

  • 垂直光标移动:新增了适当的垂直光标移动功能,允许用户在多行输入中更自然地上下移动光标,提升了交互效率。
  • 多行命令编辑:支持在输入多行命令时进行编辑。例如,开发者可以在输入以下代码时随时修改:
    function example() {
      console.log('Hello');
      // 可以插入新行或编辑现有行
    }
    
    这一功能仅在 TTY 环境中有效,且需要命令不完整(如缺少闭合括号)才能插入新行。未来可能通过快捷键(如 Shift + Enter)进一步优化。
5. SQLite 模块
  • 备份功能增强:为 sqlite.backup() 方法添加了 namelength 参数,允许更精确地控制备份操作。例如:
    db.backup({ name: 'backup.db', length: 1024 });
    
  • 构建选项:新增了无 SQLite 的构建选项,开发者可以通过配置跳过 SQLite 依赖,适合轻量级部署场景。
6. 测试运行器(test_runner
  • 行为统一:当隔离模式为 none 时,统一了 --require--import 的行为,确保测试运行器在不同模块加载方式下表现一致。这减少了配置错误的可能性。
7. URL 模块
  • 性能改进:优化了 format 函数的性能,提高了 URL 处理的效率。例如:
    import { format } from 'url';
    format(new URL('http://example.com')); // 更快地生成格式化 URL
    
    这在高负载的 Web 应用中尤为重要。
8. Util 模块
  • 内部函数:新增了内部函数 assignFunctionName(),用于更方便地管理函数名称,可能提升代码的可读性和调试效率。
9. 其他改进
  • 代码覆盖率:相关更改的代码覆盖率较高,例如 REPL 模块的补丁覆盖率达到 99.55%,确保了新功能的稳定性。
  • 平台支持Node.js 24.1.0 提供了适用于 WindowsmacOSLinux 等多个平台的二进制文件和源代码,开发者可从官方 Node.js 网站下载。

这些更新体现了 Node.js 社区对性能、调试能力和用户体验的持续关注,为开发者提供了更强大的工具来应对现代开发需求。

最后

Node.js 24.1.0 版本通过一系列功能增强和优化,为开发者带来了更强大的开发支持。从文件系统和 HTTP/2 的改进,到调试能力和 REPL 体验的提升,这些更新都为构建高效、可靠的应用程序提供了便利。希望本文能帮助您快速掌握这一版本的亮点,并在实际开发中灵活运用。

今天的分享就这些了,感谢大家的阅读,如果文章中存在错误的地方欢迎指正!

往期精彩推荐

她说:JSON 没错,但就是 parse 不过?我懂了!

作者 洛小豆
2025年5月22日 08:27

技术纯享版:《不规范 JSON 怎么办?三种修复思路+代码实现》

开篇:夜色渐浓,佳人亦在

那天晚上,办公室的灯已经灭了大半,只剩几个工位发出轻轻的蓝光。中央空调早就熄了,但显示器的热度依然在屏幕前形成一圈圈淡淡的光晕。

我坐在靠窗的位置,刚把代码提交推送完,正打算收键盘走人。

这时,小语走过来,端着还冒着热气的速溶咖啡——她果然又是那个留下来最晚的人之一。

“诶~”她蹲在我旁边的桌子边上,语气带着一丝挫败,“你这边有没有遇到 JSON 字符串明明格式看着没错,却死活 JSON.parse 不过的情况?”

一个普通的错误,却不是普通的崩溃

原来她在调试一个用户日志上传模块,前端接收到的日志数据是从后端来的 JSON 字符串。

问题出在一个看似再平常不过的解析操作上——

const logData = JSON.parse(incomingString);

可是控制台总是报错:Unexpected token。数据一眼看去也没问题,{'name': 'Tom', 'age': 30} —— 结构清晰,属性齐全,但偏偏就是“坏掉了”。

她抿了一口咖啡,苦笑,“我知道是引号的问题,可这种数据是从破旧的系统里吐出来的,量还特别大,我不可能一个个手动改。”

风起 · JSON.parse 不是万灵药

我们一起回顾了她的实现方式。她用的是最基础的 JSON.parse(),这是我们在项目里默认的处理方式——简单、直接、快速。

但这个方法对 JSON 格式的要求极其严格:

  • 只能使用双引号 "
  • 属性名必须加引号
  • 不容忍任何额外字符或注释

一旦出现诸如单引号、缺少逗号、多余空格这些“微小过失”,就直接抛错了。

小语叹气,“很多时候这些 JSON 是设备端拼出来的,不规范,又没有错误提示,我根本不知道该怎么修。”

我翻了翻之前的代码,从夹缝中找出来一张破旧的黄皮纸,我们俩一起瞅了上去,看到上面写着

function tryParseJSON(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (e) {
    // 尝试简单修复:去除可能的多余字符
    const cleaned = jsonString.replace(/[^\x20-\x7E]/g, '').trim();
    try {
      return JSON.parse(cleaned);
    } catch (e2) {
      console.error("无法解析JSON:", e2);
      return null;
    }
  }
}

下面备注了一行小字:此法在一些更轻量的场景里,做一些“简陋修复“,对于简单的问题有时能奏效,但对于更复杂的错误,比如混合了单引号和双引号的情况,只能再实现另一个方法可以做更针对性的修复方法

function fixQuotes(jsonString) {
  // 将单引号替换为双引号(简单情况)
  return jsonString.replace(/'/g, '"');
}

小语感叹一声:“没有更好的了吗?”

解决篇 · 来自大佬的一句话

恰好这时,阿杰从会议室出来,耳机还挂在脖子上。

他听了一耳朵后随口说了句:“你们试过 jsonrepair 吗?那玩意能把坏 JSON 修回来,就像修车。”

“json... repair?”小语一脸困惑。

我忽然想起,之前有个日志监控服务也碰到类似的问题,当时就是用了这个库一把梭。

我打开编辑器,快速翻出来了这一段:

npm install jsonrepair
const { jsonrepair } = require('jsonrepair');

const damaged = "{name: 'John', age: 30}";
const fixed = jsonrepair(damaged); // => {"name":"John","age":30}
const obj = JSON.parse(fixed);

小语凑过来看了一眼,眼睛一亮:“它真的把引号补好了?”

我点头。这个工具是为了解决类似“非标准 JSON”问题的,它会尽可能地补全缺失引号、逗号,甚至处理 Unicode 异常字符。

当然,也不是所有情况都适用。

比如碰到乱码或者非法嵌套结构,jsonrepair 有时也会无能为力。这时可以退一步——用更宽松的解析器,比如 JSON5

const JSON5 = require('json5');
const result = JSON5.parse("{name: 'John', age: 30}"); // 也能解析

我看着认真学习的小语,语重心长的讲道:它不是修复,而是扩展 JSON 标准,让一些非标准写法也能解析(JSON5 能容忍的内容包括:单引号、尾逗号、注释、未加引号的属性名、十六进制、科学计数法等数字格式)

接着我们还讨论了更复杂的修复方式,比如用正则处理批量日志,甚至用 AST 工具逐步构建 JSON 树。但那是更远的故事了。

面对当前的问题,我们准备搞一套组合拳:

function parseJson(jsonString) {
  // 第一步:尝试标准JSON解析
  try {
    return JSON.parse(jsonString);
  } catch (e) {
    console.log("标准JSON解析失败,尝试修复...");
    
    // 第二步:尝试使用jsonrepair修复
    try {
      const { jsonrepair } = require('jsonrepair');
      const fixedJson = jsonrepair(jsonString);
      return JSON.parse(fixedJson);
    } catch (e2) {
      console.log("修复失败,尝试使用JSON5解析...");
      
      // 第三步:尝试使用JSON5解析
      try {
        const JSON5 = require('json5');
        return JSON5.parse(jsonString);
      } catch (e3) {
        // 最后:如果所有方法都失败,返回错误信息
        console.error("所有解析方法都失败了:", e3);
        throw new Error("无法解析JSON数据");
      }
    }
  }
}

结局

一段时间后,小语在前端监控日志里贴了段截图:原本一天上千条的 parse error 错误,几乎消失了。

她补了一句:“终于不用再一个个点开调日志了。”

我回头看她的工位,屏幕亮着,浏览器里是一个模拟器页面,console 正在缓缓输出内容。

她突然抬起头看着我,问道:“AST是什么?听说也能实现json修复?”

JavaScript篇:懒加载 vs 预加载:前端性能优化的"太极之道"

2025年5月22日 08:16

        大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。

        我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。

作为前端开发者,我经常在项目性能优化时面临这样的抉择:是让资源"随用随取"(懒加载)还是"提前备好"(预加载)?今天就来聊聊这两种加载策略的艺术。

懒加载:网页中的"按需点餐"

记得我第一次负责电商项目时,发现商品列表页加载特别慢,因为一口气加载了上百张图片。后来我学会了懒加载这个"绝招"。

懒加载(Lazy Loading) 的核心思想是:只有当资源需要显示或使用时才加载。

实现方式

1. 图片懒加载(原生方式)

<!-- 我使用loading="lazy"实现图片懒加载 -->
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="商品图片">

现代浏览器已经原生支持图片懒加载,loading="lazy"属性告诉浏览器:"等这个图片快进入视口时再加载我"。

2. 组件懒加载(React示例)

// 我使用React.lazy动态导入组件
const ProductGallery = React.lazy(() => import('./ProductGallery'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <ProductGallery />
    </Suspense>
  );
}

适用场景

  • 长列表中的图片或内容
  • 非首屏资源
  • 可能不会被访问的页面内容

预加载:网页中的"未雨绸缪"

有一次我负责一个动画密集型页面,发现动画播放时总有卡顿。通过预加载关键资源,体验立刻流畅了许多。

预加载(Preloading) 的理念是:提前加载后续可能需要的资源。

实现方式

1. 资源预加载(HTML方式)

<!-- 我预加载关键CSS和字体 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="font.woff2" as="font" crossorigin>

2. 路由预加载(React示例)

// 我在用户hover导航时预加载路由组件
const handleMouseEnter = () => {
  import('./AdminPanel');
};

<NavLink onMouseEnter={handleMouseEnter}>管理后台</NavLink>

适用场景

  • 关键渲染路径上的资源
  • 高概率被访问的页面
  • 需要流畅体验的动画资源

性能优化中的"阴阳平衡"

在实际项目中,我通常会这样搭配使用:

  1. 首屏关键资源:预加载(CSS、字体、首屏图片)
  2. 非关键资源:懒加载(轮播图后续图片、评论区内容)
  3. 预测性加载:当用户可能访问下一页时提前加载
// 我实现了一个智能预加载策略
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const lazyElement = entry.target;
      lazyElement.src = lazyElement.dataset.src;
      observer.unobserve(lazyElement);
      
      // 预加载下一个可能查看的资源
      const nextElement = lazyElement.nextElementSibling;
      if (nextElement && nextElement.dataset.src) {
        const img = new Image();
        img.src = nextElement.dataset.src;
      }
    }
  });
});

document.querySelectorAll('.lazy-img').forEach(img => {
  observer.observe(img);
});

我踩过的性能坑

  1. 过度预加载:曾经在一个项目中预加载了太多资源,反而拖慢了首屏速度。后来我学会了只预加载真正的关键资源。
  2. 懒加载阈值不当:设置懒加载触发点太近,导致用户快速滚动时看到空白。解决方案是适当调整IntersectionObserver的rootMargin。
  3. 移动端适配:在低速网络下,懒加载可能造成明显延迟。我现在的做法是在移动端适当增加预加载范围。

现代框架中的最佳实践

Next.js中的自动优化

// Next.js自动处理图片优化和懒加载
import Image from 'next/image';

function MyComponent() {
  return (
    <Image
      src="/product.jpg"
      alt="商品图片"
      width={500}
      height={500}
      // 自动懒加载+优化
      placeholder="blur"
      blurDataURL="placeholder.jpg"
    />
  );
}

Vue中的动态导入

// 我在Vue中使用异步组件
const AsyncComponent = () => ({
  component: import('./AsyncComponent.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000
});

性能监控与调优

我习惯使用Chrome DevTools来验证加载策略是否有效:

  1. Network面板:查看资源加载顺序和时间线
  2. Coverage面板:发现未使用的JS/CSS
  3. Lighthouse审计:获取加载策略的优化建议

总结:没有银弹,只有平衡

经过多个项目的实践,我发现:

  • 懒加载能减少初始负载,但可能增加交互延迟
  • 预加载能提升后续体验,但可能浪费带宽
  • 最佳方案是根据用户行为模式找到平衡点

你的项目中是如何平衡这两种策略的?有没有什么独特的优化技巧?欢迎在评论区分享你的经验!

❌
❌