普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月26日首页

Lodash源码阅读-uniq

作者 好_快
2025年4月26日 07:38

Lodash 源码阅读-uniq

概述

uniq 函数用于创建一个数组的去重版本,只保留数组中每个元素的第一次出现。这个函数使用 SameValueZero 进行相等性比较,结果数组中元素的顺序取决于它们在原数组中首次出现的位置。

前置学习

函数依赖:

  • baseUniq: 不支持迭代器简写形式的 uniq 函数的基本实现。

技术知识:

  • JavaScript 的类型转换
  • JavaScript 中的相等性比较(SameValueZero
  • 数组去重算法
  • 哈希表的使用

源码实现

function uniq(array) {
  return array && array.length ? baseUniq(array) : [];
}

实现思路

uniq 函数的实现非常简洁,主要逻辑委托给了 baseUniq 函数。首先进行了参数验证:检查 array 是否存在且有长度,如果是则调用 baseUniq 处理数组去重,否则返回空数组。而真正的去重逻辑在 baseUniq 函数中实现。

源码解析

让我们逐行解析 uniq 函数:

function uniq(array) {

函数定义,接收一个参数 array,这是需要去重的数组。

return array && array.length ? baseUniq(array) : [];

这行代码做了两件事:

  1. 检查 array 是否存在且有长度(array && array.length
  2. 根据检查结果:
    • 如果 array 存在且有长度,则调用 baseUniq(array) 执行去重操作
    • 如果 array 不存在或长度为 0,则直接返回空数组 []

这样的处理方式确保了函数的健壮性,避免了对无效输入的处理。

uniq 函数的核心去重逻辑委托给了内部函数 baseUniq。当 uniq 调用 baseUniq(array) 时,它仅传递了原数组作为参数,没有提供 iterateecomparator 参数。

在这种调用模式下,baseUniq 的核心任务是实现基于 SameValueZero 相等性比较的数组去重,并会根据数组的大小(length >= LARGE_ARRAY_SIZE)选择不同的性能优化策略(如使用 SetSetCache 或直接遍历比较)。虽然 baseUniq 内部有处理 iterateecomparator 的逻辑分支,但对于 uniq 的直接调用,这些分支不会被激活。

总而言之,uniq 依赖 baseUniq 来完成实际的去重工作,通过提供简化的 API 接口使开发者能够方便地进行数组去重操作。这种设计让 uniq 能够保持简洁的接口,同时在内部利用功能更强大的 baseUniq 实现高效的去重处理。

总结

uniq 函数是 Lodash 提供的一个简单易用但功能强大的数组去重工具。它的特点包括:

  1. 简单的 API 设计:只需传入一个数组参数即可使用
  2. 健壮的参数处理:能够处理各种边缘情况,如空数组、nullundefined
  3. 高效的实现:针对大数组提供了优化
  4. 正确的语义:使用 SameValueZero 进行相等性比较,正确处理特殊值如 NaN-0

在实际开发中,uniq 函数可以用于去除数组中的重复元素,简化数据处理流程,是数组操作中常用的工具函数。

从设计模式角度看,uniq 采用了委托模式,将具体实现委托给 baseUniq 函数,这种设计使得 API 保持简洁的同时,内部实现可以更加灵活和高效。

Lodash源码阅读-baseUniq

作者 好_快
2025年4月26日 07:38

概述

baseUniq 是 Lodash 内部的基础函数,用于数组去重操作,支持自定义迭代器和比较器,是 _.uniq_.uniqBy 等方法的底层实现。

前置学习

  • 依赖函数:

    • arrayIncludes: 检查数组是否包含指定元素
    • arrayIncludesWith: 使用比较器检查数组是否包含指定元素
    • createSet: 创建一个 Set 集合
    • setToArray: 将 Set 转换为数组
    • cacheHas: 检查缓存中是否存在某个值
    • SetCache: 一个用于存储唯一值的缓存类
  • 技术知识:

    • JavaScript 数组操作
    • Set 集合的使用
    • 标签化循环与 continue
    • NaN 值的特殊处理 (NaN !== NaN)

源码实现

function baseUniq(array, iteratee, comparator) {
  var index = -1,
    includes = arrayIncludes,
    length = array.length,
    isCommon = true,
    result = [],
    seen = result;

  if (comparator) {
    isCommon = false;
    includes = arrayIncludesWith;
  } else if (length >= LARGE_ARRAY_SIZE) {
    var set = iteratee ? null : createSet(array);
    if (set) {
      return setToArray(set);
    }
    isCommon = false;
    includes = cacheHas;
    seen = new SetCache();
  } else {
    seen = iteratee ? [] : result;
  }
  outer: while (++index < length) {
    var value = array[index],
      computed = iteratee ? iteratee(value) : value;

    value = comparator || value !== 0 ? value : 0;
    if (isCommon && computed === computed) {
      var seenIndex = seen.length;
      while (seenIndex--) {
        if (seen[seenIndex] === computed) {
          continue outer;
        }
      }
      if (iteratee) {
        seen.push(computed);
      }
      result.push(value);
    } else if (!includes(seen, computed, comparator)) {
      if (seen !== result) {
        seen.push(computed);
      }
      result.push(value);
    }
  }
  return result;
}

实现思路

baseUniq 根据输入参数和数组大小选择最优的去重策略。函数会遍历数组,对每个元素应用可选的迭代函数,然后检查转换后的值是否已经在结果集中出现过。如果有自定义比较器,使用比较器进行比较;如果数组较大,使用缓存或 Set 提高性能;对于一般情况,采用简单数组比较。函数还特别处理了 JavaScript 中的特殊值如 NaN 和 -0/+0。

源码解析

1. 变量初始化与准备工作

var index = -1,
  includes = arrayIncludes,
  length = array.length,
  isCommon = true,
  result = [],
  seen = result;

这部分代码初始化了几个关键变量:

  • index = -1:数组遍历指针,从-1 开始是因为循环中使用前置自增(++index
  • includes = arrayIncludes:默认使用的元素查找函数,可以检查一个值是否在数组中
  • length = array.length:缓存数组长度,避免循环中重复计算
  • isCommon = true:标记是否使用通用模式(简单的遍历比较)
  • result = []:存储去重后的结果数组
  • seen = result:存储已经见过的值,初始时指向结果数组(共享引用可以节省内存)

2. 智能策略选择

if (comparator) {
  // 提供了自定义比较器
  isCommon = false;
  includes = arrayIncludesWith;
} else if (length >= LARGE_ARRAY_SIZE) {
  // 大数组优化
  var set = iteratee ? null : createSet(array);
  if (set) {
    return setToArray(set);
  }
  isCommon = false;
  includes = cacheHas;
  seen = new SetCache();
} else {
  // 小数组 + 可能有迭代器
  seen = iteratee ? [] : result;
}

这段代码根据输入参数智能选择最佳去重策略:

情况 1:有自定义比较器

// 例如:_.uniqWith([1, 1.5, 2], function(a, b) { return Math.floor(a) === Math.floor(b); })
if (comparator) {
  isCommon = false; // 不使用通用模式
  includes = arrayIncludesWith; // 使用支持自定义比较的查找函数
}

情况 2:大数组 + 无迭代器

// 例如处理有1000个元素的数组:_.uniq([1, 2, 3, ..., 1000])
else if (length >= LARGE_ARRAY_SIZE) { // LARGE_ARRAY_SIZE通常是200
  var set = iteratee ? null : createSet(array);
  if (set) {
    // 如果环境支持Set且能创建成功,直接用Set去重(O(n)复杂度)
    return setToArray(set); // 直接返回结果,不再继续执行
  }

  // 如果无法使用Set(有迭代器或环境不支持),使用SetCache
  isCommon = false;
  includes = cacheHas; // 使用缓存查找函数
  seen = new SetCache; // SetCache是Lodash自己实现的类似Set的结构
}

情况 3:小数组 + 可能有迭代器

// 例如:_.uniqBy([{id:1}, {id:2}, {id:1}], 'id')
else {
  // 如果有迭代器,需要单独的数组存储计算后的值
  seen = iteratee ? [] : result;
}

3. 主循环与去重核心逻辑

outer: // 定义循环标签,方便从内层循环跳出
while (++index < length) {
  var value = array[index],
      computed = iteratee ? iteratee(value) : value;

  value = (comparator || value !== 0) ? value : 0;
  // ...

主循环遍历数组的每个元素:

  • value = array[index]:获取当前元素值
  • computed = iteratee ? iteratee(value) : value:如果提供了迭代器,计算转换后的值
    • 例如:_.uniqBy([{id:1}, {id:2}, {id:1}], 'id') 中,computed 会是 1, 2, 1
  • value = (comparator || value !== 0) ? value : 0:特殊处理 +0-0
    • 在 JavaScript 中 +0 === -0true,但 1/+0 !== 1/-0,这里确保它们被视为相同
    • 示例:Object.is(0, -0)false,但 Lodash 需要把它们当作相同处理

4. 通用模式去重(简单数组比较)

if (isCommon && computed === computed) {
  var seenIndex = seen.length;
  while (seenIndex--) {
    if (seen[seenIndex] === computed) {
      continue outer;
    }
  }
  if (iteratee) {
    seen.push(computed);
  }
  result.push(value);
}

这是处理常见情况的代码:小数组 + 无自定义比较器。

  • computed === computed:这个看似奇怪的条件其实是用来排除 NaN,因为 NaN !== NaN

    • 如果 computedNaN,这个条件为 false,会走到 else 分支
    • 示例:对于数组 [1, NaN, 2, NaN],第二个 NaN 会进入不同的处理分支
  • 从后往前遍历 seen 数组,检查 computed 是否已存在:

    var seenIndex = seen.length;
    while (seenIndex--) { // 从后往前查找通常更高效,因为新添加的元素在末尾
      if (seen[seenIndex] === computed) {
        continue outer; // 如果找到重复,跳过当前元素处理,继续处理下一个元素
      }
    }
    
  • 如果没有找到重复,将值添加到结果中:

    if (iteratee) {
      seen.push(computed); // 如果有迭代器,记录计算后的值
    }
    result.push(value); // 把原始值添加到结果数组
    

示例:

_.uniq([1, 2, 1, 3]); // 最终 seen 和 result 都是 [1, 2, 3]
_.uniqBy([{ id: 1 }, { id: 2 }, { id: 1 }], "id"); // seen 是 [1, 2],result 是 [{id:1}, {id:2}]

5. 非通用模式去重(缓存或自定义比较)

else if (!includes(seen, computed, comparator)) {
  if (seen !== result) {
    seen.push(computed);
  }
  result.push(value);
}

这部分处理特殊情况:大数组/自定义比较器/NaN 值

  • !includes(seen, computed, comparator):检查 computed 是否已存在于 seen

    • 如果使用比较器:includes = arrayIncludesWith,会用 comparator 比较
    • 如果使用缓存:includes = cacheHas,会在 SetCache 中查找
    • 示例:_.uniqWith([1.2, 1.8, 2.5], function(a, b) { return Math.floor(a) === Math.floor(b); }) 这里 1.21.8 会被视为相同(因为都向下取整为 1)
  • 如果是新元素(includes 返回 false):

    if (seen !== result) {
      seen.push(computed); // 如果seen不是result,记录computed值
    }
    result.push(value); // 把原始值添加到结果
    

6. 处理特殊值示例

NaN 值处理

_.uniq([1, NaN, 2, NaN]); // 结果: [1, NaN, 2]

对于数组中的 NaN,由于 NaN !== NaN,用普通的 === 无法检测到重复的 NaNbaseUniq 通过 computed === computed 检测 NaN,对 NaN 使用特殊路径处理。

-0 和 +0 处理

_.uniq([-0, +0]); // 结果: [0]  (而不是 [-0, +0])

虽然 -0 === +0true,但在其他场景下它们有区别。baseUniq 通过 value = (comparator || value !== 0) ? value : 0 确保它们被视为同一个值。

7. 最终返回

return result; // 返回去重后的数组

至此,baseUniq 完成了数组去重的任务,返回一个包含所有不重复元素的新数组。

总结

baseUniq 是一个高效、灵活的数组去重实现,通过根据不同场景选择最优算法达到性能最优。其特点包括:

  1. 自适应策略选择:根据数组大小和参数选择不同的去重策略
  2. 支持自定义迭代器:可以在比较前对元素进行转换
  3. 支持自定义比较器:灵活定制元素间的相等性判断
  4. 特殊值处理:正确处理 JavaScript 中的特殊值如 NaN 和 +0/-0
  5. 性能优化:对大数组使用 Set 或缓存机制提高性能

这种实现体现了 Lodash 库重视性能和正确性的设计理念,以及处理 JavaScript 各种边界情况的严谨态度。

webpack JS meta 信息插件 /lib/JavascriptMetaInfoPlugin.js

作者 excel
2025年4月26日 07:24
  • 插件名称定义为 JavascriptMetaInfoPlugin,并应用于 Webpack 的编译流程中。

  • 监听 compiler.hooks.compilation 钩子,在编译过程中注入逻辑。

  • 为三种 JavaScript 模块类型(AUTO、DYNAMIC、ESM)注册 parser 钩子,统一处理逻辑。

  • 在解析阶段监听 eval() 的调用

    • 标记 buildInfo.moduleConcatenationBailout = "eval()",阻止模块作用域连接优化。
    • 设置 buildInfo.usingEval = true,记录模块使用了 eval()
    • 使用 InnerGraph.getTopLevelSymbol() 尝试获取当前上下文的顶层符号。
    • 如果存在顶层符号,则调用 InnerGraph.addUsage() 记录引用关系。
    • 否则调用 InnerGraph.bailout(),中断内部图优化。
  • 在解析结束时(parser.finish)记录顶层变量声明

    • 初始化 buildInfo.topLevelDeclarations 为一个 Set
    • 遍历作用域中定义的变量名。
    • 如果变量没有自由变量信息(非闭包变量),视为顶层声明加入集合。
  • 最终导出插件类用于 Webpack 插件系统使用。

/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Sergey Melyukov @smelukov
*/
// 这是插件的许可证信息和作者署名

"use strict";
// 启用严格模式,提高代码的规范性和安全性

const {
JAVASCRIPT_MODULE_TYPE_AUTO,
JAVASCRIPT_MODULE_TYPE_DYNAMIC,
JAVASCRIPT_MODULE_TYPE_ESM
} = require("./ModuleTypeConstants");
// 引入三种 JavaScript 模块类型常量:自动识别、动态模块、ES 模块

const InnerGraph = require("./optimize/InnerGraph");
// 引入 InnerGraph,用于分析模块的内部依赖关系图(变量引用、作用域等)

/** @typedef {import("./Compiler")} Compiler */
// 引入类型定义:Compiler

/** @typedef {import("./Module").BuildInfo} BuildInfo */
// 引入类型定义:模块的构建信息 BuildInfo

/** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */
// 引入类型定义:JavaScript 语法解析器 JavascriptParser

const PLUGIN_NAME = "JavascriptMetaInfoPlugin";
// 定义插件名称常量,后续钩子绑定中使用

class JavascriptMetaInfoPlugin {
/**
 * 插件应用函数
 * @param {Compiler} compiler 编译器实例
 * @returns {void}
 */
apply(compiler) {
// 在 compilation 阶段注册钩子
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation, { normalModuleFactory }) => {
/**
 * 处理 JavaScript 解析器的钩子函数
 * @param {JavascriptParser} parser 解析器实例
 * @returns {void}
 */
const handler = parser => {
// 为 eval() 函数调用绑定钩子
parser.hooks.call.for("eval").tap(PLUGIN_NAME, () => {
const buildInfo =
/** @type {BuildInfo} */
(parser.state.module.buildInfo);
// 使用 eval() 会导致模块无法进行作用域提升优化
buildInfo.moduleConcatenationBailout = "eval()";
// 标记当前模块使用了 eval
buildInfo.usingEval = true;

// 获取当前作用域的顶层符号
const currentSymbol = InnerGraph.getTopLevelSymbol(parser.state);
if (currentSymbol) {
// 如果获取到了符号,则记录其引用关系
InnerGraph.addUsage(parser.state, null, currentSymbol);
} else {
// 否则标记当前模块无法进行内联图分析(bailout)
InnerGraph.bailout(parser.state);
}
});

// 在解析完成时记录顶层声明
parser.hooks.finish.tap(PLUGIN_NAME, () => {
const buildInfo =
/** @type {BuildInfo} */
(parser.state.module.buildInfo);

let topLevelDeclarations = buildInfo.topLevelDeclarations;
if (topLevelDeclarations === undefined) {
// 初始化 topLevelDeclarations 为 Set 集合
topLevelDeclarations = buildInfo.topLevelDeclarations = new Set();
}

// 遍历当前作用域中定义的变量名
for (const name of parser.scope.definitions.asSet()) {
const freeInfo = parser.getFreeInfoFromVariable(name);
// 如果变量没有被视为自由变量,认为它是顶层声明
if (freeInfo === undefined) {
topLevelDeclarations.add(name);
}
}
});
};

// 为三种 JS 模块类型分别注册 parser 钩子
normalModuleFactory.hooks.parser
.for(JAVASCRIPT_MODULE_TYPE_AUTO)
.tap(PLUGIN_NAME, handler);
normalModuleFactory.hooks.parser
.for(JAVASCRIPT_MODULE_TYPE_DYNAMIC)
.tap(PLUGIN_NAME, handler);
normalModuleFactory.hooks.parser
.for(JAVASCRIPT_MODULE_TYPE_ESM)
.tap(PLUGIN_NAME, handler);
}
);
}
}

module.exports = JavascriptMetaInfoPlugin;
// 导出插件类

webpack 运行时模版 第 六 节 /lib/RuntimeTemplate.js

作者 excel
2025年4月26日 07:20

blockPromise(options)

  • 功能:生成用于加载 chunk 的 Promise 表达式。

  • 用途:支持异步模块(如 import())在 runtime 中正确加载 chunk。

  • 逻辑

    • 如果没有 block 或 block 对应的 chunkGroup 为空,返回 Promise.resolve(...)
    • 获取 block 所属的 chunkGroup,并筛选出没有 runtime 且有 ID 的 chunk。
    • 若只需加载一个 chunk,生成 __webpack_require__.e(chunkId) 表达式。
    • 若有多个 chunk,生成 Promise.all([...]) 包含多个 ensureChunk 调用。
    • 自动收集 RuntimeGlobals.ensureChunk 和可能的 RuntimeGlobals.hasFetchPriority 到 runtimeRequirements 中。

asyncModuleFactory(options)

  • 功能:生成异步模块的工厂函数代码。

  • 用途:用于代码分割中异步模块的运行时代码生成。

  • 逻辑

    • 获取异步 block 的依赖对应模块。
    • 调用 blockPromise 获取 chunk 加载表达式。
    • 创建工厂函数(实际调用 __webpack_require__ 载入模块)。
    • 若 chunk 加载是异步的,使用 .then(factory) 包裹;否则直接返回工厂函数。

syncModuleFactory(options)

  • 功能:生成同步模块的工厂函数代码。

  • 用途:用于普通(非异步)模块的运行时加载工厂。

  • 逻辑

    • 获取依赖模块。
    • 构建 (() => __webpack_require__(moduleId)) 的函数形式。

defineEsModuleFlagStatement(options)

  • 功能:生成标记 exports.__esModule = true 的语句。

  • 用途:兼容 ESModule 模式,确保 importrequire 之间正确识别模块类型。

  • 逻辑

    • 添加对 RuntimeGlobals.makeNamespaceObjectRuntimeGlobals.exports 的依赖。
    • 返回调用 __webpack_require__.r(exports) 的代码。
class RuntimeTemplate {
/**
 * @param {object} options options 配置项
 * @param {AsyncDependenciesBlock | undefined} options.block 异步模块块(如 import())
 * @param {string} options.message 注释信息
 * @param {ChunkGraph} options.chunkGraph Chunk 图结构(模块和 chunk 的依赖关系)
 * @param {RuntimeRequirements} options.runtimeRequirements 存储运行时代码所需依赖
 * @returns {string} 返回用于异步模块加载的代码字符串
 */
blockPromise({ block, message, chunkGraph, runtimeRequirements }) {
// 如果 block 不存在,直接返回一个已完成的 Promise
if (!block) {
const comment = this.comment({ message });
return `Promise.resolve(${comment.trim()})`;
}

// 获取该 block 所对应的 chunk group(一个或多个 chunk 的集合)
const chunkGroup = chunkGraph.getBlockChunkGroup(block);
// 若 chunkGroup 不存在或为空,也返回已完成 Promise
if (!chunkGroup || chunkGroup.chunks.length === 0) {
const comment = this.comment({ message });
return `Promise.resolve(${comment.trim()})`;
}

// 过滤掉包含 runtime 的 chunk,并只保留有 id 的 chunk
const chunks = chunkGroup.chunks.filter(
chunk => !chunk.hasRuntime() && chunk.id !== null
);

// 添加注释信息,包括 chunkName
const comment = this.comment({
message,
chunkName: block.chunkName
});

// 如果只需加载一个 chunk
if (chunks.length === 1) {
const chunkId = JSON.stringify(chunks[0].id);
// 添加 ensureChunk 到运行时依赖(__webpack_require__.e)
runtimeRequirements.add(RuntimeGlobals.ensureChunk);

// 获取 fetch 优先级(用于 HTTP/2 优化)
const fetchPriority = chunkGroup.options.fetchPriority;
if (fetchPriority) {
runtimeRequirements.add(RuntimeGlobals.hasFetchPriority);
}

// 返回加载单个 chunk 的表达式
return `${RuntimeGlobals.ensureChunk}(${comment}${chunkId}${
fetchPriority ? `, ${JSON.stringify(fetchPriority)}` : ""
})`;
}
// 如果有多个 chunk 需要加载
else if (chunks.length > 0) {
runtimeRequirements.add(RuntimeGlobals.ensureChunk);
const fetchPriority = chunkGroup.options.fetchPriority;
if (fetchPriority) {
runtimeRequirements.add(RuntimeGlobals.hasFetchPriority);
}

/**
 * @param {Chunk} chunk 单个 chunk
 * @returns {string} 返回 ensureChunk 加载语句
 */
const requireChunkId = chunk =>
`${RuntimeGlobals.ensureChunk}(${JSON.stringify(chunk.id)}${
fetchPriority ? `, ${JSON.stringify(fetchPriority)}` : ""
})`;

// 返回 Promise.all([...]) 加载多个 chunk 的表达式
return `Promise.all(${comment.trim()}[${chunks
.map(requireChunkId)
.join(", ")}])`;
}

// 无 chunk 时,返回已完成 Promise
return `Promise.resolve(${comment.trim()})`;
}

/**
 * 生成异步模块的工厂函数
 * @param {object} options 配置项
 * @param {AsyncDependenciesBlock} options.block 异步模块块
 * @param {ChunkGraph} options.chunkGraph chunk 图
 * @param {RuntimeRequirements} options.runtimeRequirements 存储运行时代码依赖
 * @param {string=} options.request 请求路径
 * @returns {string} 返回模块工厂函数代码
 */
asyncModuleFactory({ block, chunkGraph, runtimeRequirements, request }) {
// 获取该 block 中的第一个依赖
const dep = block.dependencies[0];
// 获取该依赖对应的模块
const module = chunkGraph.moduleGraph.getModule(dep);

// 生成 ensureChunk 调用代码(可能是 Promise.resolve 或 ensureChunk)
const ensureChunk = this.blockPromise({
block,
message: "",
chunkGraph,
runtimeRequirements
});

// 生成模块工厂函数 (() => __webpack_require__(moduleId))
const factory = this.returningFunction(
this.moduleRaw({
module,
chunkGraph,
request,
runtimeRequirements
})
);

// 返回最终的异步模块工厂 (() => Promise.then(() => require(id)))
return this.returningFunction(
ensureChunk.startsWith("Promise.resolve(")
? `${factory}`
: `${ensureChunk}.then(${this.returningFunction(factory)})`
);
}

/**
 * 生成同步模块的工厂函数
 * @param {object} options 配置项
 * @param {Dependency} options.dependency 模块依赖
 * @param {ChunkGraph} options.chunkGraph chunk 图
 * @param {RuntimeRequirements} options.runtimeRequirements 存储运行时代码依赖
 * @param {string=} options.request 请求路径
 * @returns {string} 返回模块工厂函数代码
 */
syncModuleFactory({ dependency, chunkGraph, runtimeRequirements, request }) {
// 获取该依赖对应的模块
const module = chunkGraph.moduleGraph.getModule(dependency);

// 生成模块工厂 (() => __webpack_require__(moduleId))
const factory = this.returningFunction(
this.moduleRaw({
module,
chunkGraph,
request,
runtimeRequirements
})
);

// 再次包一层返回函数(用于构建 factory 的引用)
return this.returningFunction(factory);
}

/**
 * 返回将 __esModule 标志注入到 exports 对象的语句
 * @param {object} options 配置项
 * @param {string} options.exportsArgument exports 对象变量名
 * @param {RuntimeRequirements} options.runtimeRequirements 存储运行时代码依赖
 * @returns {string} 代码语句字符串
 */
defineEsModuleFlagStatement({ exportsArgument, runtimeRequirements }) {
// 添加运行时对 __webpack_require__.r 和 exports 的依赖
runtimeRequirements.add(RuntimeGlobals.makeNamespaceObject);
runtimeRequirements.add(RuntimeGlobals.exports);

// 返回 __webpack_require__.r(exports); 语句
return `${RuntimeGlobals.makeNamespaceObject}(${exportsArgument});\n`;
}
}

module.exports = RuntimeTemplate; // 导出 RuntimeTemplate 类

李斌提出“新三大件”,蔚来2025能否画出上升曲线?

2025年4月26日 00:17

两年前,合资品牌、豪华车企的代表团涌入上海车展,为中国车企的智能电动“神速进步”感到震惊与讶异。

两年后的4月上海车展依旧热闹,但上述的“盛况”已经不明显。

相反,日系车、德系车以肉眼可见的速度,集体补齐了高阶智能智能驾驶、智能座舱等“短板”,这成为今年上海车展最值得关注的亮点之一。

这正是智能电动车时代的残酷:智能技术的快速普及,中国车企们好不容易先发的智能化优势在被加速抹平。

中国新造车势力早有警觉。4月23日的媒体沟通会上,蔚来创始人、董事长、CEO李斌坦诚:冰箱彩电大沙发,这些看得见的应用创新,确实非常容易同质化。

李斌认为真正有壁垒的技术,在于用户看不到的地方。

他将智驾芯片、全域操作系统、智能底盘,称之为智能汽车核心技术的“新三大件”,这些核心技术决定了智能汽车的体验上限和安全上限。

“看不见的核心技术创新,投入周期长,用户感知可能没有那么强,但它是真正的智能汽车底座。”

例如,李斌在发布会上展示了蔚来自研的、全球首颗量产5纳米智驾芯片神玑NX9031。这颗芯片随着蔚来智能电动行政旗舰ET9的交付而正式量产落地。

这帮蔚来赢得了车型产品落地的时间优势。李斌介绍,1颗神玑芯片抵得上4颗英伟达Orin X芯片,两颗抵8颗。单颗神玑NX9031和满血版的英伟达最新一代芯片Thor-X在同一算力水平,比入门版的Thor-U早上车数月。而同行中,一部分车企为了等英伟达的芯片量产,不得不推迟车型产品的发布时间。

又或者在蔚来展台上“起舞”的ET9,背后则是蔚来集成了线控转向、后轮转向、全主动悬架的天行底盘的支撑,以及深入整车底层的整车操作系统天枢SkyOS的技术结晶。

“核心技术创新是蔚来的底色。”李斌强调。

他表示,在核心技术创新之下,叠加中国供应链优势,与产品和技术竞争力,“中国汽车产业年产量有机会达到四千万辆,占全球汽车产业份额超40%。”

对于处于组织变革中的蔚来而言,坚持这样的犀利预判并不容易。但风暴中的坚持,往往更能看出一家企业的信仰。

蔚来将这种技术底色,贯穿于三大品牌之中。4月23日上海车展首日,蔚来公司携NIO蔚来、ONVO乐道、firefly萤火虫三大品牌11款车型、12项全栈技术集体亮相。

凭借全阵容车型与体系化的创新技术,蔚来展台始终人头攒动,其中不乏来自海外汽车厂商的同行。

德国宝马集团董事长、首席执行官齐普策率领十几位高管组团走访蔚来展台。前大众汽车集团CEO赫伯特·迪斯、宝马全球设计总监阿德里安·范·霍伊东克、美国凯迪拉克高管团、日本雷克萨斯设计总监在ET9和萤火虫内长时间体验。

这些来自海外汽车厂商的高管并不关心流量,更关注的是汽车核心技术上的创新。与去年北京车展相比,本次上海车展不再有光怪陆离的喧闹,跳出了荒诞的流量狂欢怪圈,开始回归本质。

流量退潮,比拼真功夫的时候到了。

「“新三大件”托起产品大年」

1989年,丰田曾用一个“香槟塔广告片”震动了豪车圈。广告片里,雷克萨斯LS400以240km/h高速行驶,但引擎盖上的香槟杯塔纹丝不动。

这背后是丰田耗时6年、耗资10亿美元的秘密研发——从零打造V8发动机、推翻50项专利、完成450万公里路测。丰田最终证明了,LS400不是“廉价丰田换壳”,雷克萨斯也成为了能够击穿欧美豪车封锁防线的实力品牌。

2025年,新势力蔚来在以另一种形式实现智能电动汽车产品的最新技术“封锁”。

蔚来连续三年研发投入超百亿,自研5nm车规级智驾芯片、重构整车操作系统、推出智能底盘,试图用“新三大件”构建智能汽车时代的技术底座与防线。

但蔚来也意识到,长期主义式的投入,不应成为一种借口。在汽车产业游戏局里,只有把投入切实转换成技术复利,才能安全进入下一赛段。

李斌也在沟通会上强调,新三大件的研发其实能换来毛利率。

例如,神玑NX9031不仅如此搭载在智能电动行政旗舰ET9上,还将搭载于蔚来新款ET5、ET5T、ES6、EC6上,为智能辅助驾驶的持续升级提供算力基础。

“神玑NX9031芯片一颗抵4颗,能帮我们省了不少钱。”此前李斌曾透露过,2024年蔚来光是购买英伟达Orin X的芯片,就花了3亿多美金,即数十亿人民币。

蔚来的天枢SkyOS整车全域操作系统也是如此,覆盖智能驾驶、车控、车联、数字座舱等6大领域。李斌也表示,在采访中表示,原来车上用第三方的操作系统也要付钱,但现在用天枢SkyOS能省很多钱。“可以理解成,除了测试车,别的量产车基本都不需要一辆车一辆车去付钱了。”

类似地,李斌还介绍道,蔚来从第二代车型就开始自研智能底盘,如今进化到天行底盘,“已经把成本大大降下来了。”

李斌说,这些技术的核心自研一方面能提高用户的体验上限和安全上限,另一方面是能够降本。“研发前期要投很多钱,但是一旦量产就进入到一个收获期。”

而在“技术收获大年”的托底下,蔚来也迎来了自己的产品大年。

李斌在发布会上表示,2025年三个品牌一共有9款新车上市,每个季度都有新品发布。

首先蔚来品牌新款ET5、ET5T、ES6、EC6将于第二季度上市;第三代ES8将于第四季度上市。

乐道品牌第二款车型发布乐道L90将于第三季度上市交付,这是一款三排大六座的SUV,例如240升超大前备舱,就是专门针对大家庭用户人群的痛点而设。乐道品牌第三款产品L80将在第四季度上市。

智能电动高端小车firefly萤火虫前不久才以11.98万元的售价进入市场,对标的是宝马旗下的MINI,将在4月底开启交付。新车还将在今年进入全球5大洲16个国家的市场。

虽然firefly萤火虫的车型外观引发了一些争议与讨论,但李斌在萤火虫上市后的群访中表示,审美是个很主观的事情,蔚来从来没有期待这辆车让所有人都觉得满意。

“我觉得,喜欢的人会非常喜欢,会认可我们这种原创性的思考,系统性的思考,萤火虫有自己内在的体系和逻辑。我认为喜欢我们的人能get到这一点,这是一个与众不同的事情。审美这个事情确实太主观了,没有对错之分。”

「基建大年:“换电与超充不是对立面”」

除了技术大年、产品大年之外,2025年还是蔚来的基建大年。蔚来的计划是,到2025年底,完成27个省级行政区的换电服务网络建设,覆盖超过2300个县级行政区,最终在2026年覆盖全国超2800个县级行政区。

但行业之中,除了换电路线之外,还存在超快充技术路线。后者是汽车行业的热门路线,今年3月,比亚迪还发布了“兆瓦闪充”技术,推出了乘用车1000V高压架构和10C高倍率电池。

回应两种技术路线的交锋时, 蔚来李斌在沟通环节有些激动。因为在他看来,充电与换电根本并非对立的两面。

一方面,蔚来本身就是充电站“建桩狂魔”,根据官方数字,蔚来部署了超过2万6千根充电桩,也落地部署了750千瓦液冷超快充桩。“我们是全中国布充电桩最努力的公司,80%的充电桩都是别的品牌在用,谁建的超充桩比我们多了?”李斌说道。

另一方面,李斌认为换电的体验优势非常明显。他曾在黑龙江漠河、黑河,新疆喀什的偏远地段都体验过换电。

“再快的超充也不可能比换电快,也不可能有换电体验好。换电不需要下车,刮风下雨,天气冷,零下30-40度的环境,都不用下车。超充如果要不下车,那等机械臂机器人搞了再说。”

他提出,不要看车企的“几分钟跑多少公里”话术,而是要看从0~93%电量到底要多长时间。蔚来换电则强调的是,三分钟就完成一次换电。

李斌还强调了第三点,也就是电池质量问题。“经常用超快充,车企能不能做15年质保?12年能不能做?一直用超快充,还保10年质保,保8年能不能?如果能,我服。”“把这三个问题都回答完,再告诉我愿意用10C、6C还是5C?”

而蔚来通过规模换电网络,可以对用户电池进行全生命周期电池健康运营与监测。蔚来的目标是,使用15年后,用户的电池健康度仍保持在85%以上。

李斌认为,最终还是要回到技术本身、用户价值层面来看待补能的问题。

他建议用更理性的观点看待超充。如果要用超快充,想在已有的电力容量基础上直接建站几乎不可能,因为站点电力容量往往不够,必须得搞储能。“如果要建光储充,那建站成本就会跟换电站一样贵。”

李斌还强调了另一笔账:从技术逻辑来看,超快充的充电和放电过程,会产生两次电损。“多10%多的电损,谁来付这个钱?服务费里收,还是提供储充的公司补贴?这是不是更贵?而换电只有一次电损。”

李斌呼吁,行业在发展超充的同时,不要将其与换电对立起来。“蔚来是可充可换可升级。”

「蔚来拨开多品牌迷雾」

此次车展,蔚来展台除了有ET9等主品牌的车型坐镇,吸引人群驻足的,还有firefly萤火虫的亮眼与活力。乐道展台则有智能大空间旗舰SUV乐道L90首发亮相。

这是蔚来公司三个品牌首次集体出场。

对新势力车企而言,单个品牌的市场竞争已经足够激烈。而蔚来三大品牌,面临的是困难系数乘以3的挑战。

今年以来,蔚来内部已经掀起了组织变革风暴,着力提升经营效率,呈现出了足够更清晰的多品牌战略罗盘。蔚来用核心技术创新贯穿三大品牌,还在销售资源、换电网络、生产研发等领域做了全面整合。

在销售侧,当前,firefly萤火虫已经进驻了蔚来主品牌的销售门店。李斌此前在firefly萤火虫上市后的面对面沟通会上表示,萤火虫如果单独建一个销售网络,成本会很高。

“蔚来的销售团队已经开始在卖萤火虫了,内部蔚来和萤火虫的销售后台报表是同一个,乐道的团队则是单独的。充换电和售后服务、交付这些,三个品牌都可以复用。”李斌在上海车展后的沟通会上表示。

乐道早就复用了蔚来换电体系。李斌表示,目前,乐道已经有接近2000多座换电站可用,“蔚来一共才3200多座,相当于60%的换电站都是共享给乐道用户的。”

但不变的是,蔚来在研发与供应层面的资源共享。李斌表示,公司60%以上的研发同事都是做平台性研发,每个品牌的车型都能用,分摊成本。比如神玑NX9031芯片和全域操作系统SkyOS。

可以看到,蔚来正在充分调用主品牌的养料与土壤,来培育不同方向的品牌枝干。随着内部的资源整合,蔚来三大品牌的协同更加清晰与紧密。蔚来正在拨开多品牌战略的迷雾,进入公司发展的下一阶段。 

多��牌战略,其实是车企的一场高端化与规模化“平衡术”。

李斌曾在多个场合讲述过丰田集团雷克萨斯的案例:丰田从1985年开始做雷克萨斯独立品牌,到登顶北美的销量榜单,丰田用了近20年时间。

如今,蔚来要用更短的时间应对更剧烈的挑战。这不仅要对技术投入保持耐心,更要在亏损泥潭中坚持战略定力,用技术创新找到赚钱的秘诀。

蔚来始终在努力尝试书写中国智能电动车企的另一种可能。2025,将尤为关键。

每日一题-统计定界子数组的数目🔴

2025年4月26日 00:00

给你一个整数数组 nums 和两个整数 minK 以及 maxK

nums 的定界子数组是满足下述条件的一个子数组:

  • 子数组中的 最小值 等于 minK
  • 子数组中的 最大值 等于 maxK

返回定界子数组的数目。

子数组是数组中的一个连续部分。

 

示例 1:

输入:nums = [1,3,5,2,7,5], minK = 1, maxK = 5
输出:2
解释:定界子数组是 [1,3,5] 和 [1,3,5,2] 。

示例 2:

输入:nums = [1,1,1,1], minK = 1, maxK = 1
输出:10
解释:nums 的每个子数组都是一个定界子数组。共有 10 个子数组。

 

提示:

  • 2 <= nums.length <= 105
  • 1 <= nums[i], minK, maxK <= 106

VUE项目发版后用户访问的仍然是旧页面?原因和解决方案都在这啦!

作者 HED
2025年4月25日 23:49

A系统(VUE项目)在版本迭代中,有时会出现打包上线后,用户仍然访问的是旧版本页面的现象。这时,用户需手动刷新,才能访问到新版本页面,影响用户体验。

这种现象出现的根本原因是浏览器缓存机制问题,那么,浏览器的缓存机制到底是怎样的?上述现象又该如何避免呢?

一、浏览器缓存机制

浏览器默认缓存机制‌主要包括两种类型:‌强缓存‌和‌协商缓存‌。

强缓存是指浏览器在加载网页时直接从本地缓存中读取资源,而不与服务器进行交互。强缓存主要通过设置HTTP响应头中的Expires和Cache-Control来实现。

协商缓存是指浏览器在加载资源时,先向服务器询问资源是否发生变化。如果服务器确认资源未变化,则返回304状态码,浏览器继续使用缓存中的资源;否则返回新的资源。协商缓存主要通过在HTTP响应头中设置Last-Modified和ETag来实现。

其中强缓存优先于协商缓存,而当没有设置缓存的时候浏览器默认的缓存策略为:

缓存时长 = (访问时间 - 最后一次修改时间) / 10

QQ_1745562382243.png

二、A系统针对浏览器缓存的既有解决方案

(一)对于JS、CSS等静态资源

A系统是基于vue-cli4脚手架开发的,其在每次Webpack打包过程中,会给生成的JS、CSS等静态资源文件名添加哈希后缀,并在index.html中引入带有相应哈希后缀的静态资源文件。

当版本无更新时,这些静态资源走浏览器缓存,当版本有更新时,浏览器会比对,重新请求,因此,每个版本的静态资源都是被正确引入的,不会因升级而出现缓存问题。

QQ_1745562432293.png

但是,public文件夹里的文件是不会被Webpack处理的,这些文件会直接被复制到目标目录,而我们的系统入口文件index.html就在public文件夹里。

也就是说,打包时,index.html文件是不会添加哈希后缀的,所以在版本更新后,依然会走浏览器缓存。

(二)对于动态数据

服务器端配置了Cache-Control:no-store,也就是说,所有接口内容浏览器都不会缓存,每次请求都是新请求。

image.png

三、A系统缓存方案优化

综上,可以猜想,A系统在版本迭代中有时会出现打包上线后用户仍然访问的是旧版本页面的现象,很可能是因为 其入口文件index.html在版本更新时会有缓存。

那么,怎么才能让index.html不被缓存呢?

要实现让静态资源被缓存,但让index.html不被缓存,则需要借助Nginx配置,通过设置针对index.html请求的header来控制缓存,具体配置如下:

# 针对 index.html 设置不缓存
location = /index.html {
    add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

# 针对 JS/CSS等 文件设置缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
  add_header Cache-Control "public, no-transform";
}

Nginx配置上述代码后第一次发版,打开页面时,如果页面还在磁盘缓存阶段,未与服务器发生交互,则页面还是旧页面。这是因为,浏览器因之前的缓存策略,将上个版本的 index.html 缓存下来,即使服务器已经设置了新版本不缓存,浏览器还是优先使用本地磁盘缓存。但是后续再发版,就没有缓存问题了,也就不会再出现发版后用户访问的仍然是旧页面的现象了!

 

从圣经Babel到现代编译器:没开玩笑,普通程序员也能写出自己的编译器!

2025年4月25日 23:41

当你自己设计一个DSL(领域特定语言)并实现解析、转译、生成时,你做的事情,本质上和Babel、Vue、React这些伟大的项目,在技术哲学层面竟然惊人地一致。

  • Babel的名字为什么来源于《圣经》?
  • Babel转译和Polyfill到底有什么本质区别?
  • 为什么说JavaScript也需要编译?
  • 如何系统性设计自己的DSL编译器?
  • 程序员常见工具名字背后的文化典故

01 | Babel:来自圣经的巴别塔故事

《圣经·创世纪》里,有一个著名的故事:巴别塔(Tower of Babel)。

那时人类讲着同一种语言,团结一心,建造一座通天塔,想要直达天堂。

上帝看到后,觉得人类太傲慢了,就施展神力:

  • 让人们的语言突然变得不同
  • 大家听不懂彼此说什么了
  • 工程无法继续,塔也没建成
  • 人类被迫分散到世界各地

从此,"Babel"在英文中意味着:语言混乱,沟通障碍

而现代前端的Babel工具,做的正是反过来的事情:

  • 不同版本的JavaScript语法标准(ES5、ES6、ESNext)像是不同的"语言"。
  • Babel帮助我们转译这些"不同的语言",让浏览器世界能统一理解我们的代码。

Babel,这个名字,致敬了"语言混乱",也致敬了"打破隔阂"。浪漫而深刻。✨

02 | Babel是编译,Polyfill是运行时补丁

很多人误以为JS是解释执行,不需要编译,其实——

✅ 传统JS是解释执行; ✅ 现代JS开发,一定要经历编译

Babel做的是:

  • 编译阶段(build时)
  • 把高级语法(比如 ??、?.、class、async/await)转成老式JS(比如var、function)

而Polyfill做的是:

  • 运行阶段(浏览器执行时)
  • 给老浏览器补上缺失的API(比如Promise、fetch、URLSearchParams)
对比 Babel Polyfill
作用 转语法 补API
时机 编译时 运行时
例子 ??转成三目运算符 给IE加上Promise支持

Babel负责"能看懂",Polyfill负责"能用上"。

03 | 为什么现代JS需要编译?

最初的JS,确实是解释型脚本语言,浏览器拿到源码,一行一行解释执行。

但是!随着前端应用变复杂,浏览器厂商(如Chrome的V8引擎)为了性能,引入了:

  • 即时编译(JIT) :边解释边编译热点代码成机器码,加速运行
  • 优化执行路径:比如隐藏类、内联缓存(IC)技术

同时,开发者也开始写越来越"未来感"的代码,比如:

  • async/await
  • ES模块
  • Proxy
  • 新的数组方法

而旧浏览器当然看不懂新语法,所以,开发过程中:

开发阶段:用现代JS自由飞翔
    ↓
编译阶段(Babel):转译成传统JS
    ↓
运行阶段(浏览器V8):解释 + JIT编译执行

现代前端开发,一定离不开编译!


04 | 自己动手,系统性设计DSL+编译器!

设计DSL,其实就是自己打造一个小型编译器!

标准三步,必不可少:

解析(Parse) → 转换(Transform) → 生成(Generate)

举个例子:

表单DSL:

{
  "type": "form",
  "fields": [
    { "type": "text", "label": "用户名", "name": "username" },
    { "type": "password", "label": "密码", "name": "password" }
  ]
}

编译过程:

  • Parse:解析成AST(表单节点+字段节点)
  • Transform:补充默认属性,比如密码字段加minLength验证
  • Generate:输出HTML结构或者Vue组件

✅ 这就是标准的编译器套路,只不过处理的对象是自己定义的小语言!


05 | 为什么自己做DSL编译器也伟大?

有人说:Vue、React很伟大,做小项目不算什么。

但实际上,伟大不是大小,而是方向

  • React定义了组件化+声明式UI新范式
  • Vue定义了轻量灵活的响应式系统

而你自己做DSL,哪怕只为解决表单渲染、路由配置、页面布局,也是在:

  • 定义一种新的领域语言
  • 通过解析-变换-生成,提升开发效率
  • 打造属于自己的小世界

只要走在正确方向,每一行代码都是积累。


06 | 程序员常见工具名字的典故大全

很多耳熟能详的技术名字,其实背后都有文化!

工具名字 典故
Babel 圣经巴别塔,打破语言隔阂。
Webpack "Web"+"Pack",打包网页资源。
Vite 法语"快",极速冷启动。
Rollup 模块打包优化,把模块roll成一坨。
React UI是数据变化的自然反应。
Vue 发音同"View(视图)",专注构建界面。
Svelte "苗条、优雅",生成小而快的代码。
Prettier 让代码更漂亮。
Jest "玩笑",测试应该轻松有趣。
Tailwind "顺风",开发快得像顺风一样。

✅ 工具名字,不只是词汇,更是精神的传达。


07 | 最后:创造自己的小世界

敢于创造属于自己的小世界,并持续打磨它。

愿你,走在创造的路上,眼里有光,心中有火。🔥

一起在创造中成长!

技术人其实也很浪漫的~✨

✅ 设计领域语言 ✅ 建立解析-变换-生成体系 ✅ 保持小而美,持续打磨 ✅ 给世界带来一点不一样的东西

这就是程序员最纯粹、最美好的创造力。

最后

如果你喜欢这篇分享,欢迎点赞、收藏!👍 也欢迎在评论区告诉我你的想法。

一起探索程序员创造小宇宙的无限可能吧!🚀✨

前端自做埋点,我们应该要注意的几个问题

2025年4月25日 23:26

一、代码埋点操作原理

代码埋点是一种数据采集技术,通过在应用程序的特定位置插入代码片段,来记录用户行为、系统状态等信息。其核心原理是:

  1. 触发机制‌:在特定事件(如点击、页面加载、API调用)发生时触发埋点
  2. 数据收集‌:收集相关信息(如用户ID、时间戳、事件类型、上下文数据)
  3. 数据传输‌:将收集的数据发送到后端服务器
  4. 数据存储与分析‌:后端接收并存储数据,供后续分析使用

二、代码埋点实现方案

1. 前端埋点实现(JavaScript示例)

// 埋点工具类
class Tracker {
  constructor(options = {}) {
    this.serverUrl = options.serverUrl || 'https://your-analytics-server.com/api/track';
    this.appId = options.appId || 'default-app';
    this.userId = options.userId || this.generateUUID();
    this.queue = [];
    this.isSending = false;
    this.maxRetry = 3;
    this.retryCount = 0;
  }

  // 生成唯一用户ID
  generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
      const r = Math.random() * 16 | 0;
      const v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }

  // 发送数据
  send(data) {
    const eventData = {
      ...data,
      appId: this.appId,
      userId: this.userId,
      timestamp: new Date().toISOString(),
      url: window.location.href,
      userAgent: navigator.userAgent
    };

    this.queue.push(eventData);
    this.processQueue();
  }

  // 处理队列
  processQueue() {
    if (this.isSending || this.queue.length === 0) return;

    this.isSending = true;
    const currentItem = this.queue.shift();

    fetch(this.serverUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(currentItem),
    })
    .then(response => {
      if (!response.ok) throw new Error('Network response was not ok');
      this.retryCount = 0;
      this.isSending = false;
      if (this.queue.length > 0) {
        this.processQueue();
      }
    })
    .catch(error => {
      console.error('Tracking error:', error);
      if (this.retryCount < this.maxRetry) {
        this.retryCount++;
        this.queue.unshift(currentItem);
        setTimeout(() => this.processQueue(), 1000 * this.retryCount);
      } else {
        this.retryCount = 0;
        this.isSending = false;
      }
    });
  }

  // 页面浏览埋点
  trackPageView() {
    this.send({
      eventType: 'pageview',
      pageTitle: document.title,
      referrer: document.referrer
    });
  }

  // 自定义事件埋点
  trackEvent(eventName, eventData = {}) {
    this.send({
      eventType: 'event',
      eventName,
      ...eventData
    });
  }

  // 错误埋点
  trackError(error) {
    this.send({
      eventType: 'error',
      errorMessage: error.message,
      errorStack: error.stack
    });
  }
}

// 初始化埋点实例
const tracker = new Tracker({
  serverUrl: 'https://your-analytics-server.com/api/track',
  appId: 'web-app-001'
});

// 监听页面加载
window.addEventListener('load', () => {
  tracker.trackPageView();
});

// 监听单页应用路由变化
window.addEventListener('popstate', () => {
  tracker.trackPageView();
});

// 示例:按钮点击埋点
document.getElementById('buy-button')?.addEventListener('click', () => {
  tracker.trackEvent('button_click', {
    buttonId: 'buy-button',
    productId: '12345'
  });
});

// 全局错误捕获
window.addEventListener('error', (event) => {
  tracker.trackError(event.error);
});

2. 后端埋点实现(Node.js示例)

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

// 连接MongoDB
mongoose.connect('mongodb://localhost:27017/analytics', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

// 定义数据模型
const EventSchema = new mongoose.Schema({
  appId: String,
  userId: String,
  eventType: String,
  eventName: String,
  timestamp: Date,
  url: String,
  userAgent: String,
  customData: Object,
  createdAt: { type: Date, default: Date.now }
});

const Event = mongoose.model('Event', EventSchema);

const app = express();
app.use(bodyParser.json());

// 接收埋点数据的API
app.post('/api/track', async (req, res) => {
  try {
    const eventData = req.body;
    
    // 数据验证
    if (!eventData.appId || !eventData.eventType) {
      return res.status(400).json({ error: 'Missing required fields' });
    }

    // 存储到数据库
    const event = new Event({
      appId: eventData.appId,
      userId: eventData.userId,
      eventType: eventData.eventType,
      eventName: eventData.eventName,
      timestamp: new Date(eventData.timestamp),
      url: eventData.url,
      userAgent: eventData.userAgent,
      customData: eventData.customData || {}
    });

    await event.save();

    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Error saving event:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Analytics server running on port ${PORT}`);
});

三、实现中常见问题与解决方案

1. 数据丢失问题

问题‌:网络不稳定导致埋点数据发送失败

解决方案‌:

  • 实现本地缓存和重试机制
  • 使用Web Worker进行后台发送
  • 考虑使用IndexedDB存储未发送的数据
// 增强版本地存储方案
class LocalStorageQueue {
  constructor(key = 'tracker_queue') {
    this.key = key;
  }

  getQueue() {
    const queueStr = localStorage.getItem(this.key);
    return queueStr ? JSON.parse(queueStr) : [];
  }

  addToQueue(item) {
    const queue = this.getQueue();
    queue.push(item);
    localStorage.setItem(this.key, JSON.stringify(queue));
  }

  removeFromQueue(count = 1) {
    const queue = this.getQueue();
    const remaining = queue.slice(count);
    localStorage.setItem(this.key, JSON.stringify(remaining));
    return queue.slice(0, count);
  }

  clearQueue() {
    localStorage.removeItem(this.key);
  }
}

// 在Tracker类中使用
class Tracker {
  constructor(options) {
    // ...其他代码
    this.storageQueue = new LocalStorageQueue('tracker_queue');
    this.loadFromStorage();
  }

  loadFromStorage() {
    const storedItems = this.storageQueue.getQueue();
    if (storedItems.length > 0) {
      this.queue.unshift(...storedItems);
      this.storageQueue.clearQueue();
      this.processQueue();
    }
  }

  send(data) {
    // 网络检查
    if (!navigator.onLine) {
      this.storageQueue.addToQueue(data);
      return;
    }
    
    // ...原有发送逻辑
  }
}

2. 性能影响问题

问题‌:频繁的埋点调用影响页面性能

解决方案‌:

  • 使用节流(throttle)和防抖(debounce)技术
  • 批量发送数据
  • 使用requestIdleCallback
javascriptCopy Code
// 批量发送实现
class BatchTracker extends Tracker {
  constructor(options) {
    super(options);
    this.batchSize = options.batchSize || 5;
    this.batchTimeout = options.batchTimeout || 1000; // 1秒
    this.batchTimer = null;
  }

  send(data) {
    this.queue.push(data);
    
    // 达到批量大小立即发送
    if (this.queue.length >= this.batchSize) {
      this.processBatch();
      return;
    }
    
    // 设置定时器,超时后发送
    if (this.batchTimer) clearTimeout(this.batchTimer);
    this.batchTimer = setTimeout(() => this.processBatch(), this.batchTimeout);
  }

  processBatch() {
    if (this.batchTimer) clearTimeout(this.batchTimer);
    if (this.queue.length === 0) return;
    
    const batchToSend = this.queue.slice(0, this.batchSize);
    this.queue = this.queue.slice(this.batchSize);
    
    fetch(this.serverUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ events: batchToSend })
    })
    .then(response => {
      if (!response.ok) throw new Error('Batch send failed');
      if (this.queue.length > 0) this.processBatch();
    })
    .catch(error => {
      console.error('Batch tracking error:', error);
      this.queue = [...batchToSend, ...this.queue]; // 重新加入队列
    });
  }
}

3. 数据一致性问题

问题‌:客户端时间与服务端时间不一致

解决方案‌:

  • 在发送埋点数据时同时发送客户端时间和服务端时间
  • 实现时间同步机制
// 时间同步实现
class TrackerWithTimeSync extends Tracker {
  constructor(options) {
    super(options);
    this.timeDiff = 0;
    this.syncTime();
    setInterval(() => this.syncTime(), 3600000); // 每小时同步一次
  }

  async syncTime() {
    try {
      const start = Date.now();
      const response = await fetch(`${this.serverUrl}/time`);
      const serverTime = await response.json();
      const end = Date.now();
      const rtt = end - start;
      this.timeDiff = serverTime.timestamp - (start + rtt/2);
    } catch (error) {
      console.error('Time sync failed:', error);
    }
  }

  getAdjustedTime() {
    return new Date(Date.now() + this.timeDiff).toISOString();
  }

  send(data) {
    const eventData = {
      ...data,
      clientTimestamp: new Date().toISOString(),
      serverTimestamp: this.getAdjustedTime()
    };
    super.send(eventData);
  }
}

4. 隐私合规问题

问题‌:需要遵守GDPR等隐私法规

解决方案‌:

  • 实现用户同意机制
  • 提供数据清除接口
  • 敏感信息脱敏处理
// 隐私合规实现
class PrivacyAwareTracker extends Tracker {
  constructor(options) {
    super(options);
    this.consentGiven = false;
    this.initConsent();
  }

  initConsent() {
    const consent = localStorage.getItem('tracking_consent');
    this.consentGiven = consent === 'true';
    
    if (consent === null) {
      // 显示同意弹窗
      this.showConsentDialog();
    }
  }

  showConsentDialog() {
    // 实际项目中这里会显示UI弹窗
    console.log('显示数据收集同意对话框');
    // 模拟用户同意
    this.setConsent(true);
  }

  setConsent(given) {
    this.consentGiven = given;
    localStorage.setItem('tracking_consent', given.toString());
  }

  send(data) {
    if (!this.consentGiven) return;
    
    // 脱敏处理
    const sanitizedData = {
      ...data,
      ipAddress: this.maskIP(data.ipAddress),
      userId: this.consentGiven ? data.userId : 'anonymous'
    };
    
    super.send(sanitizedData);
  }

  maskIP(ip) {
    if (!ip) return null;
    return ip.replace(/.\d+$/, '.0');
  }

  async clearUserData(userId) {
    // 调用后端API清除该用户数据
    try {
      const response = await fetch(`${this.serverUrl}/clear`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId })
      });
      return await response.json();
    } catch (error) {
      console.error('Clear data failed:', error);
      throw error;
    }
  }
}

四、高级实现建议

  1. 使用Web Worker‌:将数据发送逻辑放到Web Worker中,避免阻塞主线程
  2. 差异化采样‌:对高频事件进行采样,减少数据量
  3. 数据压缩‌:对批量数据进行压缩后再发送
  4. 多通道发送‌:优先使用fetch,失败后降级到img标签或sendBeacon
  5. A/B测试支持‌:集成A/B测试功能,记录实验分组信息
// 多通道发送实现
class MultiChannelTracker extends Tracker {
  send(data) {
    // 尝试使用fetch
    this.sendViaFetch(data).catch(() => {
      // fetch失败后尝试使用sendBeacon
      this.sendViaBeacon(data).catch(() => {
        // 最后降级到img标签
        this.sendViaImage(data);
      });
    });
  }

  sendViaFetch(data) {
    return fetch(this.serverUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    }).then(response => {
      if (!response.ok) throw new Error('Fetch failed');
    });
  }

  sendViaBeacon(data) {
    const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
    const success = navigator.sendBeacon(this.serverUrl, blob);
    if (!success) throw new Error('Beacon failed');
    return Promise.resolve();
  }

  sendViaImage(data) {
    return new Promise((resolve) => {
      const img = new Image();
      const params = new URLSearchParams();
      for (const key in data) {
        params.append(key, data[key]);
      }
      img.src = `${this.serverUrl}?${params.toString()}`;
      img.onload = resolve;
      img.onerror = resolve; // 即使失败也不抛出错误
    });
  }
}

实现一个健壮的代码埋点系统需要考虑多个方面:

  1. 数据可靠性‌:确保数据不丢失,实现本地存储和重试机制
  2. 性能影响‌:优化发送策略,减少对应用性能的影响
  3. 数据质量‌:保证数据准确性和一致性
  4. 隐私合规‌:遵守相关法律法规,提供用户控制选项
  5. 可扩展性‌:设计灵活的架构,便于添加新的事件类型和属性

实际项目中,可以根据具体需求选择开源解决方案(如Google Analytics、百度统计等)或自建埋点系统。自建系统虽然开发成本较高,但可以提供更大的灵活性和数据控制权。

Two Pointers

作者 tsreaper
2022年10月16日 12:07

解法:Two Pointers

原题 https://atcoder.jp/contests/abc247/tasks/abc247_e ,原题题解里还有一个容斥的解法也可以看看。

注意到定界子数组里一定不包含小于 minK 以及大于 maxK 的数。从原数组里把不符合要求的数都去掉后,原数组被分成好多个子数组,每个子数组里的元素都在 [minK, maxK] 之间。最终答案就是所有子数组的答案之和。接下来我们只考虑元素在 [minK, maxK] 之间的子数组。

由于 minK 是数组里的最小值,maxK 是数组里的最大值,定界子数组的定义可以变为:

子数组里出现过 minK,同时还出现过 maxK

这就是经典的 two pointers 问题。维护两个指针 ij,计算以 i 为结尾,且 minKmaxK 不同时出现的最长的子数组是多长即可。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    long long countSubarrays(vector<int>& A, int X, int Y) {
        int n = A.size();
        long long ans = 0;

        auto gao = [&](int L, int R) {
            int cntX = 0, cntY = 0;
            for (int i = L, j = L; i <= R; i++) {
                if (A[i] == X) cntX++;
                if (A[i] == Y) cntY++;
                // 计算以 i 为结尾,且 minK 和 maxK 没有同时出现的最长子数组
                while (j <= i && cntX > 0 && cntY > 0) {
                    if (A[j] == X) cntX--;
                    if (A[j] == Y) cntY--;
                    j++;
                }
                // 更新答案:以 i 为结尾的所有子数组数量,减去不符合要求的子数组数量
                ans += (i - L + 1) - (i - j + 1);
            }
        };

        int cnt = 0;
        for (int i = 0; i < n; i++) {
            // 将数组分成若干子数组求解,每个子数组的元素都在 [X, Y] 之间
            if (A[i] < X || A[i] > Y) gao(i - cnt, i - 1), cnt = 0;
            else cnt++;
        }
        gao(n - cnt, n - 1);
        return ans;
    }
};

另注

这种同时限制子数组最小最大值的问题有一种通用的计数方法,详见 https://codeforces.com/problemset/problem/1730/E 。

分析性质 + 一次遍历

作者 newhar
2022年10月16日 12:07

定界子数组满足性质:

  • 子数组不能包含越界的数字($nums[i] > maxK$ 或 $nums[i] < minK$);
  • 子数组必须同时包含 $maxK$ 和 $minK$。

根据上述条件,我们从左到右遍历数组,统计以 $i$ 为右端点的定界子数组数量:

  • 维护左侧第一个越界数字的位置 $l$,表示左端点不能等于或越过 $l$;
  • 同时,分别维护 $maxK$ 和 $minK$ 在左侧第一次出现的位置 $r_1$ 和 $r_2$,表示左端点必须在 $\min(r_1, r_2)$ 及其左侧,否则子数组中会缺少 $maxK$ 或 $minK$;
  • 因此,以 $i$ 为右边界的子数组数量(如果存在)= $\min(r_1, r_2) - l$。
class Solution:
    def countSubarrays(self, nums: List[int], minK: int, maxK: int) -> int:
        l, r1, r2, ret = -1, -1, -1, 0
        for i in range(len(nums)):
            if nums[i] > maxK or nums[i] < minK: l = i
            if nums[i] == maxK: r1 = i
            if nums[i] == minK: r2 = i
            ret += max(0, min(r1, r2) - l)
        return ret
class Solution {
public:
    long long countSubarrays(vector<int>& nums, int minK, int maxK) {
        int n = nums.size();
        long long ret = 0;
        for(int i = 0, l = -1, r1 = -1, r2 = -1; i < n; ++i) {
            if(nums[i] > maxK || nums[i] < minK) l = i;
            if(nums[i] == maxK) r1 = i;
            if(nums[i] == minK) r2 = i;
            ret += max(0, min(r1, r2) - l);
        }
        return ret;
    }
};

一次遍历,简洁写法(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2022年10月16日 12:06

从特殊到一般。首先考虑一个简单的情况,$\textit{nums}$ 的所有元素都在 $[\textit{minK},\textit{maxK}]$ 范围内。

在这种情况下,问题相当于:

  • 同时包含 $\textit{minK}$ 和 $\textit{maxK}$ 的子数组的个数。

核心思路:枚举子数组的右端点,统计有多少个合法的左端点。

遍历 $\textit{nums}$,记录 $\textit{minK}$ 最近出现的位置 $\textit{minI}$,以及 $\textit{maxK}$ 最近出现的位置 $\textit{maxI}$,当遍历到 $\textit{nums}[i]$ 时,如果 $\textit{minK}$ 和 $\textit{maxK}$ 都遇到过,则左端点在 $[0,\min(\textit{minI},\textit{maxI})]$ 中的子数组,包含 $\textit{minK}$ 和 $\textit{maxK}$,最小值一定是 $\textit{minK}$,最大值一定是 $\textit{maxK}$。

以 $i$ 为右端点的合法子数组的个数为

$$
\min(\textit{minI},\textit{maxI})+1
$$

回到原问题,由于子数组不能包含在 $[\textit{minK},\textit{maxK}]$ 范围之外的元素,我们需要额外记录在 $[\textit{minK},\textit{maxK}]$ 范围之外的最近元素位置,记作 $i_0$,则左端点在 $[i_0+1,\min(\textit{minI},\textit{maxI})]$ 中的子数组都是合法的。

以 $i$ 为右端点的合法子数组的个数为

$$
\min(\textit{minI},\textit{maxI})-i_0
$$

例如 $\textit{nums}=[1,4,3,4,2,2,3,3]$,如下图所示。

lc2444-c.png{:width=700}

代码实现时:

  • 可以初始化 $\textit{minI}=\textit{maxI}=i_0=-1$,兼容没有找到相应元素的情况。
  • 如果 $\min(\textit{minI},\textit{maxI})-i_0 < 0$,则表示在 $i_0$ 右侧 $\textit{minK}$ 和 $\textit{maxK}$ 没有同时出现,此时以 $i$ 为右端点的合法子数组的个数为 $0$。所以加到答案中的是 $\max(\min(\textit{minI},\textit{maxI})-i_0, 0)$。
class Solution:
    def countSubarrays(self, nums: List[int], minK: int, maxK: int) -> int:
        ans = 0
        min_i = max_i = i0 = -1
        for i, x in enumerate(nums):
            if x == minK:
                min_i = i  # 最近的 minK 位置
            if x == maxK:
                max_i = i  # 最近的 maxK 位置
            if not minK <= x <= maxK:
                i0 = i  # 子数组不能包含 nums[i0]
            ans += max(min(min_i, max_i) - i0, 0)
        return ans
class Solution:
    def countSubarrays(self, nums: List[int], minK: int, maxK: int) -> int:
        ans = 0
        min_i = max_i = i0 = -1
        for i, x in enumerate(nums):
            if x == minK:
                min_i = i
            if x == maxK:
                max_i = i
            if not minK <= x <= maxK:
                i0 = i
            j = min_i if min_i < max_i else max_i
            if j > i0:
                ans += j - i0
        return ans
class Solution {
    public long countSubarrays(int[] nums, int minK, int maxK) {
        long ans = 0;
        int minI = -1, maxI = -1, i0 = -1;
        for (int i = 0; i < nums.length; i++) {
            int x = nums[i];
            if (x == minK) {
                minI = i; // 最近的 minK 位置
            }
            if (x == maxK) {
                maxI = i; // 最近的 maxK 位置
            }
            if (x < minK || x > maxK) {
                i0 = i; // 子数组不能包含 nums[i0]
            }
            ans += Math.max(Math.min(minI, maxI) - i0, 0);
        }
        return ans;
    }
}
class Solution {
public:
    long long countSubarrays(vector<int>& nums, int minK, int maxK) {
        long long ans = 0;
        int min_i = -1, max_i = -1, i0 = -1;
        for (int i = 0; i < nums.size(); i++) {
            int x = nums[i];
            if (x == minK) {
                min_i = i; // 最近的 minK 位置
            }
            if (x == maxK) {
                max_i = i; // 最近的 maxK 位置
            }
            if (x < minK || x > maxK) {
                i0 = i; // 子数组不能包含 nums[i0]
            }
            ans += max(min(min_i, max_i) - i0, 0);
        }
        return ans;
    }
};
#define MIN(a, b) ((b) < (a) ? (b) : (a))
#define MAX(a, b) ((b) > (a) ? (b) : (a))

long long countSubarrays(int* nums, int numsSize, int minK, int maxK) {
    long long ans = 0;
    int min_i = -1, max_i = -1, i0 = -1;
    for (int i = 0; i < numsSize; i++) {
        int x = nums[i];
        if (x == minK) {
            min_i = i; // 最近的 minK 位置
        }
        if (x == maxK) {
            max_i = i; // 最近的 maxK 位置
        }
        if (x < minK || x > maxK) {
            i0 = i; // 子数组不能包含 nums[i0]
        }
        ans += MAX(MIN(min_i, max_i) - i0, 0);
    }
    return ans;
}
func countSubarrays(nums []int, minK, maxK int) (ans int64) {
    minI, maxI, i0 := -1, -1, -1
    for i, x := range nums {
        if x == minK {
            minI = i // 最近的 minK 位置
        }
        if x == maxK {
            maxI = i // 最近的 maxK 位置
        }
        if x < minK || x > maxK {
            i0 = i // 子数组不能包含 nums[i0]
        }
        ans += int64(max(min(minI, maxI)-i0, 0))
    }
    return
}
var countSubarrays = function(nums, minK, maxK) {
    let ans = 0, minI = -1, maxI = -1, i0 = -1;
    for (let i = 0; i < nums.length; i++) {
        const x = nums[i];
        if (x === minK) {
            minI = i; // 最近的 minK 位置
        }
        if (x === maxK) {
            maxI = i; // 最近的 maxK 位置
        }
        if (x < minK || x > maxK) {
            i0 = i; // 子数组不能包含 nums[i0]
        }
        ans += Math.max(Math.min(minI, maxI) - i0, 0);
    }
    return ans;
};
impl Solution {
    pub fn count_subarrays(nums: Vec<i32>, min_k: i32, max_k: i32) -> i64 {
        let mut ans = 0;
        let mut min_i = -1;
        let mut max_i = -1;
        let mut i0 = -1;
        for (i, x) in nums.into_iter().enumerate() {
            let i = i as i32;
            if x == min_k {
                min_i = i; // 最近的 min_k 位置
            }
            if x == max_k {
                max_i = i; // 最近的 max_k 位置
            }
            if x < min_k || x > max_k {
                i0 = i; // 子数组不能包含 nums[i0]
            }
            ans += 0.max(min_i.min(max_i) - i0) as i64;
        }
        ans
    }
}

复杂度分析

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

相似题目

分类题单

如何科学刷题?

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

我的题解精选(已分类)

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

昨天 — 2025年4月25日首页

Linux平台实现低延迟的RTSP、RTMP播放

2025年4月25日 22:02

在流媒体播放器的开发过程中,RTSP(实时流协议)和RTMP(实时消息协议)是广泛应用的流媒体协议。本博客将介绍如何使用大牛直播SDK实现一个Linux平台下的RTSP/RTMP播放器。大牛直播SDK的Linux平台播放SDK,支持RTSP/RTMP,功能丰富,性能优异,超低延迟,并能够在X11窗口中渲染视频。

依赖库与环境

  1. Linux环境:支持X11图形库,能够在X窗口系统中渲染视频。

  2. Smart Player SDK:这是一款功能强大的流媒体播放SDK,支持多种音视频格式。

  3. X11:作为Linux上的图形显示系统,X11用于渲染视频流。

系统初始化与SDK配置

首先,需要进行SDK的初始化,获取播放所需的API接口,并设置相关的回调函数以处理事件和视频帧。代码如下:

// 初始化SDK日志功能
void NT_SDKLogInit() {
    SmartLogAPI log_api;
    memset(&log_api, 0, sizeof(log_api));
    GetSmartLogAPI(&log_api);

    log_api.SetLevel(SL_INFO_LEVEL);
    log_api.SetPath((NT_PVOID)"./");
}

// 初始化播放器SDK
bool NT_PlayerSDKInit(SmartPlayerSDKAPI& player_api) {
    memset(&player_api, 0, sizeof(player_api));
    GetSmartPlayerSDKAPI(&player_api);
    auto ret = player_api.Init(0, nullptr);
    if (NT_ERC_OK != ret) {
        fprintf(stderr, "player_api.Init failed!\n");
        return false;
    }
    return true;
}

在此过程中,我们初始化了日志和SDK的相关API接口。

播放器配置与窗口创建

接下来,我们为播放器配置X11显示窗口,并将视频流渲染到窗口中。代码如下:

// 创建X11显示和窗口
auto display = XOpenDisplay(nullptr);
if (!display) {
    fprintf(stderr, "Cannot connect to X server\n");
    player_api.UnInit();
    return 0;
}

auto screen = DefaultScreen(display);
auto root = XRootWindow(display, screen);
XWindowAttributes root_win_att;
if (!XGetWindowAttributes(display, root, &root_win_att)) {
    fprintf(stderr, "Get Root window attri failed\n");
    player_api.UnInit();
    XCloseDisplay(display);
    return 0;
}

// 创建播放窗口
main_wid_ = XCreateSimpleWindow(display, root, 0, 0, root_win_att.width / 2, root_win_att.height / 2, 0, white_pixel, black_pixel);
XSelectInput(display, main_wid_, StructureNotifyMask | KeyPressMask);

// 创建子窗口用于渲染视频
auto sub_wid = CreateSubWindow(display, screen, main_wid_);

播放流的设置

接下来,我们将RTSP流地址传递给SDK,并启动播放。代码如下:

const char* player_url_ = "rtsp://your-stream-url";
NT_HANDLE handle = nullptr;

// 设置播放URL
player_api.SetURL(handle, player_url_);
player_api.SetRenderXWindow(handle, sub_wid);
player_api.StartPlay(handle);

此时,我们已经成功将RTSP流与窗口关联,并开始播放视频。

回调函数与事件处理

为了处理视频帧数据,我们设置了回调函数。当SDK收到视频帧时,系统将调用这个回调函数来进行处理。代码如下:

void NT_SDKVideoFrameCallBack(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 status, const NT_SP_VideoFrame* frame) {
    if (!frame) return;

    // 打印视频帧信息
    fprintf(stdout, "OnSDKVideoFrameCallBack handle:%p frame:%p, timestamp:%llu\n", handle, frame, frame->timestamp_);
    
    // 处理视频数据或渲染
}

除了视频帧,SDK还支持音频数据和事件的回调,开发者可以根据需要进行相应的处理。

错误与事件回调

SDK提供了多种事件回调接口来处理连接、播放状态、下载速度等信息。例如,我们可以设置事件回调来监控播放状态:

void NT_OnSDKEventHandle(NT_HANDLE handle, NT_PVOID user_data,
                          NT_UINT32 event_id, NT_INT64 param1, NT_INT64 param2,
                          NT_UINT64 param3, NT_PCSTR param4, NT_PCSTR param5,
                          NT_PVOID param6) {
    if (event_id == NT_SP_E_EVENT_ID_DOWNLOAD_SPEED) {
        fprintf(stdout, "OnSDKEventHandle handle:%p speed:%lldkbps\n", handle, (param1 * 8) / 1000);
    }
}

停止播放与资源清理

播放完成或用户按下退出键时,需要清理资源并关闭播放器。代码如下:

// 停止播放并关闭SDK
player_api.StopPlay(handle);
player_api.Close(handle);
XDestroyWindow(display, sub_wid);
XDestroyWindow(display, main_wid_);
XCloseDisplay(display);
player_api.UnInit();

功能支持

如不单独说明,系Windows、Linux(含x86_64|aarch64)、Android、iOS全平台支持。

  • [支持播放协议]高稳定、超低延迟(毫秒级,行业内几无效果接近的播放端)、业内领先的RTMP直播播放器SDK;
  • [多实例播放]支持多实例播放;
  • [事件回调]支持网络状态、buffer状态等回调;
  • [视频格式]支持RTMP扩展H.265和Enhanced RTMP H.265,H.264;
  • [音频格式]支持AAC/PCMA/PCMU/Speex;
  • [H.264/H.265软解码]支持H.264/H.265软解;
  • [H.264硬解码]Windows/Android/iOS支持特定机型H.264硬解;
  • [H.265硬解]Windows/Android/iOS支持特定机型H.265硬解;
  • [H.264/H.265硬解码]Android支持设置Surface模式硬解和普通模式硬解码;
  • [缓冲时间设置]支持buffer time设置;
  • [首屏秒开]支持首屏秒开模式;
  • [低延迟模式]支持低延迟模式设置(公网150~300ms);
  • [复杂网络处理]支持断网重连等各种网络环境自动适配;
  • [快速切换URL]支持播放过程中,快速切换其他URL,内容切换更快;
  • [音视频多种render机制]Android平台,视频:SurfaceView/GLSurfaceView,音频:AudioTrack/OpenSL ES;
  • [实时静音]支持播放过程中,实时静音/取消静音;
  • [实时音量调节]支持播放过程中实时调节音量;
  • [实时快照]支持播放过程中截取当前播放画面;
  • [只播关键帧]Windows平台支持实时设置是否只播放关键帧;
  • [渲染角度]支持0°,90°,180°和270°四个视频画面渲染角度设置;
  • [渲染镜像]支持水平反转、垂直反转模式设置;
  • [等比例缩放]支持图像等比例缩放绘制(Android设置surface模式硬解模式不支持);
  • [实时下载速度更新]支持当前下载速度实时回调(支持设置回调时间间隔);
  • [ARGB叠加]Windows平台支持ARGB图像叠加到显示视频(参看C++的DEMO);
  • [解码前视频数据回调]支持H.264/H.265数据回调;
  • [解码后视频数据回调]支持解码后YUV/RGB数据回调;
  • [解码后视频数据缩放回调]Windows平台支持指定回调图像大小的接口(可以对原视图像缩放后再回调到上层);
  • [解码前音频数据回调]支持AAC/PCMA/PCMU/SPEEX数据回调;
  • [音视频自适应]支持播放过程中,音视频信息改变后自适应;
  • [扩展录像功能]完美支持和录像SDK组合使用;
  • Linux平台支持x64_64架构、aarch64架构(需要glibc-2.21及以上版本的Linux系统, 需要libX11.so.6, 需要GLib–2.0, 需安装 libstdc++.so.6.0.21、GLIBCXX_3.4.21、 CXXABI_1.3.9);

总结

通过调用大牛直播SDK的播放模块,您可以轻松地在Linux平台上实现RTSP/RTMP播放器,并通过X11窗口渲染视频流。SDK提供了丰富的回调机制,允许开发者实时获取视频帧、音频数据和播放状态,为多种流媒体应用提供支持。

在此博客中,我们介绍了如何配置SDK、创建窗口、处理视频流并进行事件回调。如果您希望更深入地了解或有其他特定需求,可以参考SDK文档并根据您的应用场景进一步定制功能。以上抛砖引玉,感兴趣的开发者,可以单独跟我沟通讨论。

前端必知必会:JavaScript 对象与数组克隆的 7 种姿势,从浅入深一网打尽!

作者 鱼樱前端
2025年4月25日 21:31

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~~~坚持自己观点

写点笔记写点生活~写点经验。

在当前环境下,纯前端开发者可以通过技术深化、横向扩展、切入新兴领域以及产品化思维找到突破口。

为什么需要克隆?

JavaScript 中直接赋值对象或数组时,传递的是引用地址而非独立副本。修改克隆对象会影响原对象,导致难以追踪的 Bug。掌握正确的克隆姿势是前端开发的必备技能!


一、浅拷贝:仅复制第一层属性

适用场景:简单对象、无嵌套结构、无需修改子属性。

1. Object.assign()

const objClone = Object.assign({}, originalObj);
  • 原理:合并对象属性到新对象。
  • 缺点:嵌套对象仍是引用。
  • 示例
    objClone.name = '新名字';      // 不影响原对象
    objClone.friends.name = '改'; // 原对象的 friends 也会被修改!
    

2. 展开运算符 ...

const objClone = { ...originalObj };
const arrClone = [ ...originalArr ];
  • 特点:语法简洁,与 Object.assign() 等效。
  • 注意:同样无法处理嵌套结构。

3. 数组的 slice()

const arrClone = originalArr.slice();
  • 本质:截取数组全部元素生成新数组。
  • 局限:仅适用于数组,且不处理嵌套对象。

二、深拷贝:完全独立副本,断绝所有引用

适用场景:复杂对象、嵌套结构、需要完全隔离修改。

4. JSON.parse(JSON.stringify())

const deepClone = JSON.parse(JSON.stringify(originalObj));
  • 原理:通过 JSON 序列化反序列化生成新对象。
  • 致命缺陷
    • 丢失 undefinedSymbol、函数属性。
    • 无法处理循环引用(如 obj.self = obj)。
  • 适用场景:纯数据对象(无特殊类型)。

5. structuredClone()

const deepClone = structuredClone(originalObj);
  • 特点
    • 浏览器原生 API,支持 undefinedDateRegExp 等。
    • 可处理循环引用(部分浏览器支持)。
  • 注意:兼容性问题(IE 不支持),检查 Can I Use

6. 手动递归实现

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (hash.has(obj)) return hash.get(obj); // 解决循环引用
  const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash);
    }
  }
  return clone;
}
  • 优势:完全可控,可扩展处理特殊类型(如 DateMap)。
  • 优化点
    • 使用 WeakMap 解决循环引用。
    • 增加类型判断处理边界情况。

三、方案对比与选型指南

方法 深浅拷贝 处理循环引用 保留特殊类型 性能
Object.assign
JSON 序列化
structuredClone ✔️(部分) ✔️
手动递归 ✔️ ✔️(需扩展) 低(复杂对象)

四、实际开发中的坑与解决方案

  1. 循环引用报错
    • 使用 WeakMap 缓存已拷贝对象(参考手动实现代码)。
  2. 函数属性丢失
    • 手动实现中增加函数处理逻辑:
      if (typeof obj === 'function') return obj.bind({});
      
  3. 特殊对象处理(如 Date
    • 在递归中识别类型并重建:
      if (obj instanceof Date) return new Date(obj);
      

附上实践案例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>对象克隆</title>
</head>
<body>
  <script>
    // 定义一个对象
    const obj1 = {
      name: 'obj1',
      age: 18,
      sex: '男',
      friends: {
        name: 'obj2',
        age: 18,
        sex: '男'
      },
      address: undefined // 包含 undefined 的属性 
    }

    // 浅拷贝
    const obj3 = Object.assign({}, obj1);
    obj3.name = 'obj3';
    obj3.friends.name = 'obj4';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj3); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj1 === obj3); // false
    console.log(obj1.friends === obj3.friends); // true
    console.log(obj1.friends.name === obj3.friends.name); // true
    
    // 浅拷贝二 如果是数组可以使用 slice() 方法
    const obj4 = {...obj1};
    obj4.name = 'obj4';
    obj4.friends.name = 'obj5';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj4); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj1 === obj4); // false
    console.log(obj1.friends === obj4.friends); // true
    console.log(obj1.friends.name === obj4.friends.name); // true

    // 克隆对象 - 深度copy Lodash 的 _.cloneDeep() 方法
    // 但从性能和某些特殊对象的处理(例如包含函数、undefined、Symbol 或循环引用的对象 则不适用)角度来看,这种方法并不是最优的深拷贝实现方式
    const obj2 = JSON.parse(JSON.stringify(obj1));

    // 修改克隆对象
    obj2.name = 'obj2';
    obj2.friends.name = 'obj3';

    // 打印对象
    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj2); // {name: "obj2", age: 18, sex: "男", friends: {name: "obj3", age: 18, sex: "男"}}
    console.log(obj1 === obj2); // false
    console.log(obj1.friends === obj2.friends); // false
    console.log(obj1.friends.name === obj2.friends.name); // false

    // 某些现代浏览器支持使用 structuredClone 方法进行深拷贝
    const obj5 = structuredClone(obj1);
    obj5.name = 'obj5';
    obj5.friends.name = 'obj6';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj5); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj1 === obj5); // false
    console.log(obj1.friends === obj5.friends); // false
    console.log(obj1.friends.name === obj5.friends.name); // false

    // 手动实现是否深度拷贝 默认是浅拷贝
    function clone(obj, deep = false) {
      // 如果是基本类型或者 null 直接返回
      if (typeof obj !== 'object' || obj === null) {
        return obj;
      }
      // 如果是数组
      if (Array.isArray(obj)) {
        return deep ? obj.map(item => clone(item, deep)) : obj.slice();
      }
      // 如果是对象
      const result = {};
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          result[key] = deep ? clone(obj[key], deep) : obj[key];
        }
      }
      return result;
    }

    // 浅拷贝
    const obj6 = clone(obj1);
    obj6.name = 'obj6';
    obj6.friends.name = 'obj7';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj6,'手动实现浅拷贝'); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj1 === obj6); // false
    console.log(obj1.friends === obj6.friends); // true
    console.log(obj1.friends.name === obj6.friends.name); // true

    // 深拷贝
    const obj7 = clone(obj1, true);
    obj7.name = 'obj7';
    obj7.friends.name = 'obj8';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {…}, address: undefined}
    console.log(obj7,'手动实现深拷贝'); // {name: 'obj7', age: 18, sex: '男', friends: {name: 'obj8', age: 18, sex: '男'}, address: undefined}
    console.log(obj1 === obj7); // false
    console.log(obj1.friends === obj7.friends); // false
    console.log(obj1.friends.name === obj7.friends.name); // false
  </script>
</body>
</html>

五、总结

  • 简单场景:优先使用 ...Object.assign()
  • 数据克隆JSON 方法快捷但需注意类型丢失。
  • 生产环境:使用 structuredClone 或成熟工具库(如 Lodash 的 _.cloneDeep())。
  • 极致控制:手动实现深拷贝,按需扩展。

记住:没有完美的深拷贝方案,只有最适合当前场景的选择!

李彦宏:便宜已经不是DeepSeek的优势了

2025年4月25日 21:27

文|周鑫雨

编辑|苏建勋

如今,模型的发展已经成了应用开发者的一把双刃剑。一方面,模型能力的增强,给场景落地带来更多的可能;另一方面,应用的能力,也随时可能被模型能力本身淘汰。

什么样的应用不会过时?

2025年4月25日,百度Create大会上,百度集团创始人、董事长兼CEO李彦宏的答案是:“你只要找对场景,选对基础模型,有时候可能还要学一点调模型的方法,那么在这个基础上做出来的应用,它是不会过时的,应用才是真正创造价值的。”

在应用层,李彦宏认为2025年以来最令人激动的突破性的应用,是数字人和Agent。

卖数字人,是百度布局电商业务的重要一环。在会上,李彦宏发布了最新的数字人能力:高说服力数字人。除了更加拟人,“高说服力数字人”的特点,是可以感知直播间环境,并智能做出发红包、换品等决策。

百度在应用层的另一个布局重点,是Agent。

一年前,李彦宏曾表示,AI Coding是他最看好的应用方向。目前,百度在代码智能体领域已经做了较为完整的布局,有面向专业工程师的编程工具Comate,面向普通人的无代码编程工具“秒哒”。

而面向通用场景,前有Manus邀请码“一码难求”,百度也快马跟上,趁Manus收费之际,推出了一款移动端的Agent应用“心响”。

在模型层,DeepSeek是绕不过去的竞争对手。

“DeepSeek不是万能的。”李彦宏开门见山,“DeepSeek不支持多模态理解,有幻觉,更重要的是,慢和贵。”

对着DeepSeek的“短板”打,百度在Create发布了新模型:文心大模型4.5 Turbo和X1 Turbo,主打多模态,强推理,低成本。尤其是成本,4.5 Turbo的成本,是DeepSeek V3的40%,X1 Turbo的成本,是DeepSeek的25%。

最后,百度将自己的野心,也放到了AI应用生态的建立上。

一方面,百度搜索推出了开放平台,邀请应用开发者给予搜索生态开发AI应用;另一方面,百度对Anthropic发布的Agent协议MCP,进行了支持——这意味着,支持MCP的模型、外部工具和数据库之间,将能够畅通无阻的交互。

百度的“App版Manus”发布了

2025年3月6日发布的Agent应用Manus,又让AI Agent成为各家抢滩的应用高地。

4月22日,大会三天前,百度的首款独立Agent应用,已经上架了安卓应用商店。这款名为“心响”的App,形式可以简单理解为百度将Manus的移动端版本做了一遍。

用户只需在“心响”App中输入自己的需求,Agent就能对任务进行执行和交付。

“心响”制作绘本。来源:百度

此前,据百度智能体业务首席架构师、心响App负责人黄际洲介绍,“心响”的实现,背靠的是百度提出的Agent Use协议。此前,Anthropic提出的Agent协议MCP,针对的是工具的调用。

而“心响”采用的是对智能体的调用。根据用户的需求,“心响”的主智能体,能根据任务,调度第三方和百度自己的智能体,实现任务的执行和交付。

重建用户心智,在心响App产品经理黎宇昕看来,是百度做Agent产品时遇到的最大难点。

在媒体沟通会上,他提到,百度之前用搜索建立的用户心智,是及时交付。这意味着,AI应用“一定会折损效果,比如通过缓存的方式,去降模型调用的次数等等”——这也是市面上大多强调及时交付的Agent产品,无法实现高质交付的原因。

黎雨昕认为,“心响”要重新建立的,是托管心智。与Manus的任务可视面板类似,“心响”在任务执行过程中,也采用了分析流的形式,将任务执行的过程和时间,呈现给用户。

目前,心响已经支持包括200种类型的任务,覆盖了工作、学习、生活的主要场景,比如试题讲解、旅游、相亲、问诊、法律咨询。

黄际洲透露,未来,“心响”计划将支持的任务类型扩展到10万+以上。与此同时,“心响”PC端也正在研发中。

会画画的新推理模型,成本仅DeepSeek的25%

百度新发布的模型文心4.5 Turbo和X1 Turbo,相较于DeepSeek V3和R1的优势,除了整体性能的超越,主要在于多模态能力和低成本。

文心4.5 Turbo性能测评。来源:百度

文心X1 Turbo性能测评。来源:百度

其中,李彦宏强调了多模态理解的能力。他认为,多模态是未来基础模型的标配,“纯文本模型的市场会越来越小,多模态的市场会越来越大”。

文心4.5 Turbo和X1 Turbo,都对图像和视频理解进行了支持。

比如,输入一张高糊的球赛照片,文心4.5 Turbo能通过周边的广告牌、球员动作等元素,识别出这是1986年墨西哥的世界杯足球赛上,阿根廷与英格兰的决赛。

文心4.5 Turbo的图像理解能力。

除了多模态理解,两个模型也支持多模态生成。

比如,在文心X1 Turbo中输入“听说武汉有种东西叫‘蒜鸟’,请你把它画出来”,X1 Turbo就能根据联网搜索到的信息,生成蒜鸟的卡通形象。

至于价格,文心4.5Turbo价格仅为文心4.5的20%、DeepSeek V3的40%,每百万token的输入价格为0.8元,输出价格3.2元;X1 Turbo的价格仅为DeepSeek-R1的25%,每百万token输入价格1元,输出价格4元。

百度电商,做上游“卖水人”

AI重燃了百度对电商信心。

自2023年5月在百度App上线“百度优选”入口以来,百度对电商的定位,不是和淘宝、京东这些规模化的货架电商竞争。

百度副总裁、百度电商总经理平晓黎表示,百度电商有两个定位,一方面,是构成百度App服务的一环,满足搜索用户的消费需求;另一方面,是用智能工具服务,成为电商的上游“卖水人”。

数字人,就是百度电商卖的水。此次百度发布的“高说服力数字人”,除了在拟人、成本、风格等层面有所优化,最重要的是能够对直播间环境进行感知,并作出实时的互动,避免了传统数字人循环播放的尴尬局面。

比如,比如满50万人观看,就给观众发红包;根据直播间用户的问题,灵活调度PPT、切换素材。

“高说服力数字人”。来源:百度

实时互动背后的技术,是多智能体调度能力。据平晓黎介绍,高说服力数字人背后,有主播专家、运营专家、场控专家等多个角色智能体,能够根据直播间实时热度和转化情况,灵活调度。

AI月活超9700万的文库,想打模型组合拳

整合了百度网盘的文库,交出了半年答卷:付费用户数超4000万,月活超9700万。

在百度内部,百度文库是模型能力应用的尖子生。此前,百度副总裁、百度文库兼百度网盘负责人王颖告诉《智能涌现》,文库是最早自研MoE(混合专家模型)架构的AI应用。

当下,采用多模型组合的底座,已经成为AI应用的基操。李彦宏认为,应用对模型的组合使用,是普遍的现象,但如何组合、如何调用,仍然是一门技术活。

为此,百度文库和网盘推出了一个技术底座:沧舟OS。

沧舟OS。

为了实现不同模型对不同内容的理解和生成,这个底座,主要分成两层:

第一层,Chatfile Plus。其能够对不同模态、不同形态、不同格式的内容进行“向量化处理”,也就是将不同内容翻译成大模型能看得懂的向量化Token,在进行混合生成。

第二层,三库+三器,也就是“公域知识库、私域知识库,记忆库”,以及“编辑器、阅读器和播放器”。这一套系统,可以根据用户的需求,被大模型来组合调用。

基于这一套OS,百度网盘发布了新功能,AI笔记。

在百度文库看来,用户学习的一个痛点,是笔记内容与原始学习资料之间缺乏关联。比如用户根据笔记复习时,需要再花一番功夫,去寻找文本、视频、图片等资料。

AI笔记的核心功能,是时间溯源和多模态整理。比如,根据百度网盘中保存的视频讲解,AI笔记可以基于对内容的理解,梳理整个视频的逻辑结构和行文顺序,并生成思维导图。

导图中,每个知识点带的时间戳,都直接溯源到视频的相应节点。

百度网盘的“AI笔记”功能。

MCP,百度也接上了“AI万能插座”

MCP,是美国模型厂商Anthropic推出的Agent协议。

就像秦统一了货币,协议的作用,就是统一了软件之间的开发标准。支持MCP协议的软件之间,也可以更为灵活的适配、相互调用。比如,不少金融公司采用MCP,让AI更好地理解金融数据的上下文。

支持MCP,也成了厂商吸引更多第三方应用入驻、建立AI生态的一场“暗战”。比如,阿里云的AI开发平台“百炼”上线了MCP服务,腾讯云也宣布大模型知识引擎支持MCP协议。

在李彦宏看来,MCP就像给AI装上了一个万能插座,能够提高不同AI软件适配、开发、整合维护的效率。对于需要自由调用工具的Agent而言,MCP的出现尤为重要,这意味着Agent可以自由调用支持MCP的第三方工具。

目前,百度智能云大模型平台“千帆”兼容了MCP,百度搜索也构建了MCP Server的索引平台,文心快码、百度电商、地图、网盘、文库等应用,也通过MCP Server的形式,对外提供了能力。

欢迎交流!

永辉超市2024年营收676亿元,现已完成61家门店调改 | 业绩快报

2025年4月25日 21:25

4月25日,永辉超市发布了2024年年度及2025年第一季度财务报告。报告显示,公司2024年营收为676亿元,净利润为-14.65亿元。而2025年第一季度,永辉超市实现营业收入174.8亿元,净利润1.48亿元。

永辉超市在财报中指出,当期营业收入下滑主要是由于公司主动进行战略和经营模式转型,截至2025 年3月底公司共完成了 47 家门店的调改且调改门店迅速大幅提升客流和销售额;由于调改的门店数占比不大,且今年一季度较去年同期减少了 273 家门店(关闭尾部门店),使得公司2025 年1-3月整体收入有所下滑。

当期净利润下降主要系毛利率较上年同期下降1.35 个百分点,公司在门店调改过程中主动优化商品结构和采购模式,在淘汰旧品、引入新品的过程中主动推动裸价和控后台的策略,此过程中,公司整体毛利率受到了一定的影响。随着3 月底供应商大会的召开,未来公司选品及定价策略将发生重大改变,在此基础上,商品毛利率将得到进一步的恢复和增长。

2024年5月,在于东来及其团队的帮扶下,永辉超市通过胖东来帮扶及学习胖东来自主调改两种模式对全国门店进行调改。调改后的门店在口碑、品质、客流和销售额上均有较大增长。这让永辉超市坚定了走胖东来模式的品质零售路线。

据永辉超市透露,2025年第一季度,已完成调改的“稳态门店”,即开业满3个月的门店,成功实现稳定盈利。截至3月底,41家开业满3个月的“稳态调改店”累计实现净利润1470万元。

至财报发布日,永辉超市共完成了61家门店的胖东来模式调改,预计至2025年6月底,永辉超市全国调改门店将突破124家,2026年农历春节前目标锁定300家,随着调改门店占比提升、闭店收尾及供应链改革深化,未来12-18个月将是改革成果集中释放期。

3月29日,永辉超市2025年度全球供应商大会上,名创优品集团创始人、董事会主席兼首席执行官、永辉超市改革领导小组组长叶国富明确提出供应链升级策略,即“三个聚焦、一个反对”:聚焦核心供应商、聚焦核心大单品、聚焦长期主义,坚决反对换一个采购员就换一批供应商。

截至2025年4月22日,永辉超市已与超300家优质供应链企业进入采购洽谈阶段,包括供货山姆、开市客等美国超市的众多中国制造企业。

另外,财报显示,截至 2025 年 3 月 31 日,永辉超市线上业务营收 31.5 亿元,占公司营业收入的18.02%。报告期内,“永辉生活”APP 覆盖 670 家门店,实现销售额 17.5 亿元,日均单量23.1万单,月平均复购率为 47.6%;第三方平台到家业务覆盖 670 家门店,实现销售额14.0 亿元,日均单量 15.3 万单。

(36氪未来消费在持续关注永辉调改进展,欢迎加微信xhht100交流)

36氪晚报|腾讯理财通上线AI识图功能;爱芯元智发布M57芯片;IMF称亚洲可以通过降息来缓和关税对经济的冲击

2025年4月25日 21:21

大公司:

2025一季度天猫新入驻商家数量较去年同期增长126%

36氪获悉,天猫数据显示,2025一季度天猫新入驻商家数量较去年同期增长126%。其中,餐杯具、休闲零食、潮流玩具、手机配件、春夏服饰成为一季度开店数量最多的TOP5品类;广州、深圳、泉州、金华、杭州成开店数量最多TOP5城市。为支持新商家快速落地淘宝天猫,实现稳健增长,2025年天猫“蓝星计划”全面升级,加大育商,重点帮助新商家打造爆款。在即将到来的618,天猫还将面向新商家提供专属流量通道、专项会场等资源,全力支持新商家在618成交爆发。

转转集团与中国资源循环集团电子电器公司签署战略合作协议

36氪获悉,中国资源循环集团粤港澳大湾区展业推进会于深圳市举办,会上,中国资源循环集团电子电器公司揭牌成立,与转转集团等循环经济头部企业达成战略合作并签署合作协议。双方将建立全面的战略合作伙伴关系,充分发挥各自优势,拟在手机高质量回收等各个领域开展框架合作,共同推动中国废旧电子电器设备回收领域的高质量发展。

英国一季度零售销售增长1.6%,为四年来最大增幅

英国零售业周五报告了自2021年以来最好的一年开局,在成本上涨和贸易战日益打击消费者信心之际,这可能是对经济的短暂提振。英国国家统计局周五表示,3月份零售销售增长了0.4%,2月份的增幅被向下修正为0.7%。就第一季度整体而言,零售销售增长了1.6%,这是四年来最强劲的增长,并为该季度的整体经济产出提供了0.08个百分点的提振。(新浪财经)

德赛西威与高通深化合作,推出一系列组合驾驶辅助解决方案

在上海国际汽车工业展览会期间,德赛西威携手高通技术公司宣布深化在先进驾驶辅助(ADAS)领域的合作,共同打造一系列组合驾驶辅助解决方案。双方合作的重要成果之一是基于Snapdragon Ride™ Flex SoC(QAM8775P)打造的舱驾融合解决方案ICPS01E。这一解决方案已被多家汽车品牌应用,目前正在其重点车型上进行开发。

腾讯理财通上线AI识图功能

36氪获悉,腾讯理财通App正式推出AI识图掘金功能。当用户上传财经新闻截图、财经数据图表、自选的股票和基金列表等图片时,腾讯理财通利用腾讯混元大模型的多模态理解能力,识别出图片里关键的金融信息。金融信息在经过DeepSeek联网搜索与金融分析推理后,再关联动态金融数据,系统将生成行情深度解读与趋势预判,基金股票产品关键指标、投资表现、风险提示等建议。

爱芯元智发布M57芯片

36氪获悉,人工智能感知与边缘计算芯片企业爱芯元智(Axera)亮相2025上海国际车展,发布全球化战略及全新一代车载芯片产品M57系列,基于M57芯片的首个量产车型已定点开发,即将出海欧洲。

投融资:

“微至航空”完成近亿元Pre-A轮融资

36 氪获悉,大型通用无人运输机研发与制造商“微至航空”宣布完成近亿元Pre-A轮融资, 由 梁溪科创产业基金和招商局创投共同领投,金沙江联合资本跟投,所筹资金将用于研发总部和总装生产基地的能力加强、试验样机的各项测试,以及团队人员补充,浪潮资本担任本轮独家财务顾问。

安徽立泰消防技术服务有限公司完成1600万天使轮融资

36氪获悉,近日,安徽立泰消防技术服务有限公司完成1600万元人民币天使轮融资。据了解,安徽立泰消防技术服务有限公司是一家专注消防工程施工与设计一体化服务、消防技术咨询、系统维护、特殊场所消防解决方案等业务的企业。此轮融资将用于能源的开发、传输与配送、销售与服务以及技术研发与创新等方面。

新产品:

北京现代全新车型ELEXIO亮相

36氪获悉,近日,北京现代在上海举办品牌战略沟通会,并带来了全新纯电架构下的首款SUV车型——ELEXIO。未来,北京现代每年将推出2-3款新能源车型,覆盖纯电、混动、增程式技术路线,包括SUV、MPV、轿车等。到2027年前,北京现代将推出6款针对中国市场优化的新能源车型。

BYDFi 发布旗下全新 Web3 产品MoonX

36氪获悉,在巴黎区块链周(Paris Blockchain Week)上,BYDFi正式发布旗下全新Web3产品MoonX。据介绍,这是一款专为MemeCoin投资者打造的链上智能交易工具,集热点发现、风险筛选、聪明钱跟单与交易优化于一体。目前,MoonX已深度集成Solana与BNB Chain两大生态,覆盖包括Pump.fun、Raydium、PancakeSwap等主流流动性池。

智慧芽焕新发布AI Agent平台Eureka

4月25日,AI驱动的科技创新和知识产权信息服务商智慧芽发布“更懂技术创新的AI Agent平台Eureka”。据介绍,Eureka专注于为知识产权、研发、生物医药、材料、科创等技术创新场景,提供一系列高度专业化的AI Agent,首批上线近20个,包括查新检索、专利说明书撰写、技术方案探索、技术问答、生物医药百科问答、材料性能分析等。这些“专家型”AI智能体能真正理解用户需求,结合业务场景的工作流,自主拆解并精准完成复杂任务,有望解放70%以上生产力。

影石Insta360发布新一代旗舰8K全景相机X5

36氪获悉,4月22日,影石Insta360发布新一代旗舰8K全景相机X5。影石发布的数据显示,X5全景相机上市首日即登上全球多个平台单品销售榜单第一。国内登顶天猫、京东、抖音主流电商平台相机和运动相机单品销售额排行榜,也拿下亚马逊美国、日本、德国、英国等8个国家站点的单品销量第一。

今日观点:

中信证券首席经济学家明明:在兼顾内外政策环境的前提下,降准、降息或适时落地

4月25日召开的中央政治局会议指出,适时降准降息,保持流动性充裕,加力支持实体经济。中信证券首席经济学家明明认为,货币政策提出“适时降准降息”,支持消费、科技,结构性工具或有创新增量。“中央政治局会议对宏观政策定调‘更加积极有为’,货币政策则是延续了‘适度宽松’的基调。”明明分析说:具体到货币政策总量工具层面,会议要求“适时降准降息,保持流动性充裕,加力支持实体经济”,预计后续在兼顾内外政策环境的前提下,降准、降息工具或适时落地。(上证报)

贾跃亭被任命为FF联席CEO,称将拿出股权激励收益一半用于偿还国内债务

4月25日,在FFAI投资者社区与FX开发者共创日上,法拉第未来迎来重大人事变动,贾跃亭被正式任命为FF联席CEO,将与FF全球CEO Matthias Aydt共同主管FF最为关键的三条经营管理线,即财务、法务以及供应链。与此同时,贾跃亭在活动中承诺,待股权激励实现后,将拿出一半收益用于偿还在中国法律框架下的债务。(每经网)

IMF称亚洲可以通过降息来缓和关税对经济的冲击

国际货币基金组织(IMF)表示,亚洲央行总体上有降息空间来支持内需,并抵消全球贸易战持续的影响,地区状况比亚洲金融危机前要好得多。IMF亚太部负责人Krishna Srinivasan周五表示,地区通胀率与中央银行的目标范围相当甚至更低,这应该有利于进一步放松货币政策。 (新浪财经)

其他值得关注的新闻:

深圳市代表团调研考察华为、中兴、荣耀、创维、传音、迈瑞等企业海外分支机构

当地时间4月16日至23日,深圳市长覃伟中率深圳市代表团访问利比里亚、埃及,与当地政府、机构和企业开展交流洽谈。访问期间,深圳市代表团调研考察了华为、中兴、荣耀、创维、传音、迈瑞等企业海外分支机构,并与部分中资企业代表座谈交流。会上传递一个重要信号,深圳将加快构建更高水平开放型经济新体制,完善“走出去”综合服务体系,全力支持企业应对外部挑战、开拓多元市场,携手增强全球经济辐射力、实现更高质量发展。(深圳特区报)

成本优化+双品牌放量,科沃斯2024年净利增长逾三成

2025年4月25日 21:12
4月25日,科沃斯发布2024年年报,全年实现营收165.42亿元,同比增长6.71%;归母净利润8.06亿元,同比增长31.70%;产品创新叠加国补政策利好,公司旗下科沃斯和添可品牌四季度出货量分别同比增长47.5%和32.7%。科沃斯同步披露一季报显示,今年1-3月实现归母净利润4.75亿元,同比增长六成,环比去年四季度增长148.43%,经营现金流同比增长超10倍,延续去年四季度以来强劲的增长势头。
❌
❌