普通视图

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

AI 不是来当员工,而是来当老板的| 46 个 AI「金句」

作者 杜晨
2025年4月4日 19:18

毫无疑问,我们处在一个 AI 的大变革时代。

每天都有新的 Agent 发布,新的模型开源。每天都有小 AI 公司正在变成巨头,而巨头们也在拿到越来越多的,甚至令人难以置信的钱。

或兴奋,或恐惧,或担忧。在所有的大变革时代,我们都难免多想。

在中国,APPSO 关注 AI 的同事们经常因为大洋彼岸的新闻而「夜半惊醒——而在美国,看起来从业者们也同样会因为 AI 的日新月异而睡不着觉。

投资网红 Greg Isenberg 就是一个经常多想、辗转反侧的人。他参与创办了上百个项目,目前是投资机构 Late Checkout 的 CEO 和 The Startup Ideas 播客的主播。

昨天,Isenberg 在他的 X 账号上发布了一条长文,说自己半夜睡不着,想出了 46 条和 AI 有关的「金句」。

其中大部分是他的个人观点,甚是有趣。我们将这条长文编译分享出来,并且展开聊聊其中的部分思考。

原文地址🔗

1. GPT-4o 的图像生成功能与 ChatGPT 发布一样震撼,将会催生上千个百万至亿级美元的垂直领域软件商机。

chatgpt 4o image gen is as big as the chatgpt launch. probably will birth 1000+ $1-$100m/year vertical software businesses.

2. 当前的 AI 生成内容,就像数字音乐早期的「MP3-Napster」时代。数百万创作者并没有意识到,他们的作品未来将成为打败他们的武器。

we’re in the “mp3 napster era” of content. millions of creators don’t realize their entire back catalog is being weaponized into their competition because of AI. 

3. 在三年内,日历、邮件和 CRM 工具将经历根本性重构——不是渐进式的AI升级,而是范式级的重新设计。

every calendar, inbox, and CRM will be rebuilt from scratch in the next 3 years. not “AI-enhanced,” fully rethought.

4. AI 不是来当员工,而是来当老板的!首批具备管理人类能力的 AI 系统将彻底重构劳动力市场,其影响力将远超工业革命。

i thought ai was creating digital employees. but it’s more like digital employers. the first ai systems that can manage human workers will cause a restructuring of labor markets more significant than the industrial revolution.

5. 如果你的工作是招聘人→训练系统→系统替代人去做招聘工作,那么你不是 HR,而是一个程序员,正在编写卸载自己的程序。

if your job is interviewing people who will train ai systems that will replace people who do interviews, you’re just a step in a weird recursive extinction.

6. AI 将难以规模化的服务生意,转变为兼具产品利润和服务溢价的新生意。能用 AI 做 80% 工作的产品化解决方案,将成为新的独角兽企业。

ai is turning “service businesses that don’t scale” into “product businesses with service margins.” the new unicorns will be productized services with ai doing 80% of the work.

7. 不要痴迷于打磨产品。社群运营更难,更决定生死。大部分创业公司死掉是因为没人在乎。

building communities is harder than building products but everyone pretends it’s the reverse. the reality is most startups fail because nobody cares.

8. 威尔·史密斯吃意面?那是 739 天前的 AI 水平。想象一下再过 739 天,生成式 AI 会进化到什么程度?

it’s been 739 days since the will smith spaghetti video. imagine what could happen to gen ai in 739 more days?

APPSO 的延展思考:在图像/视频的生成式 AI 方向,上一次重大破圈事件是3 月的「OpenAI 吉卜力风格迁移生成」,而两年前的威尔·史密斯吃意面是上上次。

这两次事件有异同之处:相同的是都和知名文化现象/符号有关,从而很容易地导致了破圈;不同则在于最初版本的吃意面视频过于粗糙,十分「魔性」。这种失真感不但没有影响人们对 AI 强大的预期,反而显著加强了这个梗的病毒传播。

9. 开发 AI 助理的人,大多没用过真人助理。真正的助理懂背景、记历史、重关系——而 95% 的聊天机器人,三者皆无。

people building “ai assistants” have never actually had assistants. real assistants need context, history, and relationship. 95% of chatbots have none of those.

APPSO 的延展思考:不需要用过助理,大多数用户已经觉得 AI 助理很蠢了。有给它交代上下文、打磨提示的工夫,还不如自己去干了。

10. 3年内,连最高级的客诉处理都将被 AI 取代,包括你现在认为必须人工的复杂客诉。

most customer support will be automated within 36 months. not just tier 1 tickets, complex, multi-step resolution that previously required senior support staff.

11. 创业最危险的结局不是归零,而是被卡在「生存线」上——足够温饱,永远不够自由。该止损还是加注?我总在思考这个困境。

the worst thing that can happen to your startup is mediocre success. enough to keep you going but not enough to change your life. most founders are trapped there. thinking about this a lot with respect to shutting down or doubling down on projects. 

12. 反抗AI的不只是失业者——每个发现自己的数字痕迹被擅自用作AI养料的人,都会加入抵抗。

the ai backlash won’t just come from replaced workers, it’ll be from everyone who realizes their entire digital identity is being converted into training data without consent. 

13. 没人会看用户条款。

no one has ever read a terms of service ever

14. AI 真正的颠覆在于「草图经济」:设计无门槛,审美和创意才是硬通货。

the “sketching economy” is the real ai revolution. when anyone can turn rough sketches into production-ready designs, taste and ideation become the only scarce resources. 

APPSO 的延展思考:作为文字和视觉的工作者,APPSO 对于这一观点深深赞同。AI 能生成看起来非常有「思考」味和「设计」感的内容,用于非严肃场景是可以的,一旦放在商业和专业场景,生成结果的破绽,以及审美的同质化、大中华是个很大的问题。

究其根本,审美和创意是一种非常「个人化」和「人性化」的概念。创作是创作者个人的行为,但也是和观者的一次交流。这种交流需要双方有着类似的人生经验、相近的认知水平。

一种极端但往往正确的观点是:大众的审美水平永远趋向降低。所以真正的审美和创意一定会成为稀缺资源。

看看现实吧,AI 并不是取代了能深度思考的人,而是让本来就懒得深度思考的人更不需要思考了。审美和创意也是一样,AI 在这方面的能力,只会让更多人彻底失去掌握这些能力的必要。

15. AI 创业的真金白银,藏在吃透行业痛点的垂直应用里——不是在提示里加两个行业黑话就行的。

i dont know how else to say it, the money (and opportunity for the avg joe) is in ai startups is in vertical-specific applications that actually understand industry context. no, adding industry terms to your prompts isn’t the same thing. 

16. 消费移动设备又复兴了。从桌面→移动→AI 优先,我们已进入 AI 原生时代——下一批独角兽,必是深度整合 AI 的移动应用。

consumer mobile is back in full swing. we went from desktop-first apps to mobile-first apps to now ai-first mobile apps. the next wave of $100m/year apps will start mobile-first with ai baked in from day one. 

17. AI 「套壳」大战才刚打响。介于底层大模型和终端行业之间的中间商,将收割最大红利。而模型和行业应用本身终将沦为廉价商品。

the ai middleman boom is just starting. companies that sit between foundation models and specific industries will capture most of the value while both ends get commoditized. 

APPSO 的延展思考:这个观点很有趣,但也有一定的讨论空间。行业里有另一种主流观点,和它几乎完全相反:未来所有的模型都将产品化,而 wrapper 等纯工程层面的公司,不会比真正掌握底座大模型研发能力的公司走得更远。因为工程能力谁都可以拥有,但买得起卡,训练的起大模型才是真正的杀手锏?

当然,这两种观点都比较极限。现实很有可能是折中的。即便大模型公司有再多的钱,它也不会比小公司小团队更敏捷、更理解所处的垂直行业。千行百业不能一家独霸,大概就是这个道理。

18. 我们正在目睹新工种的诞生:AI 流程设计师将成为新一代金领,专精于把人类流程转化为 AI 增强的工作流。

we’re witnessing the birth of a whole new job category: ai workflow designers. people who can map human processes into ai-augmented workflows will be the highest-paid consultants of the next decade. 

19. AI 加速「赢家通吃」的局面:垂直行业的龙头之争,窗口期仅 6-12 个月,错过即出局,要么再等十年。想到这我彻底失眠了😂

ai is creating winner-take-most markets overnight. the window to establish yourself as the go-to solution in a specific vertical is maybe 6-12 months before it closes for a decade. this isn’t helping my sleep lollll. 

20. 创业金点子:用AI重塑传统行业产品——这就是你的杀手锏。找一个已被验证的非 AI 产品,用 AI 思维彻底重构,再找行业 KOL 引爆卖点。这把稳赢。

really smart strategy to rebuild traditional products with ai as your unfair advantage, hiding the complexity behind familiar interfaces. basically, just look at proven apps that have no ai, make them ai-first (if it adds a ton of value to end customer). use ai features (don’t sell ai) in creator-led marketing. this is the playbook. 

21. 直达用户的渠道是唯一的护城河。你的产品、技术、团队都可以被复制,渠道不能。

distribution is the only moat left. your product, tech, and team can all be replicated. your direct connection to customers cannot. 

22. 临界点就快到了:对小企业而言,定制 AI 工具将比雇佣员工更划算。

we’ll soon hit the tipping point where custom ai tools are cheaper than hiring humans, even for small businesses. 

23. 很少有人在讨论这件事:AI 正让曾经「无人问津」的企业变得抢手。当业务能自动化运转时,收购逻辑也变了,投资人会追逐那些「买来就能自动赚钱」的公司。

nobody’s talking about how ai is making previously “un-acquirable” businesses suddenly attractive targets. when you can automate operations, the owner-dependent business problem disappears. 

24. 续上条:即将到来的中小企业收购潮,会比 2021 年的科技泡沫更夸张。当 AI 把运营成本砍掉了 60%,小企业会成为现金流机器。

the coming smb acquisition frenzy will make the 2021 tech bubble look tame. when ai drops operating costs by 60%, every small business becomes a cash flow engine. 

25. 如果「氛围编程」(AI 写代码)是一个千亿美元的机会的话,那么「氛围营销」的市场会有多大?。

if vibe coding will be a $100B opportunity, how big of an opportunity is vibe marketing? (you can follow my co-founder @boringmarketer for more on that) 

APPSO 的延展思考:上面这几条和中小企业、创业方向有关的思考都非常值得参考。当然不是说直接跟着他走,而是如果你的企业正好符合他的描述,你应该考虑更积极地拥抱 AI,探索 AI 工具和自动化能否为你提高效率,甚至让你可以对行业里的既得利益者和巨头们发起挑战。

当然,不要因为过度使用 AI 而让你的核心员工和老员工们对你失望。如果你因为 AI 逆天改命,也应该让他们从中分得一杯羹。

26. 游戏工作室将会两极分化:一边是 AI 智能体驱动的「内容农场永动机」,可以批量生成无限的素材;另一边是专注核心玩法的「精品工作室」。无法转型的中间层将被淘汰。

Video game studios will separate into two distinct types: agent-driven content farms that generate infinite assets, and boutique studios focused on core mechanics. The middle will disappear entirely.

APPSO 的延展思考:看看过去几年最受欢迎的游戏和背后的工作室就知道了。前者是你在短视频里经常见到的那种,素材千变万化但机制万变不离其宗的放置、射击,以及羊了个羊等消消乐类,杀时间的游戏。它们不招你待见,但市场巨大。后者是小岛的《死亡搁浅》、战马的《天国:拯救》、雾影的《二人成行》等发布即封神的大作,也是《Only Up!》、《Getting Over it with Bennett Foddy》、《Get To Work》这样的机制魔性、折磨到让人砸手柄的独立游戏。

27. 企业可能更愿意花每月几十美元订阅 AI 工具无限生成素材,而不是 2000 美元/天雇一个商业摄影师。商业摄影赛道凉凉。

Corporate photography is effectively dead. No company will pay $2K for a stock-style photoshoot when they can generate unlimited perfectly on-brand imagery for the cost of a subscription. 

28. AI 能够颠覆企业销售模式:它能精准识别最佳销售时机,锁定买家,并自动触发销售流程。

enterprise sales is being completely inverted by ai. using ai to identify exactly when and how to talk to the right buyer, and set off automations. ill probably talk about this more on a pod soon. 

29. 我在思考:AGI 是否会从相互连接的智能体网络中「涌现」出来?这些智能体网络可能自己涌现出意料之外的特性,而我们正在不知不觉中构建着它们的「神经网络」。

i wonder if AGI will emerge from interconnected agent networks that develop emergent properties nobody designed? we’re building the neural connections without realizing it. 

30. 虽然生成式 AI 看似将成就万亿级市场,真正的「隐形金矿」其实在预测式 AI 领域。预知未来的价值,永远高于创造内容。

while genai looks to be the $1T category, many quiet fortunes will be built in predictive ai. knowing what will happen is more valuable than generating new content. 

APPSO 的延展思考:目前的生成式 AI,生成的是它自己认为将会发生的东西。基于大语言模型的 AI 会遵循文本的规则和概率的规则。而预测式 AI(比如天气、地质、金融、社会工程学的垂类 AI)需要遵循现实规则。后者是不是大语言模型,也说不定,可能是基于,或者从大语言模型精修特调的的专家模型。

当然,如果大语言模型发展太快、太普及,甚至成为实际的权威和统治者——那么将会发生什么,也是大语言模型说了算……

31.所谓的「AI 泡沫」,不过是 VC 们因为分不清 API wrapper 和真·创新而交的学费。

the “ai bubble” is actually an excise tax on vcs who can’t tell the difference between genuine innovation and repackaged openai apis. 

32. 人机交互即将迎来「人格化革命」。当每个工具都能与你对话时,AI 的氛围和腔调,将会决定用户信任、忠诚、留存率。

interfaces will become personalities. when every tool can talk back, vibe and tone will drive trust, loyalty, and retention. It’s why I’m investing more in our design firm for the AI age @meetLCA (you can follow for more insights on designing/taste/brand that will stand out) 

33. AI 将杀死传统首页。未来的入口界面会因人而异、因需而变、因时而动。

ai will kill the homepage. interfaces will get replaced by entry points that change based on who you are, what you need, and when you show up. 

34. 用户不为「AI」买单,只为结果付费。

no one will pay for “ai”, they’ll pay to solve a $10,000/hour problem in 3 clicks. sell outcomes, hide the ai. 

35. Google 的万亿帝国可能被 AI 拆解:旅游搜索、商品比价、本地服务等等,每个细分领域都是待掘的金矿。

ai is unbundling google. every vertical search engine, directory, and comparison tool is a billion-dollar opportunity in disguise. 

36. 未来的小企业标准配置:1个创始人+5个 AI 员工组成的「影子团队」,自动搞定财务、销售、营销全流程。

every small business will get a “ghost team.” automated bookkeepers, sales agents, marketers—run by one founder and 5 bots. 

37. AI 生成内容导致了文化的同质化危机,当全球共享同一套模型,我们得到的将是无限重复的「数字回声」。具有原创性人类思考将会成为最终级的附加值。怪异会成为卖点,请保持怪异。

ai-generated content is creating a monoculture of ideas. when everyone uses the same models, we get the same outputs. original human thinking is becoming the ultimate premium. be weird. weird will sell. 

APPSO 的延展思考:不只人过度依赖 AI,就连 AI 自己也在不断加强对自己的依赖。

机器学习的经验指出,过度依赖生成的数据再次训练,有可能会发生「过拟合」现象 (overfitting),甚至导致模型工作机制熵增,使得生成的结果变得更加不可靠,违背常理,失去代表性。

这可以类比为生物学上的近亲繁殖,对同族遗传资源(基因/数据)的过度利用,更有可能导致错误的因素在生成结果中纯合。只是这种 AI 的「近亲繁殖」结果不像生物学那么容易一眼看出来。事实上大部分人不具备分辨能力,而这会导致 AI 生成的错误结果被更多采纳和再利用,最终形成一种脱离现实的逻辑闭环。

38. AI  不会颠覆学校,而是实现教育的「去中介化」。未来的神童们将绕过传统教育体系,通过直接获取受众、实践验证的方式来快速成长。今天的孩子都当 KOL,而创业者才会是 Z 世代孩子的理想职业。

schools won’t be disrupted by ai. they’ll be disintermediated. smart teens will skip formal education, build audiences, run experiments, and learn faster. kids say they want to become creators but creators are becoming entrepreneurs. entrepreneurship becomes the most popular profession. 

39. AI 创业泡沫的结果:18 个月后,八成的 AI 创业公司将会像劣质小广告一样无人搭理,而剩下的两成会晋升为数字基建。

in 18 months, 80% of the “ai startup” category will look like spam. the rest will become infrastructure. 

40. A/B 测试没意义了。当 AI 能在一夜之间完成200次迭代实验,何必再争论一个按钮该用什么颜色?

conversion rate product debates are obsolete. Why argue over 2 button colors when AI focus groups can test 200 variations overnight? 

41. 传统营销即将被 AI 接管,营销专员必须向上游迁移。讲述品牌故事、营造独特氛围、传递品牌能量——才是未来营销人的核心竞争力。

most of what we call “marketing” is about to be done by ai. humans will move upstream into storytelling, vibes, and brand energy. 

42. 今年最明智的招聘策略?聘请一位 AI 运营总监,一个能搭建 AI 工作流、整合工具链,并交付实际成果的人才。

the best hiring decision you can make this year? a head of ai ops. someone who can build workflows, glue tools, and ship outcomes. 

43. 第一头估值十亿的 AGI 独角兽,初看必定像个玩具。所有改变世界的东西都是这样。

the first $1b AGI startup will look like a toy at first. all world-changing interfaces do.

APPSO 的延展思考:忘了哪个投资人好像说过类似的话。

「当你的孩子拿着一个玩具说它什么都懂的时候,赶快查查背后公司的估值。」

44. AI 驱动的渠道大于 AI 驱动的产品。二流产品一流分发,好过无人问津的一流产品。AI 驱动的分发能力 > AI驱动的产品力。在注意力经济时代,二流产品加上顶级流量 > 顶级产品却没有流量。

ai-powered distribution > ai-powered product. a mid product with elite reach will beat a great product with no attention every time. 

45. 用户对订阅制的反感仍然存在,而为有效结果付费的模式尚处于蓝海。率先采用后者的企业将获得碾压传统 SaaS 巨头的绝对优势。

people still hate monthly subscriptions. outcome-based pricing is still in early days. implementing this will be a competitive advantage for lots of companies. large saas wont be able to compete with you. 

46. 我们正处在商业规则全面重写的黄金时代,它会持续多久我不清楚。但明确的是:那些率先驾驭新工具、构建受众社群的人,拥有绝对竞争优势。

i don’t know how long this window stays open, but we’re in a moment where all the rules of building businesses are being rewritten. for the people playing with these new tools, creating audiences and communities, you’ve got an unfair advantage. 

 

写在最后:

生前何必久睡,死后自会长眠?

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


海南修订细则支持大型演唱会等演出,最高奖励300万元

2025年4月4日 17:16
日前,海南省旅文厅修订并印发《海南省关于支持大型演唱会、音乐节及高品质优秀剧目文艺演出实施细则》,明确符合相关奖励标准的大型演唱会、音乐节,将给予最高300万元一次性奖励。该细则自公布之日起施行,有效期至2025年12月31日,奖补申报时间截至2026年2月28日。(新浪财经)

蔚来推“5年免费换电”+5年0息限时购车政策

2025年4月4日 16:53
36氪获悉, 4月4日,蔚来宣布,即日起至4月30日期间,用户支付定金购买蔚来ET5、ET5T、ES6、EC6、ET7、EC7、ES8车型,可获赠240张免费换电券(换电券5年有效)。新政策可与现有5年0息金融方案,最高10,000元换电车型专享补贴,以及3年NOP+免费使用权(价值13,680元)同享。此外,以旧换新用户还可享受最高15,000元汽车以旧换新补贴,以及各地消费补贴(以各地公布实施细则为准)。

美油跌至2023年5月以来最低水平

2025年4月4日 16:14
WTI原油期货跌至2023年5月以来最低水平,最低报65.2美元/桶,日内跌2.5%。由于美国推出的所谓“对等关税”严重超出市场预期,投资者对全球经济陷入衰退的担忧加剧。主要产油国5月起将增产,石油输出国组织(欧佩克)3日发表声明说,8个欧佩克和非欧佩克产油国决定自今年5月起日均增产41.1万桶。这一增产量远高于市场预期。(财联社)

vue3基础知识(结合TypeScript)

作者 Kagerou
2025年4月4日 15:26

一.使用vite创建项目

不选择vue-cli脚手架,换成使用vite,vite最大特点就是快,不需要进行打包,热加载快 是一个通用的脚手架工具

image.png

image.png

这里出了一个问题,就是vetur不支持vue3,所以HelloWorld组件就不能正常使用,需要把它卸载换成

二.ESlint初步使用

安装配置eslint: image.png

添加rules之后:

eslint.config.js:(检测有没有分号)

import globals from "globals";
import tseslint from "typescript-eslint";


/** @type {import('eslint').Linter.Config[]} */
export default [
  { files: ["**/*.{js,mjs,cjs,ts}"] },
  { languageOptions: { globals: globals.browser } },
  ...tseslint.configs.recommended,
  {
    rules: {
      semi: 2
    }
  },
];

image.png

代码中也会有相应的提示:

image.png

extends字段:添加规则组

后期和vite结合,添加vue和typeScript的规则组

三.响应式API:ref、reactive

ref:

<script lang="ts">
import { defineComponent,ref } from 'vue';
export default defineComponent({
     name:'APP',
     setup()
     {
         const count=ref(0); //声明响应式对象
         const increase=()=>{
          count.value++; //这里要加上value
         }
         return {
          count,
          increase
         }
     }
})
</script>

<template>
  <div>
    <h1>{{ count }}</h1>
    <button type="button" v-on:click="increase">increase</button>
    
    <a href="https://vite.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
</template>

reactive:参数必须是一个对象

怎么把响应式对象和TS结合起来呢,使用泛型来定义ref的类型,value++之前进行判断

<script lang="ts">
import { defineComponent,ref,reactive} from 'vue';
export default defineComponent({
     name:'APP',
     setup()
     {
         const count=ref<string | number>(0); //声明响应式对象
         const user=reactive({
             name:'viking',
             age:30,
         })
         const increase=()=>{
          if(typeof count.value==='number')
          count.value++; //这里要加上value
          user.age++; //不用加value进行访问
         }

         return {
          count,
          increase,
          user,
         }
     }
})
</script>

给reactive标记类型:

<script lang="ts">
import { defineComponent,ref,reactive} from 'vue';
interface person {
  name:string,
  age:number
}
export default defineComponent({
     name:'APP',
     setup()
     {
         const count=ref<string | number>(0); //声明响应式对象
         const user:person = reactive({
             name:'viking',
             age:30,
         })
         const increase=()=>{
          if(typeof count.value==='number')
          count.value++; //这里要加上value
          user.age++; //不用加value进行访问
         }

         return {
          count,
          increase,
          user,
         }
     }
})
</script>

四.computed计算属性

 <button type="button" :disabled="user.age<10">{{ user.age>=10 ?'可以参与':'不可以参与'}}</button>

如果要都写表达式的话,会使模板变得很复杂,所以就可以使用计算属性

const buttonStatus=computed(()=>{
              return {
                text:user.age>=10?'可以参加':'不可以参加',
                disable:user.age<10
              }
         })
<button type="button" :disabled="buttonStatus.disable">{{ buttonStatus.text }}</button>

计算属性在数据更新的时候才会重新计算

五.watch监听器

watch(count,(newValue,oldValue)=>{
              console.log('old',oldValue);
              console.log('new',newValue);
              document.title=`目前点击的数是${newValue}`
         })

不能直接监听一个对象的属性,要添加在箭头函数返回值后面

watch(()=>user.age,(newValue,oldValue)=>{
              console.log('old age',oldValue);
              console.log('new age',newValue);
              document.title=`目前点击的数是${newValue}`
         })

或者直接传入一个参数

watch(user,(newValue,oldValue)=>{
              console.log('old age',oldValue);
              console.log('new age',newValue);
              document.title=`目前点击的数是${newValue}`
         })

立即监听执行器,加上 {immediate:true}

六.生命周期、模板引用

onMounted(()=>{
            console.log('mounted');
          })
onUpdated(()=>{
console.log('updated');
})

onMounted用得是最多的,加载完毕的时候进行一些操作,比如发送异步请求

模板引用

const headline=ref(null);
<h1 ref="headline">{{ count }}</h1>

//onmounted中:
onMounted(()=>{
            console.log('mounted',headline.value);
          })

可以访问到dom节点:

image.png 使用TS的联合类型定义:(组件未加载完毕的时候可能是null)

const headline=ref<null|HTMLElement>(null);
onMounted(()=>{
            if(headline.value)
            console.log('mounted',headline.value);
          })

七.组件基础-属性Props

MyFile组件:

<template>
    <div class="profile-component"></div>
    <h1>name:{{ name }}</h1>
    <h1>age:{{ age }}</h1>
    <h1>doubleAge:{{ doubleAge }}</h1>
 </template>
 
 <script lang="ts">
 import { computed, defineComponent } from 'vue';
 export default defineComponent ({
     name:'MyFile',
     props:{
         name:{
             type:String,
             required:true
            
         },
         age:{
             type:Number,
             default:30
         }
     },
     setup(props) {
         const doubleAge=computed(()=>props.age*2)
         return{
             doubleAge
         }
     },
 })
 </script>
 
 <style scoped>
 
 </style>

在App.vue中给它传值:

<MyFile name="lyt"/>

把属性修改成一个对象:使用TS来标注类型(PropType)

<template>
    <div class="profile-component"></div>
    <h1>name:{{ user.name }}</h1>
    <h1>age:{{ user.age }}</h1>
    <h1>doubleAge:{{ doubleAge }}</h1>
 </template>
 
 <script lang="ts">
 import type { PropType } from 'vue';
 import { computed, defineComponent} from 'vue';
 interface Person{
    name:string,
    age:number
 }
 export default defineComponent ({
     name:'MyFile',
     props:{
         user:{
            type:Object as PropType<Person>,
            required:true
         }

     },
     setup(props) {
        const doubleAge=computed(()=>props.user.age*2)
        return {
            doubleAge
        }
     },
 })
 </script>
 
 <style scoped>
 
 </style>

在App.vue中使用要进行改变:

<MyFile :user="user"/>

八.组件自定义事件

子组件显示和隐藏功能:emit中添加了change事件,把isHidden的value传给父组件

<template>
    <div class="profile-component"></div>
    <h1>name:{{ user.name }}</h1>
    <h1 v-if="!isHidden">age:{{ user.age }}</h1>
    <h1 v-if="!isHidden">doubleAge:{{ doubleAge }}</h1>
    <button type="button" @click="toggleHide">{{ isHidden?'显示':'隐藏' }}</button>
 </template>
 
 <script lang="ts">
 import type { PropType } from 'vue';
 import { computed, defineComponent,ref} from 'vue';
 interface Person{
    name:string,
    age:number
 }
 export default defineComponent ({
     name:'MyFile',
     props:{
         user:{
            type:Object as PropType<Person>,
            required:true
         }
     },
     emits:['change'],
     setup(props,ctx) {
        const isHidden=ref(false);
        const doubleAge=computed(()=>props.user.age*2)
        const toggleHide=()=>{
            isHidden.value=!isHidden.value
            ctx.emit('change',isHidden.value)
        }
        return {
            doubleAge,
            isHidden,
            toggleHide
        }
     },
 })
 </script>
 
 <style scoped>
 
 </style>

App.vue中写相应的事件:获取到子组件的状态

const onChange=(hidden:boolean)=>{
             document.title=hidden?'隐藏':'显示'
          }
          
          
 <MyFile :user="user" @change="onChange"/>   
          

JavaScript 闭包:强大特性背后的概念、应用与内存考量

2025年4月4日 15:19

在 JavaScript 编程的世界里,闭包是一个既强大又微妙的概念。它能为我们的程序带来诸多便利,但如果使用不当,也可能引发一些棘手的问题。

一、闭包的概念

闭包是函数和其词法作用域的组合。它使内部函数(返回的函数)能够访问并记住其外层函数作用域中的变量,即使是在外部函数执行结束且在其作用域之外进行调用时,依然可以使用这些变量。

来看一个简单的示例代码:

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

let counter = createCounter();
counter(); // 输出 1
counter(); // 输出 2
counter(); // 输出 3

这个例子中,闭包中的函数为 increment 函数,它在 createCounter 函数内部定义,是内部函数,且作为 createCounter 函数的返回值。

词法作用域的概念

词法作用域,也叫静态作用域,由代码编写位置决定。在该规则下,变量和函数的作用域取决于其定义位置,而非调用位置。函数在定义时就记住了所在作用域,无论在何处调用,都能访问定义时所处作用域的变量。

词法作用域与闭包形成

increment 函数的词法作用域是 createCounter 函数的作用域。由于函数在定义时记住词法作用域,increment 函数在 createCounter 函数内定义,便记住了该作用域。

createCounter 函数执行时,创建局部变量 count 并初始化为 0,同时定义 increment 函数,该函数引用了 count 变量。createCounter 函数返回 increment 函数后,increment 函数与 createCounter 函数的词法作用域结合形成闭包。

闭包对变量的影响

createCounter 函数执行结束,其执行上下文从调用栈移除,按常规作用域规则 count 变量应被销毁。但因 increment 函数形成的闭包对 count 有引用,count 变量所在内存空间不会释放。

将 increment 函数赋值给 counter 变量,在 createCounter 函数作用域之外调用 counter 函数(即 increment 函数),increment 函数仍能访问并修改 count 变量,每次调用 counter 函数,count 就增加 1 并打印。

二、闭包的用途

(一)封装私有变量

在传统的面向对象编程里,存在访问控制的概念,比如私有成员(private members),这些成员只能在类的内部被访问和修改。不过 JavaScript 原生并没有直接提供这样的私有成员机制,但借助闭包就能模拟实现。

以下是一个简单的封装示例:

function Person() {
    let name = "John";
    return {
        getName: function() {
            return name;
        },
        setName: function(newName) {
            name = newName;
        }
    };
}
let person = Person();
console.log(person.getName()); // 输出 John
person.setName("Jane");
console.log(person.getName()); // 输出 Jane
  • Person 函数里定义了一个局部变量 name,并将其初始化为 "John"。这个变量仅存在于 Person 函数的作用域内。

  • Person 函数返回了一个对象,该对象包含两个方法:getName 和 setName。这两个方法都形成了闭包,因为它们引用了 Person 函数作用域中的 name 变量。

  • 外部代码调用 Person() 时,会得到返回的对象,并将其赋值给 person 变量。

  • 外部代码无法直接访问 name 变量,只能通过 person.getName() 和 person.setName() 这两个方法来获取和修改 name 的值。这样就实现了对 name 变量的封装,保护了数据的安全性和完整性。

(二)数据缓存

在软件开发中,有些计算操作的成本非常高,比如需要大量的 CPU 时间、内存或者网络资源等。如果在程序运行过程中,多次对相同的输入进行这些高成本的计算,会造成资源的浪费,降低程序的性能。而闭包可以用来实现数据缓存,避免重复计算,提高程序的运行效率。

考虑这样一个场景,我们需要频繁计算某个复杂数学函数的结果:

function expensiveCalculation() {
    let cache = {};
    return function(n) {
        if (cache[n]) {
            return cache[n];
        } else {
            let result = n * n * n; // 这里假设是一个复杂计算
            cache[n] = result;
            return result;
        }
    };
}
let calculate = expensiveCalculation();
console.log(calculate(5)); // 计算并输出 125
console.log(calculate(5)); // 从缓存中获取并输出 125

在这个例子中:

  • expensiveCalculation 函数内部定义了一个空对象 cache,用于存储已经计算过的结果。
  • 当调用 calculate(5) 时,首先检查 cache 对象中是否已经有 5 对应的计算结果。如果没有,就进行计算,并将结果存储到 cache 对象中,然后返回计算结果。
  • 当再次调用 calculate(5) 时,由于 cache 对象中已经有 5 对应的计算结果,所以直接从 cache 对象中获取并返回该结果,避免了重复计算。

(三)实现函数柯里化

函数柯里化指的是把一个多参数函数转换为一系列单参数函数的过程。通过这种转换,函数可以逐步接收参数,利用闭包记住已传入的参数,从而实现更灵活的函数调用和复用。

  • 假设去超市买东西,结账的时候需要做两件事:一是扫码商品,二是付款。原本这两件事可能需要一次性把所有商品信息(价格总和)和付款金额这两个 “参数” 提供给收银员,就像一个多参数函数一样。

  • 而函数柯里化就像是把这个过程拆分开来。你可以先把商品扫码,记录下商品的总价,这就相当于先传入了一个参数;之后再去付款,提供付款金额,这相当于传入了另一个参数。这样原本需要一次性完成的两个操作,现在可以分步进行了。

以一个简单的加法函数为例,来展示如何通过闭包实现柯里化:

function add(a) {
    return function(b) {
        return a + b;
    };
}
let add5 = add(5);
console.log(add5(3)); // 输出 8
console.log(add(2)(4)); // 输出 6
  • 多参数函数的常规理解:通常的加法函数可能是 function add(a, b) { return a + b; },调用时需要一次性传入两个参数,比如 add(5, 3),就像在超市一次性把商品总价和付款金额告诉收银员一样。

  • 函数柯里化的过程

  • add 函数接收一个参数 a,此时,add 函数内部定义了一个新的函数,这个新函数引用了外部 add 函数的参数 a,从而形成了闭包。它会把这个 a 值保存起来,即使 add 函数执行完毕,这个值也不会丢失。

  • add 函数返回一个新的函数,这个新函数等待接收另一个参数 b,就像你记录好商品总价后,等待付款时提供付款金额 b。由于闭包的存在,这个新函数能够随时获取之前记录的 a 值。

  • let add5 = add(5); 这一步先传入了参数 5,得到了一个新的函数 add5。这里的 add5 其实就是之前形成闭包的那个新函数,它通过闭包记住了之前传入的 5,即使 add 函数已经完成了它的使命。

  • add5(3) 这一步,你传入了另一个参数 3,新函数 add5 利用闭包的特性,把之前记住的 5 和现在传入的 3 相加,得到结果 8。闭包确保了 add5 函数在任何时候都能准确地使用之前记录的 5 这个值来进行计算。

三、闭包与内存管理

闭包虽功能强大,但在使用中需留意其对内存管理的影响。当闭包形成,内部函数对外部函数作用域变量的引用,会致使这些变量在外部函数执行完毕后依旧驻留在内存中。这一特性在某些场景下,可能会引发内存泄漏问题。

例如在处理 DOM 事件的闭包场景中,如果闭包持续持有对 DOM 元素的引用,即便该元素已从 DOM 树移除,元素所占用的内存也无法正常释放,进而导致内存占用不必要地增加。

避免闭包引发内存问题的方法

使用具名函数作为事件监听器

将事件监听器设为具名函数,而不是匿名函数。这样在需要移除监听器时,可以通过函数名精准操作。

  • 匿名函数没有明确的标识符,当你想要移除它时,无法直接引用该函数。这就会导致即使你不再需要这个事件监听器,它依然会和元素绑定,无法被垃圾回收机制回收,进而造成内存泄漏。
// 具名函数作为事件监听器
function handleClick() {
    console.log('按钮被点击');
}
const button = document.getElementById('myButton');
button.addEventListener('click', handleClick);
// 当不再需要该事件监听器时
button.removeEventListener('click', handleClick);

及时清理闭包引用

若闭包中存在对大对象或不再使用对象的引用,应在合适时机手动将这些引用设为null,以便垃圾回收机制回收内存。如:

function outerFunction() {
    let largeObject = { /* 一个大对象 */ };
    function innerFunction() {
        console.log(largeObject);
    }
    // 在某些条件下,当确定不再需要largeObject时
    innerFunction();
    largeObject = null;
    return innerFunction;
}

控制闭包的生命周期

确保闭包仅在必要时段存在。闭包在函数内部创建时,如果它的功能仅在该函数执行期间有用,就要避免将其返回或赋值给全局变量,否则闭包会因被长期持有,而让其引用的外部变量一直占用内存。

例如:

function processData() {
    let data = [1, 2, 3, 4, 5];
    function inner() {
        // 处理数据
        return data.map(num => num * 2);
    }
    // 仅在processData函数内部使用inner闭包
    let result = inner();
    // inner闭包在processData函数执行结束后不再被引用,其引用的data等变量可被回收
    return result;
}

在上述代码中,processData 函数内的 inner 函数形成闭包,它引用了外部的 data 变量。但 inner 仅在 processData 函数内部被调用,当 processData 执行完毕,inner 不再被引用,data 变量就可被垃圾回收机制回收。

与之相反,如果闭包被长期持有,情况就大不一样。比如:

let globalClosure;
function processData() {
    let data = [1, 2, 3, 4, 5];
    function inner() {
        return data.map(num => num * 2);
    }
    // 将闭包返回并赋值给全局变量
    globalClosure = inner;
    return inner();
}
processData();

这里 inner 闭包被赋值给全局变量 globalClosure,即便 processData 函数执行结束,inner 因被全局变量引用而持续存在,其引用的 data 变量也会一直占用内存,无法被释放,进而可能引发内存泄漏问题 。

使用 WeakMap

  1. 强引用

强引用是最常见的引用类型。当一个变量直接指向一个对象时,就形成了强引用。只要这个变量在作用域内存在且没有被重新赋值或销毁,那么它所引用的对象就会一直存在于内存中,垃圾回收机制不会回收该对象。

例如:

let person = {
    name: "Alice",
    age: 30
};

在上述代码中,person 变量对创建的对象 {name: "Alice", age: 30} 进行了强引用,只要 person 变量还在作用域内,这个对象就会一直占用内存空间。

  1. 弱引用
  • 弱引用不会阻止对象被垃圾回收。当一个对象只被弱引用所指向,而没有其他任何强引用时,在垃圾回收机制运行时,这个对象就会被回收,无论它是否还在被使用。

  • 需要注意的是,在 JavaScript 中,WeakMap 是与弱引用密切相关的一种数据结构,它的键就是基于弱引用实现的。

例如:

const weakRefObj = {};
const weakMap = new WeakMap();
weakMap.set(weakRefObj, { data: "相关数据" });
weakRefObj = null; 
  • 在 JavaScript 中,变量是对对象的引用。当执行 const weakRefObj = {}; 时,创建了一个对象 {},并让 weakRefObj 变量引用它。此时,对象有一个强引用,即 weakRefObj

  • 当执行 weakRefObj = null; 时,weakRefObj 不再指向之前创建的对象,而是被赋值为 null。这意味着原来对象的强引用被移除了,因为 weakRefObj 这个唯一的强引用变量不再引用它。

  • 由于 WeakMap 的键是弱引用,当对象的所有强引用(这里就是 weakRefObj)都被移除后,垃圾回收机制就会认为这个对象可以被回收了。并且,因为 WeakMap 中该对象作为键所关联的整个键值对依赖于这个键对象,当键对象被回收时,与之对应的键值对也会从 WeakMap 中被移除(在垃圾回收机制运行时) 。

第五章 使用Context和订阅来共享组件状态

2025年4月4日 14:51

在前面两章,我们学习了如何使用Context和订阅来实现全局状态。它们二者各有其利弊:Context允许我们在不同的子树注入不同的值,而订阅可以避免一些额外的重新渲染。

在这一章,我们会学习一个新的方法:把React Context 和 订阅 组合起来。这样组合的话,我们可以得到这两个方法各自的优点:

  • Context可以为一棵子树提供全局状态,而且Context的provider可以被嵌套。Context允许我们在React 组件生命周期内,通过类似 useState风格的钩子来获取全局状态。
  • 另一方面,订阅允许我们控制重新渲染。

同时享有这两个方法的优点,对于大型应用是很好的 - 因为,我们可以为不同的子树注入不同的值,同时还避免额外的重新渲染。

在这一章,我们会讨论这几个主题:

  • 探索模块状态的局限
  • 理解何时使用Context
  • 实现Context和订阅的组合

探索模块状态的局限

因为模块状态是定义在React 组件之外,它有一个局限:这个模块状态是走单例模式的,所以,你无法为不同的子树注入不同的值。

让我们复习一下在第四章实现的 createStore方法:

const createStore = (initialState) => {
    let state = initialState;
    const callback = new Set();
    const getState = () => state;
    const setState = (nextState) => {
        state = typeof nextState === 'function'
        ? nextState(state) : nextState;
        callbacks.forEach((callback) => callback());
    };
    const subscribe = (callback) => {
        callbacks.add(callback);
        return () => { callbacks.delete(callback)' };
    };
    return { getState, setState, subscribe };
}

我们可以使用createStore来定义一个store:

const store = createStore({ count: 0 })

注意,store是定义在React组件之外的。

为了在React组件中使用store,我们要借助 useStore。下面这个例子,是有两个组件展示共同的来找store的值。我们可以借助在第四章实现的 useStore来实现:

const Counter = () => {
    const [state, setState] = useStore(store);
    const inc = () => {
        setState((prev) => {
            ...prev,
            count: prev.count + 1
        });
    };
    
    return (
        <div>
            {state.count} <button onClick={inc}>+1</button>
        </div>
    )
}

const Component = () => {
    <>
        <Counter />
        <Counter />
    </>
}

我们有用来展示store对象内 count 的 Counter组件,还有一个用于 更新 count 值的 button。因为Counter组件是可复用的,Component组件有两个 Counter组件 实例。Component组件 会为 我们展示一对组件展示共同的状态。

现在,假设我们想展示另一对 Counter组件。我需要在 Component组件 中 展示 另外两个新的组件,但是这对组件 所展示的 counter 参数需要和原来不一样。

我们可以生成一个 新的 count值。我们可以 为 我们已经定义的 store 对象 添加一个新的值。但是如果我们想把不同的 sotre 拆分开来,我们可以这样:

const store2 = createStore({ count: 0 });

因为 createStore 是可复用的,创建 store2 因此很简单。

现在,我们可以创建Counter2 组件:

const Counter2 = () => {
    const [state, setState] = useStore(store2);
    const inc = () => {
        setState((prev) => {
            ...prev,
            count: prev.count + 1
        });
    };
    
    return (
        <div>
            {state.count} <button onClick={inc}>+1</button>
        </div>
    )
}

const Component2 = () => {
    <>
        <Counter2 />
        <Counter2 />
    </>
}

你也许发现了,CounterCounter2 组件的 相似性 - 它们都有14行,唯一的不同在于store的指向不同。我们也许还需要Counter3Counter4组件来展示更多store的值。理想情况而言,Counter应该是可复用的,但是因为 块级状态是定义在 React外的,所以它无法服用。这是块级状态的局限。

如果Counter可以给不同的store复用,将会非常好。这是伪代码:

const Component = () => (
     <StoreProvider>
         <Counter />
         <Counter />
     </StoreProvider>
);

const Component2 = () => (
     <Store2Provider>
         <Counter />
         <Counter />
     </Store2Provider>
);

const Component3 = () => (
     <Store3Provider>
         <Counter />
         <Counter />
     </Store3Provider>
);

观察这段代码,你会发现Component1,Component2, Component3, Component4几乎一样。唯一的不同在于Provider组件。这正是React的Context该登场的时候。

现在,你已经知道了 块级状态的局限了,也知道了多sotre的理想模式。接下来,我们要复习Context并探索其使用。

理解何时使用Context。

在我们学习如何 组合 Context 与 订阅之前,我们先复习一下 Context是如何运作的。

下面是一个Context的简单用例:

const ThemeContext = createContext("light");


const Component = () => {
    const theme = useContext(ThemeContext);
    return <div>Theme: {theme}</div>
}

useContext的返回值,取决于Context所以在的组件树。

如果要改变Context的值,我们可以使用Provider:

<ThemeContext.Provider value="dark">
    <Component />
</ThemeContext.Provder>

如此一来,Compoenent展示的是 dark。

Provider是可以被嵌套的。而useContext取值是遵从就近原则:

<ThemeContext.Provider value="this value is not used">
    <ThemeContext.Provider value="this value is not used">
        <ThemeContext.Provider value="this is the value used">
            <Component />
        </ThemeContext.Provider>
    </ThemeContext.Provider>
</ThemeContext.Provider>

如果组件树中没有provider,useContext取默认值。

比如说,我们假设Root组件位于组件树最顶端:

const Root = () => {
    <>
        <Component />
    </>
}

此时,Component组件展示的是light。

再看看provider提供的是默认值的情况:

const Root = () => {
    <ThemeContext.Provider value="light">
        <Component />
    <ThemeContext.Provider/>
}

一样,Component组件展示的还是light。

接下来,我们讨论一下何时该使用Context。首先,我们要回顾之前的例子,有provider和无provider有什么区别?我们可以说,没有。没有provider返回的是默认值。

对于一个Context来说,设置合适的默认值是有必要的。而Context的provider可以理解为针对默认值的重写函数。

ThemeContext的例子中,我们已经有一个可用的默认值了,那么设置provider的意义为何?因为,我们需要在不同的子树注入不同的值。否则,我们直接使用默认值即可。

使用Context来管理全局状态时,你可能只会在根节点处使用一个提供者(Provider)。这是一种合理的使用场景,但这种场景也可以通过第 4 章 中介绍的带订阅功能的模块状态来实现。鉴于模块状态能够涵盖在根节点使用一个上下文提供者的使用场景,那么只有当我们需要为不同的子树提供不同的值时,才需要使用上下文来管理全局状态。

在本节中,我们回顾了 React 上下文,并了解了何时使用它。接下来,我们将学习如何将Conetxt和订阅结合使用。

实现Context和订阅的组合

我们知道,使用Context来注入全局状态时,会有一个问题:它会产生不必要的 重新渲染。

模块状态的订阅没有额外 重新渲染的问题,但它有另一问题:它只能为整个组件树提供一个值。

我们现在要把这两者结合起来,来避免这两者的缺点。让我们来实现这个特性。我们先从createStore开始:

getState: () => T;
    setState: (action: T | ((prev: T) => T)) => void;
    subscribe: (callback: () => void) => () => void;
};

const createStore = <T extends unknown>(
    initialState: T
    ): Store<T> => {
    let state = initialState;
    const callbacks = new Set<() => void>();
    const getState = () => state;
    const setState = (nextState: T | ((prev: T) => T)) => {
        state =
            typeof nextState === "function"
            ? (nextState as (prev: T) => T)(state)
            : nextState;
        callbacks.forEach((callback) => callback());
    };
 
    const subscribe = (callback: () => void) => {
        callbacks.add(callback);
        return () => {
            callbacks.delete(callback);
        };
    };
    return{ getState, setState, subscribe };
};

在第四章,我们把createStore用在模块状态上。现在,我们要把createStore用在Context的值上。

下面这段代码用于创建Context。其默认值用于传给createContext,其指向为默认store:

type State = { count: number; text?: string };

const StoreConext = createContext<Store<State>>(
    createStore<State>({ count: 0, text: "hello" })
)

此时,这个store有两个属性: counttext

为了把这些值提供给不同的子树,我们要创建StoreContext:

const StoreProvider = ({
    initialState,
    children
} : {
    initialState: State,
    children: ReactNode
}) => {
    const storeRef = useRef<Store<State>>();
    if (!storeRef.current) {
        storeRef.current = createStore(initialState);
    }
    return (
        <StoreContext.Provider value={storeRef.current}>
            {children}
       </StoreContext.Provider>
    )
}

useRef用于确保store对象只会在第一次渲染时进行初始化。

为了使用store对象,我们要实现一个useSelector钩子。不像第四章的useStoreSelector以store为参数,useSelector的参数中没有store:

const useSelector = <S extends unknown>(
    selector: (state: State) => S
) => {
    const store = useContext(StoreContext);
    return useSubscription(
        useMemo(
            () => ({
                getCurrentValue: () => selector(store.getState()),
                subscribe: store.subscribe,
            }),
            [store, selector]
        )
    )
}

useContextuseSubscription,是这个模式的关键。

不像模块状态,我们需要提供一个用Context来更新状态的方法。useSetState是一个简单的可以返回setState的钩子:

const useSetState = () => {
    const store = useContext(StoreContext);
    return store.setState;
}

现在,我们可以使用我们实现的方法了。下面是一个用于展示store中count的组件。我们在组件外定义了一个selectCount,否则,我们要用useCallback来包裹它:

const selectCount = (state: State) => state.count;

const Component = () => {
    const count = useSelector(selectCount);
    const setState = useSetState();
    const inc = () => {
        setState((prev) => ({
            ...prev,
            count: prev.count + 1,
        }));
    };
    return (
        <div>
            count: {count} <button onClick={inc}> +1</button>
        </div>
    )
};

需要注意的是,这个Component组件并不被绑定到任何特定stor。这个组件可以用在不同的store上。

我们可以让这个Component组件在不同的地方:

  • 在任何provider之外
  • 在第一个provider之中
  • 在第二个provider之中

下面这个App组件,把Component组件放在了三个地方:1.provider之外;2.在第一个provider之中;3.在第二个provider之中。在不同provider中的Component,则享有不同的值:

const App = () => (
     <>
         <h1>Using default store</h1>
         <Component />
         <Component />
         <StoreProvider initialState={{ count: 10 }}>
             <h1>Using store provider</h1>
             <Component />
             <Component />
             <StoreProvider initialState={{ count: 20 }}>
                 <h1>Using inner store provider</h1>
                 <Component />
                 <Component />
            </StoreProvider>
        </StoreProvider>
    </>
);

消费了相同store对象的Component组件会享有相同的count值。在这个例子中,在不同组件树层级中的组件,会消费不同的store,所以在不同地方的组件会展示不同的count:

image.png

如果你在 “使用默认存储” 中点击 “+1” 按钮,你会看到 “使用默认存储” 中的两个计数会一起更新。如果你在 “使用存储提供者” 中点击 “+1” 按钮,你会看到 “使用存储提供者” 中的两个计数会一起更新。“使用内部存储提供者” 的情况也是如此。

在本节中,我们学习了如何利用Context和订阅的相关优势来实现全局状态。由于上下文的存在,我们能够将状态隔离在一个子树中;而由于订阅的作用,我们能够避免额外的重新渲染。

概要

在这一章,我们学习了一个新的模式,组合Context和订阅。它将两者的优点结合在了一起:为不同的子树注入独立的值,并且避免不必要的重新渲染。这个模式对中大型项目特别有用。在中大型应用中,时候会发生不同的子树有不同的值的问题,而使用这个模式可以解决这一问题,还避免了不必要的重新渲染。

从下一章开始,我们要深入一些全局状态库。我们将会学习这些库是如何基于我们现在学习的知识建立起来的。

【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案

2025年4月3日 15:28
前言 自适应进化宣言 当监控网络精准定位病灶,真正的挑战浮出水面:系统能否像生物般自主进化? 五维感知——通过设备传感器实时捕获环境指纹(如地铁隧道弱光环境自动切换省电渲染) 基因调参——150个性能

机构:欧洲股市料小幅低开,美国关税继续拖累市场

2025年4月4日 15:01
预计欧洲股市今日开盘将小幅走低,此前美国总统特朗普宣布征收全面贸易关税,导致周四股市大幅下挫。据IG,预计欧洲斯托克600指数将低开0.15%;德国DAX指数和英国富时100指数预计将低开0.1%。投资者担心美国关税政策对全球经济的影响,此外,周五的注意力转向美国的就业数据。这将让人们初步了解美国关税政策宣布前的就业市场状况。投资者还在关注任何有关针对美国关税的反制措施的声明。丹麦银行的分析师在一份研究报告中称:“亚洲股市[周五]上午继续走低,欧洲和美国的股指期货也是如此。(新浪财经)

umi+模块联邦-配置指南

作者 carterwu
2025年4月4日 14:47

背景

目前我们将公共组件封装到npm包里,在每个应用使用npm包的方式进行消费

问题

每次更新组件需要发布npm包,本地开发不能实时看到组件更新效果,需要重新安装npm包才能生效。

解决方法

模块联邦可以在多个 webpack 编译产物之间共享模块、依赖、页面甚至应用,通过全局变量的组合,还可以在不同模块之前进行数据的获取,让跨应用间做到模块共享真正的插拔式的便捷使用。比如a应用如果想使用b应用中table的组件,通过模块联邦可以直接在a中进行import('b/table')非常的方便。

线上 Runtime(运行时) 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装npm 包、构建再发布了。

umi提供了**Module Federation 插件**

怎么做

共享应用

封装公共组件

导出的模块按照约定,将源代码目录下的 exposes 一级子目录名作为导出项,导出文件为该目录下的 index 文件

//   /exposesCounter/index.tsx
import React from 'react';

export default (props: { init?: number }) => {
  const [c, setC] = React.useState(props.init ?? 10);

  return (
    <div>
      <h1> remote Counter</h1>
      <div>
        <button
          data-testid="remote-button"
          onClick={() => {
            setC((c) => c + 1);
          }}
        >
          click to add new
        </button>
      </div>
      <div>
        remote hooks counter
        <span data-testid="remote-counter">{c}</span>
      </div>
    </div>
  );
};

umirc.ts配置

import { defineConfig } from '@umijs/max';

const shared = {
  react: {
    singleton: true,
    eager: false,
  },
  'react-dom': {
    singleton: true,
    eager: false,
  },
};

const moduleFederationName = 'remoteCounter'; // 模块名称

export default defineConfig({
  mfsu: false,
  mf: {
    name: moduleFederationName,
    shared,
  },
  publicPath: 'http://127.0.0.1:9000/',
});

使用方

  • keyResolver 用于在运行时决定使用 entries 哪个 key; 推荐使用 立即调用函数表达式 的形式,可以在函数中实现较复杂的功能。不支持异步的函数。
  • keyResolver 也可以使用静态的值,配置形式 keyResolver: '"PROD"'

umirc.ts配置

import { defineConfig } from "@umijs/max";

const shared = {
  react: {
    singleton: true,
    eager: false,
  },
  "react-dom": {
    singleton: true,
    eager: false,
  },
};

export default defineConfig({
  mfsu:false,
  mf: {
    name: "hostUser",
    remotes: [
      {
        name: "remoteCounter",
        entries: {
          DEV: "http://127.0.0.1:9000/remote.js", // 本地 地址
          PROD: "http://127.0.0.1:9000/remote.js",// 线上地址
        },
        keyResolver: `(()=> 'DEV')()`,
      }
    ],
    shared,
  },
});

使用公共组件

import React, { Suspense } from 'react';

const RemoteCounter = React.lazy(() => {
  // @ts-ignore
  return import('remoteCounter/Counter');
});
export default () => {
    <RemoteCounter />
}

配置项说明

(一)webpack ModuleFederationPlugin配置

  1. name 必须,当前应用的名字,全局唯一ID,通过 name/{expose} 的方式使用
  2. library 可选,打包方式,与 name 保持一致即可
  3. filename 可选,打包后的文件名,对应上面的 remoteEntry.js
  4. remotes 可选,表示当前应用是一个 Host,可以引用 Remote 中 expose 的模块
  5. exposes 可选,表示当前应用是一个 Remote,exposes 内的模块可以被其他的 Host 引用,引用方式为 import(name/{expose})
  6. shared 可选,依赖的包(下面包含了 shared 中包含的配置项)
  • 如果配置了这个属性。webpack在加载的时候会先判断本地应用是否存在对应的包,如果不存在,则加载远程应用的依赖包。

  • 以 app2 来说,因为它是一个远程应用,配置了["react", "react-dom"] ,而它被 app1 所消费,所以 webpack 会先查找 app1 是否存在这两个包,如果不存在就使用 app2 自带包。 app1里面同样申明了这两个参数,因为 app1 是本地应用,所以会直接用 app1 的依赖。

  • shared 配置项指示 remote 应用的输出内容和 host 应用可以共用哪些依赖。 shared 要想生效,则 host 应用和 remote 应用的 shared 配置的依赖要一致。

  • import 共享依赖的实际的 package name,如果未指定,默认为用户自定义的共享依赖名,即 react-shared。如果是这样的话,webpack 打包是会抛出异常的,因为实际上并没有 react-shared 这个包。

  • singleton 是否开启单例模式,true 则开启。如何启用单例模式,那么 remote 应用组件和 host 应用共享的依赖只加载一次,且与版本无关。 如果版本不一致,会给出警告。不开启单例模式下,如果 remote 应用和 host 应用共享依赖的版本不一致,remote 应用和 host 应用需要分别各自加载依赖。

  • requiredVersion 指定共享依赖的版本,默认值为当前应用的依赖版本。- 如果 requiredVersion 与实际应用的依赖的版本不一致,会给出警告。

  • strictVersion 是否需要严格的版本控制。单例模式下,如果 strictVersion 与实际应用的依赖的版本不一致,会抛出异常。默认值为 false。

  • shareKey 共享依赖的别名, 默认值值 shared 配置项的 key 值。

  • shareScope 当前共享依赖的作用域名称,默认为 default。

  • eager 共享依赖在打包过程中是否被分离为 async chunk。eager 为 false, 共享依赖被单独分离为 async chunk; eager 为 true, 共享依赖会打包到 main、remoteEntry,不会被分离。默认值为 false,如果设置为 true, 共享依赖其实是没有意义的。

  • shareScope 所用共享依赖的作用域名称,默认为 default。如果 shareScope 和 share["xxx"].shareScope 同时存在,share["xxx"].shareScope 的优先级更高。

示例代码

gitlab.evaluateai.cn/liangchaofe…

参考

umijs.org/docs/max/mf…

Mf配置项: ModuleFederationPlugin | webpack

mf配置项的一些解读:juejin.cn

mf原理:Module Federation | webpack 中文文档

Zustand状态管理库:轻量级、高效的React解决方案

2025年4月4日 14:36

Zustand是一个轻量级的React状态管理库,旨在为开发者提供一个简洁、可扩展和高效的状态管理解决方案。它使用简单的API,基于React Hooks机制,允许开发者通过自定义钩子来访问和更新状态。

常用场景

Zustand常用于以下场景:

  • 状态管理:Zustand帮助开发者集中化管理应用状态,特别适用于小型和中型应用。
  • 异步数据处理:支持异步操作,例如数据获取和更新。
  • 持久化存储:通过中间件实现状态持久化,例如使用localStorage存储数据。
  • 组件间状态共享:使得组件间状态共享变得简单,无需复杂的上下文或提供者。

解决的问题

Zustand解决了以下问题:

  • 简化状态管理:提供了比Redux等库更简洁的API,减少了样板代码。
  • 提高性能:仅在状态变化时重新渲染相关组件,避免不必要的渲染。
  • 灵活性和可扩展性:支持中间件扩展,适应不同项目需求。
  • 易于集成:可以与其他状态管理库共存,方便迁移。

基本用法

安装

使用npm或yarn安装Zustand:

npm install zustand

yarn add zustand

创建状态存储

import create from 'zustand';

const useStore = create(set => ({
  count: 0,
  increase: () => set(state => ({ count: state.count + 1 })),
  decrease: () => set(state => ({ count: state.count - 1 })),
}));

使用状态存储

import React from 'react';
import { useStore } from './store';

function Counter() {
  const { count, increase, decrease } = useStore();
  
  return (
    
      {count}
      Increase
      Decrease
    
  );
}

异步状态管理

import React, { useEffect } from 'react';
import create from 'zustand';

const useStore = create(set => ({
  data: null,
  loading: false,
  error: null,
  fetchData: async () => {
    set({ loading: true });
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      set({ data, loading: false });
    } catch (error) {
      set({ error, loading: false });
    }
  },
}));

function DataFetcher() {
  const { data, loading, error, fetchData } = useStore();
  useEffect(() => {
    fetchData();
  }, [fetchData]);

  if (loading) return Loading...;
  if (error) return Error: {error.message};

  return (
    
      {data && (
        
          {data.map(item => (
            {item.name}
          ))}
        
      )}
    
  );
}

持久化存储

Zustand支持通过中间件实现状态持久化,例如使用localStorage存储数据。

import create from 'zustand';
import { persist } from 'zustand/middleware';

const useStore = create(
  persist(
    set => ({
      theme: 'light',
      toggleTheme: () => set(state => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
    }),
    {
      name: 'theme', // 存储名称
      getStorage: () => localStorage, // 存储介质
    }
  )
);

中间件支持

Zustand支持中间件扩展,允许开发者添加额外功能,如日志记录、持久化存储等。

import create from 'zustand';

const logTrace = config => (set, get, api) => config(
  (...args) => {
    console.log('Before applying: ', args);
    set(...args);
    console.log('After applying: ', get());
  },
  get,
  api
);

const useStore = create(
  logTrace(set => ({
    count: 0,
    increase: () => set(state => ({ count: state.count + 1 })),
  }))
);

Zustand的轻量级、简洁的API和灵活的扩展能力,使其成为React状态管理的优雅解决方案。

chromium魔改——navigator.webdriver 检测

2025年4月4日 14:30

chromium源码官网 https://source.chromium.org/chromium/chromium/src

说下修改的chromium源码思路:

首先在修改源码过检测之前,我们要知道它是怎么检测的,找到他通过哪个JS的API来做的检测,只有知道了如何检测,我们才能想办法去绕过,俗话说,知己知彼百战百胜嘛!

绕过 navigator.webdriver 检测

在浏览器中环境对抗的时候,会通过 navigator.webdriver 来检测是否为自动化在控制网站,如果返回为true,则表示是自动化在控制,否则不是,所以我们只需要将他的返回值永远返回false即可

自动化控制返回的值 在这里插入图片描述

正常浏览器返回的值 在这里插入图片描述

在chromium源码中,找到以下路径

\src\third_party\blink\renderer\core\frame\navigator.cc

有以下两种修改方式,选其中一种即可。

方式一: 在这里插入图片描述

方式二: 在这里插入图片描述

修改完之后保存代码即可,在修改的时候不要将原始代码删除,注释即可方便出错的时候查问题。

保存好了之后,运行以下命令,这个命令就是前面编译章节中的编译命令,你当时编译的时候用的是什么,这里就写什么,他不是重新编译,是增量编译,只对新增的内容进行编译,所以速度会快很多。

autoninja -C out/Default chrome

在这里插入图片描述

编译好之后,通过自动化程序检测一下

我们这里使用 playwright 来测试

from playwright.sync_api import sync_playwright

def run():
    with sync_playwright() as p:
        browser = p.chromium.launch(
            executable_path="E:\my_code\chromium\src\out\Default\chrome.exe",
            headless=False
        )
        # 创建一个新的浏览器上下文
        context = browser.new_context()
        # 打开一个新页面
        page = context.new_page()
        # 访问目标网站
        page.goto("https://www.baidu.com")

        # 等待用户输入后再关闭浏览器
        input("Press Enter to close the browser...")
        
        # 关闭浏览器
        browser.close()

if __name__ == "__main__":
    run()

可以看到是已经成功了

在这里插入图片描述

日产撤回美国工厂减产计划 应对特朗普关税

2025年4月4日 14:25
日产汽车4日透露,撤回作为合理化措施之一、原定4月起实施的美国工厂减产计划,维持当前生产体制。鉴于美国特朗普政府对进口汽车加征关税的措施3日生效,此举旨在让不征收关税的本地生产保持规模。(新浪财经)

中国电动汽车领衔以色列2025年一季度销量

2025年4月4日 14:24
以色列汽车进口商协会近日发布数据显示,2025年第一季度,中国品牌电动汽车继续在以色列市场热销,中国稳居以色列最大汽车供应国。数据显示,今年1月至3月以色列共售出13132辆中国品牌电动汽车,占同期电动汽车总销量的82.8%。其中,比亚迪车型ATTO 3以1939辆的销量位居第一,成为以色列最畅销电动汽车。小鹏汽车的中型SUV G6以1783辆的销量位列第二,吉利旗下品牌领克的车型02以1276辆位列第三。同期,包括电动汽车和燃油车在内的中国汽车在以色列的总销量为24976辆,继续高居所有出口国之首,韩国和日本汽车分列第二和第三位。(新华社)
❌
❌