阅读视图

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

[Python3/Java/C++/Go/TypeScript] 一题一解:差分数组(清晰题解)

方法一:差分数组

我们可以使用差分数组来解决这个问题。

定义一个长度为 $n + 1$ 的数组 $d$,初始值全部为 $0$。对于每个查询 $[l, r]$,我们将 $d[l]$ 加 $1$,将 $d[r + 1]$ 减 $1$。

然后我们遍历数组 $d$ 在 $[0, n - 1]$ 范围内的每个元素,累加前缀和 $s$,如果 $\textit{nums}[i] > s$,说明 $\textit{nums}$ 不能转换为零数组,返回 $\textit{false}$。

遍历结束后,返回 $\textit{true}$。

###python

class Solution:
    def isZeroArray(self, nums: List[int], queries: List[List[int]]) -> bool:
        d = [0] * (len(nums) + 1)
        for l, r in queries:
            d[l] += 1
            d[r + 1] -= 1
        s = 0
        for x, y in zip(nums, d):
            s += y
            if x > s:
                return False
        return True

###java

class Solution {
    public boolean isZeroArray(int[] nums, int[][] queries) {
        int n = nums.length;
        int[] d = new int[n + 1];
        for (var q : queries) {
            int l = q[0], r = q[1];
            ++d[l];
            --d[r + 1];
        }
        for (int i = 0, s = 0; i < n; ++i) {
            s += d[i];
            if (nums[i] > s) {
                return false;
            }
        }
        return true;
    }
}

###cpp

class Solution {
public:
    bool isZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
        int n = nums.size();
        int d[n + 1];
        memset(d, 0, sizeof(d));
        for (const auto& q : queries) {
            int l = q[0], r = q[1];
            ++d[l];
            --d[r + 1];
        }
        for (int i = 0, s = 0; i < n; ++i) {
            s += d[i];
            if (nums[i] > s) {
                return false;
            }
        }
        return true;
    }
};

###go

func isZeroArray(nums []int, queries [][]int) bool {
d := make([]int, len(nums)+1)
for _, q := range queries {
l, r := q[0], q[1]
d[l]++
d[r+1]--
}
s := 0
for i, x := range nums {
s += d[i]
if x > s {
return false
}
}
return true
}

###ts

function isZeroArray(nums: number[], queries: number[][]): boolean {
    const n = nums.length;
    const d: number[] = Array(n + 1).fill(0);
    for (const [l, r] of queries) {
        ++d[l];
        --d[r + 1];
    }
    for (let i = 0, s = 0; i < n; ++i) {
        s += d[i];
        if (nums[i] > s) {
            return false;
        }
    }
    return true;
}

时间复杂度 $O(n + m)$,空间复杂度 $O(n)$。其中 $n$ 和 $m$ 分别为数组 $\textit{nums}$ 和 $\textit{queries}$ 的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-零数组变换 I🟡

给定一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] = [li, ri]

对于每个查询 queries[i]

  • 在 nums 的下标范围 [li, ri] 内选择一个下标 子集
  • 将选中的每个下标对应的元素值减 1。

零数组 是指所有元素都等于 0 的数组。

如果在按顺序处理所有查询后,可以将 nums 转换为 零数组 ,则返回 true,否则返回 false

 

示例 1:

输入: nums = [1,0,1], queries = [[0,2]]

输出: true

解释:

  • 对于 i = 0:
    • 选择下标子集 [0, 2] 并将这些下标处的值减 1。
    • 数组将变为 [0, 0, 0],这是一个零数组。

示例 2:

输入: nums = [4,3,2,1], queries = [[1,3],[0,2]]

输出: false

解释:

  • 对于 i = 0: 
    • 选择下标子集 [1, 2, 3] 并将这些下标处的值减 1。
    • 数组将变为 [4, 2, 1, 0]
  • 对于 i = 1:
    • 选择下标子集 [0, 1, 2] 并将这些下标处的值减 1。
    • 数组将变为 [3, 1, 0, 0],这不是一个零数组。

 

提示:

  • 1 <= nums.length <= 105
  • 0 <= nums[i] <= 105
  • 1 <= queries.length <= 105
  • queries[i].length == 2
  • 0 <= li <= ri < nums.length

【模板】差分数组(Python/Java/C++/Go)

题意可以转换成:

  • 把 $[l_i,r_i]$ 中的元素都减一,最终数组中的所有元素是否都 $\le 0$?

如果所有元素都 $\le 0$,那么我们可以撤销一部分元素的减一,使其调整为 $0$,从而满足原始题意的要求。

这可以用差分数组计算,原理讲解(推荐和【图解】从一维差分到二维差分 一起看)。

本题视频讲解,欢迎点赞关注~

###py

class Solution:
    def isZeroArray(self, nums: List[int], queries: List[List[int]]) -> bool:
        diff = [0] * (len(nums) + 1)
        for l, r in queries:
            # 区间 [l,r] 中的数都加一
            diff[l] += 1
            diff[r + 1] -= 1

        for x, sum_d in zip(nums, accumulate(diff)):
            # 此时 sum_d 表示 x=nums[i] 要减掉多少
            if x > sum_d:  # x 无法变成 0
                return False
        return True

###java

class Solution {
    public boolean isZeroArray(int[] nums, int[][] queries) {
        int n = nums.length;
        int[] diff = new int[n + 1];
        for (int[] q : queries) {
            // 区间 [l,r] 中的数都加一
            diff[q[0]]++;
            diff[q[1] + 1]--;
        }

        int sumD = 0;
        for (int i = 0; i < n; i++) {
            sumD += diff[i];
            // 此时 sumD 表示 nums[i] 要减掉多少
            if (nums[i] > sumD) { // nums[i] 无法变成 0
                return false;
            }
        }
        return true;
    }
}

###cpp

class Solution {
public:
    bool isZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
        int n = nums.size();
        vector<int> diff(n + 1);
        for (auto& q : queries) {
            // 区间 [l,r] 中的数都加一
            diff[q[0]]++;
            diff[q[1] + 1]--;
        }

        int sum_d = 0;
        for (int i = 0; i < n; i++) {
            sum_d += diff[i];
            // 此时 sum_d 表示 nums[i] 要减掉多少
            if (nums[i] > sum_d) { // nums[i] 无法变成 0
                return false;
            }
        }
        return true;
    }
};

###go

func isZeroArray(nums []int, queries [][]int) bool {
diff := make([]int, len(nums)+1)
for _, q := range queries {
// 区间 [l,r] 中的数都加一
diff[q[0]]++
diff[q[1]+1]--
}

sumD := 0
for i, x := range nums {
sumD += diff[i]
// 此时 sumD 表示 x=nums[i] 要减掉多少
if x > sumD { // x 无法变成 0
return false
}
}
return true
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+q)$,其中 $n$ 是 $\textit{nums}$ 的长度,$q$ 是 $\textit{queries}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

更多相似题目,见下面数据结构题单中的「§2.1 一维差分」。

分类题单

如何科学刷题?

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

我的题解精选(已分类)

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

差分

解法:差分

我们首先把题目的包装拆开。如果有 $x$ 个区间覆盖了某个元素,则那个元素最多可以被减去 $x$ 次。因此题目等价于:问每个元素 nums[i] 是否被至少 nums[i] 个询问区间覆盖。

这就是非常经典的差分问题。用差分维护每个元素被几个区间覆盖即可。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###cpp

class Solution {
public:
    bool isZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
        int n = nums.size();
        // 差分维护每个元素被几个区间覆盖
        vector<int> d(n + 1);
        for (auto &qry : queries) {
            d[qry[0]]++;
            d[qry[1] + 1]--;
        }
        // 枚举每个元素,求区间覆盖数
        for (int i = 0, now = 0; i < n; i++) {
            now += d[i];
            if (now < nums[i]) return false;
        }
        return true;
    }
};

vue3中的form表单层级嵌套问题

先上代码

parent.vue

<script setup>
import { ref, reactive } from "vue";
import TaskList from "./ChildForm.vue";

const formRef = ref();
const formData = reactive({
  projectName: "",
  manager: "",
  tasks: [],
});

const rules = reactive({
  projectName: [
    { required: true, message: "项目名称不能为空", trigger: "blur" },
    { min: 3, max: 50, message: "长度在3到50个字符", trigger: "blur" },
  ],
  manager: [{ required: true, message: "负责人不能为空", trigger: "change" }],
});

const validateTasks = (rule, value, callback) => {
  if (formData.tasks.length === 0) {
    callback(new Error("至少需要添加一个任务"));
  } else {
    callback();
  }
};

const submit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      console.log("提交数据:", formData);
    }
  });
};
</script>

<template>
  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
    <el-form-item label="项目名称" prop="projectName">
      <el-input v-model="formData.projectName" />
    </el-form-item>

    <el-form-item label="负责人" prop="manager">
      <el-select v-model="formData.manager">
        <el-option label="张三" value="zhangsan" />
        <el-option label="李四" value="lisi" />
      </el-select>
    </el-form-item>

    <el-form-item prop="tasks" :rules="[{ validator: validateTasks }]">
      <TaskList v-model="formData.tasks" />
    </el-form-item>

    <el-button type="primary" @click="submit">提交</el-button>
  </el-form>
</template>

child.vue

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

const props = defineProps({
  modelValue: {
    type: Array,
    default: () => [],
  },
});

const emit = defineEmits(["update:modelValue"]);

const taskRules = reactive({
  name: [{ required: true, message: "任务名称不能为空", trigger: "blur" }],
  priority: [{ required: true, message: "请选择优先级", trigger: "change" }],
});

const tasks = computed({
  get: () => props.modelValue,
  set: (value) => emit("update:modelValue", value),
});

const addTask = () => {
  tasks.value.push({ name: "", priority: "medium" });
};

const removeTask = (index) => {
  tasks.value.splice(index, 1);
};
</script>

<template>
  <div class="task-list">
    <el-button @click="addTask">添加任务</el-button>

    <el-form
      v-for="(task, index) in tasks"
      :key="index"
      :model="task"
      :rules="taskRules"
      class="task-form"
    >
      <el-form-item prop="name">
        <el-input v-model="task.name" placeholder="任务名称" />
      </el-form-item>

      <el-form-item prop="priority">
        <el-select v-model="task.priority">
          <el-option label="高" value="high" />
          <el-option label="中" value="medium" />
          <el-option label="低" value="low" />
        </el-select>
      </el-form-item>

      <el-button @click="removeTask(index)">删除</el-button>
    </el-form>
  </div>
</template>

<style scoped>
.task-form {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
}
</style>

效果如下:

image.png

一、组件结构设计原理

  1. 数据流向设计父组件通过v-model实现数据双向绑定:
// ProjectForm.vue
const formData = reactive({
  tasks: [] // 自动同步到子组件
})

// TaskList.vue
const tasks = computed({
  get: () => props.modelValue,
  set: (v) => emit('update:modelValue', v)
})

2. 验证责任划分

  • 父组件:验证任务列表非空
  • 子组件:验证单个任务字段

二、分步实现流程

步骤1:父组件基础验证

// ProjectForm.vue
const validateTasks = (_, __, callback) => {
  formData.tasks.length === 0 
    ? callback(new Error('至少需要1个任务')) 
    : callback()
}

步骤2:子组件独立验证

// TaskList.vue
const taskRules = {
  name: { 
    required: true, 
    trigger: 'blur',
    message: '任务名称必填' 
  }
}

步骤3:动态prop绑定

<el-form-item 
  :prop="`tasks[${index}].name`"
  :rules="taskRules.name">
  <el-input v-model="item.name"/>
</el-form-item>

三、验证联动机制

  1. 提交时联合验证
// ProjectForm.vue
const submit = () => {
  formRef.value.validate().then(() => {
    taskListRef.value.validate().then(() => {
      // 全部验证通过
    })
  })
}

2. 实时错误反馈

// TaskList.vue
watch(() => props.modelValue, () => {
  formRef.value?.validate()
}, { deep: true })

四、异常处理方案

// 统一错误捕获
try {
  await Promise.all([
    formRef.value.validate(),
    taskListRef.value.validate()
  ])
} catch (errors) {
  console.error('验证失败:', errors)
}

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

什么是A记录(Address Record)

简而言之,A记录是域名到ip地址的映射关系

例如,如果有一个网站example.sun.com,可能需要将其指向服务器的IP地址192.0.1.1。在DNS配置中添加一条A记录,将example.sun.com指向192.0.1.1即可实现

什么是CNAME记录(Canonical Name Record

也称为别名记录。它允许您为一个域名创建别名。

例如,如果您设置了example.sun.com作为www.dns.com的CNAME,这意味着所有访问example.sun.com的请求实际上都会被路由到www.dns.com。

为什么需要CNAME

简化了DNS记录的管理

例如,如果有好多个域名同时指向一个ip地址

www.cc.com → 1.1.1.1
www.xx.com → 1.1.1.1
www.kk.com → 1.1.1.1

那麽。当换服务器ip的时候,就需要对每个域名重新配置

而如果用CNAME的话呢

www.cc.com → www.cxk.com → 1.1.1.1
www.xx.com → www.cxk.com → 1.1.1.1
www.kk.com → www.cxk.com → 1.1.1.1

那么就只需要改动www.cxk.com映射的ip地址

CNAME的应用场景是什么

CDN是CNAME的主要应用场景

随着网站访问量越来越多,服务器顶不住了,就需要找CDN提供商购买CDN加速服务,这个时候他们要求你的域名做个CNAME指向他们给你的一个域名叫www.dd.cdn.com

www.dd.com → www.dd.cdn.com

当用户访问www.dd.com的时候,本地DNS会获得CDN提供的CNAME域名:www.dd.com,然后再次向DNS调度系统发出请求,通过DNS调度系统的智能解析,把离客户端地理位置最近的(或者相对负载低的,主要看CDN那边智能解析的策略)CDN提供商的服务器IP返回给本地DNS,然后再由本地DNS回给客户端,让用户就近取到想要的资源(如访问网站),大大降低了延迟。

3d66c87c40b30616357b79c0dbe75ed.jpg

本地运营商的DNS服务器怎么知道一个域名的授权(权威)服务器是哪台?这个域名应该在哪里取解析呢?

首先公司会去找运营商买域名,比如CDN公司买了cdn.com这个一级域名,那么本地运营商会做一个NS记录,即匹配到这个cdn.com后缀的域名都会到CDN服务提供商的DNS服务器做解析,即到权威服务器做解析。

什么是 NS 记录?

NS 记录,即域名服务器记录,用于指定域名应该由哪个 DNS 服务器来进行解析。简单来说,当用户在浏览器中输入一个域名时,NS 记录会告诉互联网中的 DNS 查询系统,应该向哪个 DNS 服务器询问解析结果。NS 记录中的 IP 地址即为负责解析该域名的 DNS 服务器的 IP 地址。

参考文章

Step - 3

Task

Using the max and step functions, paint only those pixels whose normalized x-coordinate is less than 0.25 or greater than 0.75.

使用maxstep函数,仅绘制那些标准化坐标下 x 坐标小于0.25或大于等于0.75

Requirements

The shader should avoid using branching or conditional statements in its code, and instead rely on the step and max functions to determine the color of each pixel.

着色器应避免在其代码中使用分支或条件语句,而是依靠stepmax函数来确定每个像素的颜色。

Theory

GLSL 中的函数max用于返回两个输入值中的最大值。它接受两个参数,并返回两个值中较大的一个。以下是该max函数在 GLSL 中的详细解释:

函数

float max(float x, float y);

  • 如果x大于或等于y,则函数返回x
  • 如果y大于x,则函数返回y

示例用法

float a = 5.0;

float b = 3.0;

float result = max(a, b); // will be 5.0

Answer

uniform vec2 iResolution;

void main() {
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = gl_FragCoord.xy / iResolution.xy;

  vec3 color = vec3(1.0, 0.3, 0.3);
  float t1 = 1.0 - step(0.25, uv.x);
  float t2 = step(0.75, uv.x);

  gl_FragColor = vec4(color * max(t1, t2), 1.0);
}

效果

image.png

练习

Step

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Shader更轻松!!

「译文」我买不起奔驰 🚗,但是奔驰买得起 Oxlint ⚓️ 和 Rolldown ⚡️!

前言

今天刷推特看到的一篇博客,作者是奔驰技术团队!

尤雨溪转发的推特

往期精彩推荐

正文

在当今快速发展的 JavaScript 生态系统中,现代工具发挥着关键作用,不仅影响我们的编码方式,还影响我们花费在编码上的时间和金钱。

JavaScript 生态系统中工作时,我们可以利用各种工具来提升质量、一致性和效率。今天,我们将重点介绍两款出色的工具,它们在实现重大里程碑的同时,仍然是大多数用例的直接替代品。

我们将讨论 捆绑器代码检查器,因为它们可能会成为开发或生产构建的瓶颈。

JavaScript 开发中,捆绑器 采用单个代码模块(无论是以 ESM 还是 CommonJS 编写),并将它们组装成一个有凝聚力的、更大的输出,例如完整的应用程序或可重用的库。

对于 Web 应用而言,即使使用 HTTP/2,此过程也能提升加载速度和运行时效率。对于库而言,提前打包可以减少使用方项目重新打包的需要,并提升执行性能。

相反,JavaScript 中的 linting 涉及检查代码以捕获潜在的错误、风格不一致以及不良的编码习惯。linter 是一种根据一组定义的规则或标准扫描代码库的工具,它有助于确保代码一致性、增强可读性,并在常见错误成为实际问题之前将其预防。

它是如何变化的?

多年来,各种构建工具层出不穷,包括 WebpackTurbopackRspackVite。其中,Vite 凭借其灵活性、用户友好性以及与各种项目的无缝兼容性,在 JavaScript 社区中得到了广泛的采用。

Vite 底层使用了两个打包器:esbuildRollup。您可以在这里找到更多详细信息,因此我们在此不再赘述。Vite 优秀的开源团队在 VoidZero 的支持下,推出了一款基于 Rust 的全新打包器,旨在取代 esbuildRollup。这款新的打包器保留了 Rollup 的 API,同时显著提升了性能并进行了许多其他改进。

在代码检查方面,ESLint 一直以来都是 JavaScript 代码中发现问题的首选工具。然而,VoidZero 也推出了一款基于 Rust 的替代方案 Oxlint,它的性能得到了提升,运行速度比 ESLint 快 50 到 100 倍。

补充一点,需要记住的是,仅仅迁移到 Rust 并不会自动提升速度。许多此类工具也借此机会考察了其他开源项目的架构,找出了设计瓶颈,并根据我们目前所了解的现实情况,为未来做出更可持续的决策。

它对我们有何影响?

这些新工具更令人印象深刻的是,它们可以直接作为替代品。无需重构代码,也无需花时间思考如何集成所有功能。

Mercedes-Benz.io,我们的前端本质上是微前端,所以我们无法仅仅测试完整构建版本并查看其改进程度。尽管如此,我仍然好奇这些工具会给我们带来多少时间和金钱上的影响,而金钱不一定是机器时间,主要是工程时间。

我从一些团队中挑选了几个代码库,一些团队拥有较多的微前端,而另一些团队拥有较少的微前端,并运行了 4 种类型的测试:

  1. 使用 Rolldown 进行直接替换
  2. 使用 SASS-EmbeddedRolldown 进行直接替换
  3. 使用 SASS-Embedded + Lightning CSSRolldown 进行直接替换
  4. 使用 Oxlint 替代 ESLint

由于我已经在测试它,所以我决定采用两个可用于生产的替代品,如上所示。

Sass-EmbeddedSass 包的替代方案。它支持与 Sass 相同的 JS API,并且由同一团队维护。然而,Sass-Embedded 是一个 JavaScript 封装器,封装了原生 Dart 可执行文件。这意味着它通常速度更快,尤其是在处理大型 Sass 编译时。

LightningCSS 是一款用 Rust 编写的超快速 CSS 解析器、转换器、打包器和压缩器。它是一款能够编译、优化和转换 CSS 的工具,旨在比其他传统工具更快、更高效。

让我们来看看数字

在您测试的所有代码库中,平均结果显示:

  1. 仅使用 Rolldown,构建时间减少了 23%,最高可达 33%。
  2. 使用 Rolldown + Sass-Embedded,构建时间减少了 25%,最高可达 33%。
  3. 使用 Rolldown + Sass-Embedded + Lightning CSS,构建时间减少了 34%,最高可达 38%。

在 linting 方面,转向 Oxlint 后,平均减少了 71%,最高减少了 97%。

这只是这些工具的开始。Rolldown 仍在开发中,但已经经过社区的大量测试,而 Oxlint 目前处于 Beta 阶段。这两款工具都计划进行更多改进。

这真是太神奇了:我们谈论的不是重构代码,也不是花时间理解和尝试适应新工具。我们谈论的是一些可以让我们的 CI 流程减少 64% 的嵌入式替换(仅指构建 + linting)。

假设大约有 100 个项目,每个项目平均每年有 600 次提交,每次提交都需要构建和 lint。这意味着每年可以节省大约 500 个小时,差不多 21 天。由于机器成本难以计算,因此不计入机器成本,我们可以想象工程成本,包括生产力、专注度,以及能否顺利进入流程。

这种节省不仅能显著提高开发效率,还能让团队有更多时间专注于功能开发和创新,从而提升整体项目的质量和响应速度。

最后

原文地址:www.mercedes-benz.io/blog/2025-0…

原推特地址:x.com/boshen_c/st…

往期精彩推荐

一次痛苦的内存泄露排查经历

这阵子接手的一个需求就是排查项目是否存在内存泄露问题,找到并解决它 目前是发现了四处问题,一处是 el-select 组件存在泄露问题,这个有做出挣扎解决,但是最终并没有彻底解决,无果;一初是项

Three.js 完全学习指南(二)场景、相机、渲染器基础

场景、相机、渲染器基础

场景(Scene)详解

场景是 Three.js 中所有 3D 对象的容器,它定义了整个 3D 空间。让我们深入了解场景的配置和使用。

场景示例

图 2.1: 包含多个几何体的场景示例

1. 场景基础配置

import * as THREE from 'three';

// 创建场景
const scene = new THREE.Scene();

// 设置背景色
scene.background = new THREE.Color(0x000000); // 黑色背景

// 添加雾效果
scene.fog = new THREE.Fog(0x000000, 10, 100); // 颜色、近平面、远平面

// 设置场景环境
scene.environment = new THREE.CubeTextureLoader().load([
    'px.jpg', 'nx.jpg',
    'py.jpg', 'ny.jpg',
    'pz.jpg', 'nz.jpg'
]);

场景环境贴图

图 2.2: 使用环境贴图的场景效果

2. 场景管理

// 添加对象到场景
scene.add(mesh);

// 从场景中移除对象
scene.remove(mesh);

// 获取场景中的所有对象
const objects = scene.children;

// 遍历场景中的对象
scene.traverse((object) => {
    if (object.isMesh) {
        // 处理网格对象
    }
});
graph TD
    A[场景] --> B[几何体]
    A --> C[光源]
    A --> D[相机]
    A --> E[辅助对象]
    B --> F[网格]
    B --> G[线条]
    C --> H[环境光]
    C --> I[平行光]
    E --> J[网格辅助]
    E --> K[坐标轴]

图 2.3: 场景对象层级结构

3. 场景优化

// 设置场景自动更新
scene.autoUpdate = true;

// 手动更新场景
scene.updateMatrixWorld(true);

// 清理场景
function disposeScene() {
    scene.traverse((object) => {
        if (object.geometry) {
            object.geometry.dispose();
        }
        if (object.material) {
            if (Array.isArray(object.material)) {
                object.material.forEach(material => material.dispose());
            } else {
                object.material.dispose();
            }
        }
    });
}

相机(Camera)详解

Three.js 提供了多种相机类型,每种类型都有其特定的用途。

1. 透视相机(PerspectiveCamera)

透视相机模拟人眼视角,是最常用的相机类型。

透视相机效果

图 2.4: 透视相机的渲染效果

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
    75, // 视角(FOV)
    window.innerWidth / window.innerHeight, // 宽高比
    0.1, // 近平面
    1000 // 远平面
);

// 设置相机位置
camera.position.set(0, 5, 10);

// 设置相机朝向
camera.lookAt(0, 0, 0);

// 更新相机参数
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

2. 正交相机(OrthographicCamera)

正交相机没有透视效果,适合用于 2D 场景或等距视图。

正交相机效果转存失败,建议直接上传图片文件

图 2.5: 正交相机的渲染效果

// 创建正交相机
const camera = new THREE.OrthographicCamera(
    -10, // 左
    10,  // 右
    10,  // 上
    -10, // 下
    0.1, // 近平面
    1000 // 远平面
);

// 设置相机位置
camera.position.set(0, 0, 10);
camera.lookAt(0, 0, 0);

3. 相机控制

使用 OrbitControls 实现相机控制:

相机控制效果

图 2.6: 使用 OrbitControls 的相机控制效果

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);

// 配置控制器
controls.enableDamping = true; // 启用阻尼效果
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 10;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI / 2;

// 在动画循环中更新控制器
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
}

渲染器(Renderer)详解

渲染器负责将场景和相机的内容绘制到屏幕上。

1. 基础配置

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true, // 抗锯齿
    alpha: true,     // 透明背景
    precision: 'highp' // 精度
});

// 设置渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);

// 设置像素比
renderer.setPixelRatio(window.devicePixelRatio);

// 设置输出编码
renderer.outputEncoding = THREE.sRGBEncoding;

// 启用阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

渲染器效果转存失败,建议直接上传图片文件

图 2.7: 不同渲染效果的对比

2. 高级配置

// 设置渲染器参数
renderer.setClearColor(0x000000, 1); // 设置清除颜色
renderer.setClearAlpha(1); // 设置清除透明度

// 配置阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 配置后期处理
const composer = new THREE.EffectComposer(renderer);
const renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);

后期处理效果

图 2.8: 使用后期处理的效果

3. 性能优化

// 设置渲染器参数
renderer.powerPreference = 'high-performance';
renderer.precision = 'highp';

// 自动清理
function disposeRenderer() {
    renderer.dispose();
    renderer.forceContextLoss();
    renderer.domElement.remove();
}

// 处理窗口大小变化
window.addEventListener('resize', () => {
    const width = window.innerWidth;
    const height = window.innerHeight;

    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.setSize(width, height);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

实战:创建一个完整的 3D 场景

让我们结合以上知识,创建一个完整的 3D 场景:

完整场景示例

图 2.9: 完整的 3D 场景示例

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

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

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

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.getElementById('app').appendChild(renderer.domElement);

// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 添加网格
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);

// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 添加平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(5, 5, 5);
directionalLight.castShadow = true;
scene.add(directionalLight);

// 创建一个立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
    color: 0x00ff00,
    metalness: 0.3,
    roughness: 0.4
});
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);

// 创建地面
const planeGeometry = new THREE.PlaneGeometry(10, 10);
const planeMaterial = new THREE.MeshStandardMaterial({
    color: 0x808080,
    side: THREE.DoubleSide
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.position.y = -0.5;
plane.receiveShadow = true;
scene.add(plane);

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

    // 更新控制器
    controls.update();

    // 旋转立方体
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

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

// 开始动画
animate();

性能优化建议

  1. 场景优化

    • 使用适当的几何体复杂度
    • 及时清理不需要的对象
    • 使用对象池管理频繁创建的对象
  2. 相机优化

    • 设置合适的视锥体范围
    • 使用适当的相机类型
    • 优化控制器参数
  3. 渲染器优化

    • 使用适当的像素比
    • 启用必要的渲染特性
    • 及时释放资源

练习

  1. 实现相机的自动旋转
  2. 添加多个光源并观察效果
  3. 实现场景的昼夜变化
  4. 添加后期处理效果

下一步学习

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

  • 几何体的创建和使用
  • 材质系统的详细配置
  • 纹理的应用
  • 对象的变换和组合

Flutter核心机制图解说明

一、三棵树协作流程详解

1. 架构关系示意图
[用户代码] → Widget树(声明式配置)
       ↓ 创建
Element树(生命周期管理)
       ↓ 绑定
RenderObject树(布局/绘制)
2. 协作流程步骤
  1. 初始化阶段

    • runApp() 触发根Widget创建
    • 生成对应的根Element(RenderObjectElement
    • Element创建关联的RenderObject
  2. 构建阶段

    • Widget树通过build()方法递归构建
    • Element树通过inflateWidget方法逐层创建子元素
    • RenderObject树执行createRenderObject初始化渲染对象
  3. 更新阶段

    • 当Widget发生变更时: a. Element树对比新旧Widget类型 b. 类型相同 → 更新现有Element配置(update()) c. 类型不同 → 销毁旧Element,创建新Element
  4. 布局阶段

    • RenderObject执行layout()方法
    • 父节点向子节点传递约束条件(Constraints)
    • 子节点返回布局尺寸(Size)
  5. 绘制阶段

    • 生成Layer树提交给Skia引擎
    • 通过OpenGL/Vulkan进行GPU渲染

二、Platform Channel架构解析

1. 通信层级结构
[Flutter层] - Dart代码
   │
   ├── MethodChannel (方法调用)
   ├── EventChannel (事件流)
   └── BasicMessageChannel (基础消息)
           │
           |
[Native层] - 平台原生代码
   │
   ├── Android (Java/Kotlin)
   └── iOS (Objective-C/Swift)
2. 数据流向示意图
Flutter → 序列化为二进制 → 平台通道 → 反序列化为原生类型 → Native处理
       ← 序列化返回数据 ←          ← 原生返回结果 ←
3. 核心组件说明表
组件 功能特点 典型使用场景
MethodChannel 支持异步方法调用与返回值 调用相机/获取地理位置
EventChannel 建立持续事件流(类似观察者模式) 传感器数据监听/实时定位更新
BasicMessageChannel 基础消息传递(支持自定义编解码器) 简单数据交换/二进制传输

三、Key机制工作原理图示

1. LocalKey复用逻辑
Widget树重建前:
Item1(Key:A) - Item2(Key:B) - Item3(Key:C)

Widget树重建后:
Item2(Key:B) - Item3(Key:C) - Item1(Key:A)

Element树保持:
ElementB ↔ ElementC ↔ ElementA(仅位置变化)
2. GlobalKey定位原理
           ┌───────────┐
           │ GlobalKey │
           └─────┬─────┘
                 │
           ┌─────▼─────┐
           │ Element树  │
           └─────┬─────┘
                 │
           ┌─────▼─────┐
           │ 获取RenderObject │
           └───────────┘

四、状态管理数据流模型

1. Provider架构模型
[ChangeNotifier] ← 数据更新
       │
       ├─── notifyListeners()
       │
[Consumer] → 局部刷新
       │
[Selector] → 精准刷新
2. GetX响应式流程
[Rx变量] → 数据变更
       │
       ├─── 自动触发更新
       │
[Obx组件] → 重建依赖部件
       │
[GetBuilder] → 手动控制刷新

五、混合开发通信时序图

1. MethodChannel调用流程
Flutter端               Native端
  │                        │
  │  invokeMethod('getInfo')│
  │───────────────────────>│
  │                        ├── 执行原生代码
  │                        │
  │     result(data)       │
  │<───────────────────────│
  │                        │
2. EventChannel事件流
Flutter端               Native端
  │                        │
  │   receiveBroadcast()   │
  │───────────────────────>│
  │                        ├── 注册监听器
  │                        │
  │     event(data)        │
  │<───────────────────────│(持续推送)
  │                        │

六、性能优化关键路径

1. 渲染优化路线
减少Widget重建 → 优化Element复用 → 降低RenderObject计算 → 精简Layer树
      ↑               ↑                  ↑
   const构造      Key精准控制        布局边界标记(RepaintBoundary)
2. 内存管理策略
图片缓存控制 → 及时销毁监听 → 避免闭包泄漏 → 使用Isolate计算
   ↑               ↑              ↑             ↑
LRU策略       dispose()清理    DevTools检测   compute()函数

通过以上文字图解,开发者可以建立清晰的架构认知:

  1. 三棵树机制:理解声明式UI的核心工作原理
  2. 平台交互:掌握混合开发的数据通信脉络
  3. 状态管理:构建可维护的响应式架构
  4. 性能优化:定位关键瓶颈实施精准优化

建议结合Flutter DevTools的以下功能进行验证:

  • Widget Inspector:实时查看三棵树状态
  • Timeline:分析渲染流水线性能
  • Memory:检测内存泄漏与溢出

跟着文档学VUE3(四)- 类与样式绑定

Class 与 Style 的绑定技巧

关键词:Vue3、class 绑定、style 绑定、动态样式、响应式 UI

在 Vue 开发中,我们经常需要根据组件状态动态地控制元素的类名和内联样式。Vue 提供了非常强大的 :class:style 指令,不仅可以绑定字符串,还能绑定对象或数组,实现灵活的 UI 样式切换。

🧱 一、绑定 class 的多种方式

💡 基本原理:

  • class 是 HTML 元素的一个 attribute。

  • 在 Vue 中,使用 v-bind:class 或简写为 :class 来动态绑定 class。

  • Vue 对 class 绑定做了增强,支持传入:

    • 字符串
    • 对象
    • 数组

✅ 1. 使用对象绑定 class

适用于需要根据布尔值来切换 class 的场景。

// 直接绑定一个对象
<div :class="classObject"></div>
// 内联字面量
<div
  class="static
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

data() {
  return {
    isActive: true,
    hasError: false,
    classObject: { active: true, 'text-danger': false }
  }
}

💡 小贴士:
  • 如果类名是烤串命名(kebab-case),需要用引号包裹,如 'text-danger'
  • 可以将对象提取成一个计算属性,提升可读性和复用性

✅ 2. 使用数组绑定 class

适用于需要绑定多个 class 名字的情况。

📌 动态条件渲染?

可以结合三元表达式或嵌套对象来实现:

<div :class="[isActive ? activeClass : '', errorClass]"></div>
<div :class="[{ [activeClass]: isActive }, errorClass]"></div>

data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger',
    isActive: true,
  }
}

// 🧾渲染结果
<div class="active text-danger"></div>

✅ 3. 在组件上使用 class

单根元素组件:

当你给组件添加 class 时,这些类会自动合并到组件的根元素上。

多根元素组件:

如果组件有多个根元素,则需显式指定哪个元素接收 class

示例代码
<!-- 子组件模板 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />

<p class="baz boo">Hi!</p>

🎨 二、绑定 style 内联样式的高级用法

💡 基本原理:

  • :style 支持绑定 JavaScript 对象,用于动态设置内联样式。
  • 支持 camelCase 和 kebab-case 的 key 写法。

✅ 1. 使用对象绑定 style

// 推荐使用 camelCase, 如果是kebab-cased,则key需要对应它在css中的实际名称
<div :style="{ 'font-size': fontSize + 'px' }"></div>

// 也可以直接绑定一个对象
<div :style="styleObject"></div>
data() { 
    return { 
        styleObject: { color: 'red', fontSize: '13px' } 
        } 
 }

✅ 2. 使用数组绑定多个样式对象

可以传入多个样式对象,后面的样式会覆盖前面的:

 
// 也可以绑定一个包含多个样式对象的数组
<div :style="[baseStyles, overridingStyles]"></div>

✅ 3. 自动处理浏览器前缀

  • 如果在css中使用了需要浏览器特殊前缀的css,Vue在运行时会检查该属性是否支持在当前浏览器中使用,如果不支持,会自动加前缀
  • 可以提供多个不同前缀的值,只会渲染浏览器支持的最后一个值
// 不需要特别前缀的浏览器,最后会渲染为display: flex
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
❌