阅读视图

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

TikTok发表声明:拜登政府需就禁令提供更多明确信息

短视频社交媒体平台TikTok当地时间17日发表声明称,除非拜登政府和美国司法部提供“不会强制执行禁令”的明确信息,否则TikTok将于19日被迫停运。TikTok称,拜登政府和司法部未向TikTok的服务提供商提供“不会强制执行禁令”的明确性保证,而这些服务提供商是维持超过1.7亿美国用户继续使用TikTok软件所不可或缺的。TikTok希望拜登政府和司法部立即提供明确性声明,表示不会强制执行即将生效的禁令,否则将不得不于19日暂停运营。 (央视新闻)

美国重要肉鸡生产州的商业家禽养殖场首次检出H5N1型禽流感病毒

当地时间1月17日晚,美国佐治亚州农业官员宣布,自2022年禽流感疫情暴发以来,该州首次在商业家禽养殖场中检测到H5N1型禽流感病毒。所有位于约10公里范围内的商业家禽养殖场都已关闭并被隔离,并进行至少两周的监测。根据佐治亚大学基于2022年数据的分析,该州是美国最大的肉鸡生产州,家禽产业的估值为67亿美元。(证券时报)

长三角铁路迎春运首个周末客流高峰 此前4天已发送近1000万人次

从中国铁路上海局集团有限公司获悉,长三角铁路17日发送旅客270.90万人次;18日迎来春运开启后的首个周末客流高峰,预计发送旅客300万人次,计划增开旅客列车192列(其中直通旅客列车138列,管内旅客列车54列),安排200列动车组重联运行,加挂车辆90辆。 2025年春运以来,长三角铁路已累计发送旅客近987.60万人次,日均发送旅客量达246.90万人次。 (财联社)

如何优雅地处理第三方网站高清 Logo 显示?

今天正式发布了我的第一个新标签页插件:Next Idea NewTab

og.webp

这是一个为开发者、独立开发者量身打造的新标签页插件,主要功能分3个模块:

  • 精选的优质开源项目,分析项目潜在价值,帮助开发者找到Idea
  • 实时更新的 GitHub Trending,不错过当下热门项目
  • 开发者工具箱,高效便捷

有了这个插件,每次打开浏览器都能查找新 idea,一下子缩短了信息获取路径。

经常用掘金官方插件的朋友都觉得中间的 GitHub Trending 很眼熟。没错,掘金,你被我致敬了!感谢过去5年掘金一直在新标签页为我提供信息🫡

这篇博客的主要目的不是介绍插件,是要讲一个插件里出现的一个 bug。

bug描述

在插件的右侧,是一大批工具的展示,每个工具都需要显示 logo,问题恰恰出现在 logo 的展示上。

logo 最初的展示方案是,手动从第三方网站里找到高清 logo 地址,如果没有高清 logo,则使用第三方服务获取 logo。这个方案在我测试的时候没发现问题,上线后有用户反馈页面上出现 bug:

bug-2.webp

我第一反应就知道和网络稳定性有关。

解决这个 bug 的思路也简单:

  • 找到高清的 Logo
  • 做好回退方案

表演一下AI编程

启动 AI,发送 “我的产品里要展示不同网站的高清 logo,并且要做好回退方案,给我写一个 React 组件”。

AI 就开发分析如何找到高清 logo:

  • 加载 https://${domain}/apple-touch-icon.png
  • 利用 Google、DuckDuckGo 等第三方服务抓取

并且提供了一份组件代码:

import React, { useState } from 'react';

const WebsiteLogo = ({ domain, size = 32, className = "" }) => {
  const [imgSrc, setImgSrc] = useState(`https://${domain}/apple-touch-icon.png`);
  const [fallbackIndex, setFallbackIndex] = useState(0);
  
  const fallbackSources = [
    `https://${domain}/apple-touch-icon.png`,
    `https://${domain}/apple-touch-icon-precomposed.png`,
    `https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
    `https://icons.duckduckgo.com/ip3/${domain}.ico`,
    `https://${domain}/favicon.ico`,
  ];

  const handleError = () => {
    const nextIndex = fallbackIndex + 1;
    if (nextIndex < fallbackSources.length) {
      setFallbackIndex(nextIndex);
      setImgSrc(fallbackSources[nextIndex]);
    }
  };

  return (
    <img 
      src={imgSrc}
      alt={`${domain} logo`}
      width={size}
      height={size}
      onError={handleError}
      className={`inline-block ${className}`}
      style={{ objectFit: 'contain' }}
    />
  );
};

export default WebsiteLogo;

这个组件已经实现了自动处理错误和降级方案,并且可以自定义大小和样式,看起来已经可行了。

但是咱们可是程序员,哪能被 AI 糊弄,继续鞭打,要求在加载状态添加占位符、logo 展示要有过渡效果、提供兜底的回退方案,并且每个回退方案只给1秒加载时间,超出即当作失败,去找下一个回退方案。

这样一份非常健壮的第三方网站 logo 展示的组件就实现好了,代码我放在开源项目 nextjs-15-starter 了,仓库的演示网站也能体验效果。

核心处理方法在这里:

  useEffect(() => {
    let timeoutId

    if (isLoading) {
      timeoutId = setTimeout(() => {
        handleError()
      }, timeout)
    }

    return () => {
      if (timeoutId) {
        clearTimeout(timeoutId)
      }
    }
  }, [imgSrc, isLoading])

  const handleError = () => {
    const nextIndex = fallbackIndex + 1
    if (nextIndex < fallbackSources.length) {
      setFallbackIndex(nextIndex)
      setImgSrc(fallbackSources[nextIndex])
      setIsLoading(true)
    } else {
      setHasError(true)
      setIsLoading(false)
    }
  }

现在组件就完成了如下任务:

  • 多重备选图标源,确保最大程度显示成功
  • 加载状态显示占位符
  • 超时处理机制
  • 优雅的降级显示(使用域名首字母)
  • 可自定义大小和样式

有了这个组件就能轻松解决不同网站的 favicon 格式不一、图标无法加载、加载超时等等痛点,希望同样有 logo 展示需求的朋友用起来!

关于我

🧑‍💻独立开发|⛵️出海|Next.js手艺人
🛠️今年致力于做独立产品和课程

欢迎在以下平台关注我:

美国电动车商Canoo申请破产保护,曾向NASA供货、与沃尔玛签协议

美国电动汽车初创公司Canoo当地时间1月17日宣布申请破产保护,并立即停止运营。Canoo表示,公司已向美国国家航空航天局、美国国防部和美国邮政总局等知名机构供货,还与沃尔玛等企业签订协议,但一直未能从美国能源部贷款项目办公室获得资金支持。最近,Canoo与海外资本有过洽谈,“鉴于这些努力均未成功,董事会做出了申请破产保护的艰难决定”。(界面)

英伟达回应美国晶圆代工限制新规:不会影响公司在华业务或产品销售

1月18日电,英伟达日前向美国证监会提交文件,就美国对华晶圆代工限制新规回应称,预计英伟达在华业务或产品销售不会受此影响。美国商务部工业与安全局(BIS)近日宣布修订出口管理法规(EAR),要求提供与高级计算集成电路(IC)相关的更多尽职调查程序,另外公布经批准的IC设计实体名单,这些企业设计的芯片将不会受到额外限制。英伟达在文件中表示,该公司不受额外尽职调查程序的影响,因该公司已是经批准的IC设计实体。 (财联社)

支付宝“碰一下”能够奇袭微信支付吗?

文|彭倩

编辑|乔芊

当“扫码支付”贴遍大街小巷,支付宝要靠什么撼动由一张张二维码构筑而成的支付高墙?

商家最早嗅到支付宝的野心。2024年中,不少线下连锁品牌商和服务商们发现,支付宝正在推广一个名为“碰一下”的新支付方式,这为他们带来了新的商机。

“每个入驻的实体商户每月最高6000元补贴;推广员也能获得奖励,办理一台,入驻一个商户,推手获得560元佣金,日入可达千元;针对服务商,开户奖励100-150元/台,动销奖40元/60元/80元/台/月X6个月。”

自7月8日正式上线上海、长沙、武汉、青岛、成都和杭州等第一批城市后,不过4个多月的时间,支付宝“碰一下”已经覆盖了50多个城市,数百万家大店小店。“支付宝看来是想干掉扫码支付,动作很快。”一位服务商向36氪评价。

在支付宝内部,“碰一下”算得上这两年的头号项目。据36氪了解,2023年初,蚂蚁集团董事长井贤栋在内部提出要调研比扫码支付更方便的支付方式,比如叠加使用NFC技术;2023年7月,原型设计启动;一年后,“碰一下”正式推出。


第三方数据显示,虽然仍是最大的第三方支付APP,但支付宝在中国第三方移动支付市场的份额已从2014年的接近80%下跌至2023年底的55%;其用户数据也遭遇增长瓶颈,逐渐落后于微信支付,支付宝官方自2019年公布用户数达到10亿后不再更新数据。

“支付宝早就想推广NFC,一开始推扫码支付是被动应战微信支付,那时NFC技术也不够普及,支付宝如今肯定希望能够继续做市场的引领者,在支付方式上有新突破。”一位支付员工告诉36氪。

2023年起,支付宝提出“双飞轮”,要同时做好互联网平台和支付APP,近期还再次调整了组织,新成立支付宝事业群和数字支付事业群两大事业群,一边加码内容、开放商业化寻找增量,一边则尝试在支付领域有所创新,巩固支付工具的定位。

支付宝碰一下设备,图片来自支付宝官方

随处可见的“碰一下”

用户很快感受到“碰一下”的存在。

居住在长沙的小敏发现,走进小区附近的仟吉饼店、零食很忙、钱大妈、名创优品等连锁店甚至餐饮店,“碰一下”的设备随处可见。

一直使用安卓手机的小敏觉得这个新玩意比扫码支付更快:最初设置好权限后,不需要再打开支付宝,手机解锁后碰一下蓝色环形识别区,几乎在同一时间,支付设备上传来一声清脆的“叮”,随即屏幕上便显示出支付成功的信息,整个过程不到3秒。

一位移动支付业内人士告诉36氪,“碰一下”基于NFC(Near Field Communication,近场通信)技术,所以使用体验上和NFC很类似,但实际上也和扫码支付同源。

支付宝重拾之前,NFC曾是个没讲好的“老故事”。十多年前,苹果就曾尝试推广NFC支付,但并不成功,主因是POS机费率高达2-3%,商家意愿不高,再加上NFC在当时并不普及,许多品牌智能机没有搭配这个技术。“碰一下”的费率只有3‰,且该技术如今也已普及。

第一批城市目前“碰一下”的覆盖密度很高。

上海是支付宝首个试点的城市,“碰一下”已经从静安大悦城的第一家店铺到了青浦郊区的华新菜市场。在长沙,无论是国金街、海信广场等21个商业街和综合体,还是黑色经典臭豆腐、绝味鸭脖、零食很忙、戴永红等零食连锁店,或是美宜佳、711等连锁便利店、餐饮品牌店等上万家店铺都已装机。

“碰一下”的使用场景也更丰富,不仅可以支付,还能点餐。小敏曾在国金街的一家火锅店“碰一下”点餐,过程很丝滑,不需要分别打开微信和美团点评,支付宝内就能完成从下单到支付的闭环。

碰一下可以直接点餐,图片由作者拍摄

扫码支付诞生后统一支付生态至今,已许久没出现能与之一战的支付方式,即便7年前因智能机均推出人脸识别得以大规模商用的刷脸支付,离成为主流支付方式仍有不小的距离。两年前微信主推的刷掌支付,除了校园等特定场景有小范围普及,实际没有激起多大水花。

企业如今纷纷缩减预算的大环境下,支付宝推广“碰一下”的决心因此显得颇为瞩目。

据36氪了解,仅4个月推广补贴加装机超过百亿元,支付宝内部也不断抽调人员为项目组补充人力。一个内部说法是,支付宝计划投入至少300亿元做首批推广。

“碰一下”的确在一定程度上起到了为支付宝拉新引流的作用。

Joe定居北京,他平常只用京东和拼多多购物,生活服务则使用微信和美团,此前没有下载过支付宝,但最近小区楼下的钱大妈店员几乎每次在他付款时都会拿起“碰一下”的机子和他推销一番。

支付宝还沿用了类似集五福的游戏化玩法

“刚开始很不习惯,现在也会时不时用一下,架不住有优惠,有时候能优惠好几块钱。”不过Joe坦言,一般没有店员推荐或者没有抢到大额优惠,还是习惯用微信扫码。

36氪采访一圈上海、杭州和长沙的用户后还发现,“碰一下”更利好安卓用户,因为这对他们而言几乎是个全新的功能,使用步骤也比扫码明显减少。

iOS用户对“碰一下”则抱怨颇多。囿于苹果iOS对生态之外NFC使用的限制,不少iOS用户称“碰一下”不如苹果自带的NFC支付方便:“不是简单碰一下,每次要唤起支付宝,特别麻烦,安卓就跟刷门禁卡似的,滴一下就行”。对于许多iOS用户而言,碰了仍要设置权限、打开支付宝APP,和扫码几乎没有区别,iOS用户的共识是,除非支付宝和Apple Pay合作推虚拟卡,否则还是不方便。

占领线下

对支付宝而言,颇具战略意义的是,那些从前只在淘宝下单时使用支付宝,在线下更喜欢用微信支付的用户,因为“碰一下”,更愿意用支付宝在线下付款了。

借助淘系电商,支付宝在线上的转化颇佳,但在线下却一直缺乏更有力、有差异化的抓手。

这是有历史原因的。支付宝曾早微信一年(2011年)推出了条码支付,还抢先推广了二维码(2012年),但当时阿里体系内缺乏成熟的移动端产品,手淘2013年10月才上线,all in移动化则是2014年,支付宝也只是单纯的收支工具,扫码支付没能在支付宝手里立刻普及开来。

这给了微信支付机会。它很快凭借微信的社交能力,结合“扫一扫”功能,以扫码点餐、识别验票等场景在线下渗透地更快更深,吃尽移动化的红利,支付宝由此也被迫从开拓者变成了追随者。

上线半年后,许多用户已经开始频繁使用“碰一下”这个新鲜的功能,但处在早期的“碰一下”仍有不少仍待解决的问题,现在谈能否取代扫码支付也尚早。

首当其冲的是安全和信任问题。在一些用户眼里,“碰一下”太方便反而弄巧成拙:扫码支付要输入密码,人脸和指纹也都需要生物识别,但是碰一下连验证步骤都省去了。“扫码付款是把钱递出去,给多少,给不给,我有变更的余地,碰一下是让人没有掌控感,没有安全感。”一位用户对36氪说。还有人担心自己如果忘记锁屏手机保持常亮,放口袋或包里,在大街上就能被随意“碰一下”。

一位“碰一下”代理商则认为,碰一下的场景其实受到一定限制。二维码可以贴得到处都是,允许的距离更远,部分商场停车场还可以提前扫小票code并行处理,碰一下仍需要贴上机器,商家得排队处理订单,高峰时期根本忙不过来。“对商家而言其实还是扫码更高效,可以多个人同时扫码付款,按照目前的硬件模式,没有补贴商家配合意愿会比较低。”

目前,愿意积极推广“碰一下”的商家仍主要是各类连锁零食糕点店和购物中心品牌店,因为“碰一下”能让用户直接加入会员体系,有利于商家私域运营。而拥有自助结算系统的大品牌如MUJI和优衣库,至今都未接入“碰一下”。

更微小的商业体,如夫妻店和各类个体户则没有足够的精力来运营和推广碰一下的设备,店主往往是拿了补贴后就把机子搁置在收银台,为了省事,不会向用户主动推荐。由于“碰一下”需要商户和用户网络环境都保持良好状态(NFC只需要商户POS联网),这类小店常常网络不稳定以至于耽误用户付款。

一位知乎用户曾感慨:“没有任何App像支付宝一样,用户多、里面又有钱,但打开APP的目的是给你展示一堆广告、小游戏、视频,唯独不想让你线下支付。”App越做越重是互联网行业难以扭转的趋势,支付宝只有增添“碰一下”这样的创新场景,才有机会重塑消费者支付心智,稳固国民支付App的地位。

美国联邦贸易委员会同意雪佛龙以530亿美元收购赫斯

美国联邦贸易委员会(FTC)周五表示,已批准一项同意令,以解决雪佛龙(CVX.US)以530亿美元收购赫斯(HES.US)所涉及的反垄断问题。虽然拟议中的收购已经通过了联邦贸易委员会的反垄断审查,但最后一个障碍仍然存在——埃克森美孚(XOM.US)对这笔交易提出了新的挑战。一个由三名法官组成的仲裁小组将于5月晚些时候审理此案。(界面)

Vue模板知识点

前言

Vue的两大特征是模板化、组件化。模板的优点是提高开发效率,按照规定的结果书写代码就能够快速完成页面开发。缺点也很明显,就是固定的模板结构牺牲了一定的灵活性,Vue提供了一系列的API来增加模板的灵活性。

一、动态数据

Mustache

 <template>
   <span>{{msg}}</span>
   <span>{{count > 99 ? 99: count}}</span>
 </template>

Mustache不仅支持数据、还支持method、computed、逻辑运算等。这是因为Vue在解析模板的时候会对Mustache里面的内容进行判断,包装成不同的解析函数,如果是数据直接返回,如果是逻辑运和函数则取运算结果。

computed和watch

思想上,computed注重运算结果,watch注重过程。watch直接进行拦截监听,数据变化时运行一些逻辑,computed则需要先进行依赖收集,对依赖进行监听,当依赖发变化的时候触发重新计算。computed是vue自发的进行依赖的收集监听。并在依赖变化时执行对应的渲染函数触发视图的更新,而watch则可以添加更多开发者自定义的逻辑。

特点:computed自发的,不可以控制,watch灵活可控,wathch有immediate、once、deep等。

<template>
 <div>选择了{{ choose }}件商品</div>
 <div>总价是:{{ totalPrice }}</div>
 <button @click="addChoose">add</button>
</template>

<script>
export default {
 name: "App",
 data() {
   return {
     choose: 0,
     price: 19.9,
   };
 },
 methods: {
   addChoose() {
     this.choose++;
   },
 },
 computed: {
   totalPrice() {
     return this.choose * this.price;
   },
 },
 watch: {
   choose(newVal, oldVal) {
     console.log(`choose从${oldVal}变成了${newVal}`);
   },
 },
};
</script>

watch中使用deep、immediate、once

  watch: {
   goodsInfo: {
     handler(newVal, oldVal) {},
     deep: true, // 深度监听,监听子属性
     immediate: true, // 初次赋值的时候也触发监听回调
     once: true, // 只监听一次
   },
 },

二、 动态结构(slot)

插槽: 父组件定义结构,子组件指定结构的位置。插槽分为三种:

  • 默认插槽
  • 具名插槽
  • 作用域插槽

2.1 默认插槽

// 父组件
<template>
 <HelloWorld>
   <span>这是传递给子组件的默认插槽</span>
 </HelloWorld>
</template>

<script>
import HelloWorld from "./HelloWorld";
export default {
 name: "App",
 components: {
   HelloWorld,
 },
};
</script>

// 子组件
<template>
 <div class="child-component">
   <slot></slot>
 </div>
</template>

<script>
export default {
 name: "HelloWorld",
};
</script>

2.2 具名插槽

默认插槽只能定义一个结构,如果想定义多个结构就要使用具名插槽。其中v-slot:header可以缩写为#header。

// 父组件
<template>
 <HelloWorld>
   <template v-slot:header> <span>header</span> </template>
   <template v-slot:content> <span>header</span> </template>
   <template #footer> <span>header</span> </template>
 </HelloWorld>
</template>

<script>
import HelloWorld from "./HelloWorld";
export default {
 name: "App",
 components: {
   HelloWorld,
 },
};
</script>

// 子组件
<template>
 <div>
   <span>HelloWorld</span>
   <slot name="header"></slot>
   <slot name="content"></slot>
   <slot name="footer"></slot>
 </div>
</template>

<script>
export default {
 name: "HelloWorld",
 data() {
   return {
     user: {
       name: "jack",
       age: 21,
     },
     date: Date.now(),
   };
 },
};
</script>

默认插槽只有一个所以没必要给它起名字,但实际上默认插槽也有自己的名字:default

   <slot></slot>

   // 等同于上面写法
   <slot name="default"></slot>

2.3 作用域插槽

作用域插槽也分为匿名作用域插槽和具名作用域插槽, 作用域插槽就是在默认插槽和具名插槽的基础上增加了“数据”的能力,即在父组件定义的插槽结构中可以使用子组件提供的数据。

  1. 默认作用域插槽
// 父组件
<template>
  <HelloWorld v-slot:default="slotProps">
    {{ slotProps.user.name }}
    {{ slotProps.user.age }}
    {{ slotProps.date }}
  </HelloWorld>
</template>

<script>
import HelloWorld from "./HelloWorld";
export default {
  name: "App",
  components: {
    HelloWorld,
  },
};
</script>

// 子组件
<template>
  <div>
    <slot :user="user" :date="date"></slot>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      user: {
        name: "jack",
        age: 21,
      },
      date: Date.now(),
    };
  },
};
</script>

针对默认具名插槽下面的三种书写是等效的:

  <HelloWorld v-slot:default="slotProps">
    {{ slotProps.user.name }}
    {{ slotProps.user.age }}
    {{ slotProps.date }}
  </HelloWorld>

   <HelloWorld v-slot="slotProps">
    {{ slotProps.user.name }}
    {{ slotProps.user.age }}
    {{ slotProps.date }}
  </HelloWorld>

    <HelloWorld #default="slotProps">
    {{ slotProps.user.name }}
    {{ slotProps.user.age }}
    {{ slotProps.date }}
  </HelloWorld>
  1. 具名作用域插槽
// 父组件
<template>
  <HelloWorld>
    <template #user="userProps">
      {{ userProps.user.name }}
      {{ userProps.user.age }}
    </template>
    <template #date="dateProps">
      {{ dateProps.date }}
    </template>
  </HelloWorld>
</template>

<script>
import HelloWorld from "./HelloWorld";
export default {
  name: "App",
  components: {
    HelloWorld,
  },
};
</script>

// 子组件
<template>
  <div>
    <slot name="user" :user="user"></slot>
    <span>content</span>
    <slot name="date" :date="date"></slot>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      user: {
        name: "jack",
        age: 21,
      },
      date: Date.now(),
    };
  },
};
</script>

三、模板的编译过程

Vue是如何将 .vue文件渲染成html的?

  1. 对tempalte进行编译,结合依赖收集和作用域插槽,生成渲染函数。
  2. 调用渲染函数,生成虚拟dom。
  3. 将虚拟dom渲染成真实dom。

虽然在vue中可以直接写render函数,但最好还是使用vue模板,因为这更符合vue的模板理念,更重要的是vue在对模板的编译过程中做了很多优化,比如dom diff算法,事件处理,渲染函数缓存等。

使用render函数的写法如下:

<script setup>
import { h } from 'vue';
</script>

<script>
export default {
  name: 'HelloWorld',
  render() {
    return h('div', 'Hello, World!');
  }
};
</script>

JavaScript 数组方法大盘点:从新手到大佬,快速掌握所有必备技能!🎉

前言

大家好!今天我们将继续深入探索 JavaScript 数组的奥秘!你可能以为 pushpop 就是数组操作的全部,但其实数组的世界远不止如此。除了这些基础方法,还有许多其他方法能够让你的数组操作如鱼得水,大幅提高开发效率。今天的目标是帮助你从新手晋升为 JavaScript 数组高手,不仅掌握常用方法,还会揭秘一些鲜为人知的“宝藏”方法。让我们继续这段魔法般的数组之旅吧!🚀


相关方法

1. push()pop()

  • push():将一个或多个元素添加到数组末尾,并返回数组的新长度。
  • pop():从数组末尾删除一个元素,返回被删除的元素。
const fruits = ['apple', 'banana'];
fruits.push('orange'); // 添加橙子
console.log(fruits); // ['apple', 'banana', 'orange']

const lastFruit = fruits.pop(); // 删除最后一个元素
console.log(lastFruit); // 'orange'
console.log(fruits); // ['apple', 'banana']

2. shift()unshift()

  • shift():删除数组中的第一个元素,返回被删除的元素。
  • unshift():将一个或多个元素添加到数组的开头,返回数组的新长度。
const numbers = [1, 2, 3, 4];
numbers.unshift(0); // 在数组开头添加 0
console.log(numbers); // [0, 1, 2, 3, 4]

const firstNumber = numbers.shift(); // 删除第一个元素
console.log(firstNumber); // 0
console.log(numbers); // [1, 2, 3, 4]

3. map()

  • map():创建一个新数组,数组中的每个元素是原数组元素调用指定函数处理后的结果。
let numbers = [1, 2, 3, 4];
let squared = numbers.map((num) => num ** 2); // 数组中每个数字平方
console.log(squared); // [1, 4, 9, 16]

4. filter()

  • filter():创建一个新数组,包含所有通过指定条件函数筛选出的元素,原数组不受影响。
let ages = [12, 18, 25, 30, 15];
let adults = ages.filter((age) => age >= 18); // 筛选出年龄大于等于 18 的人
console.log(adults); // [18, 25, 30]

5. reduce()

  • reduce():对数组中的每个元素执行指定的累加操作,最终返回单一结果(如求和、拼接等)。
let numbers = [1, 2, 3, 4];
let sum = numbers.reduce((acc, num) => acc + num, 0); // 数组求和
console.log(sum); // 10

6. forEach()

  • forEach():对数组的每个元素执行指定的回调函数,不返回结果,常用于遍历。
let colors = ['red', 'green', 'blue'];
colors.forEach(color => console.log(color));  // 打印每个颜色
// 输出:
// red
// green
// blue

7. find()findIndex()

  • find():返回第一个满足条件的元素,如果没有找到,则返回 undefined
  • findIndex():返回第一个满足条件的元素的索引,如果没有找到,则返回 -1。
let users = [{ name: 'Tom', age: 20 }, { name: 'Jerry', age: 25 }];
let user = users.find(user => user.name === 'Tom');  // 查找 Tom
console.log(user);  // { name: 'Tom', age: 20 }

let index = users.findIndex(user => user.name === 'Jerry');  // 查找 Jerry 的索引
console.log(index);  // 1

8. sort()

  • sort():对数组元素进行排序,默认按字符串字典序排列。如果要按数字大小排序,需要提供比较函数。
let nums = [4, 2, 8, 5];
nums.sort((a, b) => a - b);  // 数字升序排序
console.log(nums);  // [2, 4, 5, 8]

9. slice()splice()

  • slice():返回数组的一个新数组,包含指定起始和结束位置之间的元素,不会改变原数组。
  • splice():可以删除或插入数组中的元素,直接改变原数组。
let animals = ['dog', 'cat', 'rabbit', 'bird'];
let selectedAnimals = animals.slice(1, 3);  // 获取从索引 1 到 3 的元素
console.log(selectedAnimals);  // ['cat', 'rabbit']
console.log(animals);  // ['dog', 'cat', 'rabbit', 'bird']

animals.splice(2, 1, 'fish');  // 删除第 2 个元素,插入 'fish'
console.log(animals);  // ['dog', 'cat', 'fish', 'bird']

10. concat()

  • concat():合并两个或多个数组,返回一个新数组。
let array1 = [1, 2];
let array2 = [3, 4];
let combined = array1.concat(array2);  // 合并数组
console.log(combined);  // [1, 2, 3, 4]

11. join()

  • join():将数组中的所有元素连接成一个字符串,元素间可以指定分隔符。
let fruits = ['apple', 'banana', 'cherry'];
let fruitString = fruits.join(' & ');  // 用 '&' 连接数组元素
console.log(fruitString);  // 'apple & banana & cherry'

12. some()every()

  • some():只要有至少一个元素符合条件,返回 true,否则返回 false
  • every():只有所有元素都符合条件,返回 true,否则返回 false
let numbers = [10, 20, 30, 40];
let hasLargeNumber = numbers.some(num => num > 25);  // 判断是否有大于 25 的元素
console.log(hasLargeNumber);  // true

let allLargeNumbers = numbers.every(num => num > 5);  // 判断所有数字是否都大于 5
console.log(allLargeNumbers);  // true

13. from()

  • from():将类数组对象或可迭代对象转换为数组。
let str = 'hello';
let arr = Array.from(str);  // 将字符串转换为数组
console.log(arr);  // ['h', 'e', 'l', 'l', 'o']

14. fill()

  • fill():用指定的值填充数组的指定位置,填充的元素会改变原数组。
let numbers = [1, 2, 3, 4];
numbers.fill(0, 2, 4);  // 从索引 2 到 4 填充为 0
console.log(numbers);  // [1, 2, 0, 0]

15. includes()

  • includes():判断数组中是否包含某个特定的元素,返回布尔值。
let fruits = ['apple', 'banana', 'cherry'];
console.log(fruits.includes('banana'));  // true
console.log(fruits.includes('orange'));  // false

16. reverse()

  • reverse():将数组的元素反转,直接修改原数组。
let numbers = [1, 2, 3, 4];
numbers.reverse();  // 反转数组
console.log(numbers);  // [4, 3, 2, 1]

17. indexOf()lastIndexOf()

  • indexOf() :返回数组中首次出现指定元素的索引,若没有找到,返回 -1
  • lastIndexOf() :返回数组中最后一次出现指定元素的索引,若没有找到,返回 -1
let fruits = ['apple', 'banana', 'cherry', 'banana'];
console.log(fruits.indexOf('banana'));  // 1(返回第一个 banana 的索引)
console.log(fruits.lastIndexOf('banana'));  // 3(返回最后一个 banana 的索引)
console.log(fruits.indexOf('grape'));  // -1(未找到 grape)

18. Array.isArray()

  • Array.isArray():判断传入的值是否是一个数组,返回 truefalse
console.log(Array.isArray([1, 2, 3]));  // true
console.log(Array.isArray('hello'));  // false
console.log(Array.isArray({ name: 'Tom' }));  // false

总结

这些 JavaScript 数组方法就像是数组的超级英雄,掌握它们,你就能让数组操作事半功倍!从基础的增删查改到高级的 mapreduce,以及一些不常用但极具威力的方法如 fromfillincludes,都能帮助你在开发中大显身手。希望今天的分享能够让你在

数组的世界里游刃有余!如果你有其他的数组操作技巧,欢迎留言讨论!🔧

eslint配置文件的名字是eslintrc.cjs,但是有的名字是eslint.config.js

以下是关于 ESLint 配置文件名称不同的解释:

1. 传统的 ESLint 配置文件:.eslintrc.cjs

1.1 历史背景

  • 早期版本

    • 早期的 ESLint 配置文件通常使用 .eslintrc 加上扩展名的形式,如 .eslintrc.json.eslintrc.js 或 .eslintrc.yaml。其中 .eslintrc.cjs 是使用 CommonJS 模块格式的 JavaScript 文件。

    • 使用 .eslintrc.cjs 的原因是在某些项目中,特别是使用 Node.js 环境时,需要使用 CommonJS 模块系统(require 和 module.exports)来配置 ESLint。例如:

收起

javascript

//.eslintrc.cjs
module.exports = {
  "rules": {
    "semi": ["error", "always"],
    "indent": ["error", 2]
  }
};

1.2 特性

  • CommonJS 模块系统

    • 使用 .eslintrc.cjs 可以使用 require 来引入其他模块,适用于 Node.js 项目或需要使用 CommonJS 的情况。例如,如果你需要根据环境或项目的不同动态加载不同的 ESLint 规则,可以使用 require 函数。

2. 新的 ESLint 配置文件:eslint.config.js

2.1 新的配置方式

  • ESLint v8 引入

    • 在 ESLint v8 及以后,引入了 eslint.config.js 作为一种新的配置文件格式。这种配置文件使用 ES 模块(import 和 export)。例如:

收起

javascript

// eslint.config.js
export default [
  {
    "rules": {
      "semi": ["error", "always"],
      "indent": ["error", 2]
    }
  }
];

2.2 优势

  • 使用 ES 模块

    • 对于使用现代 JavaScript 开发,尤其是使用 ES 模块的项目,eslint.config.js 提供了更自然的配置方式,符合现代 JavaScript 的开发习惯。

3. 选择使用哪种配置文件

3.1 项目环境和需求

  • Node.js 项目或 CommonJS 环境

    • 如果你的项目使用 Node.js 或依赖 CommonJS 模块系统,使用 .eslintrc.cjs 可能更方便,因为你可以利用 Node.js 的模块加载机制,方便地引入其他模块和进行动态配置。
  • 现代 JavaScript 项目

    • 对于使用 ES 模块的现代 JavaScript 项目,使用 eslint.config.js 更合适,它与项目的模块系统相匹配,避免了在使用 ES 模块时可能出现的配置文件导入导出问题。

3.2 兼容性和工具支持

  • 工具支持

    • 大多数 ESLint 工具和编辑器插件都支持这两种配置文件,但有些旧的工具可能对 .eslintrc.cjs 支持更好,而有些新的工具可能更倾向于 eslint.config.js

4. 转换和迁移

4.1 从 .eslintrc.cjs 到 eslint.config.js

  • 转换示例

    • 如果你想从 .eslintrc.cjs 转换到 eslint.config.js,可以将配置从 module.exports 转换为使用 export default。例如:

收起

javascript

//.eslintrc.cjs
module.exports = {
  "rules": {
    "semi": ["error", "always"],
    "indent": ["error", 2]
  }
};
  • 可以转换为:

收起

javascript

// eslint.config.js
export default [
  {
    "rules": {
      "semi": ["error", "always"],
      "indent": ["error", 2]
    }
  }
];

4.2 注意事项

  • 配置结构的变化

    • eslint.config.js 的配置结构可能有些不同,它支持配置的扁平化和更多高级特性,在迁移时要注意这些细节。

5. 总结

  • .eslintrc.cjs 是传统的 ESLint 配置文件,使用 CommonJS 模块系统,适用于 Node.js 或 CommonJS 环境。
  • eslint.config.js 是 ESLint v8 引入的新配置文件,使用 ES 模块,适用于现代 JavaScript 开发。
  • 根据项目的模块系统和开发环境,选择合适的配置文件,并注意配置文件的迁移和转换。

在electron中实现一个桌面悬浮球

 概要

在electron + vue3 搭建的应用中实现了一个桌面悬浮球/mini窗口的功能,支持任意拖拽、丝滑的菜单折叠展开动画效果。在实现过程中需要关注的一些点:

1、管理悬浮球窗口创建以及配置:需要一个透明的窗口来承载视图。

2、解决electron拖拽和点击事件冲突(核心):因为使用 -webkit-app-region: drag 样式的方式会导致拖拽和点击事件冲突,所以需要通过渲染进程和主进程的通信来解决窗口位置的更新。

3、初始化组件位置,计算窗口拖动位置:这里需要一些拖拽状态的判断、还有更新位置信息。

4、折叠展开动画和事件处理

最终效果

代码细节实现

首先需要窗口electron窗口来承载vue页面,在窗口管理模块中配置需要的参数,主要是frame、transport、skipTaskbar,然后注入preload中的进程交互事件,来实现渲染进程和主进程的通信。

windowList.set(WINDOW_ROUTE_NAME.MINI_WINDOW, {
  options() {
    return {
      width: 190,
      height: 170,
      frame: false,
      show: true,
      skipTaskbar: true,
      transparent: true,
      resizable: false,
      alwaysOnTop: true,
      webPreferences: {
        preload,
        nodeIntegration: true,
        contextIsolation: true,
      }
    }
  },
  callback(window: any) {
    loadUrl(window, WINDOW_URLS.MINI_WINDOW)
    // 初始化悬浮球位置
    const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize
    window.setPosition(screenWidth - window.getSize()[0] -100, screenHeight - window.getSize()[1] - 100)
  }
})

同时还要注册监听事件,来接受渲染进程的唤起动作

  ipcMainService.on("app:show:mini-window", (event, {
    name,
  }) => {
    const miniWindow = windowManager.createWindow(WINDOW_ROUTE_NAME.MINI_WINDOW)
    miniWindow.show()
  })

在页面中触发窗口唤起的动作,发送事件到主进程

const showMiniWindow = (value: boolean) => {
  ipcRenderService.send('app:show:mini-window', value)
}

在vue模板中添加基本的dom结构,注册事件handleMouseDown、handleMouseEnter、handleMouseLeave来实现位置计算、进程通信、折叠展开动画。

<template>
  <div class="mini-window"
       :class="{ 'expanded': isExpanded }"
       @mousedown="handleMouseDown"
       @mouseenter="handleMouseEnter"
       @mouseleave="handleMouseLeave">
    <!-- 折叠状态 -->
    <div class="mini-content">
      <span class="mini-bg"></span>
    </div>
    
    <!-- 展开状态 -->
    <div class="expanded-content" @click.stop>
      <div class="actions">
        <div class="action-item" @click="handleAction('restore')">
          <el-icon><FullScreen /></el-icon>
          <span>还原</span>
        </div>
        <div class="action-item" @click="handleAction('settings')">
          <el-icon><Setting /></el-icon>
          <span>设置</span>
        </div>
        <div class="action-item" @click="handleAction('dashboard')">
          <el-icon><House /></el-icon>
          <span>仪表盘</span>
        </div>
      </div>
    </div>
  </div>
</template>

下面是需要用到的样式

.mini-window {
  position: relative;
  margin-left: 125px;
  margin-top: 109px;
  width: 50px;
  height: 50px;
  border-radius: 25px;
  background: #fff;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
  transition: all 0.3s ease;
  overflow: hidden;
  user-select: none;
  
  &.expanded {
    width: 160px;
    height: 150px;
    border-radius: 12px;
    transform: translate(-110px, -100px);
    
    .mini-content {
      opacity: 0;
      pointer-events: none;
    }
    
    .expanded-content {
      opacity: 1;
      pointer-events: auto;
    }
  }
  
  .mini-content {
    position: absolute;
    bottom: 1px;
    right: 5px;
    opacity: 1;
    transition: opacity 0.3s;
    .mini-bg {
      cursor: pointer;
      display: inline-block;
      background: var(--app-color-gradient-blue);
      width: 40px;
      height: 40px;
      border-radius: 20px;
    }
  }
  
  .expanded-content {
    position: absolute;
    bottom: 0;
    right: 0;
    width: 160px;
    height: 150px;
    opacity: 0;
    padding: 9px 12px;
    pointer-events: none;
    transition: opacity 0.3s;
    
    .actions {
      display: flex;
      flex-direction: column-reverse;
      gap: 8px;
      
      .action-item {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 10px 12px;
        border-radius: 8px;
        cursor: pointer;
        transition: all 0.2s ease;
        color: var(--ep-color-primary);
        
        .el-icon {
          font-size: 18px;
        }
        
        span {
          font-size: 14px;
        }
        
        &:hover {
          background-color: var(--menu-active-bg-color);
          transform: scale(1.06);
          outline: 1px solid var(--ep-color-primary);
        }
      }
    }
  }
}

鼠标按下事件用来获取窗口初始位置,通过ipcRenderService.invoke和主进程通信获取位置信息,然后注册鼠标移动和鼠标抬起事件。

// 处理鼠标按下事件
const handleMouseDown = (e: MouseEvent) => {
  if (isExpanded.value) return // 展开状态不允许拖动
  
  isDragging = false
  initialMouseX = e.screenX // 使用screenX/screenY获取相对于屏幕的坐标
  initialMouseY = e.screenY
  mouseDownTime = Date.now()
  // 获取窗口初始位置
  ipcRenderService.invoke('app:window:get-position').then(([x, y]: [number, number]) => {
    windowInitialX = x
    windowInitialY = y
    
    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)
  })
}

 鼠标移动时,判断阈值并计算新的位置,然后和主进程通信设置当前的坐标位置。

// 处理鼠标移动事件
const handleMouseMove = (e: MouseEvent) => {
  const deltaX = e.screenX - initialMouseX
  const deltaY = e.screenY - initialMouseY
  
  // 判断是否达到拖动阈值
  if (!isDragging && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) {
    isDragging = true
  }

  if (isDragging) {
    // 计算新位置
    const newX = windowInitialX + deltaX
    const newY = windowInitialY + deltaY
    
    // 发送新位置到主进程
    ipcRenderService.send('app:window:set-position', { x: newX, y: newY })
  }
}

鼠标抬起时需要移除前面注册的鼠标移动和鼠标抬起事件

const handleMouseUp = () => {
  document.removeEventListener('mousemove', handleMouseMove)
  document.removeEventListener('mouseup', handleMouseUp)
  
  // 如果不是拖拽且点击时间小于200ms,则触发展开/收起
  if (!isDragging && (Date.now() - mouseDownTime < 200)) {
    toggleExpand()
  }
}

这样就实现了整个交互的过程,详细讲解可以看这个视频。

electron 实现一个丝滑的桌面悬浮球/mini窗口_哔哩哔哩_bilibili

Node.js系列:事件驱动的核心机制事件循环

❤️ 事件循环:处理异步操作的机制

目的:使Nodejs能够在单线程环境下高效运行

一、事件循环运行机制:依赖libuv库

事件循环机制是基于libuv库(一个多平台的异步I/O的库)构建的;

libuv为Nodejs提供了高效的事件驱动的I/O操作能力,libuv负责在底层进行实际的操作调度,当这些操作完成时,通过事件循环将对应的回调函数在合适的阶段进行调用;事件循环依赖kibuv实现高效的异步操作

比如在定时器管理方面,libuv 提供了精准的定时器机制,让事件循环能够准确地在合适的时间执行定时器回调函数(像setTimeoutsetInterval相关的回调)。

在非阻塞 I/O 操作上,事件循环借助 libuv 可以在等待 I/O 完成的同时处理其他事务,避免了线程的大量阻塞,提高了程序的整体性能。

二、事件循环的6个阶段:每个阶段都对应一个任务队列

当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段

1. 定时器Timers阶段:执行setTimeout 和setInterval的回调函数

当设定的时间到达后,回调函数会被添加到 定时器阶段的任务队列中。(定时任务不一定按照设定的时间执行)

2.I/O回调阶段:主要用于处理各种I/O操作(如文件读取,网络请求等)完成后的回调函数

当一个I/O操作完成后, 其对应的回到函数就会被添加到这个任务队列中; 比如fs.readFile,文件读取完成后的回调函数就会在这个阶段会执行

3.闲置阶段:这是一个内部使用的过渡阶段

  • 主要用于一些内部操作和准备工作,一般开发者很少直接涉及这个阶段的具体操作

4.轮询(Poll)阶段:事件循环的关键,主要有两个功能

  • 等待新I/O事件到来
  • 处理定时器到期后的任务(如果定时器阶段没来得及处理)

如果没有新的I/O事件并且定时器也没有到期任务,这个阶段会阻塞等待

5.检查(check)阶段:主要用于执行setImmediate的回调函数

  • 在当前轮询阶段结束后立即执行

6.关闭事件回调阶段:TPC服务器对象关闭时,对应的关闭回调函数

  • 例如关闭一个服务器套接字段后,用于清理资源等的关闭回调函数会在这个阶段被调用;
    • 如:socket.on('close', ...)

三、任务队列和执行顺序

微任务:

  • process.nextTick: 会在当前操作完成后立即执行,在微任务之前执行
  • promise.then
  • queueMicrotask():是标准的微任务

宏任务:

  • setTimeout、setInterval
  • IO事件
  • 检查阶段的setImmediate
  • 关闭事件

执行顺序:

  • nextTick microtask queue
  • other microtask queue
  • timer queue
  • poll queue
  • check queue
  • close queue

微任务会在当前执行栈为空的时候立即执行,宏任务会根据事件循环的阶段顺序来执行

其他:

queueMicrotask 与 process.nextTick 的区别?

  • process.nextTick 会在当前操作完成后立即执行,甚至在事件循环的下一个阶段开始之前,而且在微任务之前执行。
  • queueMicrotask 是标准的微任务,会在当前事件循环的微任务队列中等待,在当前执行上下文的同步代码和 process.nextTick 之后,但在宏任务之前执行。

setTimeoutsetImmediate的输出顺序

  • 遇到setTimeout,虽然设置的是0毫秒触发,但实际上会被强制改成1ms,时间到了然后塞入times阶段;
  • 先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
  • 跳过空的阶段,进入check阶段,执行setImmediate回调

这里的关键在于这1ms,如果同步代码执行时间较长,进入Event Loop的时候1毫秒已经过了,setTimeout先执行,如果1毫秒还没到,就先执行了setImmediate

【虚拟列表·终章】不定高度+动态图片加载,十万条数据流畅渲染全攻略!

大家好,我是 前端大卫

虚拟列表-示例.gif

在线 Demo 地址: codesandbox.io/p/devbox/ad…

今天是 虚拟列表 系列的终章,我将带大家深入探讨 不定高度列表项 的处理方式。如果你还没有看过前两篇内容,可以先点击下面的链接:

解决方案的优势

相较于市面上其他虚拟列表实现,我的这个方案具备以下优势:

  1. 高效索引查找
    根据滚动方向精准定位起始和结束索引,显著提升性能。
  2. 创新高度调整机制
    无需依赖传统二分查找算法,而是通过记录高度调整值,保证列表项的连续性。
  3. 动态监听高度变化
    利用 ResizeObserver 实时监听可视区域和整体列表高度的变化,确保数据准确。

如果你的项目不支持 ResizeObserver,欢迎在评论区留言,我会单独出一篇文章讲解如何用其他技术解决监听问题。

接下来,我会通过实例,从 简单到复杂 手把手讲解解决方案,并分享一些重要注意事项。

核心实现步骤

1. 初始化数据结构

假设以下场景:

  • 每项预估高度为 100
  • 可视区域高度为 450

初始数据结构如下:

[
  { "top": 0, "bottom": 100, "height": 100 },
  { "top": 100, "bottom": 200, "height": 100 },
  { "top": 200, "bottom": 300, "height": 100 },
  ...
]

预估高度.png

可以发现:

  • 每项的 top 值为前一项的 bottom 值。
  • 每项的 bottom 值为自身 top + height

2. 精确查找索引

根据滚动高度,确定起始和结束索引的公式为:

if (scrollTop >= item.top && scrollTop <= item.bottom) {
   // 找到对应索引
}

注意:
列表项之间必须保持连续,否则会出现无法匹配的情况。例如,如果滚动高度为 140,而列表项如下:

[
  { "top": 0, "bottom": 100 },
  { "top": 200, "bottom": 300 },
  { "top": 300, "bottom": 400 }
]

此时无法找到对应的索引,导致无法渲染虚拟列表。

3. 预估高度的作用

预估高度用于初始渲染,后续会根据实际高度进行调整。例如:
滚动高度为 0,起始索引为 0,结束索引为 4,渲染如下:

预估高度.png

4. 高度修正

渲染完成后,各列表项的实际高度可能不同。通过 ResizeObserver,我们可以动态监听每项高度并修正:

  • 如果列表项 高度较低:结束索引会增加,例如从 4 变为 7

高度较低修正.png

  • 如果列表项 高度较高:结束索引会减少,例如从 4 变为 3

高度较高修正.png

5. 确保列表项连续性

这是实现的核心难点。未渲染的列表项需要与已渲染项保持连续,以下分两种情况讨论:

情况 1:高度连续

如果下一项的 top 大于上一项的 bottom,简单赋值即可:

if (nextItem.top >= lastItem.bottom) {
  nextItem.top = lastItem.bottom;
}

高度连续.png

情况 2:高度不连续

如果下一项的 top 小于上一项的 bottom,需要记录调整值:

const heightAdjustment = lastItem.bottom - nextItem.top;

高度不连续.png

无需逐项更新所有列表项,只需在需要时应用调整值即可。

6. 滚动方向的优化查找

根据 newScrollTopprevScrollTop,判断滚动方向:

  • 向下滚动:新起始索引在旧索引下方。
  • 向上滚动:新起始索引在旧索引上方。

利用方向判断,比二分查找效率更高。

7. 动态调整后的处理

当某项被删除或高度变化时,确保页面流畅性:

  • 利用 uid 唯一标识复用旧列表项的数据。
  • 根据规则修正:
    • top = previous.bottom
    • bottom = top + height

结语

代码细节可以查看我的 GitHub 项目,希望大家点个 ⭐ 支持!

GitHub 源码地址:
github.com/feutopia/fe…

如果你对虚拟列表有其他问题或建议,欢迎留言讨论!

最后

点赞👍 + 关注➕ + 收藏❤️ = 学会了🎉。

更多优质内容关注公众号,@前端大卫。

前端性能优化中的技能CD和回城

🚀 前端性能优化必备技巧:深入理解防抖与节流

📚 前言

在前端开发中,性能优化是一个永恒的主题。当我们处理高频触发事件时,如果不进行适当处理,可能会导致以下问题:

  • 🔥 频繁触发事件导致性能下降
  • 💾 不必要的服务器请求
  • 🖥️ 页面卡顿
  • ⚡ 资源浪费

🎯 常见的高频触发场景

  1. 搜索框实时搜索 🔍

    • 用户输入时频繁发起请求
    • 每次按键都触发搜索
  2. 窗口调整事件 📱

    • resize 事件频繁触发
    • 需要重新计算布局
  3. 滚动事件处理 📜

    • scroll 事件持续触发
    • 可能影响页面性能
  4. 按钮提交事件 🖱️

    • 用户重复点击提交
    • 可能导致重复请求

🛡️ 防抖(Debounce)

🎮 生动的游戏类比

想象英雄联盟中的回城机制:

  • 按 B 开始回城,等待 8 秒
  • 受到伤害立即打断,重新计时
  • 必须完整等待才能回城成功

💻 实际应用案例

  1. 搜索建议功能
// 实现搜索框防抖
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce(async (query) => {
  const results = await fetchSearchResults(query);
  updateSearchSuggestions(results);
}, 300);

searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});
  1. 表单验证
// 实现实时表单验证
const emailInput = document.querySelector('#email');
const debouncedValidate = debounce(async (email) => {
  const isValid = await validateEmail(email);
  updateValidationUI(isValid);
}, 500);

emailInput.addEventListener('input', (e) => {
  debouncedValidate(e.target.value);
});

⚡ 节流(Throttle)

🎮 游戏类比

类似英雄联盟技能冷却:

  • 释放技能进入冷却时间
  • 冷却期间无法再次释放
  • 冷却结束才能再次使用

💻 实际应用案例

  1. 无限滚动加载
// 实现滚动加载
const container = document.querySelector('#infinite-list');
const throttledLoad = throttle(async () => {
  if (isNearBottom()) {
    const newItems = await fetchMoreItems();
    appendItems(newItems);
  }
}, 200);

window.addEventListener('scroll', throttledLoad);
  1. 数据统计上报
// 实现用户行为统计
const tracker = throttle((event) => {
  sendAnalytics({
    type: event.type,
    timestamp: Date.now(),
    data: event.data
  });
}, 1000);

document.addEventListener('mousemove', tracker);

🔄 如何选择防抖还是节流?

选择防抖的场景 🛡️

  • ✅ 搜索框输入查询
  • ✅ 表单实时验证
  • ✅ 调整窗口大小
  • ✅ 用户输入校验

选择节流的场景 ⚡

  • ✅ 页面滚动处理
  • ✅ 数据统计上报
  • ✅ 游戏中的按键处理
  • ✅ 射击类游戏的武器发射

💡 性能优化建议

  1. 延迟时间设置 ⏱️

    • 搜索框:300-500ms
    • 表单验证:400-600ms
    • 滚动处理:150-300ms
    • 统计上报:1000-2000ms
  2. 代码优化 📈

    • 使用闭包保存状态
    • 注意内存泄漏
    • 及时清除定时器
    • 考虑是否需要立即执行

📝 总结

选择合适的方案:

  • 防抖:关注最终结果
  • 节流:关注执行频率

🔗 完整实现代码

防抖实现:

// debounce.js
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input style="width: 80%; height: 30px" type="text" id="id" />
    <script>
      let timer = null;
      function debounce(func, delay, immediate = false) {
        return (...args) => {
          //清除定时器  如果注释的话还是那么多频次,只是每次推迟了
          if (timer) {
            clearTimeout(timer);
          }
          if (!timer && immediate) {
            func(...args);
          }
          timer = setTimeout(() => {
            func(...args);
          }, delay);
        };
      }

      function pureFn(...params) {
        console.log('%c执行成功params:', 'color: red; font-size: 14px;', params);
      }

      const dom = document.getElementById('id');
      const debounceFn = debounce(pureFn, 500, true);
      dom.addEventListener('input', e => {
        const value = e.target.value;
        debounceFn(value, Date.now());
      });
    </script>
  </body>
</html>

节流实现:

// throttle.js
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input style="width: 80%; height: 30px" type="text" id="id" />
    <script>
      const throttle = (func, delay) => {
        let last = 0;
        let deferTimer = null;
        return args => {
          // 干掉触发
          let now = Date.now();
          if (last && now < last + delay) {
            clearTimeout(deferTimer);
            deferTimer = setTimeout(() => {
              last = now;
              func(args);
            }, delay);
          } else {
            last = now; // 第一次时间
            func(args); // 先执行一次
          }
        };
      };

      function pureFn(...params) {
        console.log('%c执行成功params:', 'color: red; font-size: 14px;', params);
      }

      const dom = document.getElementById('id');
      const throttleFn = throttle(pureFn, 1000, false);
      dom.addEventListener('input', e => {
        const value = e.target.value;
        throttleFn(value, Date.now());
      });
    </script>
  </body>
</html>

🌟 如果这篇文章对你有帮助,欢迎点赞、收藏和评论!

📢 关注我,一起探讨前端技术!

隔夜国际贵金属和油价普遍收跌

36氪获悉,隔夜(1月17日)国际贵金属普遍收跌,伦敦金现货跌0.43%,周累涨0.46%;COMEX黄金期货跌0.4%,周涨0.92%;伦敦银现货跌1.54%,周累跌0.18%;COMEX白银期货跌2.14%,周跌0.86%。国际油价全线走低,美油3月合约跌0.62%报77.37美元/桶,周涨1.04%;布油3月合约跌0.79%报80.65美元/桶,周涨1.12%。

隔夜美股市场集体收涨,大型科技股全线上涨

36氪获悉,周五(1月17日),欧美股市集体收涨,纳指涨1.51%,周累涨2.45%;标普500指数涨1.00%,周累涨2.91%;道指涨0.78%,周累涨3.69%。大型科技股全线上涨,万得美国科技七巨头指数涨1.72%。其中,英伟达涨超3%,特斯拉涨逾3.1%,亚马逊涨2.4%,谷歌涨1.6%,微软涨1.1%。中概股普遍上涨,纳斯达克中国金龙指数涨3.18%,周累涨6.19%。个股方面,京东集团涨超10%,富途控股涨8.9%,老虎证券涨8.5%,拼多多涨5.3%,蔚来涨4.7%。
❌