普通视图

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

企业微信5.0推出AI新能力

2025年8月20日 12:13
36氪获悉,8月20日,企业微信5.0正式发布。新版本全新推出了智能搜索、智能总结、智能机器人三大核心AI功能,帮助用户解决办公场景中方方面面的问题。企业微信5.0正式向客户推出智能表格功能,不仅能够自动跟踪任务进度、任务逾期提醒、自动生成可视化数据看板,同时还借助AI能力实现更多自动化智能玩法。此外,AI害可帮企业用户写邮件,能自动从用户有权限访问的聊天、文档以及会议中,找出相关信息、整合起来,总结为邮件内容。

JavaScript事件处理程序全揭秘:从HTML到IE的各种事件绑定方法!

作者 coding随想
2025年8月20日 12:11

在前端开发的江湖中,事件处理程序就像武林高手手中的秘籍,既能化解用户交互的千变万化,又能掌控浏览器行为的暗流涌动。然而,许多人对事件处理程序的认知仍停留在"点击按钮弹窗"的初级阶段。今天,我们将深入JavaScript事件世界的三大门派——HTML事件处理、DOM0/DOM2事件模型与IE特有机制,揭开它们背后的玄机。


一、HTML事件处理:初入江湖的入门秘籍

定义
HTML事件处理程序是将JavaScript代码直接写入HTML标签属性的方式,如onclick="alert('Hello')"。这种写法如同武林菜鸟的第一本剑谱,简单粗暴却暗藏杀机。

常见属性

  • onclick:点击触发(如按钮、链接)
  • onload:页面加载完成时触发
  • onsubmit:表单提交时触发
  • onmouseover/onmouseout:鼠标悬停/离开

使用技巧

<button onclick="handleClick()">点击我</button>
<script>
function handleClick() {
  console.log("按钮被点击!");
}
</script>

注意事项

  1. HTML与JS高度耦合:代码可维护性差,修改需要同时改动HTML和JS文件。
  2. 作用域陷阱:内联事件中的this指向当前DOM元素,而非预期的JS对象。
  3. 动态绑定失效:无法通过JS动态修改绑定函数。

应用场景

  • 快速原型开发
  • 简单交互需求(如弹窗提示)

二、DOM0事件处理:独孤九剑的锋芒

定义
DOM0事件处理是将事件处理函数直接赋值给DOM对象的属性,如element.onclick = function() {...}。这种写法如同独孤九剑的破招之术,简单高效却功能有限。

常见方法

const btn = document.getElementById("myButton");
btn.onclick = function() {
  console.log("DOM0方式触发");
};

特点

  1. 动态可变:可通过JS动态修改事件处理函数。
  2. 单函数限制:同一事件类型只能绑定一个处理函数(后绑定的会覆盖前一个)。
  3. 默认冒泡阶段:事件默认在冒泡阶段触发。

注意事项

  1. 覆盖风险:多次绑定时需手动保存旧函数。
  2. 兼容性优势:兼容IE6及更早版本,适合老旧项目维护。

应用场景

  • 对兼容性要求极高的项目
  • 简单的事件绑定需求

三、DOM2事件处理:九阳神功的境界

定义
DOM2事件模型通过addEventListenerremoveEventListener实现事件绑定,支持捕获/冒泡阶段控制。这种写法如同九阳神功,既能防御又能反击,是现代开发的首选。

核心方法

element.addEventListener(type, listener, options);
element.removeEventListener(type, listener, options);

参数详解

  • type:事件类型(如click
  • listener:事件处理函数
  • options:配置对象(可选):
    • capture:布尔值,控制事件阶段(捕获/冒泡)
    • once:布尔值,事件触发一次后自动移除
    • passive:布尔值,提升滚动性能(常用于scroll事件)

进阶用法

// 事件委托(性能优化利器)
document.getElementById("list").addEventListener("click", function(e) {
  if (e.target.classList.contains("item")) {
    console.log("点击了列表项");
  }
});

注意事项

  1. 事件委托:通过父元素监听子元素事件,减少内存占用。
  2. 选项配置:合理使用passive选项可显著提升滚动性能。
  3. 兼容性:IE9+支持,需注意IE8及以下版本兼容问题。

应用场景

  • 复杂的交互逻辑
  • 需要绑定多个事件处理函数的场景
  • 性能敏感的大型应用(如数据表格操作)

四、IE事件处理:独门绝技的残卷

定义
在IE8及以下版本中,使用attachEventdetachEvent实现事件绑定。这种写法如同江湖失传的残卷,如今已鲜有人问津,但在历史遗留项目中仍有用武之地。

核心方法

element.attachEvent("on" + type, listener);
element.detachEvent("on" + type, listener);

特点

  1. 事件对象获取:通过window.event访问事件对象。
  2. this指向:事件处理函数中的this指向window对象。
  3. 无捕获阶段:仅支持冒泡阶段。

兼容性代码示例

function addEvent(el, type, handler) {
  if (el.addEventListener) {
    el.addEventListener(type, handler);
  } else if (el.attachEvent) {
    el.attachEvent("on" + type, handler);
  }
}

注意事项

  1. 内存泄漏风险:IE中的闭包可能导致内存泄漏,需手动解绑。
  2. 事件对象差异:需统一处理事件对象(e || window.event)。

应用场景

  • 维护IE8及以下版本的老项目
  • 需要兼容老旧浏览器的企业级应用

五、实战技巧与避坑指南

1. 事件传播的玄机

事件传播分为三个阶段:捕获 → 目标 → 冒泡。通过设置addEventListenercapture参数,可以精准控制事件处理时机。例如:

// 捕获阶段处理
parent.addEventListener("click", handler, true);

2. 事件委托的妙用

通过父元素代理子元素事件,可减少事件绑定次数。例如:

document.getElementById("container").addEventListener("click", function(e) {
  if (e.target.matches(".delete-btn")) {
    e.target.parentElement.remove();
  }
});

3. 阻止默认行为的秘诀

// 标准方式
e.preventDefault();
// IE兼容方式
e.returnValue = false;

4. 移除事件的正确姿势

// 需传入相同的函数引用
element.removeEventListener("click", handler);
// 错误示例:匿名函数无法移除
element.addEventListener("click", () => {});

5. 事件对象的统一处理

const event = e || window.event; // 兼容IE
const target = e.target || e.srcElement; // 获取触发元素

六、总结:选择你的兵器库

事件处理方式 优点 缺点 适用场景
HTML内联事件 简单直观 耦合严重 快速原型开发
DOM0事件 动态可变 单函数限制 简单交互
DOM2事件 功能强大 兼容IE8需额外处理 现代开发首选
IE事件 老项目兼容 性能差 维护老旧项目

在江湖中游历,选择合适的事件处理方式如同选择趁手的兵器。DOM2事件模型无疑是现代开发的首选,但了解HTML内联和IE特有机制,能让你在面对历史遗留代码时游刃有余。记住,真正的高手,不仅能写出优雅的代码,更能看穿浏览器背后的玄机!

Vue 响应式原理

作者 索西
2025年8月20日 12:06

Vue 的响应式原理是其核心特性之一,核心目标是实现「数据驱动视图」—— 当数据发生变化时,依赖该数据的视图会自动更新,无需手动操作 DOM。这一机制在 Vue 2 和 Vue 3 中实现方式有较大差异,以下分别详细说明:

Vue 2 的响应式原理(基于 Object.defineProperty

Vue 2 采用 Object.defineProperty 对数据进行劫持,结合「发布 - 订阅模式」实现响应式,核心流程包括数据劫持依赖收集触发更新三个环节。

Vue 2 的响应式系统核心是通过「数据劫持」+「依赖收集」实现数据变化到视图更新的自动触发,整体流程可分为初始化、依赖收集、触发更新三个阶段。

初始化阶段: Vue 会对组件的 data 进行处理。(1)通过 Object.defineProperty 为每个属性添加 getset 拦截器,将普通对象转为响应式对象;(2)同时为每个属性创建对应的 Dep 实例(依赖管理器),用于存储依赖该属性的 Watcher

依赖收集阶段: 发生在数据首次被读取时。当组件初始化或 watch/计算属性触发时,会先实例化 Watcher(如渲染 Watcher 负责视图更新、watchWatcher 对应回调逻辑),并执行 Watcherget 方法——此时会将 Dep.target 设为当前 Watcher,再执行 getter 函数(如渲染函数读取 data 属性)。读取属性时触发 get 拦截器,Dep 会通过 Dep.target 识别当前活跃的 Watcher,将其添加到依赖列表中,完成“数据-Watcher”的关联。

触发更新阶段: 发生在数据被修改时。当属性值变化,会触发 set 拦截器,此时对应的 Dep 会遍历依赖列表中的所有 Watcher,调用其 update 方法;Watcher 会通过异步队列(避免频繁更新)触发最终操作,如渲染 Watcher 重新执行渲染函数更新视图,watchWatcher 执行用户定义的回调,从而实现数据变化后视图或逻辑的自动响应。

数据劫持:拦截数据的读写

Object.defineProperty 是 ES5 提供的 API,通过定义对象属性的 get(读取时触发)和 set(修改时触发)方法,实现对属性的拦截。Vue 2 会对 data 中的数据递归执行这一操作,使其成为「响应式数据」。

// 递归将对象转为响应式
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return; // 非对象类型无需处理
  }
  // 遍历对象属性,逐个劫持
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

// 劫持单个属性
function defineReactive(obj, key, val) {
  // 递归处理子对象(如 data 中的嵌套对象)
  observe(val);

  // 依赖管理器:收集依赖当前属性的订阅者
  const dep = new Dep();

  Object.defineProperty(obj, key, {
    get() {
      // 读取属性时,收集依赖(当前活跃的 Watcher)
      if (Dep.target) {
        dep.addSub(Dep.target); // 将 Watcher 加入依赖列表
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return; // 值未变化则不处理
      val = newVal;
      observe(newVal); // 新值如果是对象,需要递归劫持
      // 修改属性时,通知所有依赖更新
      dep.notify();
    }
  });
}

只要访问了响应式对象的属性,就会触发 get() 拦截器,无论这种访问是来自模板渲染、watchcomputed,还是手动代码。get() 的核心作用是在属性被访问时,记录 “谁在访问它”(即 Dep.target 指向的 Watcher ,从而建立 “数据→依赖” 的关联,为后续数据变化时的更新通知打下基础。

依赖收集:跟踪使用数据的地方

当响应式数据被读取时(如组件渲染、watch 监听),Vue 需要记录「谁在使用这个数据」(即「依赖」),这一过程称为依赖收集。核心通过 Dep(依赖管理器)和 Watcher(订阅者)实现:

Dep :每个响应式属性对应一个 Dep 实例,用于存储依赖该属性的所有 Watcher

class Dep {
  static target = null; // 静态属性,指向当前活跃的 Watcher
  subs = []; // 存储订阅者(Watcher)

  // 添加订阅者
  addSub(sub) {
    this.subs.push(sub);
  }

  // 通知所有订阅者更新
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

Watcher :组件的渲染逻辑、watch 选项、computed 属性等都会被包装成 Watcher。当依赖的数据变化时,Watcher 会触发更新(如重新渲染组件)。

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm; // 当前组件实例
    this.cb = cb; // 更新时执行的回调(如重新渲染)
    this.getter = expOrFn; // 依赖的表达式或渲染函数
    this.get(); // 初始化时触发 get,收集依赖
  }

  get() {
    Dep.target = this; // 标记当前活跃的 Watcher
    this.getter.call(this.vm); // 执行渲染函数,触发数据的 get 拦截
    Dep.target = null; // 重置,避免重复收集
  }

  // 数据变化时触发更新
  update() {
    this.cb(); // 如重新执行渲染函数
  }
}

依赖收集过程:当组件首次渲染时,会执行渲染函数,过程中会读取 data 中的属性,触发 get 拦截器。此时 Dep.target 指向当前组件的 WatcherDep 会将该 Watcher 加入订阅列表,完成依赖收集。

触发更新:数据变化时通知依赖

当修改响应式数据时,会触发 set 拦截器,Dep 会调用 notify() 方法,通知所有订阅的 Watcher 执行 update(),最终触发组件重新渲染或 watch 回调,实现「数据变 → 视图变」。

Vue 2 响应式的局限性

由于 Object.defineProperty 的设计限制,Vue 2 存在以下问题:

  • 无法监听对象新增 / 删除的属性:只能劫持初始化时已存在的属性,新增属性需通过 this.$set(obj, key, val) 手动触发响应式。
  • 无法监听数组的部分操作:数组的索引修改(如 arr[0] = 1)、length 变化不会触发 set,因此 Vue 2 重写了数组的 7 个方法(pushpopsplice 等)以支持响应式。
  • 深层对象递归劫持的性能问题:初始化时需递归劫持所有嵌套对象,数据结构复杂时可能影响性能。

Vue3

Vue 3 的响应式系统基于 Proxy 代理Effect 副作用机制实现,核心是建立“数据变化-副作用执行”的自动关联,流程可分为初始化、依赖收集、触发更新三个阶段。

初始化阶段: 通过 reactiveref 将数据转为响应式对象:reactive 针对对象/数组,使用 Proxy 代理整个对象,拦截 get(读取)、set(修改)、deleteProperty(删除)等操作;ref 针对基本类型,通过封装成带 value 属性的对象,内部同样用 Proxy 代理 value 的读写。同时,每个响应式对象的属性会对应关联的“依赖容器”,用于存储依赖该属性的副作用。

依赖收集阶段: 发生在副作用函数执行时。当通过 effect 注册副作用(如组件渲染函数、watch 回调),effect 会先将当前副作用标记为“活跃状态”,再执行副作用函数。函数执行中若读取响应式属性,会触发 Proxy 的 get 拦截器:拦截器会定位该属性对应的依赖容器,将活跃副作用添加到容器中,完成“数据-副作用”的关联。

触发更新阶段: 发生在数据被修改时。当修改响应式属性(如赋值、删除),会触发 Proxy 的 setdeleteProperty 拦截器:拦截器会先更新数据,再取出该属性依赖容器中的所有副作用,通过调度器(如控制执行时机、避免重复执行)触发副作用重新执行,从而实现数据变化后视图或逻辑的自动响应。 相比 Vue 2 的 Object.defineProperty,Proxy 能原生支持对象新增属性、数组索引/方法修改等场景,无需额外处理,响应式覆盖更全面。

cn.vuejs.org/guide/extra…

数据劫持:Proxy 拦截整个对象

Proxy 可以创建一个对象的代理,直接拦截对象的读取、修改、新增、删除等操作,无需逐个拦截属性,支持更全面的响应式劫持。

// 创建响应式对象
function reactive(target) {
  return new Proxy(target, {
    // 读取属性时触发(包括对象属性、数组索引、length 等)
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver); // 反射读取,确保正确的 this 指向
      // 收集依赖
      track(target, key);
      // 如果属性值是对象,递归创建代理(懒递归:访问时才处理,优化性能)
      if (typeof res === 'object' && res !== null) {
        return reactive(res);
      }
      return res;
    },

    // 修改/新增属性时触发
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver);
      if (oldValue !== value) {
        Reflect.set(target, key, value, receiver);
        // 触发更新
        trigger(target, key);
      }
      return true;
    },

    // 删除属性时触发
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) {
        // 触发更新
        trigger(target, key);
      }
      return result;
    }
  });
}

依赖收集与触发:tracktrigger

Vue 3 用 ReactiveEffect(替代 Vue 2 的 Watcher)管理依赖,核心逻辑更简洁。


track (收集依赖) :当读取响应式数据时,记录当前的 ReactiveEffect 与数据的关联。

// 存储依赖:target → key → 依赖集合
const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) return; // 没有活跃的 effect 则不收集
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(activeEffect); // 将当前活跃的 effect 加入依赖
}

trigger (触发更新) :当数据变化时,找到所有关联的 ReactiveEffect 并执行其更新逻辑。

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect.run()); // 执行所有依赖的更新
  }
}

ReactiveEffect :封装副作用函数(如渲染函数),当依赖变化时重新执行。

let activeEffect = null;

class ReactiveEffect {
  constructor(fn) {
    this.fn = fn; // 副作用函数(如组件渲染函数)
  }

  run() {
    activeEffect = this; // 标记当前活跃的 effect
    this.fn(); // 执行副作用函数(触发数据读取,进而收集依赖)
    activeEffect = null; // 重置
  }
}

对基本类型的支持:ref

Proxy 只能代理对象,对于基本类型(如 numberstring),Vue 3 提供 ref 包装。使用时通过 .value 访问,在模板中会自动解包(无需显式写 .value)。

// 模拟 Vue 内部的 ref 函数
function ref(value) {
  // 如果是对象,先转为 reactive(Proxy 代理)
  if (isObject(value)) {
    value = reactive(value);
  }

  // 创建 Ref 对象(通过 getter/setter 劫持 value 属性)
  const refObject = {
    get value() {
      track(refObject, 'value'); // 收集依赖
      return value;
    },
    set value(newValue) {
      // 如果新值是对象,也需要转为 reactive
      if (isObject(newValue)) {
        newValue = reactive(newValue);
      }
      value = newValue;
      trigger(refObject, 'value'); // 触发更新
    }
  };

  return refObject;
}

Vue 3 响应式的优势

  • 支持对象新增 / 删除属性Proxy 能拦截 set(新增)和 deleteProperty(删除)操作,无需手动调用 $set
  • 原生支持数组响应式:可监听数组索引修改、length 变化等,无需重写数组方法。
  • 懒递归劫持:嵌套对象只有在被访问时才会创建代理,初始化性能更优。
  • 支持复杂数据结构:如 MapSet 等,Proxy 可拦截其 setdelete 等操作。

恒指午间休盘跌0.57%,恒生科技指数跌1.26%

2025年8月20日 12:02
36氪获悉,恒指午间休盘跌0.57%,恒生科技指数跌1.26%;软件服务、半导体、媒体板块走弱,中手游跌超15%,知乎跌超7%,金山云、上海复旦跌超4%;零售、银行板块走强,周生生涨超19%,江西银行涨超1%;南向资金净流出8.76亿港元。

Vue3组件通信:父子相传

2025年8月20日 11:49

父传子

父传子是通过props(组件上自定义的attribute。子组件注册attribute,父组件给这些attribute赋值,子组件通过对应的attribute名称获取对应的值) 来完成组件之间的通信

Props

  1. 数组用法
  • 数组中的字符串就是attribute的名称
<template>
  <div class="card-container">
    <div class="card-header">
      <div class="card-title">{{ title }}</div>
      <div v-if="showBackBtn">aaa</div>
    </div>
    <div class="card-content">
      <slot></slot>
    </div>
  </div>
</template>

<script setup>
const props = defineProps(["title","showBackBtn"])
</script>
<template><ContentCard title="状态修改" :showBackBtn="true"> </ContentCard></template>

<script setup>
import ContentCard from '@/components/admin/ContentCard.vue'
</script>

2. 对象用法

  • 可以对传入的内容进行限制(指定传入的attribute的类型,是否必传,默认值)
<template>
  <div class="card-container">
    <div class="card-header">
      <div class="card-title">{{ title }}</div>
      <button v-if="showBackBtn" @click="handleBack" class="back-button">
        <el-icon :size="18"><Back /></el-icon>返回
      </button>
    </div>
    <div class="card-content">
      <slot></slot>
    </div>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'
import { Back } from '@element-plus/icons-vue'

const { title, showBackBtn } = defineProps({
  title: {
    type: String,
    required: true,
  },
  showBackBtn: {
    type: Boolean,
    default: false,
  },
})

const router = useRouter()
const handleBack = () => router.go(-1)
</script>
<template><ContentCard title="状态修改"> </ContentCard></template>

<script setup>
import ContentCard from '@/components/admin/ContentCard.vue'
</script>

非Prop的Attribute

传递给子组件的某些个属性,这些属性不在子组件的props和emits里面

常见class、style、id

  1. 若组件有单个根节点,非prop的attribute会自动添加到根节点的attribute中(继承)

  1. 禁用attribute继承和多个根节点情况
  • 组件中设置inheritAttrs:false可以禁用根组件自动attribute继承,我们可以在子组件中使用$attrs来访问非Prop的Attribute
<template>
  <div class="card-container">
    <div class="card-header">
      <div class="card-title">{{ title }}</div>
      <div v-if="showBackBtn">aaa</div>
    </div>
    <div :class="$attrs.class">
      <slot></slot>
    </div>
  </div>
</template>

<script setup>
const props = defineProps(["title","showBackBtn"])
</script>
  • 若组件有多个根节点且没有显示绑定,此时会报警告,需手动绑定
<template>
    <div class="card-header">
      <div class="card-title">{{ title }}</div>
      <div v-if="showBackBtn">aaa</div>
    </div>
    <div :class="$attrs.class">
      <slot></slot>
    </div>
</template>

<script setup>
const props = defineProps(["title","showBackBtn"])
</script>

子传父

  1. 子组件defineEmits()定义可以触发并且被父组件监听的事件
  2. 父组件@+子组件定义的事件名监听该事件
  3. 当子组件使用emit()触发某事件时,父组件监听到该事件被触发,便执行父组件里该事件对应的逻辑
<template>
  <div class="child">
    <h3>子组件</h3>
    <button @click="sendData">向父组件传递数据</button>
  </div>
</template>

<script setup>
import { defineEmits } from 'vue'
// 定义可以触发的事件
const emit = defineEmits(['send-message'])
// 子组件发送数据的方法
const sendData = () => {
  // 可以是任意类型的数据:字符串、数字、对象等
  const message = "Hello 父组件,这是子组件发送的数据!"
  const count = 100
  
  // 通过emit触发事件,传递数据给父组件
  emit('send-message', message, count)
}
</script>
<template>
  <div class="parent">
    <h2>父组件</h2>
    <p>子组件传递的消息:{{ receivedMessage }}</p>
    <p>子组件传递的数字:{{ receivedCount }}</p>
    <!-- 引入子组件并监听事件 -->
    <ChildComponent @send-message="handleMessage" />
  </div>
</template>

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

// 父组件用于存储接收的数据
const receivedMessage = ref('')
const receivedCount = ref(0)

// 处理子组件传递过来的数据
const handleMessage = (msg, count) => {
  receivedMessage.value = msg
  receivedCount.value = count
}
</script>

Tips

  • 父传子时,当需要传递 非字符串数据(如布尔值、数字、对象、数组等)时,必须加 :,只有传递纯字符串时可以省略
  • 在Vue 3的<script setup>语法中,当我们使用defineProps定义props后,Vue会自动将props解构到当前作用域中,所以我们可以直接使用title而不是props.title
  • HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名
<template><ContentCard title="状态修改" :show-back-btn="ture"> </ContentCard></template>

<script setup>
import ContentCard from '@/components/admin/ContentCard.vue'
</script>

泡泡玛特文德一:将布局中东、南亚、中欧、中南美等新兴市场,预计年底海外门店将超200家

2025年8月20日 11:40
36氪获悉,在今日泡泡玛特召开2025中期业绩发布会,联席首席运营官文德一表示,将聚焦中东、南亚、中南美及俄罗斯等国家和地区,同时继续在法国巴黎、澳大利亚悉尼、意大利米兰、美国纽约等核心城市推进旗舰店及旅游零售门店的布局,预计年底海外会超过200家门店。

Vue3 特性标志

2025年8月20日 11:38

Vue3 特性标志完全指南

Vue3 通过特性标志(Feature Flags)为开发者提供了精细的构建控制能力,让你可以根据项目需求定制 Vue 的功能集,实现更小的包体积和更好的性能。

🎯 什么是特性标志

特性标志是 Vue3 引入的构建时配置选项,允许开发者在编译阶段控制哪些功能被包含在最终的构建产物中。通过合理配置特性标志,可以:

  • 减少包体积:移除不需要的功能
  • 提升性能:避免运行时检查
  • 向后兼容:控制兼容性选项
  • 开发调试:配置开发工具支持
vue3源码中的特性标志位置

image.png

🔧 核心特性标志详解

1. __VUE_OPTIONS_API__

作用:控制是否支持 Vue2 风格的选项式 API

// vite.config.js
export default {
  define: {
    __VUE_OPTIONS_API__: true, // 默认: true
  },
};

使用场景

// 当设置为 true 时,支持选项式 API
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

// 当设置为 false 时,只支持组合式 API
import { ref } from 'vue'
export default {
  setup() {
    const count = ref(0)
    const increment = () => count.value++
    return { count, increment }
  }
}

优化建议

  • 新项目完全使用组合式 API 时,设为 false 可减少约 10-15% 的包体积
  • 迁移项目或需要兼容老代码时保持 true

2. __VUE_PROD_DEVTOOLS__

作用:控制生产环境是否支持 Vue DevTools

// vite.config.js
export default {
  define: {
    __VUE_PROD_DEVTOOLS__: false, // 默认: false
  },
};

应用场景

// 生产环境调试场景
if (
  process.env.NODE_ENV === "production" &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__
) {
  // 可以在生产环境使用 DevTools
  console.log("DevTools 可用于生产环境调试");
}

配置策略

  • 开发/测试环境:设为 true,便于调试
  • 生产环境:设为 false,减少安全风险和包体积

3. __VUE_PROD_HYDRATION_MISMATCH_DETAILS__

作用:控制生产环境 SSR 水合错误的详细信息

// vite.config.js
export default {
  define: {
    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true, // 默认: false
  },
};

SSR 应用示例

// 服务端渲染组件
export default {
  setup() {
    const serverData = ref(null);

    onMounted(() => {
      // 客户端数据可能与服务端不同
      serverData.value = getClientData();
    });

    return { serverData };
  },
};

最佳实践

  • 开发阶段:设为 true,快速定位水合问题
  • 生产环境:根据调试需求决定,通常设为 false

4. __FEATURE_SUSPENSE__

作用:控制 Suspense 功能的支持

// vue3内部硬编码true,无法修改

Suspense 使用示例

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from "vue";

const AsyncComponent = defineAsyncComponent(() =>
  import("./HeavyComponent.vue")
);
</script>

适用场景

  • 异步组件加载
  • 数据获取等待
  • 代码分割优化

5. __COMPAT__

作用:控制 Vue2 兼容模式

// vite.config.js
export default {
  define: {
    __COMPAT__: true, // 默认: false
  },
};

兼容模式配置

// main.js
import { createApp } from "vue";
import { configureCompat } from "vue";

// 全局兼容配置
configureCompat({
  // 启用特定的兼容性功能
  GLOBAL_MOUNT: false,
  GLOBAL_EXTEND: false,
  CONFIG_WHITESPACE: false,
});

const app = createApp({});

迁移策略

// 组件级别的兼容配置
export default {
  name: "LegacyComponent",
  compatConfig: {
    MODE: 2, // Vue2 模式
    GLOBAL_MOUNT: false,
    PROPS_DEFAULT_THIS: false,
  },
  // Vue2 风格的代码...
};

📊 性能优化效果

包体积对比

// 功能配置对包体积的影响(gzipped)
const bundleSizes = {
  全功能: "34.2 KB",
  禁用选项式API: "29.8 KB (-12.9%)",
  禁用DevTools: "33.1 KB (-3.2%)",
  禁用兼容模式: "32.6 KB (-4.7%)",
  精简配置: "26.4 KB (-22.8%)",
};

运行时性能

// 特性标志对运行时性能的影响
const performanceMetrics = {
  选项式API检查: "减少运行时判断,提升 5-10% 性能",
  DevTools钩子: "移除调试代码,减少内存占用",
  兼容性检查: "避免向下兼容开销,提升启动速度",
};

🎉 总结

Vue3 特性标志提供了强大的构建时优化能力:

包体积优化:通过禁用不需要的功能减少 15-25% 的包体积
性能提升:移除运行时检查,提升 5-10% 的执行性能
渐进迁移:兼容模式支持平滑的 Vue2 到 Vue3 迁移
开发体验:灵活的调试和开发工具配置
定制化构建:根据项目需求精确控制功能集

掌握这些特性标志,让你能够构建出更加轻量、高效的 Vue3 应用!


如果你觉得这篇文章有用,别忘了点赞 👍 和收藏 ⭐!

想了解更多 Vue3 优化技巧?关注我获取最新的前端技术分享 🚀

实现一个时间轴组件

作者 Sky_Ax
2025年8月20日 11:33

本文讲述了一个时间轴实现的核心原理,搞懂这个后,可以在此基础上开发更多功能。

效果展示:

time-bar.png

功能点:

  1. 分钟级高精度(支持"HH:MM"格式)
  2. 状态条基于百分比的精确定位
  3. 刻度线和时间标签自适应容器宽度

功能拆解:

时间轴本身就是一个白色背景的div,刻度线通过flex实现均分,五条刻度线为一个小时。核心点在于计算偏移量和宽度,先把时间转化为分钟如:"1:30" → 1 × 60 + 30 = 90 分钟,一天总分钟数:24 * 60 = 1440分钟。

偏移计算:开始时间在一天中的比例 × 100% = 左偏移百分比。 6:00:(360 ÷ 1440) × 100% = 25%

宽度计算:会议持续时间在一天中的比例 × 100% = 宽度百分比 6:00-7:30: (90 ÷ 1440) × 100% = 6.25%

具体实现:

基本结构:

<!-- 时间轴 -->
<div class="meeting-time">
  <div class="time-container">
    <!-- 选中的时间端 -->
    <div class="time-line">
      <div v-for="(meeting, index) in meetings" :key="index" class="status-bar" :style="getStatusBarStyle(meeting)"
        :class="meeting.status"></div>
    </div>
    <div class="time-scale">
      <div v-for="hour in 24" :key="hour" class="hour-mark">
        <!-- 刻度线 -->
        <div class="tick-line first"></div>
        <div class="tick-line"></div>
        <div class="tick-line"></div>
        <div class="tick-line"></div>
        <div class="tick-line last"></div>
        <!-- 时间 -->
        <span class="time-text">{{ hour - 1 }}:00</span>
        <span v-if="hour === 24" class="time-text time-right">00:00</span>
      </div>
    </div>
  </div>
</div>

定位实现:

methods: {
    // 将时间字符串转换为分钟数 (如 "14:30" -> 870分钟)
    timeToMinutes(timeStr) {
        const [hours, minutes] = timeStr.split(':').map(Number);
        return hours * 60 + minutes;
    },

    // 计算状态条的样式
    getStatusBarStyle(meeting) {
        const { startTime, endTime } = meeting;

        const startMinutes = this.timeToMinutes(startTime);
        const endMinutes = this.timeToMinutes(endTime);

        // 一天总共1440分钟
        const totalMinutesInDay = 24 * 60;

        // 计算左边距百分比:开始分钟数 / 总分钟数 * 100%
        const left = (startMinutes / totalMinutesInDay) * 100;
        // 计算宽度百分比:(结束分钟数 - 开始分钟数) / 总分钟数 * 100%
        const width = ((endMinutes - startMinutes) / totalMinutesInDay) * 100;

        return {
                left: `${left}%`,
                width: `${width}%`
        };
    }
},

样式部分:

.meeting-time {
  height: 156px;
  margin-top: 60px;
  position: relative;

  .time-container {
    height: 100%;
    padding: 0 30px;
    background: var(--base-bg-color);
    box-shadow: var(--bg-shadow);
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .time-scale {
    width: 100%;
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
    font-size: 18px;
    color: #666;

    .hour-mark {
      flex: 1;
      text-align: center;
      position: relative;
      display: flex;
      justify-content: space-between;

      .tick-line {
        width: 1px;
        height: 8px;
        background: #ccc;
        margin-bottom: 2px;

        &.first {
          height: 15px;
          width: 2px;
        }

        &.last {
          height: 15px;
        }

      }

      .time-text {
        position: absolute;
        left: -20px;
        top: 15px;
        font-size: 18px;
        font-weight: bold;

        &.time-right {
          left: auto;
          right: -20px;
        }
      }
    }
  }

  .time-line {
    width: 100%;
    height: 14px;
    background: #fff;
    position: relative;
    display: flex;
    overflow: hidden;

    .status-bar {
      position: absolute;
      top: 0;
      height: 100%;
      border-radius: 2px;

      // 不同状态的颜色
      &.finished {
        background: #999; // 已结束 - 灰色
      }

      &.ongoing {
        background: var(--primary-bg-color); // 进行中 - 蓝色
      }

      &.reserved {
        background: #ccc; // 已预约 - 浅灰色
      }

      &.idle {
        background: #fff; // 空闲中 - 白色
        border: 1px solid #ddd;
      }
    }
  }
}

数据部分:

data() {
    return {
        meetings: [
            {
                startTime: '1:15',
                endTime: '2:30',
                status: 'ongoing'   // 进行中
            },
            {
                startTime: '9:15',
                endTime: '18:45',
                status: 'finished'  // 已结束
            },
            {
                startTime: '23:00',
                endTime: '23:59',
                status: 'reserved'  // 已预约
            }
        ]
    }
},

★感谢您看到最后★

字节前端三面复盘:基础不花哨,代码要扎实(含高频题解)

2025年8月20日 11:32

本篇来自于:前端周刊-面试资源,加群交流

👤 作者:咸鱼翻身

关键词:nextTick、懒加载、trimPromise.all、事件循环、岛屿数量、边界合并/溢出、拼手气红包、定时输出、64马8道、SQL分组取Top、动态规划(最大体力)、TCP可靠与拥塞、B+树、进程调度


一面(JS/浏览器 & 基础编码)

1)Vue.nextTick 的实现原理(Vue2/3 通吃的本质)

  • 本质:把回调压进微任务队列,等本轮 DOM 变更“flush”后再执行,保证你拿到的是真实更新后的 DOM。

  • 关键点:

    • 微任务优先级高于宏任务:Promise.then > setTimeout

    • Vue2:用 Promise / MutationObserver / setImmediate / setTimeout 多重降级;维护 callbacks 队列 + pending 标志,统一 flushCallbacks

    • Vue3:统一调度器(scheduler)里有 queueJob/queueFlushnextTick 实际是 Promise.resolve().then(flushJobs)

// 极简 polyfill 示意(核心思路)
const callbacks = [];
let pending = false;
function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice();
  callbacks.length = 0;
  for (const cb of copies) cb();
}
export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    Promise.resolve().then(flushCallbacks); // 微任务
  }
}

2)图片懒加载(两种实现)

方案A:IntersectionObserver(推荐)

<img data-src="https://example.com/pic.jpg" alt="demo" />
<script>
const io = new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      const img = e.target;
      img.src = img.dataset.src;
      io.unobserve(img);
    }
  });
}, { rootMargin: '200px' }); // 提前预加载

document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
</script>

方案B:滚动监听 + 节流(兼容兜底):监听 scroll,计算 getBoundingClientRect(),结合节流避免频繁触发。


3)实现 trim

function myTrim(str) {
  // 简洁可靠:去掉首尾空白(含各种空白符)
  return str.replace(/^\s+|\s+$/g, '');
}
// 或者手写指针法,避免正则
function myTrim2(str) {
  let l = 0, r = str.length - 1;
  while (l <= r && /\s/.test(str[l])) l++;
  while (r >= l && /\s/.test(str[r])) r--;
  return str.slice(l, r + 1);
}

4)实现 Promise.all

function myPromiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const arr = Array.from(iterable);
    const res = new Array(arr.length);
    let done = 0;
    if (arr.length === 0) return resolve([]);
    arr.forEach((p, i) => {
      Promise.resolve(p).then(
        v => {
          res[i] = v;
          if (++done === arr.length) resolve(res);
        },
        err => reject(err)
      );
    });
  });
}

5)事件循环“看输出”

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
});
console.log('script end');

输出顺序:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

解释要点:

  • 同步:script startsetTimeout入宏任务 → async1 执行同步部分 → promise1script end

  • 微任务队列:await 的后续(async1 end)与 thenpromise2

  • 宏任务队列:setTimeout

小抄:同步 → 微任务 → 宏任务await 把后续逻辑塞进微任务。


6)编程题:岛屿数量(DFS/BFS/并查集)

function numIslands(grid: string[][]): number {
  if (!grid.length) return 0;
  const m = grid.length, n = grid[0].length;
  let count = 0;
  const dirs = [[1,0],[-1,0],[0,1],[0,-1]];

  const dfs = (i: number, j: number) => {
    if (i<0||j<0||i>=m||j>=n||grid[i][j]!=='1') return;
    grid[i][j] = '0';
    for (const [dx,dy] of dirs) dfs(i+dx, j+dy);
  };

  for (let i=0;i<m;i++) for (let j=0;j<n;j++) {
    if (grid[i][j] === '1') { count++; dfs(i,j); }
  }
  return count;
}

一面复盘:题目基础但要求准确。写代码时把边界条件说清楚,按测试思路讲解(空输入、最小规模、复杂连通块)。心态比什么都重要。


二面(CSS/工程 & 思维题 + 实战编码)

1)“边界合并”和“边界溢出”

  • 边界合并(Margin Collapsing):垂直方向上父子/兄弟的外边距会折叠为更大者

    • 断开折叠:给父元素创建 BFCoverflow: auto|hiddendisplay: flow-rootposition: absolute/fixedfloat)、或给父加 padding/border
  • 边界溢出(Overflow):内容超过盒子产生滚动/裁剪;注意 overflow: hidden 同时会创建BFC,影响 margin 折叠与清浮动。

口诀:flow-root 断折叠overflow 慎用(既裁剪又建BFC)。


2)拼手气“红包”算法(单位:分;每人至少1分)

二倍均值法(简单公平、无偏易实现):

function splitLucky(totalCents, n) {
  if (n <= 0 || totalCents < n) throw new Error('非法参数');
  const res = [];
  let remain = totalCents, remainN = n;

  for (let i = 0; i < n - 1; i++) {
    // 每次在 [1, 2*平均-1] 间随机(保证至少留1分给后面每人)
    const max = Math.floor((remain / remainN) * 2) - 1;
    const cur = Math.max(1, Math.floor(Math.random() * max) + 1);
    res.push(cur);
    remain -= cur;
    remainN--;
  }
  res.push(remain); // 最后一个兜底
  return res;
}

工程要点:都用整数分;前端展示再转元;必要时可加上下限偏度控制(例如不希望极端大红包)。


3)每隔一秒输出一个数(用 setTimeout,不用 setInterval

function print1ToN(n) {
  let i = 1;
  function tick() {
    if (i > n) return;
    console.log(i++);
    setTimeout(tick, 1000);
  }
  setTimeout(tick, 1000);
}

面试常问点:为什么链式 setTimeoutsetInterval 更稳?——可控漂移、避免任务堆积。


4)智力题:64匹马、8赛道,最少场次找最快的4匹

  • 思路扩展自“25匹马5赛道求前三”。

  • 步骤: 1)分组比赛:8组×8匹 → 8场,得到每组名次; 2)冠军赛:8组第一再赛一场 → 第9场,得到组排名 A>B>C>D>E>F>G>H; 3)候选仅可能来自前4组:A(前4名)、B(前3)、C(前2)、D(前1) —— 共 10 匹(其余不可能进前4)。 4)由于赛道只有8条,再加两场筛选即可,总计 最少 11 场

  • 说明:第10场先赛 8 匹(如 A2,A3,A4,B1,B2,B3,C1,C2),第11场把边缘选手(如 D1 与第10场的第2~5名)再比较,结合已知组内/冠军赛相对次序即可判定前4。

结论:最少 11 场。难点在剪枝与必须比较的最小集合推导。

二面复盘:面试官会引导,核心是你能否把不确定问题拆成可验证的子结论(谁必然无缘前4、谁还需比较)。代码题强调边界、复杂度、可测试性


三面(全栈/系统 + 算法 & 数据库/网络/OS)

1)线程安全是什么?

  • 定义:多线程并发访问共享数据时,程序的行为可预测不出现竞态(数据竞争、可见性、原子性问题)。

  • 手段:锁(互斥量/读写锁)、原子操作、线程局部存储、无锁结构(CAS)、不变对象、消息传递等。


2)SQL:按部门取工资最高的员工

窗口函数版(推荐)

SELECT *
FROM (
  SELECT e.*, 
         ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
  FROM employees e
) t
WHERE rn = 1;

聚合连接版

SELECT e.*
FROM employees e
JOIN (
  SELECT department_id, MAX(salary) AS max_salary
  FROM employees
  GROUP BY department_id
) m ON e.department_id = m.department_id AND e.salary = m.max_salary;

3)滑窗:给定字符集 [a,b,c,d],在 tbcacbdata 中找长度为4、恰好包含这4个字符(顺序无关)的连续子串起始下标

function findAnagramPos(s: string, keys: string[]): number {
  const need = new Map<string, number>();
  for (const ch of keys) need.set(ch, (need.get(ch) || 0) + 1);

  const win = new Map<string, number>();
  let valid = 0, left = 0, right = 0;
  const reqKinds = need.size, L = keys.length;

  while (right < s.length) {
    const c = s[right++];
    if (need.has(c)) {
      win.set(c, (win.get(c) || 0) + 1);
      if (win.get(c) === need.get(c)) valid++;
    }
    while (right - left >= L) { // 固定窗口长度
      if (valid === reqKinds) return left;
      const d = s[left++];
      if (need.has(d)) {
        if (win.get(d) === need.get(d)) valid--;
        win.set(d, win.get(d)! - 1);
      }
    }
  }
  return -1;
}
// 例:findAnagramPos('tbcacbdata', ['a','b','c','d']) -> 3(子串 'acbd')

4)动态规划:100格跳跃 + 蘑菇增减体力,初始体力 m,跳距离消耗=距离,问能否到终点并最大剩余体力

  • 建模:格子 0..100,已知每格蘑菇能量 delta[i](可正可负)。 从 i 跳到 jj>i)后体力:dp[i] - (j - i) + delta[j]。 递推:dp[j] = max_{i<j}(dp[i] + i) - j + delta[j];若 <0 视为不可达。

  • 线性优化:维护当前 best = max(dp[i] + i),每步 O(1) 更新。

function maxRemainEnergy(m: number, delta: number[]): number|false {
  const n = 100; // 终点编号
  const dp = Array(n+1).fill(-Infinity);
  dp[0] = m; // 起点不吃蘑菇
  let best = dp[0] + 0;

  for (let j = 1; j <= n; j++) {
    dp[j] = best - j + (delta[j] || 0);
    if (dp[j] < 0) dp[j] = -Infinity; // 体力耗尽即死亡
    if (dp[j] !== -Infinity) best = Math.max(best, dp[j] + j);
  }
  return dp[n] === -Infinity ? false : dp[n]; // false 表示到不了
}

面试点:把“任意跳”转成 max(dp[i]+i)技巧;说明可达性负值处理


5)TCP 如何保证可靠 + 拥塞控制流程

  • 可靠性:序号/确认(Seq/Ack)、重传(超时/快速重传)、校验和、滑动窗口、流量控制(接收窗口)、乱序重组。

  • 拥塞控制(典型 Reno/CUBIC 思想):

    1. 慢启动cwnd 从 1 MSS 指数增长,至 ssthresh

    2. 拥塞避免:线性增长;

    3. 丢包

      • 3 次重复 ACK:快速重传 + 快速恢复(乘法减小,进入拥塞避免);

      • 超时:回到慢启动,ssthresh=cwnd/2

心得:应用层别乱开无脑并发;配合应用层限速/重试,避免“拥塞雪崩”。


6)为什么数据库索引用 B+ 树

  • 高扇出、低高度:节点存多Key,多级磁盘I/O少;

  • 叶子链表范围查询天然友好;

  • 非叶仅存 Key:内存命中率高;

  • 稳定性:插删平衡成本可控。

对比:B 树叶/内节点都存数据,范围扫描没 B+ 流畅;跳表/哈希适合点查,不擅长范围。


7)进程调度方式

  • FCFSSJF/短作业优先(平均等待短但易饿死)、优先级时间片轮转多级反馈队列(MLFQ)Linux CFS(红黑树按虚拟运行时间公平)。

三面复盘:先讲定义与痛点,再讲典型策略与取舍,最后给工程建议(比如 TCP 并发连接与限速策略、索引设计的过滤性/选择度评估)。


心态与策略(通关要点)

  • 引导面试官:自我介绍里明确你的强项与“能聊深的项目”。

  • 代码要能跑:思路→复杂度→边界→可测试;别上来就写一屏。

  • 遇到不确定:先约束版本(如单位用“分”、空输入返回啥),再实现。

  • 算法题:先给朴素解,再给优化思路(如 DFS→并查集 / O(n²)→O(n))。

  • 心态:紧张就复述题意 + 列边界,把大脑切回“确定性步骤”。


小抄区(可直接面试时口述/板书)

事件循环口诀:同步 → 微任务(await/then)→ 宏任务(setTimeoutnextTick:微任务触发回调队列 flush,Vue2 多重降级,Vue3 统一调度器 岛屿:网格 DFS/BFS,把访问过的置 0 红包:二倍均值法,单位用分,注意兜底 64马8道:最少 11 场(8 组赛 + 冠军赛 + 2 场筛选) SQL TopN:窗口函数 ROW_NUMBER() DP 最大体力dp[j] = max(dp[i]+i) - j + delta[j] TCP:Seq/Ack/重传/窗口 + 慢启动→拥塞避免→快重传/快恢复 B+树:高扇出、叶子链表、范围查询友好 调度:MLFQ/CFS 说得出优缺点与适用场景

A股三大指数午间休盘集体下跌,白酒股走强

2025年8月20日 11:32
36氪获悉,A股三大指数午间休盘集体下跌,沪指跌0.06%,深成指跌0.66%,创业板指跌1.71%;软件、制药、多元金融板块领跌,中科海迅跌超13%,贝达药业跌超8%,弘业期货跌超4%;餐饮旅游、汽车零部件、白酒板块走强,西安饮食、酒鬼酒涨停,舍得酒业涨超8%,苏奥传感涨超4%。

关键词匹配,过滤树

2025年8月20日 11:26

仅匹配树的最后一级

// 搜索关键字,把对应的枝干从树里过滤出来
export function rebuildTree(keyWord, arr) {
  if (!arr) {
    return []
  }
  let newarr = []
  arr.forEach((node) => {
    const keyReg = new RegExp(keyWord.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')
    const matchedChildren = rebuildTree(keyWord, node.child)
    if (keyReg.test(node.name)) {
      // 最后一层节点 匹配到关键字,push进去
      if (matchedChildren.length === 0 && node.child.length === 0) {
        const obj = {
          ...node,
          child: matchedChildren,
        };
        newarr.push(obj);
      }
    }
    // 后代有匹配到的,该树留着
    if (matchedChildren.length > 0) {
      const obj = {
        ...node,
        child: matchedChildren,
      };
      newarr.push(obj);
    }
  })
  return newarr
}

树的每一级都匹配

export function rebuildTree(keyWord, arr) {
  if (!Array.isArray(arr)) {
    return []
  }
  if (!keyWord) return arr
  let result = []
  arr.forEach((node) => {
    const matchedChildren = node.child ? rebuildTree(keyWord, node.child) : [];
    if (node.name.toLowerCase().includes(keyWord.toLowerCase())) {
      result.push({ ...node });
    } else if (matchedChildren.length > 0) {
      // 如果子节点中有匹配的,保留当前节点,并替换 child 为过滤后的子树
      result.push({
        ...node,
        child: matchedChildren
      });
    }
  })
  return result
}

匹配文字有两种方式

  • 方式一:靠正则(不推荐)
 const keyReg = new RegExp(keyWord.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')
 keyReg.test(node.name)
  • 方式二:(推荐)
name.toLowerCase().includes(keyWord.toLowerCase())

智氪|人形机器人万亿市场,A股哪些公司分蛋糕?

2025年8月20日 11:25

作者 | 范亮

编辑 | 丁卯

7月份以来,人形机器人赛道利好频频。

先是在7月8日,智元机器人及相关方拟收购上市公司上纬新材至少63.62%的股份,被市场解读为智元有望借壳冲击资本市场;紧接着7月18日,宇树正式开启上市辅导,并在7月25日发布售价3.99万元起的新款机器人Unitree R1;随后7月24日,马斯克在财报会议上官宣,预估2026年开始量产Optimus,目标5年内年生产100万台。

进入8月份后,2025世界机器人大会,以及首个人形机器人运动会相继召开,根据北京市经济和信息化局数据显示,2025世界机器人大会现场参会人数达到27.1万人次,观看会展赛直播人数高达5200万人次,较上届增长62%。

尽管市场此前有不少声音指出,人形机器人短期可能存在泡沫,但近期一系列密集事件表明,人形机器人的商业化进程或许早已经走在路上。

与一级市场不同的是,二级市场人形机器人产业链相关的“玩家”,除了少数几个以优必选等为代表的整机玩家外,大部分参与者均聚集在产业链相关的上下游,且本身已是相对成熟的上市公司,在自身原有业务领域经营多年。

所以,换句话说,这些成熟的供应链公司切入人形机器人赛道的目的,其实更趋近于打造第二增长曲线、驱动公司产业升级,而这对市场而言,恰好也是公司的预期差所在。

人形机器人市场空间有多大?

近年来兴起的赛道投资,其核心思路为先预测远期市场空间,再根据远期市场空间“切蛋糕”,倒推相关企业的市值空间,后续根据产业渗透率、景气度变化进行加减仓操作。因此,了解一个赛道的远期空间,既是赛道投资的起点,也是相关参与公司的可以想象的天花板。

目前,市场有不少机构发布了对人形机器人行业空间的预测,这些机构对人形机器人远期的市场预测存在较大的方差。

在2030年这一时间节点,东海证券预测全球人形机器人销量100万台,对应市场空间1500亿人民币,而华创证券预测的数据接近东海证券的两倍。

在2035年这一节点,高盛认为全球人形机器人销量约140万台,对应市场空间380亿美元,瑞银证券认为全球人形机器人销量约200万台,对应市场空间300-500亿美元,与高盛预测接近,但东吴证券则给出年销量2380万台的预测。

到了2050年节点,摩根士丹利认为全球机器人市场空间将达到5万亿美元,知名投资机构ARK Invest则给出一个更加激进的“终局”预测,即人形机器人空间有望达到24万亿美元。而根据Zion Market Research数据,2024年全球汽车销售规模空间约23.52万亿美元,这意味着人形机器人远期的想象力可以再造一个汽车产业。

如果不关注各家机构预测的细节,只在总量上对人形机器人有一个趋势上的判断,那就是当下人形机器人产业正处于0-1的阶段,5-10年后人形机器人可达到数千亿市场规模,更远期可突破万亿,甚至超越整个汽车产业。

图:机构对人形机器人销量、市场空间预测  资料来源:公开信息,36氪整理

巨大想象力的加持下,在资本市场,人形机器人板块呈现出明显的事件驱动特征。如果将国金人形机器人30指数与创业板指进行对比,会发现,在2024年11月之前,人形机器人指数走势基本与创业板指数相吻合,只是振幅相对更高;但2024年11月后,人形机器人指数开始走出独立行情,表现显著跑赢创业板指数。

这其中猜想有三大原因,一是“924”政策后,市场风险偏好大幅提升,对人形机器人板块估值容忍度提高;二是特斯拉在2024年10月展示了Optimus最新视频,效果超市场预期;三是Deepseek带来的大模型降本增效令人形机器人“大脑”能力获得突破,加上宇树机器人春节期间的催化,行情呈现出明显的事件驱动特征。

作为一个远期规模超万亿的大赛道,人形机器人产业链相关公司无疑是未来几年市场持续关注的焦点,这背后既有长期增长逻辑的支撑,又有短期事件驱动的提振。鉴于此,本文则将对A股人形机器人产业相关公司进行梳理,以期为投资者提供一些投资思考。

图:国金人形机器人30指数与创业板指对比  资料来源:Wind、36氪整理

人形机器人包含哪些环节?

按人形机器人的功能模块划分,可以分为感知层、决策层、执行层三个模块。

感知层相当于机器人的“五官”,主要由各类传感器组成。负责收集来自外部环境和机器人自身状态的信息,是机器人与世界交互的入口。具体包括视觉系统(摄像头、激光雷达)、定位与导航系统(惯性传感器、GPS、雷达)、力觉系统(六维力/力矩传感器、关节力矩传感器)、听觉系统(麦克风)。

决策层相当于机器人的“大脑”,主要由各类芯片、基础模型、算法软件组成。负责处理来自感知层的信息,进行理解、推理、规划,并向执行层发出指令。具体包括处理器(CPU、GPU)、认知系统(大模型和各种算法)、运动规划与控制算法。

执行层相当于机器人的“骨骼与肌肉”,主要由各类机械零部件组成。负责接收并执行来自决策层的指令,完成具体的物理动作,如行走、抓取、操作等。具体包括关节、灵巧手(空心杯电机、减速器、驱动器、蜗杆、触觉传感器)、骨架(轻量化合金、PEEK材料、碳纤维复合材料)、能源系统(电池)等四大类。其中,关节又包括线性关节(无框力矩电机、丝杠、驱动器)、旋转关节(无框力矩电机、减速器、驱动器)两个部分。

从二级市场投资的角度看,当下资本市场主要围绕人形机器人的硬件展开交易,有关人形机器人价值量的分布也围绕硬件展开。原因在于,人形机器人决策层相关的基础模型等难以估量其在人形机器人中的具体成本占比,且相关技术主要被人形机器人集成商把握,而硬件供应链标的具备更清晰的估值锚点,可以动态追踪行业内的生产研发动向。

目前对价值量的拆分,主要有两种方式,一种是按照前述人形机器人的功能模块拆分,一种是按核心零部件拆分。

按照功能模块拆分价值量,根据光大证券统计,以远期20万单价测算,执行系统BOM(剔除电池、骨架)占比53.2%,感知系统占7.3%,其他芯片、电池等部件合计占比39.5%。将执行系统价值量进一步拆分,直线关节、旋转关节、灵巧手分别占31.0%、17.9%、4.3%。

图:人形机器人按功能模块划分价值量  资料来源:中泰证券,36氪整理

按核心零部件拆分价值量,根据东海证券,预计到2030年,人形机器人的核心零部件价值量占比分别为行星滚柱丝杠(19%)、无框力矩电机(16%)、减速器(13%)、力矩传感器(11%)、空心杯电机(8%)、惯性测量单元(2%)。此外,电池、芯片、雷达、驱动器、编码器等零部件统一归类到其他,合计价值量占比为31%。

图:人形机器人按核心零部件划分价值量  资料来源:东海证券,36氪整理

哪些上市公司真正受益?

从产业链链条来看,人形机器人的产业链条呈现出基础零部件—核心模组—整机制造三大环节,其中在整机制造环节,摩根士丹利此前的一份报告指出主要包含五类企业,分别是汽车制造商(如小鹏、特斯拉)、消费电子公司(如小米)、电商和互联网公司、传统机器人制造商(如美的旗下库卡、埃斯顿)以及新兴人形机器人制造商(如宇树、优必选),上述企业中或者原有业务体量较大导致投资弹性低,或者还未IPO,因此市场将目光聚焦在了标的丰富、弹性较高的已上市的零部件生产厂商。

通过对A股上市公司2023年以来的公告进行统计,一共有80多家上市公司在公告中表明生产/规划生产人形机器人上游的零部件。

按万得三级行业划分,汽车零部件、机械行业的公司占比在一半以上,剩余则零散分布在电子设备、电气设备、家电、半导体、化工等领域。按地域划分,这些公司一半以上位于长三角地区,其次为珠三角地区,剩余则零散分布于山东、四川、湖南等地。

按最终零部件划分,相关公司情况如下:

1、行星滚柱丝杠(价值量占比约19%)

丝杠是将旋转运动转化为直线运动的传动元件,技术原理类似于日常生活中“拧螺丝”,主要包括滑动丝杠、滚动丝杠、静压丝杠三大类,行星滚柱丝杠是滚动丝杠的一个细分品类,具备高精度、高承载、小型化的优势。

此前,行星滚柱丝杠主要用在汽车制动、医疗器械、工程机械等多个行业,但市场规模较低。根据智研咨询数据,2024年,中国行星滚柱丝杠市场规模为13.13亿元,同比增长19.26%。特斯拉首次把行星滚珠丝杆用在人形机器人后,行星滚柱丝杠的想象空间被大大提升

市场格局方面,根据中邮证券统计,2022年国外行星滚柱丝杠龙头制造商Rollvis、GSA及Ewellix在中国市场的份额占比分别为26%、26%、14%,中国本土行星滚柱丝杠厂商合计市场份额占比为19%。

目前,国内已经至少有17家上市公司公告了其在行星滚柱丝杠的布局,主要处于研发/募集资金规划生产的阶段。

具体来看,汇川技术、长华集团、恒立液压等,已经取得了实质性的研发进展。夏厦精密、五洲新春、北特科技等公司,已经进入募集资金规划产能的阶段,如五洲新春指出公司拟募集资金用于年产98万套行星滚柱丝杠、210万套微型滚珠丝杠、7万组通用机器人专用轴承等项目。

图:行星滚柱丝杠相关上市公司  资料来源:公司公告,36氪整理

2、无框力矩电机(价值量占比约17%)

无框力矩电机保留了传统电机中用于产生扭矩和速度的部分,但没有轴、轴承、外壳或端盖。相比于有框架电机,无框力矩电机拥有小体积、轻重量,高性能的特性。

包含无框力矩电机在内的力矩电机行业同样是一个“小市场”,根据Proficient Market Insights数据,全球无框无刷直流电机(含无框力矩电机)2024年的市场规模约2.5亿美元。市场格局方面,科尔摩根、穆格等外资企业占据主要市场份额。

目前,国内已经至少有10家上市公司公告了其在无框力矩电机的布局,主要处于送样/已商业化阶段。

具体来看,步科股份指出,公司无框力矩电机是成熟的、规模化、平台化的工业化产品,2024年公司包含无框力矩电机在内的驱动系统业务营收为3.5亿元,毛利率32%。雷赛智能指出,公司无框力矩电机、空心杯电机等组件与解决方案已经取得若干规模商业订单,形成一定的销售收入,但整体营收占比较小。

图:无框力矩电机相关上市公司  资料来源:公司公告,36氪整理

3、减速器(价值量占比约13%)

减速器的作用是将电机的高速转动转变为低转速、高扭矩的转动,以便于人形机器人完成搬运、行走等动作。

人形机器人主要会用到谐波减速器、行星减速器、RV减速器等多种类型的减速器,这些减速器本身已经在工业机器人、新能源汽车等领域有成熟的应用,市场规模相对较大,参与者也较多。

从各类减速器的市场规模数据看,QYResearch数据显示,2024年全球谐波减速机市场销售额达到了4.62亿美元;华经产业研究院数据显示,2022年全球行星减速器销售金额为12.03亿美元;Straits Research数据显示,2024年全球RV减速器市场规模约为20.8亿美元。

市场格局上,日企过去在减速器市场占据主要地位,但国内厂商近年来持续挤压日企份额。

谐波减速器方面,根据天风证券统计,全球市场中,日企哈默纳科约占82%份额,绿的谐波约占7%,位列第二,其他厂商共占11%。在国内市场,哈默纳科约占36%的市场份额,同样位列第一。RV减速器方面,日企纳博特斯克公司作为RV减速器的发明者,在中大型工业机器人精密减速器市占率达60%,位居世界第一。行星减速器方面,市场格局相对分散,但赛威传动、纽卡特、威腾斯坦、日本新宝等外资企业仍占据主要地位,据QYResearch数据,前三大厂商占有全球35.52%的市场份额。

目前,国内公告生产上述各类减速器产品的上市公司超28家,这些公司的业务进展各不相同。处于产品开发阶段,或者还未量产的公司有宁波东力、西菱动力、美湖股份、圣龙股份等企业,已经进入量产/商业化的企业有绿的谐波、巨轮智能、中大力德等企业。

其中,绿的谐波为国内谐波减速器龙头企业,2024年公司谐波减速器及金属部件营收3.25亿元,毛利率达到36.13%;双环传动为RV减速器龙头企业,其拟拆分旗下主营RV减速器的子公司环动科技于科创板上市,2024年,环动科技RV减速器产品国内市占率24.98%,营收3.41亿元,毛利率34.84%;中大力德为行星减速器龙头企业,2024年中大力德减速器业务营收2.43亿元,毛利率23.26%。

图:减速器相关上市公司  资料来源:公司公告,36氪整理

4、力/力矩传感器(价值量占比约11%)

力/力矩传感器用于感受每个关节输出力/力矩的大小,目前,人形机器人主要使用六维力传感器,该传感器可同时感知x、y、z轴的力和力矩的大小,让人形机器人拥有了“力觉”,可以更加精准地感知自身的运动状态。

此前,六维力传感器主要应用在工业机器人、医疗器械等领域,根据QYResearch数据,2023年全球六维力传感器市场规模为2.24亿美元,市场规模同样较小。市场格局方面,根据中商产业研究院数据,美国ATI(精度0.1%FS)、德国Schunk和瑞士Kistler凭借技术优势垄断汽车测试及航空航天等高端市场,而中国厂商宇立仪器(特斯拉供应商)、蓝点触控(人形机器人市占率70%)正在强势崛起。

目前,国内有约8家上市公司公告布局于机器人相关的力/力矩传感器业务,大多处于研发和试制阶段。其中,柯力传感用于AI理疗机器人机械臂上的六维力传感器在2025年上半年已实现了数百台的批量出货,但用于人形机器人的产品仍然处于客户送样阶段;东华测试六维力传感器处于小批量试制阶段。

图:力/力矩传感器相关上市公司  资料来源:公司公告,36氪整理

5、空心杯电机(价值量占比约8%)

传统电机转子中间有一个用于导磁和固定用的铁芯,空心杯电机则取消了铁芯。此举虽降低了电机的扭矩,但具备体积微小、响应快、功率密度高(功率/质量)的特点。根据NTCysd测算,2022年全球空心杯电机市场规模为51亿人民币。

市场格局上,根据东吴证券统计,海外企业Faulhaber和Maxon空心杯电机合计占据全球市场近半份额。2021年Faulhaber和Maxon合计占全球空心杯电机份额超45%,top3厂商市场份额占55.43%。

目前,国内空心杯电机生产商与无框力矩电机生产商画像高度重合,大部分拥有无框力矩电机产品的上市公司也会推出空心杯电机产品,并且也处于送样/已商业化阶段。

鸣志电器指出,公司的步进电机、无刷电机、伺服电机、空心杯电机、直线电机模组等产品被国内、外客户广泛使用;拓邦股份披露,公司自2007年便开始从事有刷/无刷空心杯电机的研发与生产,是国内最早打破欧美日垄断、成功实现空心杯电机量产的厂家之一。信捷电气则披露,公司自主研发的空心杯电机样机已有产品开发完成,目前还在开发更多种类和型号的产品。

图:空心杯电机相关上市公司  资料来源:公司公告,36氪整理

6、惯性测量单元IMU(价值量占比约2%)

IMU内置了加速度计、陀螺仪等多个惯性传感器,类似于人形机器人的“前庭系统”,让机器人可以感知身体各个部位的位置和方向。

根据Yole Intelligence数据,2021年全球IMU市场空间约为18.3亿美元,应用在消费电子、汽车电子、工业控制等多个领域。市场格局方面,根据QYResearch,全球惯性测量单元主要厂商有Honeywell International、Northrop Grumman Corp、SAFRAN和Thales等,全球前三大厂商共占有超过50%的市场份额。

目前,A股公开拥有IMU产品的上市公司至少在6家以上。具体来看,中海达IMU产品已经在新能源汽车领域开启量产交付;赛微电子正在小批量试产阶段;敏芯股份则启动了六维力传感器、机器人用IMU以及手套型压力及温度传感器的研发立项。

图:惯性测量单元相关上市公司 资料来源:公司公告,36氪整理

7、PEEK材料

PEEK,又名聚醚醚酮,是一种新型的热塑性特种工程塑料。相较于传统的金属材料(如钢、铝合金),PEEK材料优势在于重量更轻的同时强度更高,同时绝缘效果、耐磨性、抗疲劳性更佳,在交通运输、航空航天、电子信息、能源及工业、医疗健康等多个领域都得到广泛的应用。

对人形机器人而言,PEEK低重量的优势可以提升人形机器人的敏捷性,降低能耗,延长续航,绝缘效果则可以避免内部电子元件短路,提高系统安全性。因此,PEEK是目前市场公认的应用于人形机器人的关节和四肢部位的优质材料。

根据东吴证券,PEEK材料最早在1978年就已经研发成功,2005年吉林大学自主研发出PEEK合成技术,打破海外垄断,而后我国进入PEEK材料商业化阶段。

目前,A股至少7家上市公司公告进入PEEK材料领域,大多处于研发/送样阶段。其中,中研股份是继英国威格斯、比利时索尔维和德国赢创之后全球第4家PEEK年产能达到千吨级的企业,2024年公司总营收2.77亿元,毛利率40%;截至2024年财报披露日,沃特股份一期聚醚醚酮(PEEK)合成树脂产线已完成建设,项目已由建设期进入试生产期。

图:PEEK材料相关上市公司  资料来源:公司公告,36氪整理

8、关键模组集成商

除了上述进入机器人核心零部件领域的企业外,还有一些耳熟能详的企业“跨界”进入人形机器人产业中游的核心模组集成环节。

例如,拓普集团在机器人领域,已经进入包括直线执行器、旋转执行器、灵巧手电机、传感器、躯体结构件、足部减震器、电子柔性皮肤等多个环节;蓝思科技2024年迅速与国内外头部人形机器人公司合作,量产人形机器人,开发头部、灵巧手、关节电机等关键模组;美的集团在2025年3月17日发布了人形机器人样机,未来会重点关注面向人形机器人的灵巧手、仿生臂和腿的设计。

图:模组集成商相关上市公司  资料来源:公司公告,36氪整理

9、其他

在一些价值量占比相对较低的零部件环节,也有不少上市公司参与。如长盛轴承在机器人领域的研究方向主要是应用于关节处的自润滑轴承、部分直线执行器中的产品,以及灵巧手的相关部件;恒辉安防研发团队通过特定编织工艺已经开发出多款腱绳测试样品;信捷电气能够为机器人提供编码器产品。同时,储备了驱动器、高性能光学编码器、机器人“小脑”控制等相关技术。

图:其他关键零部件相关上市公司  资料来源:公司公告,36氪整理

*免责声明:

本文内容仅代表作者看法。

市场有风险,投资需谨慎。在任何情况下,本文中的信息或所表述的意见均不构成对任何人的投资建议。在决定投资前,如有需要,投资者务必向专业人士咨询并谨慎决策。我们无意为交易各方提供承销服务或任何需持有特定资质或牌照方可从事的服务。

TypeScript 函数重载入门:让你的函数签名更精确

作者 烛阴
2025年8月20日 11:22

一、什么是函数重载?

函数重载的核心思想是:对外声明多种调用方式,对内用一个统一的实现来处理。

一个完整的函数重载包含两个主要部分:

  1. 重载签名:定义了函数的各种调用形式,包括参数的类型、数量和返回值的类型。这些签名没有函数体。
  2. 实现签名:这是函数 唯一 的实现,它包含函数体。它的签名必须能够 兼容 所有重载签名。

示例: 一个 add 函数,既可以用于数字相加,也可以用于字符串拼接。

// 1. 重载签名 (Overload Signatures)
function add(x: number, y: number): number;
function add(x: string, y: string): string;

// 2. 实现签名 (Implementation Signature)
function add(x: any, y: any): any {
    // 3. 函数体实现
    if (typeof x === 'number' && typeof y === 'number') {
        return x + y;
    }
    if (typeof x === 'string' && typeof y === 'string') {
        return x + y;
    }
    throw new Error('Invalid arguments');
}

// 调用
const numSum = add(5, 10); // numSum 的类型被推断为 number
console.log(numSum); // 15
const strSum = add('Hello, ', 'World!'); // strSum 的类型被推断为 string
console.log(strSum); // Hello, World!

分析:

  • 外部可见性:当外部代码调用 add 函数时,TypeScript 会看到两个重载签名。它会根据你传入的参数,从上到下查找第一个匹配的签名。
  • 内部实现:实现签名 function add(x: any, y: any): any 对外部是不可见的,你不能用 (any, any) 的方式直接调用 add 函数(除非强制类型转换)。
  • 兼容性:实现签名必须涵盖所有重载签名。在这里,x: any, y: any 可以接受 (number, number)(string, string) 的情况。

二、重载的顺序

函数重载的顺序至关重要,因为 TypeScript 在解析调用时会 按顺序检查。一旦找到匹配的签名,它就会停止搜索。

  • 顺序一般是代码中从上而下的顺序。
  • 注:在有类型包含关系的情况下一般有小而大,例如:先number,再any

function move(p: Point): void; 
function move(p: any): void; 

// ... 实现 ...
function move(p: any): void {
  // ...
}

move({ x: 1, y: 2 }); // OK, p 的类型被正确推断为 Point

三、常见的几种函数重载的优化方案

1. 联合类型的应用

参数类型不同,但逻辑和返回类型相似

// 使用重载 (显得冗余)
function printId(id: number): void;
function printId(id: string): void;
function printId(id: number | string): void {
  console.log("Your ID is: " + id);
}

// 使用联合类型 (更简洁)
function printIdSimple(id: number | string): void {
  console.log("Your ID is: " + id);
}

2. 可选参数 或 默认参数

如果只是参数数量不同,可以使用 可选参数默认参数

// 使用重载
function greet(person: string): string;
function greet(person: string, greeting: string): string;
function greet(person: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${person}!`;
}

// 使用可选参数 (更简洁)
function greetSimple(person: string, greeting?: string): string {
    return `${greeting || "Hello"}, ${person}!`;
}

3. 泛型

当函数的输入类型和输出类型之间存在一种模式或关联,但具体的类型是可变的,泛型 是最佳选择。

// 使用重载 (无法穷举所有类型)
function getFirstElement(arr: number[]): number | undefined;
function getFirstElement(arr: string[]): string | undefined;
function getFirstElement(arr: any[]): any | undefined {
    return arr[0];
}

// 使用泛型 (终极解决方案)
function getFirstElementGeneric<T>(arr: T[]): T | undefined {
    return arr[0];
}

const firstNum = getFirstElementGeneric<number>([1, 2, 3]); // 推断为 number
console.log(firstNum);
const firstStr = getFirstElementGeneric<string>(['a', 'b', 'c']); // 推断为 string
console.log(firstStr);

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

最近很火的MCP,究竟是什么?Trae用大白话教会你

2025年8月20日 11:22

什么是MCP?

MCP(Model Context Protocol)是一种让AI模型与外部工具通信的标准协议,它通过标准输入输出(stdin/stdout)进行通信,使用JSON格式交换数据。

MCP的通信原理

  • 标准输入(stdin) : 接收AI模型的请求
  • 标准输出(stdout) : 返回处理结果
  • 标准错误(stderr) : 输出调试信息 这种设计让MCP服务器可以独立运行,通过管道与任何支持标准输入输出的程序通信。

今天让Trae来帮我们理解一下Mcp的标准输出输入是怎么样的

image-20250820110925644

消息格式详解

首先是初始化消息,来作为mcp的初始化

image-20250820111010526

服务端node会响应

{"type":"initialize_result","protocol":"mcp","version":"1.
0"}

然后是Trae帮我们做得简易工具来理解mcp的调用消息"tool_name": "add"

image-20250820111205218

调用的json格式

{
  "type": "call_tool",
  "tool_name": "add",
  "arguments": {"a": 5, "b": 3}
}

服务器会响应以下的格式

{
  "type": "tool_result",
  "tool_name": "add",
  "result": {"result": 8, "operation": "5 + 3"}
}

实际测试验证

然后我让Trae帮我生成测试脚本,通过测试脚本 quick-test.js ,帮我们验证了以下功能

✅ 初始化 :服务器正确响应初始化请求

✅ 加法运算 :5 + 3 = 8,返回详细计算过程

✅ 乘法运算 :4 × 7 = 28,包含操作描述

✅ 消息回显 :正确返回输入消息及其长度

image-20250820111406274

完整示例代码

这是Trae生成服务器端(mcp-demo.js)

const readline = require('readline');

// 定义工具函数
const tools = {
  add: (a, b) => ({ result: a + b, operation: `${a} + ${b}
  ` }),
  multiply: (a, b) => ({ result: a * b, operation: `${a} × 
  ${b}` }),
  echo: (message) => ({ result: message, length: message.
  length })
};

// 处理消息
function handleMessage(message) {
  switch (message.type) {
    case 'initialize':
      return { type: 'initialize_result', protocol: 'mcp', 
      version: '1.0' };
    
    case 'call_tool':
      const tool = tools[message.tool_name];
      if (!tool) {
        return { type: 'error', message: `Unknown tool: $
        {message.tool_name}` };
      }
      const result = tool(...Object.values(message.
      arguments));
      return { type: 'tool_result', tool_name: message.
      tool_name, result };
    
    default:
      return { type: 'error', message: `Unknown message 
      type: ${message.type}` };
  }
}

// 启动服务器
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

console.error('🚀 MCP演示服务器已启动!');
console.error('输入格式: JSON消息');
console.error('示例: {"type":"initialize"}');
console.error('示例: {"type":"call_tool","tool_name":"add",
"arguments":{"a":5,"b":3}}');
console.error('---');

rl.on('line', (line) => {
  try {
    const message = JSON.parse(line.trim());
    console.error('收到消息:', JSON.stringify(message, 
    null, 2));
    const response = handleMessage(message);
    console.log(JSON.stringify(response));
  } catch (error) {
    console.error('错误:', error.message);
    console.log(JSON.stringify({ type: 'error', message: 
    error.message }));
  }
});

生成的测试客户端(quick-test.js)

const { spawn } = require('child_process');

const testCases = [
  { name: '初始化', message: { type: 'initialize' } },
  { name: '加法', message: { type: 'call_tool', tool_name: 
  'add', arguments: { a: 5, b: 3 } } },
  { name: '乘法', message: { type: 'call_tool', tool_name: 
  'multiply', arguments: { a: 4, b: 7 } } }
];

const server = spawn('node', ['mcp-demo.js'], { stdio: 
['pipe', 'pipe', 'inherit'] });
server.stdout.on('data', (data) => {
  console.log('响应:', JSON.parse(data.toString()));
});

testCases.forEach((test, index) => {
  setTimeout(() => {
    server.stdin.write(JSON.stringify(test.message) + 
    '\n');
  }, index * 500);
});

使用指南

可以通过以下的命令来快速开始

  1. 启动服务器 : node mcp-demo.js
  2. 手动测试 :直接输入JSON消息
  3. 自动化测试 : node quick-test.js

Trae帮我们也加了调试,方便我们看到运行的每一个过程和输出结果

  • 使用 console.error() 输出调试信息到stderr
  • 每条消息必须是 单行JSON
  • 响应后立即读取stdout,避免缓冲区溢出

控制台输出的结果

image-20250820111709404

扩展建议

  • 添加更多工具函数到 tools 对象
  • 支持异步操作(返回Promise)
  • 增加错误处理和验证
  • 实现工具发现机制

通过这种标准输入输出方式,MCP实现了AI模型与工具的无缝集成,为构建强大的AI应用提供了坚实基础。

看完Trae帮我们理解的Mcp,你是不是更加清晰Mcp的原理呢?如果有疑问,在评论区留下你的疑惑吧

企业微信:接入企业超1400万,每日服务7.5亿微信用户

2025年8月20日 11:19
36氪获悉,8月20日,企业微信团队举行了2025新品发布。现场,腾讯公司副总裁、企业微信负责人黄铁鸣发布了企业微信最新数据,截至目前,企业微信已接入超过1400万真实的企业与组织,企业每天通过企业微信服务的微信用户数超过7.5亿。据了解,企业微信5.0更新私有部署版,已覆盖了上千家“国字头”和高精尖企业。

国家药监局:7月共批准注册医疗器械产品295个

2025年8月20日 11:09
36氪获悉,国家药监局公告,2025年7月,国家药监局共批准注册医疗器械产品295个。其中,境内第三类医疗器械产品240个,进口第三类医疗器械产品21个,进口第二类医疗器械产品29个,港澳台医疗器械产品5个。
❌
❌