普通视图

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

Vue 自定义进度条实现方法与应用场景解析

作者 spionbo
2025年6月8日 10:06

在前端开发中,进度条是一种常见的用户界面元素,用于展示任务的完成情况或者指示加载过程。然而,有时候标准的进度条并不能完全满足我们的需求,因此,我们可能需要创建自定义的进度条来实现特定的效果或功能。

在本文中,我将介绍如何使用Vue.js创建一个灵活多样的自定义进度条组件,该组件可以根据传入的数据动态渲染进度段,并且支持动画效果和内容展示。

Vue自定义进度条组件实践

一、组件设计目标

我们的进度条组件应具备以下功能:

  1. 接受一个包含进度段数据的数组作为输入。
  2. 根据传入的数据动态渲染进度段。
  3. 支持动画效果,当启用动画时,进度条会以动画形式展示。
  4. 可选择是否展示进度段的内容。
  5. 当前进度超出总长时超出部分红色填充。

二、实现步骤

  1. 创建Vue组件并定义props 首先,创建一个Vue组件,在组件中定义需要的props。这里我们需要定义一个数组props来接收进度段数据,还需要定义一些布尔类型props来控制是否展示内容、是否启用动画等。
export default {
  name: 'CustomProgress',
  props: {
    segmentsData: {
      type: Array,
      default: () => []
    },
    showContent: {
      type: Boolean,
      default: false
    },
    isAnimated: {
      type: Boolean,
      default: false
    },
    totalValue: {
      type: Number,
      default: 100
    }
  }
}
  1. 动态计算各进度段的宽度和样式 在组件的计算属性中,根据传入的进度段数据和总长度,计算每个进度段的宽度和对应的样式。
computed: {
  segments() {
    let currentValue = 0;
    return this.segmentsData.map((segment, index) => {
      const percentage = (segment.value / this.totalValue) * 100;
      const width = `${percentage}%`;
      const isLast = index === this.segmentsData.length - 1;
      const isExceed = currentValue + segment.value > this.totalValue;
      currentValue += segment.value;
      return {
      ...segment,
        width,
        isLast,
        isExceed
      };
    });
  }
}
  1. 在模板中使用v-for指令渲染进度段 在模板中,使用v-for指令遍历计算后的segments数组,为每个进度段渲染对应的DOM元素,并根据其属性应用相应的样式。
<template>
  <div class="progress-bar">
    <div
      v-for="(segment, index) in segments"
      :key="index"
      :style="{ width: segment.width, backgroundColor: segment.color }"
      :class="{ 'is-last': segment.isLast, 'is-exceed': segment.isExceed }"
    >
      <div
        v-if="segment.content && segment.value!== 0"
        :class="{ 'is-last': segment.isLast }"
      >
        {{ segment.content }}
      </div>
    </div>
    <div v-if="hasExceed" class="exceed-part"></div>
  </div>
</template>
  1. 处理动画效果 为了实现动画效果,我们可以利用CSS的动画特性。当isAnimatedtrue时,为进度条容器添加特定的类名,触发动画。
/* 定义动画 */
@keyframes ant - progress - active {
  0% {
    opacity: 0;
    transform: translateX(-100%);
  }
  50% {
    opacity: 0.5;
  }
  100% {
    opacity: 0;
    transform: translateX(100%);
  }
}
.is - animated {
  position: relative;
}
.is - animated::before {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z - index: 3;
  background: #fff;
  opacity: 0;
  animation: ant - progress - active 3s cubic - bezier(0.23, 1, 0.32, 1) infinite;
  content: '';
  cursor: default;
}

在模板中,根据isAnimated的值为进度条容器添加类名:

<template>
  <div class="progress-bar" :class="{ 'is - animated': isAnimated }">
    <!-- 进度段内容 -->
  </div>
</template>
  1. 处理超出部分 当当前进度超出总长时,需要将超出部分以红色填充。在计算属性中判断是否有超出部分:
computed: {
  hasExceed() {
    return this.segments.some(segment => segment.isExceed);
  }
}

在模板中,当有超出部分时,渲染一个额外的DOM元素来显示超出部分,并通过CSS设置其样式为红色:

.exceed - part {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 100%;
  background: #cc0000;
  z - index: 1;
}

三、组件使用示例

在父组件中使用我们创建的CustomProgress组件,传入相应的数据和配置。

<template>
  <div>
    <CustomProgress
      :totalValue="totalValue"
      :showContent="true"
      :isAnimated="true"
      :segmentsData="segmentsData"
    />
  </div>
</template>
<script>
import CustomProgress from './CustomProgress.vue';
export default {
  components: {
    CustomProgress
  },
  data() {
    return {
      totalValue: 80,
      segmentsData: [
        { value: 20, color: '#0099ff', content: '训练' },
        { value: 40, color: '#00b23b', content: '测试' }
      ]
    };
  }
}
</script>

Vue, 自定义进度条,前端开发,进度条实现方法,应用场景,JavaScript,HTML,CSS, 组件化开发,响应式设计,用户体验,Web 开发,动画效果,交互式组件,前端框架



资源地址: pan.quark.cn/s/35324205c…


四、总结

通过以上步骤,我们成功创建了一个功能丰富的Vue自定义进度条组件。它可以根据不同的数据和配置,灵活地展示进度情况,并且具有动画效果和对超出部分的处理。在实际项目中,你可以根据具体需求对这个组件进行进一步的优化和扩展,比如添加更多的自定义样式选项、支持不同的主题等,以更好地满足项目的UI设计和用户体验要求。

通过这篇文章,你应该对Vue自定义进度条有了初步认识。你在实际应用中,是否有特定的样式或交互需求想融入进度条呢?可以和我分享,咱们进一步探讨如何实现 。

管理不同权限用户的左侧菜单展示以及权限按钮的启用 / 禁用之其中一种解决方案

2025年6月7日 21:42

一、权限管理方案设计

1. 权限模型

推荐采用 RBAC(基于角色的访问控制)  模型,把用户分配到不同角色,角色再关联对应的权限。

  • 权限项:是最小的控制单元,像 view_useredit_product 这种。
  • 角色:由多个权限项组合而成,例如 admineditorviewer
  • 用户:和一个或多个角色相对应。

2. 权限数据存储

  • 前端存储:登录成功后,从后端获取权限数据,然后存到 Vuex/Pinia 或者 localStorage 中。

  • 权限数据结构示例

javascript

{
  user: { id: 1, name: "张三", role: "admin" },
  permissions: ["view_user", "edit_user", "view_product", "edit_product"]
}

二、左侧菜单动态展示

1. 菜单配置

创建一个菜单配置文件,把权限和菜单项关联起来。

javascript

// src/config/menu.js
export const menuList = [
  {
    path: "/dashboard",
    name: "Dashboard",
    icon: "dashboard",
    permission: "view_dashboard" // 访问该菜单所需权限
  },
  {
    path: "/user",
    name: "用户管理",
    icon: "user",
    permission: "view_user",
    children: [
      {
        path: "/user/list",
        name: "用户列表",
        permission: "view_user"
      },
      {
        path: "/user/add",
        name: "添加用户",
        permission: "add_user"
      }
    ]
  },
  // 其他菜单项...
];

2. 菜单组件实现

在组件里依据用户权限过滤菜单项。

vue

<!-- src/components/Sidebar.vue -->
<template>
  <div class="sidebar">
    <el-menu :default-active="activeMenu" mode="vertical">
      <template v-for="item in filteredMenu">
        <!-- 一级菜单 -->
        <el-menu-item 
          v-if="!item.children && hasPermission(item.permission)"
          :key="item.path"
          :index="item.path"
        >
          <i :class="item.icon"></i>
          <span slot="title">{{ item.name }}</span>
        </el-menu-item>
        
        <!-- 子菜单 -->
        <el-submenu 
          v-else-if="item.children && hasPermission(item.permission)"
          :key="item.path"
          :index="item.path"
        >
          <template slot="title">
            <i :class="item.icon"></i>
            <span>{{ item.name }}</span>
          </template>
          <el-menu-item
            v-for="child in item.children"
            :key="child.path"
            :index="child.path"
            v-if="hasPermission(child.permission)"
          >
            {{ child.name }}
          </el-menu-item>
        </el-submenu>
      </template>
    </el-menu>
  </div>
</template>

<script>
import { mapState } from "vuex";
import { menuList } from "@/config/menu";

export default {
  computed: {
    ...mapState(["permissions"]),
    // 过滤后的菜单项
    filteredMenu() {
      return menuList.filter(item => this.hasPermission(item.permission));
    }
  },
  methods: {
    // 权限检查方法
    hasPermission(permission) {
      // 如果没有设置权限,默认可见
      if (!permission) return true;
      // 检查用户是否拥有该权限
      return this.permissions.includes(permission);
    }
  }
};
</script>

三、权限按钮的展示与禁用

1. 自定义指令实现

借助自定义指令来控制按钮的显示和禁用状态。

javascript

// src/directives/permission.js
export const permission = {
  inserted(el, binding, vnode) {
    const { value } = binding;
    const permissions = vnode.context.$store.state.permissions;
    
    if (value) {
      // 检查是否有该权限
      const hasPermission = permissions.includes(value);
      
      if (!hasPermission) {
        // 没有权限:隐藏按钮
        el.parentNode && el.parentNode.removeChild(el);
        // 或者禁用按钮(根据需求选择)
        // el.disabled = true;
        // el.classList.add('is-disabled');
      }
    } else {
      console.error('需要指定权限标识!');
      el.parentNode && el.parentNode.removeChild(el);
    }
  }
};

2. 全局注册指令

在 main.js 里全局注册这个指令。

javascript

// src/main.js
import Vue from "vue";
import { permission } from "./directives/permission";

Vue.directive("permission", permission);

3. 在组件中使用

vue

<!-- 使用示例 -->
<template>
  <div>
    <!-- 有权限时显示 -->
    <el-button 
      v-permission="'add_user'"
      type="primary"
      @click="addUser"
    >
      添加用户
    </el-button>
    
    <!-- 无权限时禁用 -->
    <el-button 
      :disabled="!hasPermission('edit_user')"
      type="success"
      @click="editUser"
    >
      编辑用户
    </el-button>
  </div>
</template>

<script>
export default {
  methods: {
    hasPermission(permission) {
      return this.$store.state.permissions.includes(permission);
    }
  }
};
</script>

四、路由权限控制

对路由访问权限进行控制,防止用户手动输入 URL 访问受限页面。

javascript

// src/router/index.js
import router from "./router";
import store from "./store";

router.beforeEach((to, from, next) => {
  // 获取用户权限
  const permissions = store.state.permissions;
  
  // 检查路由是否需要权限
  if (to.meta.permission) {
    if (permissions.includes(to.meta.permission)) {
      next(); // 有权限,放行
    } else {
      next({ path: "/403" }); // 无权限,跳转到403页面
    }
  } else {
    next(); // 无需权限,直接放行
  }
});

五、权限管理流程

  1. 用户登录:用户输入账号密码登录系统。
  2. 权限验证:后端验证用户身份,返回用户角色和权限信息。
  3. 权限存储:前端把权限信息存到 Vuex/Pinia 或者 localStorage 中。
  4. 菜单渲染:根据用户权限动态渲染左侧菜单。
  5. 按钮控制:在组件里通过自定义指令或者方法控制按钮的显示和禁用。
  6. 路由拦截:对路由进行拦截,防止未授权访问。

六、优缺点分析

优点

  • 可扩展性强:能够轻松添加新的角色和权限,而不用修改大量代码。
  • 维护便捷:权限配置集中管理,降低了维护成本。
  • 安全性高:从菜单、按钮、路由三个层面进行权限控制,有效防止越权访问。

缺点

  • 初期配置复杂:需要设计合理的权限模型和数据结构。
  • 性能影响:在复杂应用中,频繁的权限检查可能会对性能产生一定影响。

eval:JavaScript里的双刃剑,用好了封神,用不好封号!

2025年6月7日 20:55

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

Snipaste_2025-06-03_13-45-06.png

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

技术qq交流群:906392632

大家好,我是小杨,一个写了6年前端的老司机。今天要聊一个让人又爱又恨的JavaScript特性——eval()。这玩意儿就像编程界的"瑞士军刀",功能强大但危险系数极高,新手容易滥用,老手又避之不及。到底该不该用?怎么安全地用?今天我就用几个血泪教训带大家彻底搞懂它!


一、eval是什么?代码里的"魔术师"

简单说,eval()把字符串当代码执行

const codeStr = 'console.log("Hello eval!")';
eval(codeStr); // 输出:Hello eval!

我第一次见到这功能时惊为天人:"这不就是动态执行代码的黑科技吗?!" 于是兴冲冲地写了个"万能计算器":

function calculate(expr) {
  return eval(expr); // 千万别学我!
}

console.log(calculate('2 + 2 * 3')); // 8
console.log(calculate('Math.pow(2, 10)')); // 1024

结果上线三天就被安全团队约谈了... (后面会讲为什么)


二、为什么说eval是"危险分子"?

1. 安全漏洞:XSS攻击直通车

如果执行用户输入的字符串:

const userInput = "alert('你的cookie是:'+document.cookie)";
eval(userInput); // 完蛋,用户脚本被执行了!

2. 性能杀手:引擎优化全失效

JS引擎原本可以预编译和优化代码,但遇到eval()就不得不:

  • 启动解释器
  • 创建新作用域
  • 放弃静态分析

3. 调试噩梦

错误堆栈会显示eval at <anonymous>,根本找不到问题源头!


三、安全使用eval的三大法则

虽然危险,但在某些场景下不得不用(比如解析JSON的老浏览器环境)。这时要遵守:

法则1:永远不要直接执行用户输入

// 错误示范
eval(req.body.userCode);

// 正确做法
function safeEval(code) {
  if (/alert|document|window/.test(code)) {
    throw new Error('危险代码!');
  }
  return eval(code);
}

法则2:用Function构造器替代

const calculator = new Function('expr', 'return (' + expr + ')');
console.log(calculator('2 + 2')); // 4

优点

  • 只在全局作用域执行
  • 稍微安全一丢丢

法则3:严格模式限制

"use strict";
eval = 1; // 报错!严格模式下不能覆盖eval

四、真实案例:我踩过的三个坑

案例1:动态生成函数

// 需求:根据API返回的函数名执行对应方法
const funcName = apiResponse.method; // 比如"showDialog"

// 菜鸟时期的我:
eval(funcName + '()'); // 可能执行任意代码!

// 现在的我:
const allowedMethods = { showDialog: true };
if (allowedMethods[funcName]) {
  window[funcName]?.();
}

案例2:JSON解析(上古时期)

// 2008年的老代码(那时候没有JSON.parse):
const data = eval('(' + jsonStr + ')');

// 2023年的正确姿势:
const data = JSON.parse(jsonStr);

案例3:沙箱环境

// 用Proxy做个简单沙箱
function safeEval(code) {
  const sandbox = new Proxy({}, {
    has: () => true, // 欺骗in操作符
    get: (target, key) => {
      if (['window','document'].includes(key)) 
        throw new Error(`禁止访问 ${key}`);
      return target[key];
    }
  });
  return (new Function('with(this){return ' + code + '}')).call(sandbox);
}

五、现代替代方案

场景 eval做法 更优方案
动态执行代码 eval(str) Function构造函数
JSON解析 eval('('+json+')') JSON.parse
动态属性访问 eval('obj.'+key) obj[key]
模板引擎 eval拼接字符串 模板字面量${}
数学表达式计算 eval('1+1') 第三方库(如math.js)

六、什么时候非用eval不可?

  1. 开发调试工具(如浏览器控制台本身)
  2. 编写DSL语言(如某些低代码平台)
  3. 教学演示(比如教人理解AST解析)

我在写在线代码编辑器时,最终选择了new Function()+Web Worker的方案,既安全又不会阻塞主线程。


总结:eval如老虎,摸前要三思

  • ✅ 能用别的方案就别用eval
  • ✅ 必须用时严格过滤输入
  • ✅ 优先考虑Function构造函数

你们在项目里用过eval吗?有没有因此翻过车?欢迎在评论区分享你的"惊魂时刻"~

我是小杨,下期可能会讲《如何安全地动态执行代码》,感兴趣的话点个关注不迷路! 🔐

JavaScript篇:前端定时器黑科技:不用setInterval照样玩转循环任务

2025年6月7日 20:34

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

Snipaste_2025-06-03_13-45-06.png

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

技术qq交流群:906392632

大家好,我是小杨,一个干了快6年的前端老司机。今天要和大家分享一个特别实用的定时器技巧——用setTimeout实现setInterval。这个方案不仅能解决setInterval的一些痛点,还能让我们的定时任务更加可控。

一、为什么不用setInterval?

先说说我为什么研究这个方案。去年在做一个大屏数据实时刷新功能时,发现直接用setInterval会有个恶心的问题:

setInterval(() => {
  // 模拟网络请求
  console.log('执行任务', new Date().getSeconds());
}, 1000);

看起来每1秒执行一次对吧?但如果网络卡顿导致函数执行超过1秒呢?这时候就会发现多个任务挤在一起执行,就像早高峰的地铁一样让人崩溃。

二、setTimeout的救场方案

后来我改用setTimeout递归调用的方式,完美解决了这个问题:

function 我的循环任务() {
  console.log('执行任务', new Date().getSeconds());
  
  // 在函数末尾重新调用自己
  setTimeout(我的循环任务, 1000);
}

// 启动任务
setTimeout(我的循环任务, 1000);

这个方案的精妙之处在于:每次都是等上次任务完全执行完,才重新计时。就像排队上厕所,必须等前一个人出来,下个人才能进去。

三、升级版:可控定时器

后来我又做了个加强版,加上了启动/停止功能:

let timer = null;
let count = 0;

function 我的可中断任务() {
  console.log(`执行第${++count}次`, new Date().getSeconds());
  
  if(count < 5) { // 只执行5次
    timer = setTimeout(我的可中断任务, 1000);
  }
}

// 启动
timer = setTimeout(我的可中断任务, 1000);

// 随时可以停止
// clearTimeout(timer);

这样写有三个好处:

  1. 避免任务堆积
  2. 可以精确控制执行次数
  3. 随时能终止任务

四、实战中的应用场景

这个技巧在我工作中帮了大忙,比如:

  1. 轮询接口:检查订单状态,直到支付成功
  2. 动画序列:实现复杂的多段动画效果
  3. 倒计时:更精准的秒表功能
// 倒计时示例
function 倒计时(剩余秒数) {
  console.log(`剩余:${剩余秒数}秒`);
  
  if(剩余秒数 > 0) {
    setTimeout(() => 倒计时(剩余秒数 - 1), 1000);
  }
}

倒计时(10); // 开始10秒倒计时

五、注意事项

虽然这个方案很香,但也要注意:

  1. 记得保存timer变量,否则没法清除
  2. 递归调用要注意停止条件,避免内存泄漏
  3. 长时间运行的任务可能会造成调用栈过深

六、总结

setTimeout实现setInterval的方案,就像是用乐高积木拼出了现成玩具的功能,虽然多写几行代码,但获得了更大的灵活性和可控性。特别适合需要精确控制执行时机的场景。

大家如果有更好的实现方案,欢迎在评论区交流~如果觉得有用,别忘了点赞收藏!

JavaScript篇:自定义事件:让你的代码学会'打小报告'

2025年6月7日 20:05

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

Snipaste_2025-06-03_13-45-06.png

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

技术qq交流群:906392632

大家好,我是小杨,一个在前端界摸爬滚打6年的老油条。今天我要和大家聊聊怎么让代码组件之间"说悄悄话"——没错,就是自定义事件!

一、为什么需要自定义事件?

想象一下这个场景:

// 传统写法:直接调用
function updateUser() {
    updateProfile();
    updateAvatar();
    updateSettings();
    // 我加了新功能还得回来改这里!
}

// 事件驱动写法
document.dispatchEvent(new CustomEvent('userUpdated'));

小杨解说:自定义事件就像办公室里的广播系统,谁想听就自己接,不用挨个通知!

二、基础用法:创建和监听

1. 创建自定义事件

// 简单版
const event = new Event('myEvent');

// 高级版(可以带数据)
const event = new CustomEvent('myEvent', {
    detail: {
        name: '我',
        age: 18
    }
});

2. 监听事件

document.addEventListener('myEvent', function(e) {
    console.log(`收到事件!数据:${e.detail.name} ${e.detail.age}`);
});

三、实战案例:购物车系统

// 商品组件
class Product {
    addToCart() {
        document.dispatchEvent(new CustomEvent('cartAdd', {
            detail: { id: 123, name: '前端秘籍' }
        }));
    }
}

// 购物车组件
document.addEventListener('cartAdd', function(e) {
    console.log(`把 ${e.detail.name} 加入购物车`);
});

// 用户组件
document.addEventListener('cartAdd', function() {
    console.log('更新用户购物车数量');
});

小杨踩坑记:曾经没加detail导致数据传丢,debug到怀疑人生!

四、高级技巧

1. 事件命名空间

// 避免冲突
document.dispatchEvent(new CustomEvent('me:cartAdd'));

2. 事件冒泡控制

const event = new CustomEvent('bubbleEvent', {
    bubbles: true,  // 允许冒泡
    cancelable: true // 允许取消
});

3. 移除监听

function handleEvent() {
    console.log('我只执行一次!');
    document.removeEventListener('oneTimeEvent', handleEvent);
}
document.addEventListener('oneTimeEvent', handleEvent);

五、Vue/React中的自定义事件

1. Vue版

// 子组件
this.$emit('me-event', { data: 123 });

// 父组件
<Child @me-event="handleEvent" />

2. React版

// 父组件
<Child onMeEvent={handleEvent} />

// 子组件
props.onMeEvent({ data: 123 });

六、性能优化

  1. 避免滥用:太多事件会让代码变成"广播体操"
  2. 及时销毁:SPA记得在组件卸载时移除监听
  3. 事件池:高频事件考虑复用事件对象

七、与原生事件的区别

特性 自定义事件 原生事件
触发方式 手动dispatch 浏览器自动触发
事件类型 任意自定义名称 click/keydown等
数据传递 通过detail 有限的事件对象

八、总结

  • 自定义事件是解耦神器
  • 适合组件通信、插件开发等场景
  • 记得给事件起个清晰的名字
  • 移除不需要的监听防止内存泄漏

思考题

const event = new CustomEvent('meetup', {
    detail: { time: new Date() }
});

document.addEventListener('meetup', function(e) {
    console.log(e.detail.time.toLocaleString());
});

setTimeout(() => {
    document.dispatchEvent(event);
}, 1000);
// 1秒后事件触发时,输出的时间是创建时还是触发时的时间?

欢迎在评论区讨论你的答案!下期我会分享更多前端设计模式的实战技巧。

🌐 CDN跨域原理深度解析:浏览器安全策略的智慧设计

作者 大知闲闲i
2025年6月6日 09:07

引言:一个看似矛盾的常见现象

"浏览器明明有严格的同源策略,为什么我们引用CDN上的jQuery、Bootstrap却从未遇到过跨域问题?"这个看似简单的现象背后,隐藏着浏览器安全模型的精妙设计。本文将带您深入理解这一机制,揭示CDN资源"豁免"同源策略的真正原因。

一、同源策略的本质与边界

1.1 同源策略的核心职责

同源策略(Same-Origin Policy)是浏览器最基本的安全机制,但它并非全面禁止所有跨域行为,而是有明确的管辖范围:

  • 限制目标:阻止JavaScript跨域读取数据

  • 不管辖范围:不阻止跨域请求的发送

  • 三要素判定:协议(protocol)、域名(host)、端口(port)必须完全相同

    // 示例:同源检查 const origin1 = new URL('example.com/page'); const origin2 = new URL('api.example.com/data'); console.log(origin1.origin === origin2.origin); // false (域名不同)

1.2 同源策略的实际限制场景

二、豁免机制:浏览器设计的"后门"

2.1 豁免标签列表与原理

浏览器为以下HTML标签默认开放跨域资源加载权限

<!-- 全部可以跨域加载 -->
<script src="https://cdn.example/lib.js"></script>
<link rel="stylesheet" href="https://cdn.example/style.css">
<img src="https://cdn.example/logo.png">
<video src="https://cdn.example/video.mp4"></video>

设计考量

  1. 历史兼容性:早期Web需要共享公共资源

  2. 内容安全假设:静态资源被视为"被动内容"

  3. 性能优化:允许并行加载第三方资源

2.2 豁免条件的深层逻辑

  • 执行而非读取:浏览器直接处理资源内容,不暴露给页面JS

  • 无数据泄露风险:图片/CSS等资源无法被JS直接提取原始数据

  • 可控的副作用:脚本执行在独立作用域,不自动共享数据

三、CDN与CORS的关键区别

3.1 CORS的工作机制

3.2 CDN不需要CORS的原因

四、安全增强与特殊案例

4.1 子资源完整性(SRI)

<script src="https://cdn.example/lib.js"
        integrity="sha384-xxxx"
        crossorigin="anonymous"></script>
  • 作用:验证CDN资源未被篡改

  • 原理:对比哈希值

  • 注意:使用SRI时需要添加crossorigin属性

4.2 例外情况:Web字体

/* 需要CORS响应头 */
@font-face {
    font-family: 'MyFont';
    src: url('https://cdn.example/font.woff2') format('woff2');
}

限制原因:字体可能通过Canvas API被提取分析

五、面试深度回答指南

5.1 结构化回答框架

  1. 明确同源策略本质:"同源策略限制的是JS代码读取跨域数据的能力,而非网络请求本身"

  2. 指出豁免机制:"浏览器为<script><img>等标签设计了豁免规则"

  3. 关联CDN实现:"CDN资源正是通过这些豁免标签引入"

  4. 对比CORS:"CORS解决的是JS发起的跨域请求,与标签加载机制不同"

5.2 进阶知识点

  • 历史演变:从<script>跨域到JSONP,再到CORS

  • 安全扩展:Content Security Policy(CSP)的作用

  • 性能影响:CDN分片域名对并行加载的优化

结语:理解设计哲学

CDN资源的跨域加载并非"漏洞",而是浏览器精心设计的安全与功能平衡的典范。理解这一点,开发者才能:

  1. 正确评估跨域风险

  2. 合理设计资源加载策略

  3. 深入掌握浏览器安全模型

正如著名安全专家Adam Barth所说:"好的安全策略应该像骨骼系统——提供支撑而不妨碍运动。"同源策略及其豁免机制正是这一理念的完美体现。

昨天 — 2025年6月7日首页

藏起来的JS(四) - GC(垃圾回收机制)

作者 ZzMemory
2025年6月6日 17:06

前言:于无声处听惊雷

小时候我们看过《倚天屠龙记》,其中的张无忌练成了九阳神功之后,神功自动护体,真气自动流转,站着让敌人攻击都没事,睡着了都能自动修炼,是不是很羡慕?我们今天要了解的GC,恰恰就有九阳神功的风采。
不过它也很低调,让你平时都难以察觉它在自动运转。善战者无赫赫之功。JavaScript 的精华所在,恰恰就是这些我们经常忽视的地方。现在,让我们一起寻找 JS 设计师藏起来的绝世神功。
本文将带你探索 JS 背后默默守护世界和平的GC(垃圾回收机制)。
PS:神功点击就送 神功.gif

一. 内存管理(GC 本质)

不管是什么编程语言编写的程序,小到一句console.log('Hello world');,大到数百亿参数、占用几十 GB 的大型AI模型,最终都需要加载到内存中去执行。可以说内存管理是编程的底层基础
如此看来,内存管理无疑是所有编程语言的必修内功,那么如何进行内存管理呢?各个语言都有自己的见解,大致可以分为纯靠程序员手工匠心雕琢的手动管理,以及解放程序员大脑的自动管理两大类,下面我们将来了解一下它们各自的优缺点。

手动管理

以C、C++为代表的偏底层语言主张将性能和内存使用效率优化到极致,榨干每一KB内存,绝不浪费。它们的特点就是程序员需要直接控制内存的分配与释放,通过mallocfree(C)或是newdelete(C++)操作。

  • 示例:

    // 分配堆内存
    int *arr = malloc(5 * sizeof(int));
    // 逐个赋值
    arr[0] = 1;
    arr[1] = 2;
    arr[2] = 3;
    arr[3] = 4;
    arr[4] = 5;
    // 释放内存,如果没有调用free()来释放内存,就会出现内存泄露情况
    free(arr);
    // 防止野指针情况,此时arr指向NULL
    arr = NULL;
    

    1. 手动管理内存示意图1.png

    2. 手动管理内存示意图2.png

    3. 手动管理内存示意图3.png

  • 优点:

    • 手动内存管理使得程序员能够像微雕大师一样让每一个字节的内存都物尽其用,带来了极致的性能和内存使用效率
    • 手动管理内存看起来原始,但是在计算机中,越是原始、偏向底层,其适配性就越是好。超高的适配性让C/C++ 代码可在几乎所有硬件架构上高效运行。
  • 缺点:

    • 对程序员的要求极高,稍有不慎就引发各种问题
      • 内存泄露:忘记释放资源,无人使用的资源会“占着茅坑不拉屎”。要知道内存总共就这么多,内存泄露的越多,能用的就越少,最终将导致程序崩溃。
      • 悬空指针(野指针):指向不存在的内存,导致系统报错,比如说访问已经被释放的内存。
      • 双重释放:将同一指针重复释放,
      • 内存碎片:可用内存充足却无法分配。比如说连续分配了100B、200B、300B三块空间,现在释放了100B和300B,然后有一个350B的内存请求,此时无法满足它的要求,因为两块空间为碎片,无法合并使用。
    • 使得代码复杂度和调试难度指数级增长,项目的开发与维护成本激增。

自动管理

那么就有被 C/C++ 狠狠折磨的朋友要问了:“C/C++ 还是太吃操作了,有没有什么简单好用的管理内存的方式呢?” 有的,兄弟!有的!
自动管理内存应运而生,GC 是自动内存管理的核心实现,拥有 GC 的编程语言都是高级编程语言,比如说JS、Python。它们主张解放程序员的大脑,让他们无需纠结内存如何分配与回收,有更多的精力去考虑业务逻辑之类的问题。
你尽管放手去做,在 JS 背后的 GC 会默默守护世界和平,堪称功德无量。

  • 示例:

    // 堆内存中存储的对象被object1引用,标记为可达 
    let object1 = { name: 'obj1' };
    // 对象失去引用,下次GC时被清除
    object1 = null;
    

    4. 自动管理内存示意图1.png

    5. 自动管理内存示意图2.png

    6. 自动管理内存示意图3.png

  • 优点:

    • 提高开发效率: 将程序员的负担交给了编程语言,解放了生产力,让程序员得以专注业务逻辑。同时,无需手动编写malloc/freenew/delete来分配与回收内存,代码更简洁。
    • 减少内存错误: 未使用的对象自动回收,防止内存泄漏。
    • 内存安全: 自动管理确保每个对象只被回收一次,防止了双重释放。
    • 简化代码维护: 内存相关Bug大幅减少,调试更轻松。由于内存管理逻辑由语言运行时统一处理,降低了人为错误,使得代码更健壮。
  • 缺点:

    • 性能开销大:
      • 传统 GC 执行时会阻塞主线程(Stop The World,STW),导致 UI 卡顿或者后台任务延迟,即使经过一代代优化仍然存在。
      • 动态类型和高频对象操作增加 GC 扫描成本,导致周期性性能波动,出现性能抖动。
      • 这些性能消耗对于高性能要求的程序来说是致命的,这也是为什么C/C++能够经久不衰。
    • 不可控性:
      • GC 实际不可预测,程序员只能依赖引擎调度,可能在进行关键操作时触发 STW。
      • GC 的回收是有延迟的,导致程序的实际内存占用高于实际需求,难以精确控制资源释放。
      • 采用 GC 的编程语言缺乏直接干预 GC 的 API(如强制回收、指定回收等),只能通过设计模式间接影响,优化手段有限。

注意

  • GC 固然方便,让开发者快乐地从繁琐的手动内存管理中解放出来。但是正如 MDN 的内存管理一节中说的那样:“这个自动性是混乱的潜在根源:它让开发者错误地以为他们不需要担心内存管理”。
  • 为了避免出现这个错误认知,我们有必要进行了解一下 GC 的运行机制。

二. 引用计数算法

引用计数算法其实不算是V8引擎的主力算法,现代浏览器引擎(如 V8、SpiderMonkey)已不再将引用计数作为主流 GC 算法,但部分场景下会结合引用计数优化性能(如 V8 对短期对象的快速计数回收)。
那么我们为什么要了解它呢?因为引用计数算法虽然简单,但它是 GC 发展史上的重要里程碑,其设计思想与现代 GC 的核心逻辑存在微妙的关联:

  • 引用计数的本质是通过计数器追踪对象引用数量(而非 “可达性标记”),当计数为 0 时判定对象可回收。这一思路虽与现代 GC 的 “标记可达性分析”(从根对象遍历引用链) 不同,但两者的核心目标一致 ——识别不再被使用的对象
  • “它山之石,可以攻玉”,引用计数是 GC 最直观的实现范式,通过它能快速建立 “对象存活 = 被引用” 的基础认知。通过学习引用计数算法,我们也可以很快理解现代 GC 的本质:通过标记可达性来判断对象是否存活,从而决定要不要清理该对象(主流算法如标记清除、标记整理、Scavenge 算法都基于这一核心本质思想,只是在实现细节和优化策略上有所不同)。

下面我们就来了解一下引用计数算法,出发!

原理

每个对象维护一个引用计数器,当有新引用指向该对象时计数 + 1,引用移除时计数 - 1。当计数器为 0 时,对象被回收。

  • 示例:

    // 此时堆内存中新创建了一个对象,其被obj1引用
    // 此时引用计数器 + 1 = 1
    let obj1 = {
        name: 'obj1'
    };
    // 此时obj2也指向该对象
    // 此时引用计数器 + 1 = 2
    let obj2 = obj1;
    
    // 引用移除
    obj1 = null; // 2-1=1
    obj2 = null; // 1-1=0
    // 此时堆内存中对应的对象引用为0,等待GC清除
    

    21. 引用计数算法示意图1.png

    22. 引用计数算法示意图2.png

重大缺陷(循环引用)

引用计数简单好用,为什么现在大家都不愿意用了呢,原因就是它有一个重大的缺陷--循环引用。我愿把它称为 “左脚踩右脚,螺旋升天”,跳出三界之外,不在五行之中,从此脱离 GC 掌控。

  • 循环引用: 两个对象互相引用,但外界无引用。这种情况下,引用计数法无法回收这两个对象。

  • 示例:

    // 循环引用
    function func() {
        let obj1 = {
            name: 'obj1'
        };
        let obj2 = {
            name: 'obj2'
        };
        // obj1 引用 obj2
        obj1.innerObj = obj2;
        // obj2 引用 obj1
        obj2.innerObj = obj1;
    }
    func();
    // 函数func执行完毕后,obj1 和 obj2 互相引用,但外界无法访问它们
    // 此时按道理应该回收掉它们,但是引用计数法无法回收这两个对象
    // 原因就是obj1和obj2的引用计数不为0(互相引用,形成死循环),JS引擎无法识别出它们要被回收
    
  • 对执行上下文有疑惑的朋友可以去我的博客《藏起来的JS(三) - 执行上下文、作用域链与闭包》补补课哈 23. 引用计数算法示意图3.png

    24. 引用计数算法示意图4.png 所以现在大家转而使用标记-清除、标记-整理、Scavenge 算法之类的算法,解决了循环引用这一情况。

三. 现代 GC 运行机制(很重要)

在 JavaScript 的内存管理体系中,栈内存负责维护程序运行时的上下文状态(如变量引用、函数调用栈),其特点是读写速度极快但空间容量较小;而堆内存则是存储对象实例和数据的主要区域,也是垃圾回收(GC)的核心作用范围。要了解 GC,我们先来了解一下堆内存中的组成。

  • 以最常用的V8引擎为例,堆内存中大致可以划分为:
    • 新生代内存区(New Space / Young Generation)
    • 老生代内存区(Old Space / Old Generation)
    • 大对象区(Large Object Space)
    • 映射区(Map Space)
    • 代码区(Code Space)
    • 其他区域

只需要重点了解新生代内存区和老生代内存区即可。

7. 堆内存结构示意图1.png

新生代内存区(New Space / Young Generation)

  • 存放存活时间短的小对象(如临时变量、局部对象)。
  • GC 触发条件: From Space 分配满时触发 Scavenge 回收。
  • 副垃圾回收器进行管理内存。

结构

  • 由大小相等的分配区(From Space)和存活区(To Space)构成。

    • From Space 为当前活跃的分配区,新对象在此创建;
    • To Space 是当前闲置的回收区,用于存储 GC 时存活的对象。
  • 新生代内存区的特点: 回收频率很高,速度很快,但是空间利用率很低,因为有一半的内存空间处于"闲置"状态。这里的“闲置”状态就是指 GC 触发时,占据新生代内存区一半空间的 To Space 处于临时闲置状态,准备要与 From Space 交替使用。

    8. 堆内存结构示意图2.png

GC算法

  • 新生代内存区采用 Scavenge 算法进行管理。
    • GC 触发: 新的对象会优先分配至 分配区(From Space),GC 触发时,From Space 中存活的对象(仍被引用的对象) 会被复制到 To Space 中,其引用地址也更新为新地址。而后未被引用的对象以及被复制完的存活对象则像垃圾一样被 GC 直接清除,其占据的空间也被一并释放,整个 From Space 都将被清空。
    • GC 结束: 分配区(From Space)和存活区(To Space)将会进行两极反转。原来的 To Space(其中存储着从上次GC中存活的对象)变为新的 From Space,等待新对象的到来,而原来的 From Space(已在GC中被清空)变为新的 To Space,等待下一次 GC 时接收存活对象。
    • Scavenge 算法确保了新生代内存区在不出意外的情况下,可以无限重复使用下去。如此循环GC多次,在此期间存活多次的存活对象将晋升到老生代(对象晋升策略)。

Scavenge 算法图解

  1. GC 未触发时

    9. Scavenge 算法示意图1.png

  2. GC 触发过程1:复制存活对象至 To Space

    10. Scavenge 算法示意图2.png

  3. GC 触发过程2:原From Space被清空

    11. Scavenge 算法示意图3.png

  4. GC 结束:原From Space与原To Space交换身份

    12. Scavenge 算法示意图4.png

老生代内存区(Old Space / Old Generation)

  • 存放生命周期长的大对象。
  • GC 触发条件: 老生代内存占用超过阈值、或新生代晋升对象过多时触发标记 - 清除 - 整理。
  • 主垃圾回收器进行管理内存。

对象晋升策略

前面我们提到,在多次GC之后存活下来的存活对象将会晋升到老生代内存区,我们将其称为对象晋升策略。这其实有些偏颇了,对象的晋升情况不是只有单单的多次GC存活。

  • 情况1 GC多次存活(年龄晋升): 对象每经历一次 Scavenge GC 后仍存活,其 GC计数器 + 1,其数值可以称之为 “年龄” ,当年龄超过阈值(默认为15次,但实际上在V8引擎的动态调整中会少得多,可能2次就行了)时,对象会被晋升到老生代,可以把这个过程称之为“熬资历”。

  • 情况2 大对象: 若对象的初始大小超过了新生代单个页的容量(“页”为内存分配的单位,在64位下新生代的页通常为1 MB),V8会直接将其分配到老生代,不经过新生代。

  • 情况3 To Space占用过多: 如果 GC 时,To Space 内的占用率超过了阈值(一般为25%),存活对象会被提前晋升到老生代(即使其年龄未达标)。

  • 晋升策略决策树:

    13. 对象晋升策略决策树.png

结构

老生代的内存比新生代大得多,存的东西也多很多,所以其内部的区域划分比起新生代来说复杂很多。

14. 堆内存结构示意图3.png

  • 基于v8 11.0+引擎的老生代结构如下
    分区名称 存储内容 垃圾回收策略
    Old Space 长期存活的普通对象(含引用关系),如闭包、全局变量引用的对象 (标记-清除)+(标记-整理)
    Code Space JIT 编译后的机器码(优化后的函数) 单独标记,与数据区回收策略分离
    Large Object Space 大小超过kMaxRegularHeapObjectSize(默认约 1MB)的对象 每个对象独占内存页,直接标记清除
    Map Space 对象的 Map 元数据(描述属性布局、原型链等) 与 Old Space 协同回收,优化元数据访问
    Cell Space 小尺寸数据(如 SMI 整数、指针) 部分版本中与老生代协同管理

老规矩,我们不可能全拿下所有的分区,大多数分区其实也很难用到,了解一下就差不多了。我们要着重了解的其实是 Old Space 分区所使用的 GC 算法 标记-清除-整理

GC 算法

因为老生代区的内存大小和其中存储的对象大小远超新生代区,所以 Scavenge 算法所进行的对半开的频繁批量复制区内的对象操作太简单粗暴,也太耗费时间了。为了性能着想,老生代需要采用更适合的 GC 操作,也就是标记-清除-整理

  • 标记-清除-整理: “标记-清除-整理” 其实是指一个 GC 过程,真正的 GC 算法要分为 标记-清除 以及 标记-整理 两个算法。
    • 标记-清除(Mark-Sweep): 这是最常用的算法,简单来说就是在要清除对象上做好标记,然后再进行清除。
      • 标记阶段: 从根对象(如全局变量、调用栈中的变量)开始,递归标记所有可达对象。
      • 清除阶段: 遍历整个堆内存,回收未被标记的对象。
    • 标记-整理(Mark-Compact): 这是为了解决标记-清除算法操作之后,堆内存中出现的碎片化问题而设计的算法。简单来说就是,将呈碎片化分布的对象打好标记,然后 V8 引擎将会出手将碎片化分布的对象重新排列好。
      • 标记阶段: 同上面的标记-清除算法的标记阶段。
      • 整理阶段: 将所有存活对象移动到内存的一端,然后清除边界外的内存。

标记-清除-整理 过程图解

  1. 标记-清除 之 标记

    15. 标记-清除-整理 算法示意图1.png

  2. 标记-清除 之 清除

    16. 标记-清除-整理 算法示意图2.png

  3. 标记-整理 之 标记

    17. 标记-清除-整理 算法示意图3.png

  4. 标记-整理 之 整理

    18. 标记-清除-整理 算法示意图4.png

优化(增量标记算法)

众所周知,JS 的 V8 引擎为单线程,也就是我们常说的主线程。那么问题来了,现在主线程又要运行你的 JS 代码,又要去管理堆内存。线程只有一个,它是否会分身乏术呢?
答案是肯定的,前面提到的 Scavenge 算法以及标记-清除、标记-整理算法都会造成主线程阻塞,因为 V8 引擎的垃圾回收器在执行这些算法时必须暂停主线程的执行,引起全局阻塞(也就是前面介绍自动内存管理时的 STW)。

19. 增量标记算法示意图1.png
其中标记-清除、标记-整理这两个算法造成的 STW 尤其严重,因为标记阶段需要从根对象(如全局变量、调用栈)出发,递归遍历整个对象图,标记所有可达对象。对于大型应用,这个过程所要花费的时间可能要数百毫秒甚至更长时间,导致出现了明显的 STW。

  • 这时候聪明的 JS 设计师就采用了增量标记算法来对标记-清除-整理这个过程进行优化。
  • 增量标记算法(Incremental Marking): 将标记过程碎片化,每次只标记一小部分对象,然后暂停标记,让主线程执行一段时间,以此减少 STW 的时间,如此循环直到标记完成。
    • 过程:
      • 1. 初始标记(STW)
        • 暂停主线程,标记所有根对象(如全局变量、当前调用栈中的变量)。
        • 这个阶段通常非常快,因为根对象数量较少。
      • 2. 增量标记(与主线程交替执行)
        • 垃圾回收器每次标记一小部分对象(例如,先遍历 100 个对象),然后暂停,让主线程继续执行。
      • 3. 重新标记(STW)
        • 再次暂停主线程,处理在增量标记阶段由写屏障记录的引用变化,确保所有可达对象都被标记。
        • 这个阶段比初始标记稍长,但远短于传统标记的总时间。
      • 4. 清除 / 整理阶段
        • 回收未标记的对象,或整理内存空间。这个阶段也可以采用增量方式或并发执行(取决于具体实现)。
增量标记算法图解
  • 增量标记算法优化后的 GC

    20. 增量标记算法示意图2.png

四. 开发习惯

像 JS 这类依赖 GC 机制的语言,我们无需手动管理内存,省去很多麻烦,但是这不代表我们不用注意这方面,养成良好的开发习惯可以让我们的代码更健壮、性能更好。

避免内存泄漏:确保 GC 能识别 “不可达对象”

GC 仅回收根对象无法访问的对象,若代码让对象长期 “被根引用”,会导致内存泄漏。未声明的变量会挂载到 window(浏览器)或 global(Node),成为根对象的直接引用,长期占用内存。

  • 措施:
    • 管控全局变量
      • 少用全局变量

        // 坏代码:意外全局,导致 leakObj 永远存活
        function bad() {
            leakObj = new Array(1e6); // 未用 let/const 声明,自动全局
        }
        
        // 好代码:显式声明局部变量,函数结束后栈引用消失
        function good() {
            const safeObj = new Array(1e6);
        }
        
      • 如果实在要用全局变量(比如说缓存),用完之后主动置为null,断开根引用

        // 全局缓存,使用后清理
        window.cache = { data: new Array(1e6) };
        window.cache = null; // 让 GC 回收缓存对象
        
    • 谨慎使用闭包
      闭包会捕获外部变量,延长其生命周期(如闭包引用大对象,导致大对象无法回收)。
      • 若闭包不再使用,主动释放外部引用:

        function createClosure() {
            let data = new Array(1e6);
            const closure = () => {
                if (data) {
                    console.log(data.length);
                    data = null; // 首次使用后释放引用
                }
            };
            return closure;
        }
        
        // 使用示例
        const fn = createClosure();
        fn(); // 第一次调用:打印长度并释放data
        fn(); // 后续调用:data已为null,这就是主动释放外部引用
        
    • 处理DOM引用
      对于 DOM 节点的循环引用(如事件监听),需显式移除监听
      • 示例:

        // 坏代码:事件监听导致循环引用
        element.onclick = function () {
            // ...
        };
        
        // 好代码:组件销毁时移除监听
        element.onclick = null;
        element.removeEventListener('click', handler);
        

优化对象生命周期:协助 GC 高效回收

  • 措施:
      1. 优先使用局部变量
      • 函数内的局部变量(如 function foo() { const obj = {} })在函数执行完后,栈引用立即消失,可被 新生代 GC(Scavenge 算法)快速回收(新生代 GC 速度远快于老生代)。
      1. 及时释放无用引用
      • 对不再使用的对象,主动置 null,帮助 GC 提前识别 “不可达”(现代 GC 虽能分析,但明确释放更高效)。

      • 示例:

        function processData() {
            const bigData = new Array(1e6);
            // 处理数据...
            bigData = null; // 主动释放,加速 GC 回收
        }
        
    • 3. 解耦循环引用
      • 虽然现代 GC(标记清除)能处理循环引用,但代码层面仍建议解耦,减少 GC 分析成本。

      • 示例:

        // 坏代码:循环引用
        const a = {};
        const b = {};
        a.inner = b;
        b.inner = a;
        
        // 好代码:使用后主动断开
        a.inner = null;
        b.inner = null; 
        

降低 GC 性能开销:减少主线程阻塞

  • 措施:
    • 1. 减少短生命周期对象的创建频率

      • 频繁创建临时对象(如循环内创建 {})会导致 新生代 GC 频繁触发(Scavenge 虽快,但太频繁仍有开销),所以我们要减少短生命周期对象的创建频率。
      • 优化:
        • 复用对象(如对象池):const obj = {}; 循环内复用 obj
        • 批量处理:合并多次操作,减少中间对象(如用 Array.join 替代多次 += 拼接字符串)。
    • 2. 控制大对象的创建

      • 大对象(超过新生代阈值的对象)会 直接进入老生代,老生代 GC(标记 - 整理)代价更高。
      • 优化:
        • 拆分大对象为小对象(让小对象在新生代回收)。
        • 避免频繁创建大对象(如图像数据、大数组)。
    • 3. 监控 GC 状态

      • 浏览器: 通过 Chrome DevTools → Performance 观察 “GC Event”,若 GC 频繁且耗时久,排查代码(如大量临时对象)。

      25. 开发习惯示意图1.png

      • Node.js: 启动时添加 --trace_gc 参数输出 GC 日志,或用 v8-gc-log-parser 分析,定位是否因新生代空间不足导致频繁 Scavenge。

五. 结语

GC 固然好用,但不意味着程序员可以高枕无忧,了解一些 GC 运行机制和 GC 算法是很有必要的,可以帮助我们避开很多坑。希望这篇博客可以让你有所收获,我不胜荣幸。
如果本篇博客中有一些错误或是缺漏,欢迎你在评论区指出我的错误,大家一起进步,万分感激! 送花.png

昨天以前首页

微前端架构设计:从理论到实践的全面指南

作者 天涯学馆
2025年6月6日 15:23

随着前端开发的复杂性和规模不断增加,传统的单体前端架构逐渐暴露出维护困难、协作效率低下、部署周期长等问题。微前端(Micro-Frontend)作为一种新兴的架构模式,借鉴了后端微服务的思想,将前端应用拆分为多个独立的小型模块,极大地提升了大型项目的可扩展性和团队协作效率。


一、前言:为什么需要微前端?

1.1 单体前端的痛点

在传统的单体前端架构中,整个应用由一个代码库组成,所有功能模块紧密耦合。这种架构在小型项目中尚能应对,但随着项目规模扩大,暴露出以下问题:

  • 代码库庞大:单一代码库可能包含数十万行代码,构建和部署耗时长。
  • 团队协作困难:多个团队同时开发,代码冲突频繁,合并成本高。
  • 技术栈限制:难以在不同模块中使用不同的框架或技术版本。
  • 迭代速度慢:新功能上线需要整体回归测试,发布周期长。
  • 可维护性差:老旧代码与新功能混杂,重构成本高。

1.2 微前端的兴起

微前端架构将前端应用拆分为多个独立的小型应用,每个应用由不同团队开发、部署和维护。微前端的核心理念是“分而治之”,其优势包括:

  • 独立开发与部署:每个微前端模块可以独立开发、测试和部署,缩短迭代周期。
  • 技术栈自由:不同模块可以使用不同的框架(如 React、Vue、Angular)或版本。
  • 团队自治:各团队负责特定业务模块,减少跨团队依赖。
  • 增量升级:支持渐进式重构,老旧系统可以与新模块共存。
  • 高可扩展性:新功能的添加只需集成新的微前端模块。

1.3 适用场景

微前端适用于以下场景:

  • 跨团队协作的大型前端项目。
  • 需要整合多个技术栈或遗留系统的应用。
  • 要求高频迭代和独立部署的业务场景。
  • 希望渐进式重构老旧前端代码库的项目。

本文将围绕微前端的架构设计与实践,展开从理论到落地的全面讲解。


二、微前端的核心概念与设计原则

2.1 微前端的核心概念

微前端架构的核心是将前端应用分解为多个独立的子应用,每个子应用具有以下特性:

  • 独立性:每个微前端模块是一个完整的应用,包含自己的代码、构建流程和部署管道。
  • 组合性:通过某种机制(如 iframe、Web Components 或模块加载器)将多个微前端模块组合成一个统一的页面。
  • 自治性:每个模块由独立的团队开发,技术选型和迭代节奏互不干扰。
  • 隔离性:模块间需避免样式冲突、JavaScript 全局污染等问题。

2.2 设计原则

在设计微前端架构时,应遵循以下原则:

  1. 单一职责:每个微前端模块专注于特定业务功能,避免功能重叠。
  2. 技术无关:主应用不强制子应用的框架或技术栈,保持灵活性。
  3. 运行时隔离:确保模块间的 CSS 和 JavaScript 不相互干扰。
  4. 通信规范:定义清晰的模块间通信机制,避免直接操作 DOM 或全局状态。
  5. 独立部署:每个模块有独立的 CI/CD 流程,支持快速上线。
  6. 用户体验一致性:通过统一的 UI 组件库或设计系统,确保页面视觉和交互一致。

2.3 微前端的挑战

尽管微前端有诸多优势,但也带来了新的挑战:

  • 性能开销:多个模块的加载可能增加页面初始化时间。
  • 复杂性增加:需要额外的架构设计和工具支持。
  • 一致性问题:模块间的 UI 和交互可能出现偏差。
  • 通信成本:模块间通信需要明确的协议和治理。

后续章节将针对这些挑战提供具体解决方案。


三、微前端的实现方式

微前端的实现方式多种多样,根据项目需求和技术场景,可以选择以下几种方案。

3.1 服务端集成

通过服务端渲染或反向代理,将多个微前端模块组合成一个页面。

实现方式

  • 使用 Nginx 或 Node.js 服务端,将不同模块的 HTML 片段拼接。
  • 各模块通过 API 或 SSR(Server-Side Rendering)提供内容。

优点

  • 模块间隔离性强,适合遗留系统集成。
  • 服务端可统一处理 SEO 和首屏性能优化。

缺点

  • 服务端逻辑复杂,增加了维护成本。
  • 客户端动态交互支持较弱。

示例
在 Node.js 中使用 express 实现服务端集成:

const express = require('express');
const axios = require('axios');
const app = express();

app.get('/', async (req, res) => {
  const [header, content, footer] = await Promise.all([
    axios.get('http://header-service/render'),
    axios.get('http://content-service/render'),
    axios.get('http://footer-service/render'),
  ]);

  res.send(`
    <!DOCTYPE html>
    <html>
    <head><title>Micro-Frontend</title></head>
    <body>
      ${header.data}
      ${content.data}
      ${footer.data}
    </body>
    </html>
  `);
});

app.listen(3000, () => console.log('Server running on port 3000'));

3.2 iframe 集成

使用 iframe 将每个微前端模块嵌入主应用。

实现方式

  • 主应用提供一个容器页面,子应用通过 iframe 加载。
  • 通过 postMessage 实现模块间通信。

优点

  • 天然的运行时隔离,CSS 和 JavaScript 互不干扰。
  • 适合整合完全独立的遗留系统。

缺点

  • iframe 性能开销大,影响页面加载速度。
  • 交互体验较差(如滚动同步、URL 管理)。
  • SEO 支持较弱。

示例
主应用的 HTML:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Micro-Frontend with iframe</title>
</head>
<body>
  <div id="header">
    <iframe src="http://header-service" frameborder="0"></iframe>
  </div>
  <div id="content">
    <iframe src="http://content-service" frameborder="0"></iframe>
  </div>
  <script>
    window.addEventListener('message', event => {
      console.log('Received message:', event.data);
    });
  </script>
</body>
</html>

子应用通过 postMessage 通信:

window.parent.postMessage({ type: 'UPDATE', data: 'Hello' }, '*');

3.3 Web Components

使用 Web Components 将微前端模块封装为自定义元素。

实现方式

  • 每个微前端模块定义为一个自定义元素(如 <micro-app>)。
  • 主应用通过 DOM 操作加载和卸载模块。

优点

  • 原生支持,隔离性较好(通过 Shadow DOM)。
  • 模块化程度高,易于复用。

缺点

  • 浏览器兼容性问题(需 polyfill)。
  • 开发复杂性较高。

示例
定义一个 Web Component:

class MicroApp extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        .container { padding: 20px; }
      </style>
      <div class="container">
        <h1>Micro Frontend Module</h1>
      </div>
    `;
  }
}

customElements.define('micro-app', MicroApp);

主应用中使用:

<micro-app></micro-app>

3.4 模块联邦(Module Federation)

Module Federation 是 Webpack 5 引入的功能,允许动态加载远程模块。

实现方式

  • 主应用和子应用通过 Webpack 配置共享模块。
  • 使用动态导入加载远程模块。

优点

  • 高性能,模块按需加载。
  • 支持跨框架共享依赖(如 React、Vue)。
  • 开发体验接近单体应用。

缺点

  • 依赖 Webpack 生态,技术栈受限。
  • 配置复杂性较高。

示例
后续章节将详细讲解 Module Federation 的实现。

3.5 单页应用(SPA)路由集成

通过前端路由将多个微前端模块组合为单页应用。

实现方式

  • 主应用维护一个路由表,映射到不同模块。
  • 使用框架(如 React Router、Vue Router)动态加载模块。

优点

  • 用户体验流畅,适合现代 SPA。
  • 开发简单,易于集成现有框架。

缺点

  • 模块间隔离需要额外处理。
  • 首屏加载可能较慢。

示例
在 React 中使用 React Router:

import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

const Header = lazy(() => import('headerApp/Header'));
const Content = lazy(() => import('contentApp/Content'));

const App = () => (
  <BrowserRouter>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route path="/header" component={Header} />
        <Route path="/content" component={Content} />
      </Switch>
    </Suspense>
  </BrowserRouter>
);

export default App;

四、微前端架构设计

4.1 项目结构

一个典型的微前端项目包含主应用和多个子应用。以下是推荐的项目结构:

micro-frontend-project/
├── host/                    # 主应用
│   ├── src/
│   │   ├── App.tsx
│   │   ├── index.tsx
│   ├── public/
│   │   ├── index.html
│   ├── webpack.config.js
│   ├── package.json
├── micro-apps/
│   ├── header/             # 微前端模块:页头
│   │   ├── src/
│   │   ├── webpack.config.js
│   │   ├── package.json
│   ├── content/           # 微前端模块:内容
│   │   ├── src/
│   │   ├── webpack.config.js
│   │   ├── package.json
├── shared/                # 共享工具和类型
│   ├── types/
│   ├── utils/
├── docker/                # 部署配置
├── README.md
  • host:主应用,负责加载和组合微前端模块。
  • micro-apps:包含多个子应用,每个子应用是一个独立的项目。
  • shared:存放共享的类型定义、工具函数或 UI 组件库。

4.2 技术选型

在设计微前端架构时,需根据项目需求选择合适的技术栈:

  • 主应用框架:React、Vue 或 Angular,通常选择团队最熟悉的框架。
  • 模块加载方式:Module Federation(现代项目)、iframe(遗留系统)或 Web Components(高隔离需求)。
  • 构建工具:Webpack(支持 Module Federation)、Vite(高性能)或 Rollup(轻量)。
  • 通信机制:自定义事件、postMessage 或状态管理库(如 Redux、Vuex)。
  • UI 一致性:使用组件库(如 Ant Design、Element Plus)或设计系统。
  • 部署工具:Docker、Kubernetes 或 CI/CD 平台(如 Jenkins、GitHub Actions)。

4.3 模块通信

微前端模块间的通信需要清晰的规范。常见方式包括:

  • 自定义事件:通过 CustomEvent 派发和监听事件。
  • postMessage:适用于 iframe 或跨窗口通信。
  • 共享状态:使用轻量级的状态管理(如 Zustand、Jotai)。
  • URL 参数:通过路由参数或查询字符串传递数据。

示例(自定义事件):
主应用监听事件:

window.addEventListener('microAppEvent', (event) => {
  console.log('Received:', event.detail);
});

子应用派发事件:

window.dispatchEvent(new CustomEvent('microAppEvent', {
  detail: { type: 'UPDATE', data: 'Hello' },
}));

4.4 运行时隔离

为避免模块间冲突,需实现 CSS 和 JavaScript 隔离:

  • CSS 隔离
    • 使用 CSS Modules 或 CSS-in-JS(如 styled-components)。
    • 通过 Shadow DOM 隔离样式(Web Components)。
    • 添加命名空间(如 BEM 规范)。
  • JavaScript 隔离
    • 使用沙箱机制(如 Qiankun 的沙箱)。
    • 避免污染全局变量(如 window 对象)。
    • 使用模块化开发(ESM 或 CommonJS)。

示例(CSS Modules):
在子应用中:

/* header.module.css */
.header {
  background-color: #f0f0f0;
}
import styles from './header.module.css';

const Header = () => <div className={styles.header}>Header</div>;

五、基于 Module Federation 的微前端实现

Module Federation 是目前最流行的微前端实现方式之一,以下是详细实现步骤。

5.1 初始化项目

创建主应用和子应用项目:

mkdir micro-frontend-demo
cd micro-frontend-demo
mkdir host micro-apps micro-apps/header micro-apps/content

在每个目录下初始化 npm 项目:

cd host && npm init -y && cd ..
cd micro-apps/header && npm init -y && cd ../..
cd micro-apps/content && npm init -y && cd ../..

5.2 配置主应用

host 目录下安装依赖:

npm install react react-dom webpack webpack-cli webpack-dev-server html-webpack-plugin @types/react @types/react-dom --save-dev

创建 host/src/index.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root')!);
root.render(<App />);

创建 host/src/App.tsx

import React, { Suspense } from 'react';

const Header = React.lazy(() => import('headerApp/Header'));
const Content = React.lazy(() => import('contentApp/Content'));

const App = () => (
  <div>
    <Suspense fallback={<div>Loading Header...</div>}>
      <Header />
    </Suspense>
    <Suspense fallback={<div>Loading Content...</div>}>
      <Content />
    </Suspense>
  </div>
);

export default App;

创建 host/public/index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Micro-Frontend Demo</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

创建 host/webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  mode: 'development',
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        headerApp: 'headerApp@http://localhost:3001/remoteEntry.js',
        contentApp: 'contentApp@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, eager: true },
        'react-dom': { singleton: true, eager: true },
      },
    }),
  ],
  devServer: {
    static: path.join(__dirname, 'dist'),
    port: 3000,
    hot: true,
    open: true,
  },
};

5.3 配置子应用(Header)

micro-apps/header 目录下安装依赖:

npm install react react-dom webpack webpack-cli ts-loader typescript @types/react @types/react-dom --save-dev

创建 micro-apps/header/src/Header.tsx

import React from 'react';

const Header = () => (
  <header style={{ background: '#f0f0f0', padding: '20px' }}>
    <h1>Header Micro-Frontend</h1>
  </header>
);

export default Header;

创建 micro-apps/header/src/bootstrap.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import Header from './Header';

const root = createRoot(document.getElementById('root')!);
root.render(<Header />);

创建 micro-apps/header/webpack.config.js

const path = require('path');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  mode: 'development',
  entry: './src/bootstrap.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'headerApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Header': './src/Header.tsx',
      },
      shared: {
        react: { singleton: true, eager: true },
        'react-dom': { singleton: true, eager: true },
      },
    }),
  ],
  devServer: {
    static: path.join(__dirname, 'dist'),
    port: 3001,
    hot: true,
  },
};

5.4 配置子应用(Content)

micro-apps/content 目录下重复类似步骤,创建 Content.tsx

import React from 'react';

const Content = () => (
  <main style={{ padding: '20px' }}>
    <h2>Content Micro-Frontend</h2>
    <p>This is the main content area.</p>
  </main>
);

export default Content;

配置 micro-apps/content/webpack.config.js,将端口改为 3002,暴露 ./Content 模块。

5.5 运行项目

在三个目录下分别运行:

cd host && npm start
cd micro-apps/header && npm start
cd micro-apps/content && npm start

访问 http://localhost:3000,主应用将加载 Header 和 Content 模块。


六、使用 Qiankun 实现微前端

Qiankun 是一个基于 single-spa 的微前端框架,提供了开箱即用的解决方案。

6.1 初始化项目

创建项目结构类似 Module Federation。

6.2 配置主应用

安装 Qiankun 和依赖:

npm install qiankun react react-dom typescript ts-loader webpack webpack-cli webpack-dev-server html-webpack-plugin @types/react @types/react-dom --save-dev

创建 host/src/main.tsx

import React, { useEffect } from 'react';
import { registerMicroApps, start } from 'qiankun';

const App = () => {
  useEffect(() => {
    registerMicroApps([
      {
        name: 'headerApp',
        entry: '//localhost:3001',
        container: '#header',
        activeRule: '/header',
      },
      {
        name: 'contentApp',
        entry: '//localhost:3002',
        container: '#content',
        activeRule: '/content',
      },
    ]);
    start();
  }, []);

  return (
    <div>
      <div id="header"></div>
      <div id="content"></div>
    </div>
  );
};

export default App;

创建 host/webpack.config.js(类似 Module Federation,但无需 ModuleFederationPlugin)。

6.3 配置子应用

micro-apps/header 中,修改 src/bootstrap.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import Header from './Header';

export async function bootstrap() {
  console.log('Header app bootstrapped');
}

export async function mount(props: any) {
  const root = createRoot(props.container || document.getElementById('root')!);
  root.render(<Header />);
}

export async function unmount(props: any) {
  const root = createRoot(props.container || document.getElementById('root')!);
  root.unmount();
}

更新 micro-apps/header/webpack.config.js,添加 publicPath:

output: {
  publicPath: '//localhost:3001/',
  // ...
},

micro-apps/content 做类似配置。

6.4 运行项目

启动主应用和子应用,访问 http://localhost:3000 查看效果。


七、性能优化

7.1 模块按需加载

使用动态导入和懒加载:

const Header = React.lazy(() => import('headerApp/Header'));

7.2 缓存共享依赖

在 Module Federation 中,使用 shared 配置共享 React 等依赖,避免重复加载。

7.3 预加载

使用 preloadprefetch 优化模块加载:

<link rel="preload" href="http://localhost:3001/remoteEntry.js" as="script">

7.4 压缩与 CDN

  • 使用 Terser 和 css-minimizer-webpack-plugin 压缩代码。
  • 将静态资源部署到 CDN,加速加载。

八、团队协作与规范

8.1 模块划分

按业务功能划分模块,例如:

  • 用户管理模块
  • 订单管理模块
  • 仪表盘模块

8.2 共享组件库

创建一个共享的 UI 组件库(如基于 Ant Design):

npm init -y
npm install antd
npm publish

在各模块中引用:

npm install @your-org/ui

8.3 CI/CD 流程

为每个模块配置独立的 CI/CD 管道:

# .github/workflows/deploy.yml
name: Deploy Micro-Frontend
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16'
      - run: npm install
      - run: npm run build
      - name: Deploy
        run: npm run deploy

😧纳尼?前端也能做这么复杂的事情了?

2025年6月5日 15:34

前言

我偶然间发现一个宝藏网站,aicut.online 是一款基于本地AI实现的背景移除工具。 我研究了一下,发现他是使用了u2net模型 + onnxruntime-web实现的本地模型推理能力,下面简单介绍一下这些概念。

github:github.com/yuedud/aicu…
体验网址:aicut.online

image.png

概念

WebAssembly

  • 基本概念:  WebAssembly 是一种低级的二进制指令格式,设计目标是成为一种高效、可移植、安全的编译目标,使其能在现代 Web 浏览器中运行。你可以把它想象成一种为 Web 设计的“通用机器语言”。

  • 核心特点:

    • 高性能:  它不是解释执行的(像传统 JavaScript),而是被设计成可以以接近原生代码的速度运行。它提供线性内存模型和低级操作,便于编译器优化。
    • 可移植性:  Wasm 模块是平台无关的,可以在支持 Wasm 的任何浏览器(或运行时环境)中运行,无需修改。
    • 安全性:  它在内存安全的沙箱环境中执行,无法直接访问主机操作系统或 DOM。只能通过明确定义的 API 与宿主环境(如浏览器)交互。
    • 多语言支持:  开发者可以使用 C、C++、Rust、Go 等多种语言编写代码,然后编译成 Wasm 模块,在浏览器中运行。这使得重用现有的高性能库或编写对性能要求极高的新功能成为可能。
  • 目标:  解决 JavaScript 在处理计算密集型任务(如游戏物理引擎、视频编辑、3D渲染、科学计算、加密解密、机器学习模型推理等)时性能不足的问题,同时保持 Web 的安全性和可移植性。

  • 简单比喻:  就像为浏览器引入了一个新的、更接近硬件的“CPU 指令集”,让浏览器能直接运行编译好的高性能代码。

Onnxruntime-Web

  • 基本概念:  onnxruntime-web 是 ONNX Runtime 的一个专门构建的版本,目的是让开发者能够直接在 Web 浏览器中运行 ONNX 格式的机器学习模型

  • 核心特点:

    • ONNX 支持:  它理解并执行符合 ONNX 标准的模型文件。ONNX 是一个开放的模型格式,允许模型在各种框架之间转换和互操作。

    • 浏览器内推理:  最大的价值在于它允许 ML 模型的推理计算完全在用户的浏览器中发生,无需依赖远程服务器。这带来了低延迟、隐私保护(数据无需离开用户设备)和离线能力。

    • 多种后端执行引擎:  为了适应不同的浏览器环境、设备性能和模型需求,它提供了多种执行引擎后端:

      • WebAssembly (Wasm):  提供接近原生的性能,是主要的跨浏览器高性能后端。支持单线程和多线程(需浏览器支持)。
      • WebGL:  利用 GPU 进行加速,尤其适合某些计算模式与图形处理相似的模型(如卷积神经网络)。性能潜力高,但兼容性和精度可能不如 Wasm。
      • WebNN (预览/实验性):  旨在利用操作系统提供的原生 ML 硬件加速(如 NPU)。性能潜力最高,但目前浏览器支持有限。
      • JavaScript (CPU):  兼容性最好但速度最慢的后备方案。
    • 优化:  包含针对 Web 环境(特别是 Wasm 和 WebGL)的特定优化,以提升模型在浏览器中的运行效率。

  • 目标:  降低在 Web 应用中集成和部署机器学习模型的门槛,提供高性能、跨平台的浏览器内推理能力。

  • 简单比喻:  它是一个专门为浏览器定制的“机器学习模型运行引擎”,支持多种“驱动方式”(Wasm, WebGL, WebNN),让各种 ONNX 格式的模型能在网页里“活”起来并高效工作。

u2net

  • 基本概念:  u2net 是一种深度学习神经网络架构,特别设计用于显著目标检测任务。它的核心任务是从图像或视频中精确地分割出最吸引人注意的前景目标

  • 核心特点:

    • 嵌套 U 型结构:  这是其名称的由来(U^2-Net)。它包含一个主 U 型编码器-解码器网络,并且在每个阶段内部又嵌套了更小的 U 型块(ReSidual U-blocks, RSU)。这种设计能更有效地捕捉不同尺度的上下文信息,同时保持高分辨率的细节。

    • 多尺度特征融合:  通过嵌套的 RSU 块和跳跃连接,模型能融合来自不同深度和尺度的特征,这对精确描绘目标边界至关重要。

    • 高效性:  相比一些非常深的网络(如 ResNet),u2net 结构相对轻量,但性能优异。

    • 应用广泛:  主要用于高质量的图像/视频前景背景分割(抠图)。典型的应用包括:

      • 移除或替换图片/视频背景
      • 创建透明 PNG 图像
      • 人像分割
      • 视频会议虚拟背景
      • 图像编辑工具
  • 目标:  提供一种高效且准确的架构,解决图像中前景目标的精确分割问题。

  • 简单比喻:  u2net 是一个专门训练出来的“智能剪刀手”,它能自动识别图片里最重要的主体(比如人、动物、物体),并用极高的精度把它从背景中“剪”出来。

技术架构

架构图

+-------------------------------------------------------+
|                 **用户层 (Web Application)**          |
+-------------------------------------------------------+
|  - 用户界面 (HTML, CSS)                               |
|  - 业务逻辑 (JavaScript/TypeScript)                   |
|    * 捕获用户输入 (e.g., 上传图片/视频流)                |
|    * 调用 `onnxruntime-web` API 执行推理               |
|    * 处理输出 (e.g., 显示抠图结果,合成新背景)            |
+-------------------------------------------------------+
                            ↓ (JavaScript API 调用)
+-------------------------------------------------------+
|               **模型服务层 (ONNX Runtime Web)**        |
+-------------------------------------------------------+
|  - **onnxruntime-web** 库 (JavaScript)                |
|    * 加载并解析 **u2net.onnx** 模型文件                  |
|    * 管理输入/输出张量 (Tensor) 的内存                    |
|    * 调度计算任务到下层执行引擎                           |
|    * 提供统一的 JavaScript API 给上层应用                |
+-------------------------------------------------------+
                            ↓ (选择最佳后端执行)
+-------------------------------------------------------+
|            **执行引擎层 (Runtime Backends)**          |
+-------------------------------------------------------+
|  +---------------------+  +---------------------+     |
|  | **WebAssembly (Wasm)** | **WebGL**          | ...  |
|  +---------------------+  +---------------------+     |
|  | * **核心加速引擎**   | * 利用GPU加速            |      |
|  | * 接近原生CPU速度    | * 适合特定计算模式        |      |
|  | * 安全沙箱环境       | * 兼容性/精度限制         |      |
|  | * 多线程支持 (可选)  |                         |      |
|  +---------------------+  +---------------------+     |
|  **首选后端**           **备选/补充后端**                |
+-------------------------------------------------------+
                            ↓ (执行编译后的低级代码)
+-------------------------------------------------------+
|              **模型层 (U2Net 神经网络)**              |
+-------------------------------------------------------+
|  - **u2net.onnx** 模型文件                             |
|    * 包含训练好的 u2net 网络架构 (嵌套U型结构)            |
|    * 包含网络权重参数                                   |
|    * 格式:开放神经网络交换格式 (ONNX)                    |
|    * 任务:显著目标检测 / 图像抠图                        |
+-------------------------------------------------------+
                            ↓ (模型文件来源)
+-------------------------------------------------------+
|            **资源层 (Browser Environment)**           |
+-------------------------------------------------------+
|  - 模型文件存储: HTTP Server / IndexedDB / Cache API  |
|  - 浏览器提供: WebAssembly 引擎, WebGL API, WebNN API |
|  - 计算资源: CPU (Wasm), GPU (WebGL), NPU (WebNN)     |
+-------------------------------------------------------+

详细解释

  1. 用户层 (Web Application):

    • 这是用户直接交互的网页界面。
    • 使用 JavaScript/TypeScript 编写应用逻辑。
    • 核心操作:获取用户输入(如图片或视频帧),调用 onnxruntime-web 提供的 API 来运行 u2net 模型进行抠图推理,接收模型输出的结果(通常是掩码图或透明度通道),最后将结果渲染给用户(如显示抠好的图或与背景合成)。
  2. 模型服务层 (ONNX Runtime Web):

    • 核心枢纽。这是集成到 Web 应用中的 JavaScript 库。
    • 负责加载存储在资源层中的 u2net.onnx 模型文件。
    • 管理模型运行所需的内存(准备输入 Tensor,接收输出 Tensor)。
    • 提供简洁的 JS API(如 InferenceSession.create()session.run())供上层应用调用。
    • 最关键的作用:根据浏览器支持情况和模型需求,智能选择并调度计算任务到下层的最佳执行引擎(首选通常是 WebAssembly)。
  3. 执行引擎层 (Runtime Backends):

    • onnxruntime-web 实际执行模型计算的地方

    • WebAssembly (Wasm) 后端是核心加速引擎

      • u2net 模型的计算密集型操作(卷积、矩阵乘等)被编译成高效的 Wasm 字节码。
      • Wasm 引擎在浏览器的安全沙箱中以接近原生代码的速度执行这些字节码。
      • 这是实现高性能浏览器内推理的关键,使得复杂的 u2net 模型能在用户设备上流畅运行。
    • WebGL 后端 (备选)

      • 利用 GPU 进行加速,特别适合 u2net 中大量使用的卷积操作。
      • 性能潜力高,但可能受浏览器兼容性、WebGL 精度限制和特定模型适配的影响。
    • (可选) WebNN 后端 (未来方向) :直接调用操作系统提供的底层 AI 硬件加速(如 NPU),潜力最大,但目前支持有限。

  4. 模型层 (U2Net 神经网络):

    • 包含训练好的 u2net 模型,以 ONNX 格式 (.onnx 文件)  存储。
    • ONNX 是一个开放的、框架无关的模型表示格式,使得 u2net 模型可以被 onnxruntime-web 加载和运行。
    • 这个文件包含了 u2net 独特的嵌套 U 型结构 (U^2-Net) 的定义以及训练得到的所有权重参数。
    • 它定义了具体的抠图任务如何执行。
  5. 资源层 (Browser Environment):

    • 提供模型文件 u2net.onnx 的来源(通过 HTTP 下载、存储在 IndexedDB 或利用 Cache API)。
    • 提供运行时环境:浏览器内置的 WebAssembly 引擎负责执行 Wasm 字节码,WebGL API 用于 GPU 加速,WebNN API (如果可用) 用于底层硬件加速。
    • 提供硬件计算资源:用户的 CPU (用于运行 Wasm)、GPU (用于 WebGL)、潜在的专用 AI 处理器 NPU/APU (用于 WebNN)。

源代码解析

Github:github.com/yuedud/aicu…

目录解析

image.png

public

public是存放静态资源的地方,存储了onnx模型和一些静态的资源图片

src

src是核心代码存放的地方,下面我们只来介绍一下关于抠图部分的代码,核心代码在src/components/ImageSegmentation.js

可以看到在进入网站之后,第一时间就开始加载模型,同时使用了indexedDB进行了模型缓存,二次使用的时候直接用indexedDB里获取模型,由于模型较大,所以加载时间会比较长。

  // 加载模型
  useEffect(() => {
    const loadModel = async () => {
      try {
        setError(null);
        const db = await openDB();
        let modelData = await getModelFromDB(db);
        if (modelData) {
          console.log('从IndexedDB加载模型.');
        } else {
          console.log('IndexedDB中未找到模型,从网络下载...');
          const response = await fetch('./u2net.onnx');
          if (!response.ok) {
            throw new Error(`网络请求模型失败: ${response.status} ${response.statusText}`);
          }
          modelData = await response.arrayBuffer();
          console.log('模型下载完成,存入IndexedDB...');
          await storeModelInDB(db, modelData);
          console.log('模型已存入IndexedDB.');
        }

        const newSession = await ort.InferenceSession.create(modelData, {
          executionProviders: ['wasm'], // 'webgl' 或 'wasm'
          graphOptimizationLevel: 'all',
        });
        setSession(newSession);
        console.log('ONNX模型加载并初始化成功');
      } catch (e) {
        console.error('ONNX模型加载或初始化失败:', e);
        setError(`模型处理失败: ${e.message}`);
      }
    };
    loadModel();
  }, []);

然后可以看到在上传完图片之后进行了图片的预处理,主要是将图片转换成了模型的入参Tensor

  const preprocess = async (imgElement) => {
    const canvas = document.createElement('canvas');
    const modelWidth = 320;
    const modelHeight = 320;
    canvas.width = modelWidth;
    canvas.height = modelHeight;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(imgElement, 0, 0, modelWidth, modelHeight);
    const imageData = ctx.getImageData(0, 0, modelWidth, modelHeight);
    const data = imageData.data;

    const float32Data = new Float32Array(1 * 3 * modelHeight * modelWidth);
    const mean = [0.485, 0.456, 0.406];
    const std = [0.229, 0.224, 0.225];

    for (let i = 0; i < modelHeight * modelWidth; i++) {
      float32Data[i] = (data[i * 4] / 255 - mean[0]) / std[0]; // R
      float32Data[i + modelHeight * modelWidth] = (data[i * 4 + 1] / 255 - mean[1]) / std[1]; // G
      float32Data[i + 2 * modelHeight * modelWidth] = (data[i * 4 + 2] / 255 - mean[2]) / std[2]; // B
    }
    return new ort.Tensor('float32', float32Data, [1, 3, modelHeight, modelWidth]);
  };

然后就是将模型的入参放到模型中去推理

  const runSegmentation = async () => {
    if (!image || !session) {
      setError('请先上传图片并等待模型加载完成。');
      return;
    }
    setError(null);
    setOutputImage(null);

    try {
      const imgElement = imageRef.current;
      if (!imgElement) {
        throw new Error('图片元素未找到。');
      }

      // 确保图片完全加载
      if (!imgElement.complete) {
        await new Promise(resolve => { imgElement.onload = resolve; });
      }

      const inputTensor = await preprocess(imgElement);
      const feeds = { 'input.1': inputTensor }; // 确保输入名称与模型一致
      const results = await session.run(feeds);
      const outputTensor = results[session.outputNames[0]];
      const outputDataURL = postprocess(outputTensor, imgElement);
      setOutputImage(outputDataURL);
    } catch (e) {
      console.error('抠图失败:', e);
      setError(`抠图处理失败: ${e.message}`);
    }
  };

当模型推理完之后,进行模型推理结果的后处理,主要是将alpha通道和原图片进行合成

  // 后处理:将模型输出转换为透明背景图像
  const postprocess = (outputTensor, originalImgElement) => {
    const outputData = outputTensor.data;
    const [height, width] = outputTensor.dims.slice(-2); // 通常是 [1, 1, H, W]

    const canvas = document.createElement('canvas');
    canvas.width = originalImgElement.naturalWidth; // 使用原始图片尺寸
    canvas.height = originalImgElement.naturalHeight;
    const ctx = canvas.getContext('2d');

    // 1. 绘制原始图片
    ctx.drawImage(originalImgElement, 0, 0, canvas.width, canvas.height);
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const pixelData = imageData.data;

    // 2. 创建一个临时的canvas来处理和缩放mask
    const maskCanvas = document.createElement('canvas');
    maskCanvas.width = width; // U2Net输出mask的原始宽度
    maskCanvas.height = height; // U2Net输出mask的原始高度
    const maskCtx = maskCanvas.getContext('2d');
    const maskImageData = maskCtx.createImageData(width, height);

    // 归一化mask值 (通常U2Net输出在0-1之间,但最好检查一下)
    let minVal = Infinity;
    let maxVal = -Infinity;
    for (let i = 0; i < outputData.length; i++) {
      minVal = Math.min(minVal, outputData[i]);
      maxVal = Math.max(maxVal, outputData[i]);
    }

    for (let i = 0; i < height * width; i++) {
      let value = (outputData[i] - minVal) / (maxVal - minVal); // 归一化到 0-1
      value = Math.max(0, Math.min(1, value)); // 确保在0-1范围内
      const alpha = value * 255;
      maskImageData.data[i * 4] = 0;     // R
      maskImageData.data[i * 4 + 1] = 0; // G
      maskImageData.data[i * 4 + 2] = 0; // B
      maskImageData.data[i * 4 + 3] = alpha; // Alpha
    }
    maskCtx.putImageData(maskImageData, 0, 0);

    // 3. 将缩放后的mask应用到原始图像的alpha通道
    // 创建一个新的canvas用于绘制最终结果,并将mask缩放到原始图像尺寸
    const finalMaskCanvas = document.createElement('canvas');
    finalMaskCanvas.width = originalImgElement.naturalWidth;
    finalMaskCanvas.height = originalImgElement.naturalHeight;
    const finalMaskCtx = finalMaskCanvas.getContext('2d');
    finalMaskCtx.drawImage(maskCanvas, 0, 0, finalMaskCanvas.width, finalMaskCanvas.height);
    const finalMaskData = finalMaskCtx.getImageData(0, 0, finalMaskCanvas.width, finalMaskCanvas.height);

    for (let i = 0; i < pixelData.length / 4; i++) {
      pixelData[i * 4 + 3] = finalMaskData.data[i * 4 + 3]; // 将mask的alpha通道应用到原始图片
    }
    ctx.putImageData(imageData, 0, 0);

    return canvas.toDataURL();
  };

至此将合成的图片渲染到屏幕上就可以了。

如何启动

首先我们要对仓库进行克隆

git clone https://github.com/yuedud/aicut.git

然后安装依赖

npm install

然后直接启动项目

npm start

启动之后你就可以在本地尝试背景移除工具。

CSS 基础知识小课堂:从“选择器”到“声明块”,带你玩转网页的时尚穿搭!

2025年6月5日 23:35

🎯 一、引子:CSS 是网页的时尚设计师

想象一下,HTML 是一个人的骨架和器官,那么 CSS 就是这个人穿的衣服、发型、配饰。没有 CSS 的 HTML,就像一个穿着睡衣上街的人——虽然功能齐全,但看起来总有点不太得体。

今天,我们就来聊聊这个让网页变得“有颜值”的神奇语言:CSS(层叠样式表) 。它不仅是前端开发的“化妆师”,更是我们控制网页外观的超级武器!

🧠 二、基础概念扫盲班:什么是 CSS?

1. 声明(Declaration):属性与值的 CP 组合

在 CSS 中,一个 声明(declearation)就是一组键值对,比如:

color: red;
font-size: 20px;

这就好比你给衣服选颜色和尺码,color 是属性,red 是它的值;font-size 是属性,20px 是它的值。它们组合在一起,就是一个完整的“穿搭指令”。

2. 声明块(Declaration Block):多个声明组成的集合

这些声明不是单打独斗的,而是被包裹在一个大括号 {} 里的,形成一个 声明块

{
  color: red;
  font-size: 20px;
}

这就像是一个完整的穿搭方案,包括了颜色、大小、字体等等所有风格细节。

3. 选择器(Selector)+ 声明块 = 规则集(Ruleset)

选择器决定了这个穿搭方案要应用在谁身上,声明块则是具体的穿搭内容。

h1 {
  color: red;
  font-size: 20px;
}

这段代码的意思是:“嘿,所有的 <h1> 标题元素,你们都给我穿上红色衣服,字号调成 20 像素。”

这就是一个完整的 规则集(ruleset) ,也就是我们常说的 CSS 样式规则。

就会修饰我们的html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        h1 {
            color: red;
            font-size: 20px;
        }
    </style>
</head>

<body>
    <h1>hello 大佬</h1>
</body>

</html>

image.png

🔍 三、CSS 选择器:精准定位你的“穿搭对象”

选择器就像是一个侦探,负责找到 HTML 页面中你想打扮的元素。不同的选择器可以让你更精确地找到目标。

1. 相邻兄弟选择器:h1 + p

h1 + p {
  color: blue;
}

意思是:“紧接在 <h1> 后面的那个 <p> 元素,你要变成蓝色。”
就像你在排队时只跟紧挨着你的人打招呼一样。

📌 比如:

 <h1>我是标题</h1>
 <p>我是紧跟其后的段落</p>

这个段落会被选中。

是只与其紧挨,且只有一个

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        h1+p {
            color: red;
            font-size: 20px;
        }
    </style>
</head>

<body>
    <h1>hello 大佬</h1>
    <p>段落1</p>
    <p>段落2</p>
</body>

</html>

image.png

2. 后续兄弟选择器:h1 ~ p

h1 ~ p {
  color: green;
}

这次就不是只找第一个兄弟了,而是找所有在 <h1> 后面的 <p> 兄弟。

📌 示例:


<h1>我是标题</h1>
<p>我也是段落</p>
<p>我也想变绿!</p>

这两个 <p> 都会被选中。 后续的兄弟元素(选中类型)都会被修饰

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        h1~p {
            color: red;
            font-size: 20px;
        }
    </style>
</head>

<body>
    <h1>hello 大佬</h1>
    <p>段落1</p>
    <p>段落2</p>
    <div>盒子1</div>
    <p>段落3</p>
</body>

</html>

image.png

3. 子元素选择器:div > p

div > p {
  background-color: yellow;
}

意思是:“只要你是 <div> 的亲生孩子(直接子元素),而且你是一个 <p>,那就给你黄色背景。”

✅ 正确匹配:

<div>
  <p>我是 div 的儿子</p>
</div>

❌ 不匹配:

<div>
  <span><p>我不是亲生的</p></span>
</div>

我们来看看具体代码:一定要是指定的后代元素

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .outer>p {
            color: red;
            font-size: 20px;
        }

        .inner>p {
            color: blue;
            font-size: 30px;
        }
    </style>
</head>

<body>
    <div class="outer">
        <p>段落1</p>
        <div class="inner">
            <p>段落2</p>
        </div>
        <span>文字1</span>
    </div>
</body>

</html>

🦋 四、伪类选择器:为元素的状态“穿衣打扮”

有时候,我们不仅想根据元素的位置来打扮它,还想根据它的状态来调整样式。这时候就要用到 伪类选择器(Pseudo-classes)。

1. 行为伪类:用户正在做什么?

  • :hover:鼠标悬停
  • :active:点击瞬间
  • :focus:获得焦点(比如输入框被点击)
button:hover {
  background-color: lightblue;
}

input:focus {
  border: 2px solid orange;
}

💡 小贴士:你可以把 :hover 理解成“靠近我我就换装”,:focus 是“被选中我就发光”。

我们来看具体实现

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        button:active {
            background-color: red;
        }

        input:focus {
            background-color: yellow;
        }

        p:hover {
            background-color: blue;
        }
    </style>
</head>

<body>
    <button>点击我</button><br>
    <input type="text" placeholder="请输入">
    <p>一段文字</p>
</body>

</html>

点击按钮变成红色,聚焦输入框,背景颜色变成黄色,移动到p元素上面,背景颜色变成蓝色

B1E595BDE5B620231219_converted.gif

2. 状态伪类:元素当前的状态

  • :checked:复选框或单选按钮被选中时
  • :enabled / :disabled:元素是否启用
input:checked + label {
  color: purple;
}

📌 应用场景:当你选中某个选项时,对应的标签文字变成紫色,非常直观。

我们来看看具体实现

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        input:checked+label {
            color: red;
            font-weight: bold;
        }

        button:disabled {
            background-color: yellow;
        }
    </style>
</head>

<body>
    <input type="checkbox">
    <label for="">选项</label><br>
    <button>点击我</button>
    <script>
        let btn = document.querySelector('button');
        btn.addEventListener('click', function () {
            btn.disabled = true
        })
    </script>
</body>

</html>

我们添加了一个点击事件,点击后按钮就不可用了,我们css的修饰,按钮不可用就变成黄色,单选框被勾选label变成红色

B1E595BDE5B620232355_converted.gif

3. 结构伪类:根据结构关系来选择元素

这部分是我们今天重点学习的内容之一,特别是 nth-child()nth-of-type() 的区别。

✅ .container p:nth-child(3):第 3 个孩子必须是 p 才行

.container p:nth-child(3) {
  color: red;
}

📌 解释:

  • 容器 .container 中第 3 个子元素必须是 <p>,才会被选中。
  • 如果第 3 个是 <div> 或其他标签,就不会命中。

🧪 示例:

<div class="container">
  <p>1</p>
  <div>2</div>
  <p>3</p> <!-- 会被选中 -->
</div>

image.png

✅ .container p:nth-of-type(3):第 3 个 p 元素,不管它排第几

.container p:nth-of-type(3) {
  color: blue;
}

📌 解释:

  • 只要是 .container 中的第 3 个 <p> 元素,无论它在整个容器中的位置如何,都会被选中。
  • 类似于“按类型排队”,不看整体顺序。

🧪 示例:

<div class="container">
  <p>1</p>
  <div>2</div>
  <p>3</p> <!-- 第 2 个 p,不会被选中 -->
  <p>4</p> <!-- 第 3 个 p,会被选中 -->
</div>

image.png

🧠 对比记忆口诀:

  • .container p:nth-child:我只认“座位号”,必须坐那个位置。并且类型也要对
  • .container p:nth-of-type:我只认“类型+数量”,哪怕你坐在后排也无所谓。

🧩 五、进阶技巧:让选择器更强大

1. 属性选择器:根据属性来筛选

input[type="text"] {
  width: 200px;
}

📌 说明:所有 type="text" 的输入框,宽度设为 200px。

2. 通配符选择器:*

* {
  margin: 0;
  padding: 0;
}

⚠️ 注意:慎用,影响全局。

3. 分组选择器:多个选择器共享一套样式

h1, h2, h3 {
  color: #333;
}

📌 效果:所有一级、二级、三级标题都使用深灰色字体。


🧵 六、总结:CSS 是一门优雅的语言

通过今天的学习,我们掌握了:

  • 声明、声明块、规则集的基本结构;
  • 多种选择器的使用方法;
  • 伪类选择器的作用及分类;
  • nth-child() 与 nth-of-type() 的区别;
  • 如何用 CSS 来为网页“穿衣打扮”。

结语:CSS 很有趣,也很值得深入研究!

CSS 并不只是写几个颜色和边距那么简单,它是构建现代网页视觉体验的重要基石。掌握好选择器,就像拥有一双能精准识别“穿搭对象”的眼睛,让你的网页美得恰到好处。

希望这篇轻松幽默的技术博客,能帮助你更好地回顾今天所学的内容。如果你觉得有用,不妨点赞收藏,下次继续带你看懂更多 CSS 的奥秘!

JavaScript闭包概念和应用详解

2025年6月5日 22:44

JavaScript闭包概念和应用详解

什么是闭包?

闭包是指能够访问外部函数作用域中的变量的函数。闭包常常被用于隐藏对象的某些属性,或者封装一些数据,只提供访问接口。

闭包的形成条件

必须满足下面三个条件才可以形成闭包。

函数嵌套

一个函数A内部嵌套另外一个函数B,函数B可以访问函数A的变量,函数A也可以返回函数B,这样函数B就形成了一个闭包。

function a() {
  var outerVar = 'I am outer';
  function b() {
    console.log(outerVar);
  }
  return b;
}

var closure = a();
closure(); // 输出:I am outer

注意 不是所有嵌套函数都会形成闭包,只有内部函数引用了外部函数的变量时,才会形成闭包。

下面的嵌套函数就没有形成闭包。

 function outer() {
  function inner() {
    console.log("Hello"); // 未引用外部变量
  }
  return inner;
}
const func = outer();
func(); // 输出 "Hello"

此时 inner 未引用 outer 的变量,不会形成闭包。
函数嵌套是结构基础,但闭包的核心在于 变量引用。

内部函数引用外部变量

内部函数引用外部变量触发闭包保留外部变量,避免被垃圾回收。

function outer() {
  let count = 0;
  function inner() {
    count++; // 引用外部变量
  }
  return inner;
}
const increment = outer();
increment(); // count 被保留

inner函数引用了outer函数的变量count,当outer执行完毕之后,count不会被销毁,因为闭包(inner)仍然保留对它的引用。 若 inner 不引用 count,则 outer 执行完毕后,count 会被回收。

内部函数被暴露

内部函数被返回或者作为参数传递到其他作用域中执行,闭包就能持续存在。

// 情况1:内部函数未被暴露
function outer() {
  let count = 0;
  function inner() { count++; }
  inner(); // 内部函数未被返回或传递
}
outer(); // outer 执行后,count 被销毁

// 情况2:内部函数被暴露
function outer() {
  let count = 0;
  function inner() { count++; }
  return inner; // 暴露到外部作用域
}
const func = outer();
func(); // count 被保留

暴露方式 不限于返回,还包括作为回调传递(如 setTimeout(inner, 100))。

闭包的核心作用

保留变量的状态

闭包可以使得变量生命周期延长,不会被垃圾回收机制回收。

实现私有化变量

通过闭包封装数据,避免全局污染。

函数柯里化

通过闭包保存参数,分步传递。

闭包的常见应用场景

在模块化开发中封装私有变量和方法

const counter = (function() {
  let privateCount = 0;
  return {
    increment() { privateCount++; },
    getValue() { return privateCount; }
  };
})();

事件处理与回调

闭包可以使得事件处理函数访问到创建它的上下文中的变量。

function setupButton(buttonId) {
  let count = 0;
  document.getElementById(buttonId).onclick = function() {
    count++;
    console.log(`点击次数:${count}`);
  };
}

节流和防抖

利用闭包保存定时器状态。 [节流和防抖的详解文章](《10分钟彻底搞懂防抖和节流!前端性能优化的核心技巧,面试必问!》前端开发中,浏览器高频事件(如resize、scrol - 掘金)

闭包注意事项

内存泄漏

闭包可能导致变量无法释放,需要在不需要时手动解除引用)(如设置为null)。

// 假设我们有一个按钮元素
const button = document.getElementById('myButton');

// 我们为这个按钮设置一个点击处理函数,该函数是一个闭包
button.onclick = (function() {
  let count = 0;
  return function() {
    count++;
    console.log(`Button clicked ${count} times.`);
  };
})();

// 现在假设我们不再需要这个按钮,我们将其从DOM中移除
document.body.removeChild(button);

// 但是,由于我们的闭包仍然引用着这个按钮,它不会被垃圾回收
// 为了避免内存泄漏,我们需要手动解除闭包对按钮的引用
button.onclick = null;

闭包会使得变量一直保存在内存中,如果闭包使用不当,可能会导致内存泄漏。因此,在使用闭包时,需要注意及时释放不再使用的变量。

循环中的闭包陷阱

// 错误示例:所有定时器输出最终值 5
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}

// 解决方法:使用 let 或 IIFE 创建新作用域
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}

总结

闭包是指一个函数能够访问并记住外部作用域中的变量,简单来说就是在函数A中嵌套了函数B,函数B可以访问函数A中的变量,函数A也可以返回函数B,这样函数B就形成了一个闭包。实际开发中,闭包常常被用于保留变量状态和封装变量,举个例子,比如实现计数器、缓存数据或者像节流防抖这种控制高频操作的场景会使用闭包保存变量状态。也可以用来封装变量,将数据私有化避免全局污染。React Hooks的useSate就是通过闭包保留状态的。使用闭包需要注意处理不当可能会导致内存泄漏,比如闭包中引用了DOM元素,如果元素被移除但是闭包没有释放,内存就无法回收,这时候需要手动解除引用,比如设置为null。

TS常规面试题1

2025年6月5日 23:08

以下是 TypeScript(TS)常见的面试题分类及参考答案,覆盖基础语法、类型系统、高级特性等核心知识点:

一、基础概念

1. TypeScript 与 JavaScript 的区别?

  • TypeScript:JavaScript 的超集,静态类型检查,提供接口、枚举、类等面向对象特性,编译后生成纯 JS 代码。
  • JavaScript:动态类型,无编译时类型检查,灵活性高但容易出现运行时错误。

2. 什么是静态类型和动态类型?

  • 静态类型:编译时检查类型,如 TS、Java;
  • 动态类型:运行时检查类型,如 JS、Python。

二、类型系统

1. 如何定义基本类型?

typescript

let num: number = 10;
let str: string = 'hello';
let isDone: boolean = false;
let arr: number[] = [1, 2, 3]; // 或 Array<number>
let tuple: [string, number] = ['a', 1]; // 元组

2. 什么是联合类型(Union Type)和交叉类型(Intersection Type)?

  • 联合类型A | B,值可以是 A 或 B 类型(如 string | number)。
  • 交叉类型A & B,值同时拥有 A 和 B 的所有属性(常用于接口合并)。

3. 如何实现类型守卫(Type Guard)?

  • typeof:判断基本类型(如 typeof x === 'string')。
  • instanceof:判断类实例(如 x instanceof Person)。
  • 自定义守卫:通过函数返回类型谓词(如 function isString(x: any): x is string)。

三、接口与类

1. 接口(Interface)和类型别名(Type Alias)的区别?

  • 接口:只能定义对象结构,支持继承(extends),自动合并同名接口。
  • 类型别名:可定义基本类型、联合类型等,通过 & 合并类型,不可重复定义。

2. 如何实现类的继承和多态?

typescript

class Animal {
  constructor(protected name: string) {}
  speak(): string { return 'Animal sound'; }
}

class Dog extends Animal {
  speak(): string { return `Dog ${this.name} barks`; } // 重写方法
}

const dog: Animal = new Dog('Buddy');
console.log(dog.speak()); // 多态:输出 "Dog Buddy barks"

四、高级特性

1. 泛型(Generics)的作用?

  • 示例

    typescript

    function identity<T>(arg: T): T { return arg; }
    const num = identity<number>(10); // 或省略类型参数,自动推断
    
  • 作用:创建可复用组件,支持多种数据类型,避免重复代码。

2. 什么是装饰器(Decorator)?

  • 示例

    typescript

    function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value;
      descriptor.value = function(...args: any[]) {
        console.log(`Calling ${propertyKey} with ${args}`);
        return originalMethod.apply(this, args);
      };
    }
    
    class Calculator {
      @log
      add(a: number, b: number) { return a + b; }
    }
    
  • 作用:在不修改原代码的情况下,扩展类、方法、属性的功能。

3. 如何处理可选链(Optional Chaining)和空值合并(Nullish Coalescing)?

  • 可选链obj?.prop 或 obj?.method(),避免 obj.prop 因 obj 为 null/undefined 报错。
  • 空值合并a ?? b,仅当 a 为 null/undefined 时返回 b(区别于 ||)。

五、类型体操

1. 如何实现 Partial<T> 工具类型?

typescript

type Partial<T> = { [P in keyof T]?: T[P] }; // 将所有属性变为可选

2. 如何获取函数返回值类型?

typescript

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;

function fetchData(): Promise<string> { return Promise.resolve('data'); }
type Data = ReturnType<typeof fetchData>; // 类型为 Promise<string>

六、工程实践

1. tsconfig.json 中常见配置项的作用?

  • compilerOptions

    • target:指定 JS 版本(如 ESNext)。
    • module:指定模块系统(如 ESNext)。
    • strict:启用所有严格类型检查(如 noImplicitAny)。
    • outDir:编译输出目录。
    • moduleResolution:模块解析策略(如 Node)。

2. 如何处理第三方库的类型声明?

  • 安装官方类型包:npm install @types/xxx

  • 自定义类型声明文件(.d.ts):

    typescript

    declare module 'library-name' {
      export function func(): string;
    }
    

七、常见场景

1. 如何处理异步函数的返回类型?

typescript

async function getData(): Promise<string> {
  return 'data';
}

// 使用时需考虑 Promise 包装
const result: string = await getData(); // 正确
const wrong: string = getData(); // 错误,类型为 Promise<string>

2. 如何在 React 中使用 TypeScript?

tsx

import React, { FC, useState } from 'react';

interface Props {
  name: string;
  age?: number;
}

const App: FC<Props> = ({ name, age = 18 }) => {
  const [count, setCount] = useState<number>(0);
  return <div>Hello {name}, age {age}, count {count}</div>;
};

八、设计模式

1. 如何实现单例模式(Singleton)?

typescript

class Singleton {
  private static instance: Singleton;
  private constructor() {} // 私有构造函数

  static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

九、性能优化

1. TypeScript 会影响运行时性能吗?

  • 编译后:TS 代码被编译为纯 JS,不会影响运行时性能。
  • 编译时:类型检查会增加构建时间,但可通过 tsconfig.json 配置优化(如 incremental 选项)。

十、易错点

1. any 和 unknown 的区别?

  • any:禁用类型检查,可赋值给任意类型。
  • unknown:安全的 “未知类型”,使用前必须先断言或类型守卫。

2. 如何避免类型断言滥用?

  • 优先使用类型守卫或泛型。
  • 仅在确定类型但编译器无法推断时使用断言(如 as Type)。

JavaScript篇:"闭包:天使还是魔鬼?6年老司机带你玩转JS闭包"

2025年6月5日 21:12

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

Snipaste_2025-06-03_13-45-06.png

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

技术qq交流群:906392632

大家好,我是小杨,一个被JS闭包折磨了6年又爱又恨的前端工程师。今天我要带大家深入理解闭包这个让人又爱又恨的特性,分享那些年我踩过的坑和总结的优化技巧。

一、闭包是什么?一个简单的例子

function outer() {
    let me = '小杨';
    return function inner() {
        console.log(`大家好,我是${me}`);
    };
}
const sayHello = outer();
sayHello(); // "大家好,我是小杨"

看到没?inner函数记住了outer函数的me变量,这就是闭包!

二、闭包的三大妙用(天使面)

1. 创建私有变量

function createCounter() {
    let count = 0;
    return {
        increment() { count++ },
        getCount() { return count }
    };
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined

2. 实现函数柯里化

function multiply(a) {
    return function(b) {
        return a * b;
    };
}
const double = multiply(2);
console.log(double(5)); // 10

3. 事件处理中的妙用

function setupButtons() {
    for(var i = 1; i <= 3; i++) {
        (function(index) {
            document.getElementById(`btn-${index}`)
                .addEventListener('click', function() {
                    console.log(`我是按钮${index}`);
                });
        })(i);
    }
}

三、闭包的三大坑(魔鬼面)

1. 内存泄漏

function leakMemory() {
    const bigData = new Array(1000000).fill('*');
    return function() {
        console.log('我还记得bigData');
    };
}
const leaked = leakMemory();
// bigData本应该被回收,但闭包让它一直存在

2. 性能问题

function slowPerformance() {
    const data = {}; // 大对象
    return function(key, value) {
        data[key] = value;
        // 每次调用都要访问闭包变量
    };
}

3. 意外的变量共享

function createFunctions() {
    let funcs = [];
    for(var i = 0; i < 3; i++) {
        funcs.push(function() {
            console.log(i); // 全是3!
        });
    }
    return funcs;
}

四、闭包优化六大法则(6年经验总结)

1. 及时释放引用

function createHeavyObject() {
    const heavy = new Array(1000000).fill('*');
    return {
        useHeavy: function() {
            // 使用heavy
        },
        cleanup: function() {
            heavy = null; // 手动释放
        }
    };
}

2. 使用块级作用域

// 修复前面的共享变量问题
function createFixedFunctions() {
    let funcs = [];
    for(let i = 0; i < 3; i++) { // 使用let
        funcs.push(function() {
            console.log(i); // 0,1,2
        });
    }
    return funcs;
}

3. 避免不必要的闭包

// 不好的写法
function unneededClosure() {
    const data = {};
    return function() {
        // 根本不使用data,却形成了闭包
        console.log('Hello');
    };
}

// 好的写法
function noClosure() {
    console.log('Hello');
}

4. 使用WeakMap管理私有变量

const privateData = new WeakMap();

class MyClass {
    constructor() {
        privateData.set(this, {
            secret: '我是私有数据'
        });
    }
    
    getSecret() {
        return privateData.get(this).secret;
    }
}

5. 合理使用IIFE

// 立即执行函数减少闭包生命周期
(function() {
    const tempData = processData();
    // 使用tempData
})(); // 执行完立即释放

6. 使用模块化

// 模块化天然适合管理闭包
const counterModule = (function() {
    let count = 0;
    
    return {
        increment() { count++ },
        getCount() { return count }
    };
})();

五、真实案例分享

案例1:我曾经在项目中遇到一个页面卡顿问题,最后发现是因为一个事件处理函数形成了闭包,持有了一个大DOM树的引用。解决方案是:

// 修复前
function setup() {
    const bigElement = document.getElementById('big');
    button.addEventListener('click', function() {
        // 持有bigElement引用
        console.log(bigElement.id);
    });
}

// 修复后
function setup() {
    const id = 'big';
    button.addEventListener('click', function() {
        // 只存储需要的id
        console.log(id);
    });
}

六、总结

闭包就像一把双刃剑:
✅ 优点:实现私有变量、函数柯里化、模块化等
❌ 缺点:可能导致内存泄漏、性能问题

记住我的6年经验总结:

  1. 及时释放不再需要的引用
  2. 优先使用块级作用域
  3. 避免不必要的闭包
  4. 合理使用WeakMap和模块化
  5. 善用开发者工具检查内存

最后留个思考题:

function createFunctions() {
    let funcs = [];
    for(var i = 0; i < 3; i++) {
        funcs.push(function(j) {
            return function() {
                console.log(j);
            };
        }(i));
    }
    return funcs;
}
const funcs = createFunctions();
funcs[0](); // 输出什么?为什么?

欢迎在评论区讨论你的答案!下期我会分享更多JS高级技巧。

JavaScript篇:解密JS执行上下文:代码到底是怎么被执行的?

2025年6月5日 20:49

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

Snipaste_2025-06-03_13-45-06.png

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

技术qq交流群:906392632

大家好,我是小杨,一个和JS相爱相杀6年的前端工程师。今天我要带大家揭开JavaScript代码执行的神秘面纱,保证让你看完后恍然大悟:"原来我的代码是这样跑的!"

一、执行上下文:代码的"舞台"

想象一下,JS引擎就像个剧场,每次函数调用就像一场新的演出。而执行上下文就是这个演出的"舞台",决定了哪些"演员"(变量和函数)可以上场。

先看个简单例子:

function sayHello() {
    let me = '小杨';
    console.log(`大家好,我是${me}`);
}
sayHello();

当调用sayHello()时,JS就会创建一个新的执行上下文。

二、执行上下文的"人生三阶段"

每个执行上下文都会经历三个阶段:

  1. 创建阶段:准备舞台

    • 创建变量对象(VO)
    • 建立作用域链
    • 确定this指向
  2. 执行阶段:正式演出

    • 变量赋值
    • 函数调用
    • 执行代码
  3. 销毁阶段:演出结束

    • 出栈等待垃圾回收

三、变量提升的真相

来看个经典例子:

console.log(me); // undefined
var me = '小杨';
console.log(me); // '小杨'

为什么不会报错?因为在创建阶段,变量声明会被提升,但赋值不会。

小杨踩坑记

function test() {
    console.log(me); // undefined
    if(false) {
        var me = '小杨';
    }
}
test();

即使if条件为false,变量声明依然会被提升!

四、作用域链:变量的"寻亲记"

let name = '全局小杨';

function outer() {
    let name = '外层小杨';
    
    function inner() {
        console.log(name); // '外层小杨'
    }
    inner();
}
outer();

JS会沿着作用域链一层层往上找变量,就像寻亲一样。

五、this指向:最难捉摸的"演员"

this的指向总让人头大,记住几个规则:

  1. 普通函数调用:this指向window(严格模式undefined)
  2. 方法调用:this指向调用对象
  3. new调用:this指向新创建的对象
let obj = {
    me: '小杨',
    say: function() {
        console.log(this.me);
    }
};

obj.say(); // '小杨'
let fn = obj.say;
fn(); // undefined (this指向window)

六、闭包:执行上下文的"遗产"

function createCounter() {
    let count = 0;
    return function() {
        count++;
        console.log(count);
    };
}

let counter = createCounter();
counter(); // 1
counter(); // 2

即使createCounter的执行上下文已经销毁,内部函数依然能访问count变量,这就是闭包的魔力。

七、实战应用:避免常见坑

  1. 避免变量污染
// 错误做法
for(var i=0; i<5; i++) {
    setTimeout(() => {
        console.log(i); // 全是5
    }, 100);
}

// 正确做法
for(let i=0; i<5; i++) {
    setTimeout(() => {
        console.log(i); // 0,1,2,3,4
    }, 100);
}
  1. 合理使用闭包
// 缓存计算结果
function createCache() {
    let cache = {};
    return function(key, value) {
        if(value !== undefined) {
            cache[key] = value;
        }
        return cache[key];
    }
}

八、总结

  • 执行上下文是JS代码执行的环境
  • 变量提升和作用域链是理解JS的关键
  • this指向需要根据调用方式判断
  • 闭包可以让函数"继承"执行上下文的变量

最后留个思考题:

let obj = {
    me: '小杨',
    say: () => {
        console.log(this.me);
    }
};
obj.say(); // 输出什么?为什么?

欢迎在评论区讨论你的答案!下期我会详细讲解箭头函数的this指向问题。

面试必备 | React项目的一些优化方案(持续更新......)

作者 天天扭码
2025年6月4日 21:23

写在前面

本专栏分享一些面试时可能遇到的React相关的问题。包括性能优化,底层原理等知识。

我会用是什么、为什么、怎么做的方式为大家讲解,便于大家逻辑清晰的知道我在讲什么。

让我们开始

一、下载依赖时的 -D与-S如何选择

1.下面我言简意赅的介绍一下 -D与-S
  • -S(--save的简写) 或 --save:将依赖添加到 dependencies(生产环境依赖)

  • -D(--save-dev的简写) 或 --save-dev:将依赖添加到 devDependencies(开发环境依赖)

如下图

image.png


2.那么什么是生产环境、什么是开发环境?
  • 开发环境:程序员写代码、调试的阶段,需要各种辅助工具 (如测试库、构建工具等),这些依赖用 -D 安装。
  • 生产环境:项目实际运行时的阶段 (比如用户访问的网站/APP),只需要核心运行依赖(如React/Vue等),这些用 -S 安装。

3.那么我们为什么要区分-S和-D(开发环境与生产环境)呢?有什么实际的意义?

我总结三个实际意义,不过多解释,相信大家可以自行理解

  1. 减少生产环境体积:避免将测试工具、构建脚本等无用代码打包进线上产品,加快用户加载速度
  2. 避免安全隐患:开发工具(如调试模块)可能包含漏洞,不打包到生产环境能降低攻击风险
  3. 清晰依赖管理:团队能快速区分「项目运行需要什么」和「开发需要什么」,便于维护

4.进一步,我们讨论何时-D何时-S(用图片上的例子分析)

这里我挑几个有代表性的依赖来讲解

-S(生产依赖,项目运行必需)

(1)react & react-dom

项目运行时核心库,用户界面渲染直接依赖。

(2)axios

网络请求工具,线上环境需调用API。

(3)react-router-dom

路由管理,直接影响用户访问的页面跳转逻辑。

判断标准:这些库会被打包到最终产品代码中,用户访问网站时实际加载。

-D(开发依赖,仅开发阶段需要)

(1)eslint & @eslint/js

代码规范检查工具,只在开发时提示错误,不影响运行。

(2)@types/react

TypeScript类型定义,编译后会被移除,不参与线上运行。

(3)vite & @vitejs/plugin-react

构建工具,仅用于开发调试和打包,成品中不存在。

判断标准:这些工具只在写代码、测试或构建时使用,不会出现在用户浏览器中。

如果看到这里还是不理解-D -S,也没有关系,在之后的项目打包阶段的讲解,大家会明白

二、代码风格(React组件风格)

良好的代码风格可以让面试官眼前一亮,我们每个人都应该固定下来自己的代码风格,下面推荐一种良好的React组件风格

1.在大家的views或者pages(或者其他名字)一般放置着页面组件,那么我们写页面组件风格应该怎样呢?

请看下图

image.png

一般来讲,我们需要每一个页面组件一个文件夹文件夹中应有相应的index.jsx\index.css或者style.moduie.less,如果有页面组件有子组件的话,也可以在页面文件夹里面再建一个子组件的文件夹,文件夹里面的内容相似,值得注意的是组件文件夹的名字要首字母大写

请看下图

image.png

2.那么,这样的风格写组件有什么好处呢?

(1)首先,我们在引入组件的时候可以如下引用

image.png

我们并 不用刻意的去引用 比如说Index中的index.jsx和index.css

(2)我们在写代码的时候也不用刻意的去将About组件文件夹下的方法写成about.jsx,这样的结构更整洁,减少决策成本

(3)我们的组件文件夹首字母大写,可以和其他非组件文件夹做区分,方便开发和维护

三、循环中的key

在React 的jsx语法中,我们经常可以看到key={...}的身影,比如

<Router>
        <Routes>
          {routes.map(route=><Route key={route.path} path={route.path} element={<route.component/>}/>)}
        </Routes>
        <Button theme='primary'>按钮</Button>
</Router>

那么为什么要有这个key呢?

在 React 中,key 是动态列表渲染的关键优化手段,它的核心作用可以总结为三点

  1. 精准更新
    React 通过 key 识别列表项的唯一性,在数据变化时只更新变动的部分,而非重新渲染整个列表。
  2. 避免渲染错乱
    如果省略 key,React 默认使用数组索引(index)作为标识。当列表顺序变化时,可能导致组件状态错乱(如删除第一项后,第二项错误继承第一项的状态)。
  3. 性能提升
    稳定的 key(如路由路径 route.path)能让 React 复用已有 DOM 节点,减少不必要的销毁和重建。

万一面试官问,“你如何理解key的底层实现?” 不要慌,我十分贴心的总结了一套话术,如下

"在React中,`key`是用于优化虚拟DOM对比(Diff算法)的特殊属性,它帮助React更高效地识别列表元素的变化。

从底层实现来看,React通过虚拟DOM来最小化真实DOM操作。当组件状态变化时,React会对比新旧虚拟DOM树,这个过程称为reconciliation(协调)。对于列表渲染,如果没有key,React会默认使用数组索引(index)作为对比依据,这在列表顺序变化时会导致性能问题和状态错乱。

具体来说,key的作用体现在React Fiber架构的reconcileChildren阶段:

1.  React会为每个列表项创建Fiber节点,并记录其key
2.  在Diff过程中,React会优先匹配相同key的节点
3.  如果key匹配且组件类型相同,React会复用现有Fiber节点(仅更新props)
4.  如果key不匹配,React会销毁旧Fiber并创建新Fiber

在实际项目中,良好的key选择(如数据库ID、路由path等唯一标识)能带来两大优势:

1.  性能优化:减少不必要的DOM操作
2.  状态保持:避免列表顺序变化导致的组件状态错乱

相反,如果使用index作为key,在列表中间插入或删除项时,会导致后续所有项的key变化,引发不必要的重新渲染。而使用随机数作为key则会使Diff算法完全失效,因为每次渲染key都不同。

从源码层面看,React内部通过map结构存储key与Fiber节点的对应关系,这使得key的查找和匹配非常高效。这也是为什么稳定的key能显著提升列表渲染性能。"

四、项目打包时的优化

项目的打包可是大有讲究,我先来讲解一下react项目的打包

下图是我的项目目录

image.png

下图打包后的目录

image.png

有些离谱吗,为什么我的项目那么多的目录文件,打包过后就三个文件(js、css、html),我的依赖关系呢,我辛辛苦苦写的代码呢?我们可以随便打开一个文件看一看,如下图

image.png

有点意思,对吧? 下面我拿几个最关键的点来讲解

1.为何编译后的代码挤在了一起,甚至空格都没有?

主要是为了 减少文件体积 和 提升加载性能:通过移除所有非必要字符(如空格、注释)、缩短变量名(如 functionA → a),并启用 Gzip 压缩,最终使浏览器能更快下载和解析代码,同时节省服务器带宽成本。

空格、换行、注释等只是给程序员看的,浏览器并不需要

2.为何我们写的.jsx等其他文件都没了,打包后的文件只有html、css、js文件

打包工具(如 Webpack/Vite)会将所有 .jsx.css、图片等资源 编译合并 为浏览器可直接运行的 .js.css 和 index.html,目的是 减少HTTP请求次数 并 优化加载性能(如合并多个组件为单一JS文件、内联小资源),同时隐藏源码细节,最终输出轻量、安全的静态文件供生产环境使用

可以优化性能,一个js文件可以减少http的并发数

3.js就这一个文件,如何处理的我们原先项目的依赖关系?

打包工具(如 Webpack)通过 依赖分析 将项目中所有模块(包括第三方库和本地组件)的依赖关系 递归解析,最终按照正确的执行顺序合并到一个或多个 JS 文件中,同时通过 作用域隔离 和 模块标识符映射 来确保代码间的依赖关系在打包后依然能正确执行,既保持了模块化逻辑又优化了运行性能

比如一个最简单的例子,被依赖的放在上面,依赖的放在下面


那么回归正题,我们如何去优化项目的打包?我们把注意力放在打包的“大小”上

现在,随便打开一个之前写过的项目,里面有没有我们不需要的依赖被import引入了?这便是我们要优化的点

下面举个例子,我打包一个没有优化的项目

image.png

下面是打优化后的效果

image.png

可以观察到index-DyW19ZCt.css不论是文件大小还是传输大小都有大幅度缩减,那么是如何实现的呢

我们需要一个插件vite-plugin-style-import,用于按需自动导入组件库的样式文件,避免全量引入 CSS 导致的性能浪费。

只需三步

1.下载依赖

image.png

2.配置文件 只需在vite.config.js文件中进行以下配置

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import {createStyleImportPlugin} from 'vite-plugin-style-import'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    createStyleImportPlugin({
      libs: [
        {
          libraryName: 'zarm',
          esModule: true,
          resolveStyle: (name) => {
            return `zarm/es/${name}/style/css`;
          },
        },
      ]
    })],
})

3.修改代码

这里举个例子(zarm)

//import 'zarm/dist/zarm.css' 

只需把导入的组件UI库样式文件注释就可以了,vite-plugin-style-import 自动导入css,而且会自动压缩css

五、移动端适配

移动端的适配是面试的热门话题,那么我们该如何适配移动端,这里给出rem的方案

1.什么是 rem 单位

rem(root em)是一个相对单位,它相对于根元素(即 元素)的字体大小。例如,如果 元素的字体大小设置为 16px,那么 1rem 就等于 16px;如果 元素的字体大小变为 20px,那么 1rem 就等于 20px。这种相对性使得 rem 单位在实现响应式设计时非常有用,因为我们可以通过动态改变根元素的字体大小来调整整个页面的布局

2.为什么选择 rem 单位进行移动端响应式设计

在移动端开发中,我们通常会避免使用固定的像素单位(px),因为不同设备的屏幕尺寸和分辨率差异很大。使用 px 单位可能会导致页面在某些设备上显示过大或过小。而 rem 单位可以根据根元素的字体大小进行自适应调整,从而确保页面在不同设备上都能保持一致的布局和视觉效果

3.设计稿与 rem 的换算关系

在实际开发中,我们通常会拿到一份设计稿,假设设计稿的宽度为 750px。为了方便换算,**我们可以将 10rem 对应设计稿的 750px,那么 1rem 就等于 75px。**也就是说,当 元素的字体大小设置为 75px 时,我们可以直接根据设计稿上的尺寸进行 rem 换算。

例如,设计稿上一个元素的宽度为 150px,那么在代码中我们可以将其宽度设置为 2rem(150px / 75px = 2rem)。

下面是代码的实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用 rem 实现响应式页面</title>
    <script>
        // 立即执行函数
        (function(){
            function calc(){
                // 获取设备屏幕的宽度
                const w = document.documentElement.clientWidth;
                console.log(w);
                // 750 是设计稿的宽度
                document.documentElement.style.fontSize = 75 * (w / 750) + 'px';
            }
            // 首次加载时计算并设置根元素字体大小
            calc();
            // 当窗口大小改变时重新计算并设置根元素字体大小
            window.onresize = function(){
                calc();
            }
        })();
    </script>
    <style>
        *{
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>
    <!-- 使用 rem 单位设置元素的宽度和高度 -->
    <div style="width: 10rem;height: 2rem;background-color: red;"></div>
    <!-- 换行符为 0 -->
    <div style="font-size: 0;">
        <div style="width: 5rem;height: 2rem;background-color: green;display: inline-block;font-size: 20px;color: white;">111</div>
        <div style="width: 5rem;height: 2rem;background-color: blue;display: inline-block">222</div>
    </div>
</body>
</html>

其实这样的写也并不难,但是到这里还不能拿到office(儿化音),我们有更好的方法来处理rem——引入依赖

npm i lib-flexible

lib-flexible 是一个移动端自适应解决方案,通过动态计算根元素的 font-size 并配合 rem 单位实现页面元素的等比缩放,适配不同屏幕尺寸。——它可以自动完成之前我们的rem配置

这就够了吗?还不够!如果仅仅是这样的话,我们在设置宽度等大小的时候还得手动换算比如100px=XXrem,这还不够,还有一个插件——postcss-px2rem

npm i postcss-px2rem

它可以完成自动换算px->rem

代码如下

.index{
    width:150px;
    span{
        color:red;
    }
}

页面的渲染如下

image.png

六、跨域解决方案

这部分下一篇再写吧,玩一会儿星露谷去,嘿嘿

结语

通过本专栏,我们从 依赖管理代码规范性能优化 到 移动端适配,系统梳理了React面试的核心知识点。掌握这些内容,能让你在开发中更高效地 减少生产包体积提升渲染性能,并轻松实现 多端适配

保持技术好奇心,持续探索React生态的深度。下期我们将深入 跨域解决方案等内容,帮你彻底打通前后端联调的任督二脉!

JavaScript篇:如何实现add(1)(2)(3)()=6?揭秘链式调用的终极奥义!

2025年6月3日 08:16

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

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

技术qq交流群:906392632

大家好,我是小杨,一个沉迷于JavaScript各种骚操作的前端老司机。今天咱们来玩点有意思的——如何实现一个可以无限链式调用的add函数,最终在空调用时返回累加结果?听起来是不是很酷?让我们一步步揭开这个技巧的神秘面纱!

一、先看看我们要实现的效果

add(1)(2)(3)();      // 期望输出 6
add(1,2,3)(4)();     // 期望输出 10
add(1)(2,3)(4,5)();  // 期望输出 15

这种函数调用方式在函数式编程中被称为柯里化(Currying) ,但比普通柯里化更灵活,因为它支持:

  1. 单参数或多参数调用
  2. 无限链式调用
  3. 空调用时返回计算结果

二、基础版:单参数链式调用

我们先从简单的单参数版本来理解核心思路:

function add(num) {
  let sum = num;
  
  const innerAdd = (nextNum) => {
    if (nextNum === undefined) {
      return sum;
    }
    sum += nextNum;
    return innerAdd;
  };
  
  return innerAdd;
}

console.log(add(1)(2)(3)()); // 输出 6

关键点解析

  1. add函数初始化累加值

  2. 返回的innerAdd函数可以:

    • 接收新数字并累加,然后返回自身(继续链式调用)
    • 无参数调用时返回累加结果
  3. 通过闭包保持对sum的引用

三、升级版:支持多参数调用

现在我们来增强功能,支持每次调用传入多个参数:

function add(...args) {
  let sum = args.reduce((acc, val) => acc + val, 0);
  
  const innerAdd = (...nextArgs) => {
    if (nextArgs.length === 0) {
      return sum;
    }
    sum += nextArgs.reduce((acc, val) => acc + val, 0);
    return innerAdd;
  };
  
  return innerAdd;
}

console.log(add(1,2,3)(4)()); // 输出 10
console.log(add(1)(2,3)(4,5)()); // 输出 15

改进点

  1. 使用剩余参数...args接收任意数量参数
  2. reduce计算参数总和
  3. 同样通过闭包保持sum的状态

四、终极版:支持直接取值和链式调用

有时候我们可能想直接获取当前值而不需要空调用:

function add(...args) {
  let sum = args.reduce((acc, val) => acc + val, 0);
  
  const innerAdd = (...nextArgs) => {
    sum += nextArgs.reduce((acc, val) => acc + val, 0);
    return innerAdd;
  };
  
  // 添加valueOf方法,可以在需要原始值时自动调用
  innerAdd.valueOf = () => sum;
  
  // 添加toString方法,方便输出查看
  innerAdd.toString = () => sum.toString();
  
  return innerAdd;
}

// 使用方式1:传统空调用
console.log(add(1)(2)(3)()); // 输出 6

// 使用方式2:直接参与运算(自动调用valueOf)
const result = add(1)(2)(3) + 4; // 6 + 4 = 10
console.log(result); // 输出 10

// 使用方式3:直接输出(自动调用toString)
console.log(add(1)(2)(3)); // 输出 6

高级技巧

  1. 实现valueOf方法让对象在需要原始值时自动转换
  2. 实现toString方法让对象在被当作字符串时友好显示
  3. 这样函数既可以被链式调用,也可以直接参与运算

五、原理深度剖析

这个实现的魔法主要依赖于几个JavaScript特性:

  1. 闭包(Closure) :内部函数保持对外部变量的引用
  2. 高阶函数(Higher-order Function) :函数返回函数
  3. 剩余参数(Rest Parameters) :处理不定数量参数
  4. 对象原始值转换:通过valueOftoString控制对象到原始值的转换

六、实际应用场景

虽然这种写法看起来很炫酷,但在实际项目中要谨慎使用。适合的场景包括:

  1. 构建数学计算库的流畅接口
  2. 创建DSL(领域特定语言)
  3. 函数式编程工具函数
  4. 面试时展示JS功底(笑)

七、扩展思考:如何实现减法?

基于同样思路,我们可以扩展出支持加减乘除的链式计算器:

function calc(initial = 0) {
  let result = initial;
  
  const methods = {
    add: (...args) => {
      result += args.reduce((a, b) => a + b, 0);
      return methods;
    },
    subtract: (...args) => {
      result -= args.reduce((a, b) => a + b, 0);
      return methods;
    },
    valueOf: () => result
  };
  
  return methods;
}

const total = calc()
  .add(1,2).subtract(3)
  .add(4).add(5,6) + 7;
console.log(total); // 输出 16

八、总结与最佳实践

  1. 核心模式:函数返回函数 + 闭包保存状态

  2. 参数处理:使用剩余参数处理多参数情况

  3. 终止条件:空调用或隐式转换触发结果返回

  4. 注意事项

    • 这种模式可能降低代码可读性
    • 在团队项目中要确保大家都理解这种写法
    • 考虑使用TypeScript添加类型提示

记住,强大的JavaScript特性是一把双刃剑,用得好能让代码更优雅,用不好会让同事抓狂。关键是找到平衡点!

❌
❌