阅读视图
兆威机电、迈威生物港股IPO获中国证监会备案
峰飞航空:累计获得eVTOL确认订单300架
澳大利亚将禁止机舱内使用充电宝
一日一技|将豆瓣书影音记录迁移至 Obsidian
IDEA研究院孵化企业视启未来获近亿元融资
《哪吒2》不参加奥斯卡竞逐
第八届进博会,上海合作交流采购团达成意向采购额7.12亿元
美联储传声筒:鲍威尔不是降息的最大障碍 美联储内部共识机制濒临瓦解
报告:“成分党”成为美妆主流消费群体
专项债投向政府投资基金将超800亿,支持科技自立自强
嵌入主线程消息循环的任务调度器
最近在网友协助下把 soluna port 到包括 wasm 在内的非 windows 平台。其间遇到很多难题,大多是多线程环境的问题。因为 soluna 的根基就是基于 ltask 的多线程调度器,如果用单线程实现它,整个项目的意义就几乎不存在,所以它是把项目维护下去必须解决的问题。
好在 lua 有优秀的 coroutine 支持,它可以把运行流程抽象成数据,而 Lua 本身并未限制数据的具体储存方式,所以完全可以存在于内存堆中,脱离于 C 栈存在,这为各种在 C 环境下的多线程难题开了后门。C 语言依赖栈运行代码逻辑,而栈绑定于线程,线程调度通常由操作系统完成,所以用常规方式无法让代码跨线程运行:即,无法通过常规手法让一段代码的流程前半段在一个线程运行,而用另一个线程运行后半段;但是,在 C 上建立一个 Lua 层,则很容易绕开这个限制,只用标准方法就可以自由控制程序运行流程。
上一次发现利用一些技巧就可以完成一些看似不可能却的确可行的调度方式是 多线程串行运行 Lua 虚拟机 。
简单复述一下当时的需求:
希望可以在单个 Lua 虚拟机内模拟多线程并发。当一个 Lua 的 coroutine 运行到 C 函数中时,若此刻 C 函数希望阻塞等待一个 IO 请求,常规的方法是 yield 回 Lua 虚拟机,让调度器持有一个 Lua coroutine 的状态,待完成 IO 请求后,再由调度器 resume 这个 coroutine 。这样做的难题是,运行到一半的 C 函数,上下文状态还在 C 所属线程的栈中,一旦 yield 回 Lua 虚拟机,必须放弃 C 栈上的状态,并在下次 resume 时可以重建。这通常难以实现,这也是为何 Lua 的 coroutine C api 难以理解又很难使用的原因。尤其使用第三方 C 库,几乎没可能适配。
另一个折中的方法是让 Lua 虚拟机在 C 函数中阻塞,硬等到 IO 操作完成。但在阻塞过程中,无法使用这个 Lua 虚拟机。若使用者期待 Lua 虚拟机中多个 coroutine 以多线程方式并行工作,恐怕会失望。即使其它 coroutine 的业务和 IO 完全无关,一个 IO 阻塞操作会让它完全无法并行工作。
变通的方式是(在编译时)打开 Lua 的线程锁。在调用 IO 阻塞前解开线程锁,只要 IO 操作本身不涉及对 Lua State 的操作,那么 Lua 解释器在调用 C 函数前的那一刻会解开线程锁,这样就可以允许阻塞操作过程中,Lua 虚拟机可以执行其它操作。
线程锁本身依赖系统线程库的调度器。不适合像 ltask 这样自己实现任务调度(即在有限个系统线程下调度远超系统线程数的任务)。但是,我们可以配合 ltask 实现类似的锁机制。这就是之前这个 patch 实现的东西:Lua 层调用可能阻塞的 C 函数前加锁通知 ltask 调度器,在 C 函数中,用户主动在阻塞操作前解锁。ltask 的调度器在 C 函数返回前就将虚拟机提前放回调度表。当阻塞操作完成后,重新加锁会等待调度器完成(如果有)正在运行的在同一个 Lua 虚拟机上的任务完成。这样,整个 Lua 虚拟机实质上还在串行运行其中的任务。而使用者看起来在一个 coroutine 尚未 yield 之前就开始运行另一个 coroutine ,直到其它 coroutine yield 后再继续未完的工作。同一个 Lua 虚拟机的多个 coroutine 是在多个操作系统线程上完成的,但却保持串行。
这个 patch 最终并未合并进 ltask ,因为我觉得它对使用者有更高的要求。但经此,我开了不少脑洞,明白在必要时牺牲一些复杂度就可以完成一些超乎寻常的任务。
这次我面临的是新的问题:sokol 并未设计成线程安全。api 不能并发。一开始我并不想使用复杂的解决方案,以为只要保证 sokol 不并发就够了。期间遇到的问题是 Windows API 死锁 ,也很容易绕过。
对于图形 API ,我只是简单的将图形 API 调用都塞在同一个 render 服务中。并在主线程的 sokol 回调函数中利用一个信号量和渲染过程同步。虽然 Direct3D ,Matal ,Vulkan 这些为多线程设计的底层图形 API 这么用都没有问题,但 OpenGL (在 Linux 上开启)却将状态放在当前线程上。一开始,我们通过额外调用 MakeCurrent 绕开限制,但在我们向 wasm 移植时却遇到障碍。
最终,我还是希望找到一个方法让所有图形 API 的调用都真正从主线程,也就是 sokol 提供的 callback 函数中发起。而不是用信号量同步,让它们在其它工作线程运行。
难题在于,主线程是通过事件消息循环驱动的,没有全部的控制权。不适合在其上实现任务调度器。一个任务调度器最好有所有时间片的控制权,它才好简单有效的分配时间片,没有任务时可以休眠而不是在事件循环没有新事件时强制休眠。我不想为这种特别的工作方式改造 ltask 的任务调度器,让主线程的事件回调函数伪装成一个功能不完整的特殊工作线程。我实际需要的是:把一个 Lua 虚拟机内的特定任务分配给主线程回调函数运行,在没有这种特定任务时,其它任务还是交给 ltask 做常规调度。
细想之下,解决方法和上一个需求有异曲同工之处:Lua 在启动这种特殊任务(必须在主线程回调函数内运行)前通知调度器。这时把虚拟机暂时移出调度表,而在主线程的回调函数中(通过信号量)发现有新任务到来,就接手处理特殊片段。处理完毕后,再把它归还给调度器。
通过这个方案,我们顺利把 soluna port 到 wasm 环境,同时简化了 Linux/OpenGL 实现。当我了解到 wasm 上有 pthread api 和原生 web worker api 两套多线程 api 后,我又信心满满的想用 worker api 来实现。但最终未能如愿。具体讨论在这个 issue 中 ,倒不是完全做不到,而是我觉得不应该牺牲太多复杂度。比如把 soluna 中所有的 IO 操作都转发到主线程中运行(这是 web worker 的限制所在,也是 wasm pthread 原本要解决的问题)。
昨天发现了上面解决方案实施中的一点纰漏:虽然给 ltask 打了个洞,可以在系统主线程夺过指定任务运行,但在交换控制权回调度器时,忽略了 ltask 的所有工作线程可能因为没有任务而全部休眠的可能性。仅仅把任务推回(线程安全的)任务队列是不够的。还需要重启调度器(如果处于休眠状态)。具体讨论见这个 issue 。
ps. 自从搬家后,我的 Linux 机器一直没有开机。昨天为了在 Linux 环境下测试,才重新装起来。bug 虽然重现,但视乎在我的机器上更为严重:一旦程序失去响应,整个系统都卡住了,甚至冷启动都没用。直接把机器弄死,而且五分钟内都开不了机(BIOS 进不去,屏幕无信号)。我怀疑是显卡驱动的 bug ,因为太久没升级系统,头一次升级还失败了,pacman 报告出现依赖问题拒绝更新。强删了几个 Electron (这个毒瘤)的几个历史版本后,系统升级才得以继续。最后更新了最新版的 Nvidia 包似乎就一切正常了。
从“版本号打架”到 30 秒内提醒用户刷新:一个微前端团队的实践
从“版本号打架”到 30 秒内提醒用户刷新:一个微前端团队的实践
1. 背景与痛点
我们团队维护着一个微前端子应用集群,每个子应用都需要同时服务 dev / test / release / online 多套环境。分支策略(master / release / test / dev / hotfix / feature_x.x.x)加上 Jenkins 自动化,让“一天多次发布”成为常态。但真正影响交付效率的并不是发布次数,而是一个顽固的问题:测试同学常年停留在旧版本页面。
1.1 真实场景
- 测试在早上打开 dev 页面,下午我们发布了新的组件样式;
- 他们继续在旧页面里回归,反馈的问题我们一眼看出“这是老版本”;
- 群里喊“刷新一下”并不靠谱,于是“无效缺陷 + 反复沟通”成了常态。
更严重的一次事故,是我们在版本检查逻辑里同时使用了 webpack DefinePlugin 与自定义插件,各自调用了一次 getAppVersion()。结果前端控制台打印的是 0.8.3-release-202511210828,而 version.json 里是 0.8.3-release-202511210829。两边只差 1 秒钟,却让线上用户始终被提示刷新,形象地被团队称为“版本号打架”。
1.2 我们的诉求
- 用户在 30 秒内感知版本更新;
- 弹窗里能看到“当前版本 / 最新版本 / 环境”;
- 支持“立即刷新 / 稍后再说”,不给用户造成中断;
- 方案需兼容现有微前端架构与 CI/CD 流程,不依赖后端改造。
2. 方案探索与取舍
在动手前,我们列出几种可行方式:
| 方案 | 实现复杂度 | 实时性 | 依赖 | 适配场景 | 关键优缺点 |
|---|---|---|---|---|---|
| 纯前端轮询 version.json | 低 | 中(30s) | 前端 + Nginx | 多环境微前端 | 成本最低;轻微网络开销 |
| Service Worker/PWA | 中 | 较高 | 现代浏览器 | PWA 应用 | 缓存控制好,但改造量大 |
| WebSocket 推送 | 高 | 最高 | 后端服务 | 强实时场景 | 需要额外服务端开发 |
| 后端接口统一管理 | 中 | 中 | 前后端 | 版本集中管理 | 带来跨团队耦合 |
综合团队资源与落地速度,我们选择了 纯前端轮询 + 静态版本文件 的做法,并明确两个原则:
-
版本号唯一,可追溯:
基础版本号-环境-时间戳; -
发布零侵入:Jenkins 仍旧运行
npm run build-xxx,无需新增步骤。
3. 技术方案总览
-
构建阶段生成 version.json:在
vue.config.js中提前计算版本号,既注入到前端(process.env.APP_VERSION),也写入输出目录的version.json; -
前端轮询比对:应用启动后每 30 秒请求一次
version.json,禁用缓存并携带时间戳,比较版本号; -
交互提示:复用 Ant Design Vue 的
Modal.confirm,展示当前/最新版本与环境; -
缓存策略:Nginx 对 HTML/
version.json禁止缓存,对 JS/CSS/图片继续长缓存; -
CI/CD 配合:所有环境沿用既有脚本,只是构建产物目录多了一份实时的
version.json。
4. 关键落地细节
4.1 版本号只生成一次(Build-time Deterministic Versioning)
vue.config.js 抽象 buildEnvName、buildVersion,并在 DefinePlugin 与生成 version.json 时复用:
const buildEnvName = getEnvName();
const buildVersion = getAppVersion();
module.exports = {
configureWebpack: {
plugins: [
new webpack.DefinePlugin({
"process.env.APP_VERSION": JSON.stringify(buildVersion),
"process.env.APP_ENV": JSON.stringify(buildEnvName),
}),
],
},
chainWebpack(config) {
config.plugin("generate-version-json").use({
apply(compiler) {
compiler.hooks.done.tap("GenerateVersionJsonPlugin", () => {
fs.writeFileSync(
path.resolve(__dirname, "edu/version.json"),
JSON.stringify(
{
version: buildVersion,
env: buildEnvName,
timestamp: new Date().toISOString(),
publicPath: "/child/edu",
},
null,
2
)
);
});
},
});
},
};
这样即使构建过程持续 5~10 分钟,注入的版本号和静态文件里的版本仍保持一致。这其实是把“构建产物视为不可变工件”的原则落地——保证任何使用该工件的入口看到的元数据都是同一个快照。
4.2 版本检查器(Runtime Polling & Cache Busting)
class VersionChecker {
currentVersion = process.env.APP_VERSION;
publicPath = "/child/edu";
checkInterval = 30 * 1000;
init() {
console.log(`📌 当前前端版本:${this.currentVersion}(${process.env.APP_ENV})`);
this.startChecking();
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible" && !this.hasNotified) {
this.checkForUpdate();
}
});
}
async checkForUpdate() {
const url = `${this.publicPath}/version.json?t=${Date.now()}`;
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) return;
const latestInfo = await response.json();
if (latestInfo.version !== this.currentVersion && !this.hasNotified) {
this.hasNotified = true;
this.stopChecking();
this.showUpdateModal(latestInfo.version, latestInfo.env);
}
}
}
这里有两个容易被忽略的细节:
-
fetch显式加cache: "no-store",再叠加时间戳参数,防止 CDN / 浏览器任何一层干预; -
visibilitychange监听,保证窗口重新激活时立即比对,避免用户在后台等了很久才看到弹窗。
入口 main.ts 在应用 mount 之后调用 versionChecker.init(),即可把整个检测链路串起来。
4.3 Nginx 缓存策略(Precise Cache Partition)
location / {
if ($request_filename ~* .html$) {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
location /child/edu {
if ($request_filename ~* .html$) {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
location ~* /child/edu/version.json$ {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
add_header Surrogate-Control "no-store";
}
这一层的思路是把资源分成两类:需要实时性(HTML、version.json)就 no-store,其余走长缓存。再配合 try_files 兜底 history 路由,微前端子应用的独立部署不会互相影响。
4.4 CI/CD 配置(Zero-touch Pipeline)
| 环境 | 构建命令 | 输出路径 | 说明 |
|---|---|---|---|
| develop | npm run build-develop |
/child/edu |
日常开发验证 |
| testing | npm run build-testing |
/child/edu |
集成测试 |
| release | npm run build-release |
/child/edu |
预发布 |
| production | npm run build-production |
/child/edu |
线上 |
所有命令都带 cross-env NODE_OPTIONS=--openssl-legacy-provider,以兼容不同系统的 OpenSSL 版本。更重要的是,这套方案没有“要求运维多做一步”——构建产物天然携带 version.json,任何环境拿到包即可上线。
5. 测试与验证
我们定义了一个完整的回归流程,确保方案不会给测试和上线带来额外负担:
-
首次访问:打开 dev 环境页面,确认控制台打印版本号,Network 里能看到
version.json且响应头无缓存; -
触发新版本:调整任意文案,重新发布,保持旧页面不刷新;
-
轮询验证:30 秒内弹出提示框,展示当前/最新版本和环境;
-
交互路径:
- 点击“立即刷新”:页面强制 reload,新版本生效;
- 点击“稍后刷新”:记录取消动作并重新开启轮询;
-
边界场景:切 tab / 清缓存 / 新设备访问 / 短时间连续发布,均能正确感知最新版本。
6. 注意事项与常见问题
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 没有弹窗 |
version.json 404 或版本未变 |
检查部署路径、确认构建是否生成文件 |
| 弹窗后刷新仍旧版本 | 静态资源被缓存 | 核实 Nginx 缓存策略、查看浏览器缓存设置 |
| 构建失败 |
cross-env 未安装或权限不足 |
补充依赖、确保 Jenkins 工作目录可写 |
| 持续误报更新 | 构建阶段多次生成版本号 | 在 vue.config.js 顶部缓存 buildVersion 并全局复用 |
7. 落地成效
- 旧页面用户在 30 秒内收到提醒,测试效率显著提升;
- “幽灵弹窗”彻底消失,版本对比逻辑稳定;
- 方案只触碰前端与 Nginx 配置,发布流程无需改造;
- 文档化后,其他子应用无需重复思考,直接复用。
8. 展望
下一步我们计划:
- 封装通用 SDK:抽象版本生成、轮询、弹窗逻辑,支持 Vue CLI / Vite;
- 可视化版本面板:在主应用汇总所有环境的版本和发布时间;
- 差异化策略:针对高优先级版本强制刷新,普通版本允许用户自行选择。
这次实践让我再次意识到:真正的坑往往藏在看似“微不足道”的细节里。当我们把问题和思考写成文档、沉淀成模板,团队就能以更小的代价获得更稳定的交付。如果你也在推进微前端版本同步,欢迎交流、互相借鉴。
比特币跌破84000美元
Flutter组件封装:标签拖拽排序 NDragSortWrap
一、需求来源
最近需要实现一个可拖拽标签需求,实现之后顺手封装一下,效果如下:
二、使用示例
//
// DraggableDemo.dart
// flutter_templet_project
//
// Created by shang on 6/2/21 5:37 PM.
// Copyright © 6/2/21 shang. All rights reserved.
//
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/n_drag_sort_wrap.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';
class DraggableDemo extends StatefulWidget {
final String? title;
const DraggableDemo({Key? key, this.title}) : super(key: key);
@override
_DraggableDemoState createState() => _DraggableDemoState();
}
class _DraggableDemoState extends State<DraggableDemo> with TickerProviderStateMixin {
final scrollController = ScrollController();
List<String> tags = List.generate(20, (i) => "标签$i");
late List<String> others = List.generate(10, (i) => "其他${i + tags.length}");
late var tabController = TabController(length: tags.length, vsync: this);
bool canEdit = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title ?? "$widget"),
),
body: buildBody(),
);
}
Widget buildBody() {
return Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildDragSortWrap(),
],
),
),
);
}
Widget buildDragSortWrap() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
tabController = TabController(length: tags.length, vsync: this);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(
child: Row(
children: [
Expanded(
child: TabBar(
controller: tabController,
isScrollable: true,
tabs: tags.map((e) => Tab(text: e)).toList(),
labelColor: Colors.black87,
unselectedLabelColor: Colors.black38,
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
indicatorPadding: EdgeInsets.symmetric(horizontal: 16),
),
),
GestureDetector(
onTap: () {
DLog.d("more");
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Icon(Icons.keyboard_arrow_down),
),
)
],
),
),
buildTagBar(
onEdit: () {
canEdit = !canEdit;
setState(() {});
},
),
NDragSortWrap<String>(
spacing: 12,
runSpacing: 8,
items: tags,
itemBuilder: (context, item, isDragging) {
return buildItem(
isDragging: isDragging,
item: item,
isTopRightVisible: canEdit,
topRight: GestureDetector(
onTap: () {
DLog.d(item);
tags.remove(item);
setState(() {});
},
child: Icon(Icons.remove, size: 14, color: Colors.white),
),
);
},
onChanged: (newList) {
tags = newList;
setState(() {});
},
),
Divider(height: 16),
Wrap(
spacing: 12,
runSpacing: 8,
children: [
...others.map(
(item) {
return buildItem(
isDragging: false,
item: item,
isTopRightVisible: canEdit,
topRight: GestureDetector(
onTap: () {
DLog.d(item);
others.remove(item);
tags.add(item);
setState(() {});
},
child: Icon(Icons.add, size: 14, color: Colors.white),
),
);
},
),
],
)
],
);
},
);
}
Widget buildItem({
required bool isDragging,
required String item,
bool isTopRightVisible = true,
required Widget topRight,
}) {
return Badge(
backgroundColor: Colors.red,
textColor: Colors.white,
offset: Offset(4, -4),
isLabelVisible: isTopRightVisible,
label: topRight,
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isDragging ? Colors.green.withOpacity(0.6) : Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Text(
item,
style: TextStyle(color: Colors.white),
),
),
);
}
Widget buildTagBar({required VoidCallback onEdit}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'我的频道',
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF303034),
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
Text(
' (点击编辑可排序)',
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF7C7C85),
fontSize: 12,
),
),
],
),
),
GestureDetector(
onTap: onEdit,
child: Text(
'编辑',
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF303034),
fontSize: 14,
),
),
),
],
),
);
}
}
三、源码
组件 NDragSortWrap
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';
class NDragSortWrap<T extends Object> extends StatefulWidget {
const NDragSortWrap({
super.key,
required this.items,
required this.itemBuilder,
this.onChanged,
this.spacing = 8,
this.runSpacing = 8,
});
final List<T> items;
final Widget Function(BuildContext context, T item, bool isDragging) itemBuilder;
final void Function(List<T> newList)? onChanged;
final double spacing;
final double runSpacing;
@override
State<NDragSortWrap<T>> createState() => _NDragSortWrapState<T>();
}
class _NDragSortWrapState<T extends Object> extends State<NDragSortWrap<T>> {
late List<T> _list;
@override
void initState() {
super.initState();
_list = List<T>.from(widget.items);
}
@override
void didUpdateWidget(covariant NDragSortWrap<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_list = List<T>.from(widget.items);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Wrap(
spacing: widget.spacing,
runSpacing: widget.runSpacing,
children: [
for (int i = 0; i < _list.length; i++) _buildDraggableItem(context, i),
],
);
}
Widget _buildDraggableItem(BuildContext context, int index) {
final item = _list[index];
return LongPressDraggable<T>(
data: item,
feedback: Material(
color: Colors.transparent,
child: widget.itemBuilder(context, item, true),
),
childWhenDragging: Opacity(
opacity: 0.3,
child: widget.itemBuilder(context, item, false),
),
onDragCompleted: () {},
onDraggableCanceled: (_, __) {},
child: DragTarget<T>(
onAcceptWithDetails: (details) {
final draggedItem = details.data;
final oldIndex = _list.indexOf(draggedItem);
final newIndex = index;
final item = _list.removeAt(oldIndex);
_list.insert(newIndex, item);
setState(() {});
widget.onChanged?.call(_list);
},
builder: (context, _, __) {
return widget.itemBuilder(context, item, false);
},
),
);
}
}
最后、总结
实现起来并不复杂,就是依赖官方组件 LongPressDraggable 提供的各种状态做数据处理,然后刷新页面即可。
基于 easy_rxdart 的轻量响应式与状态管理架构实践
把原型链画成地铁图:坐 3 站路就能看懂 JS 的“继承”怎么跑
前言
在 JavaScript 里,“原型”这个词听起来高大上,其实就是一个“默认备胎”:当对象自己找不到属性时,就沿着原型这条暗道去“亲戚家”借。没有类、没有蓝图,仅靠这条备胎链,就能把公共方法层层复用,让内存省一半、代码少一半。本文只聊“原型”本身——prototype、__proto__ 这些眼前能用的工具,把“借东西”的流程画成一张家谱图,帮你先看清“亲戚”是谁、住哪、怎么串门。至于后面更高阶的封装、多态、模块化,等我们把这条链走熟再升级也不迟。
一: 原型 prototype
又称显示原型,函数天生拥有的一个属性 ,将构造函数中的一些固定的属性和方法挂载到原型上,在创建实例的时候,就不需要重复执行这些属性和方法了,我们先来创造一个环境,主角依然是我们的小米 su7 ,su7 的属性有无数个,但是各个车主只需要选择并改动的属性并没有那么多,这个时候我们就能用得到原型。
Car.prototype.name = 'su7-Ultra'
Car.prototype.lang = 4800
Car.prototype.height = 1400
Car.prototype.weight = 1.5
function Car(color) {
this.color = color
}
const car1 = new Car('pink')
const car2 = new Car('green')
console.log(car1);
用原型之后我们只需要输入想要的颜色即可,不需要反反复复创建函数。同时挂载在原型上的属性是可以直接被实例对象访问到的(如下图)
并且实例对象无法修改 构造函数 原型上的属性值,
Person.prototype.say = '我太帅了'
function Person() {
this.name = '饶总'
}
const p = new Person()
p.say = 'hello'
const p2 = new Person()
console.log(p2.say);
这个时候同时有两个 key 都为 say ,但 value 不相同,一个被挂在构造函数的原型上,一个被挂在第一个实例对象 p 上,按照上面说法实例对象无法修改构造函数原型上的属性值,但是打印出来真是这样吗,究竟是 '我太帅了' ,还是 'hello',我们来揭晓答案
果真是实例对象无法修改构造函数原型上的属性值。
二:对象原型 __proto__
又称隐式原型,每一个对象都拥有一个 __proto__ 属性,该属性值也是一个对象, v8在访问对象中的一个属性时,会先访问该对象中的显示属性,如果找不到,就回去对象的隐式原型中查找,实例对象的隐式原型 === 构造函数的显示原型,所以如果实例对象的隐式原型找不到那么就再会去构造函数的显示原型上找
这不得不再引出一个概念—— 原型链:v8 在访问对象中的属性时,会先访问该对象中的显示属性,如果找不到,就去对象的隐式原型上找,如果还找不到,就去__proto__.__proto__ 上找,层层往上,直到找到null为止。这种查找关系被称为原型链
为了更好的理解它,我们来举个继承例子
function Parent() {
this.lastName = '张'
}
Child.prototype = new Parent() // {lastName: '张'}.__proto__ = Parent.prototype
function Child() {
this.age = 18
}
const c = new Child()
console.log(c.lastName);
在实例对象中我们只能找到儿子的年龄属性,姓氏张是儿子从父亲那里继承的,我们要查到儿子的姓氏,根据原型链原理我们先从实例对象 c 中找有没有显示属性是关于姓氏的,很明显并没有,接着就去实例对象的隐式原型上找,也没有,最后就来到了构造函数的显示原型上查找,在代码的第四行可以看到构造函数的显示原型被赋值上了 lastName 属性,所以最终是否可以查找得到姓氏张呢?我们来直接看结果
好你说这个也太简单了吧,就父子继承而已。话不多说我再附上一串代码和打印结果
Grand.prototype.house = function() {
console.log('四合院');
}
function Grand() {
this.card = 10000
}
Parent.prototype = new Grand() // {card: 10000}.__proto__ = Grand.prototype.__proto__ = Object.prototype.__proto__ = null
function Parent() {
this.lastName = '张'
}
Child.prototype = new Parent() // {lastName: '张'}.__proto__ = Parent.prototype
function Child() {
this.age = 18
}
const c = new Child() // {age: 18}.__proto__ = Child.prototype
console.log(c.card);
c.house()
// console.log(c.toString());
这里我们要注意一点:如果让你查找一个整个页面都没有的属性又该会是什么打印结果呢?我们注意看上面最后一行注释掉的代码,他的输出结果如下
他是直接找到了全局的对象上,经历了一遍原型链查找在 Object.prototype上找到,如果再不找到最终就会停留在null上 ,下面放一张 js 界中广为流传的一张图,如果你能看懂那么你就是彻底会了!
三:new 在干什么?
这时候你会说什么?上篇文章不是讲了 new 究竟干了些什么吗,怎么又问,不必惊讶,其实上次没讲全,这次来带你真正看看 new 究竟究竟都干了些什么(这绝对是最终理解)直接一套小连招先上五个步骤
- 创建一个空对象
- 让构造函数中的
this指向这个空对象 - 执行构造函数中的代码 (等同于往空对象中添加属性值)
- 将这个空对象的隐式原型(
__proto__) 赋值成 构造函数的显示原型(prototype) - 返回该对象
再上代码(加注释)
Car.prototype.run = function() {
console.log('running');
}
function Car() { // new Function()
// const obj = {} //1
// Car.call(obj) // call 方法将 Car 函数中的 this = obj 2
this.name = 'su7' // 3
// obj.__proto__ = Car.prototype // 4
// return obj 5
}
const car = new Car() // {name: 'su7'}.__proto__ == Car.prototype
car.run()
最后输出
结语
- 显式原型(
prototype)是函数自带的“样板房”,所有实例都能来蹭住。 - 隐式原型(
__proto__)是实例手里的“门禁卡”,刷卡就能进样板房找方法。 - 原型链就是一张“门禁卡链”:刷不到就再刷上一层的卡,直到
null到头。 -
new的五步曲:空对象→认证→绑卡→执行→返回,一口气把“样板房”继承给新实例。
把这四点串成一张地铁图,以后看任何“找不到属性”的问题,先问一句:它刷卡刷到第几站了?原型链通了,继承就不再是黑魔法。
从回调到async/await:JavaScript异步编程的进化之路
从回调到async/await:JavaScript异步编程的进化之路
在JavaScript的世界里,异步编程是绕不开的核心命题。从最初的回调函数,到ES6的Promise,再到ES8的async/await,每一次语法升级都在解决前一阶段的痛点,让异步代码更贴近人类的线性思维。本文将结合文件读取的实际案例,带你看清JavaScript异步编程的进化脉络。
一、ES6之前:回调函数的“地狱”与坚守
在ES6引入Promise之前,JavaScript处理异步操作的唯一方案就是回调函数。其核心逻辑是:将异步操作完成后需要执行的代码,作为参数传入异步函数,当异步任务结束时,由JavaScript引擎自动调用这个回调函数。
以文件读取API fs.readFile 为例,传统回调写法如下:
fs.readFile('./1.html','utf-8',(err,data) => {
if(err) {
console.log(err);
return;
}
console.log(data);
console.log(111);
})
这种写法的优势是直观易懂,对于单一异步任务完全够用。但它的缺陷也极为明显:当多个异步任务存在依赖关系时,代码会嵌套成“回调地狱”。比如先读A文件,再根据A文件内容读B这段回调函数代码是ES6之前的主流异步写法,核心依赖Node.js的fs模块(文件系统模块)实现文件读取。我们从API参数到执行逻辑逐行拆解:
fs.readFile('./1.html','utf-8',(err,data) => { ... }) 中,fs.readFile 作为异步读取方法,接收三个关键参数:第一个参数'./1.html'是文件路径,指定读取当前目录下的1.html文件;第二个参数'utf-8'是编码格式,确保读取的二进制数据转为字符串而非Buffer对象;第三个参数是回调函数,这是异步的核心——JS引擎不会等待文件读取完成,而是继续执行后续代码,读取结束后自动调用此函数处理结果。
回调函数遵循“错误优先”规范,err参数优先接收错误信息:若读取失败(如文件不存在),err为错误对象,执行console.log(err)打印错误并通过return终止函数;若成功,err为null,data接收文件内容,随后打印内容与数字111。
这种写法对单一异步任务足够简洁,但多任务依赖时会陷入“回调地狱”。比如读完1.html后需根据内容读2.html,代码会嵌套成多层缩进,可读性与维护性急剧下降,这也催生了ES6的Promise方案。
二、ES6 Promise:异步流程的“标准化”升级
Promise是ES6为解决回调地狱推出的异步容器,它将异步操作的“成功/失败”状态标准化,通过链式调用替代嵌套。Promise封装代码,正是对文件读取异步任务的规范化改造:
// es6 Promise
const p = new Promise((resolve,reject) => {
fs.readFile('./1.html', 'utf-8', (err, data) => {
if (err) {
reject(err); // 异步失败,传递错误
return;
}
resolve(data); // 异步成功,传递结果
})
})
p.then(data => {
console.log(data);
console.log(111);
})
Promise构造函数接收一个“执行器函数”,该函数有两个内置参数resolve和reject,均为函数:resolve用于标记异步成功,将结果数据传递给后续处理;reject用于标记失败,传递错误信息。
上述代码中,文件读取的回调逻辑被重构:失败时调用reject(err),成功时调用resolve(data),Promise实例p便承载了异步任务的状态。p.then(data => { ... })是结果处理方式,then方法接收resolve传递的数据,实现成功逻辑。若需处理错误,可链式调用.catch(err => { ... })捕获reject的错误。
Promise的核心优势是链式调用。若需连续读取两个文件,只需在第一个then中返回新的Promise,再链式调用then即可,代码始终保持扁平,彻底摆脱嵌套困境。但多个链式调用时,“then链”仍会略显冗余,ES8的async/await在此基础上实现了进一步优化。
三、ES8 async/await:异步代码的“同步化”终极方案
async/await是ES8推出的Promise语法糖,它让异步代码具备同步代码的线性逻辑,堪称异步编程的“终极形态”。async/await基于前文的Promise实例实现,大幅简化了结果获取逻辑:
// es8 async
const main = async() => {
const html = await p;
console.log(html);
}
main();
这段代码的核心是两个关键字的配合:async用于修饰函数(如这里的main函数),表明该函数是异步函数,其返回值必然是Promise;await只能在async函数内使用,用于等待Promise完成——它会“暂停”函数执行,直到Promise状态变为成功(fulfilled),再将resolve的数据赋值给左侧变量(如html)。
需要补充的是,实际开发中需完善错误处理:若Promise状态为失败(rejected),await会抛出异常,需用try/catch捕获,优化后的代码如下:
const main = async() => {
try {
const html = await p;
console.log(html);
} catch (err) {
console.log('错误:', err); // 捕获reject的错误
}
}
main();
async/await的价值不仅在于简洁,更在于逻辑贴近人类思维。比如连续执行三个异步任务,只需用三个await依次等待,代码顺序与任务执行顺序完全一致,无需关注Promise的链式调用细节。
四、进化总结:从工具到思维的贴近
回顾JavaScript异步的进化之路,每一步都是对“开发体验”的优化:回调函数是异步的基础工具,却违背线性思维;Promise通过标准化容器规范异步流程,解决嵌套问题;async/await则彻底抹平异步与同步的语法差异,让代码逻辑与人类思考顺序完全统一。
实际开发中无需拘泥于单一方案:简单异步任务可用回调;多任务依赖优先用Promise链式调用;复杂业务逻辑则首选async/await,兼顾可读性与维护性。理解三者的关联与演进逻辑,才能根据场景灵活选择最合适的异步方案,写出高效优雅的JavaScript代码。
如何快速实现响应式多屏幕适配
项目涉及的场景比较简单,所以我个人的配置也比较粗糙简单,如果要对于更详细的多端适配,可能需要更细致的设定,如果希望一键快速实现大体上过得去的pc端多端适配,可以用这个法子。
- 安装postcss(必须)+tailwindcss(可选)
- 安装postcss-plugin-px2rem(必须)
-
在豆包搜索postcss-plugin-px2rem如何配置应用在postcss.config.js文件里面按需配置(关键)
module.exports = {
plugins: [
require('postcss-plugin-px2rem')({
rootValue: 16, // 根元素字体大小(默认 16px,即 1rem = 16px)
unitPrecision: 5, // 转换后的 rem 保留小数位数(默认 5)
propList: ['*'], // 需要转换的 CSS 属性(默认 ['*'],即所有属性)
selectorBlackList: [], // 不转换的选择器(如 ['body'],则 body 下的 px 不转换)
replace: true, // 是否直接替换原 px 值(默认 true,不保留原 px)
mediaQuery: false, // 是否转换媒体查询中的 px(默认 false,不转换)
minPixelValue: 0, // 最小转换像素值(默认 0,即所有 px 都转换)
exclude: /node_modules/i // 排除的文件路径(如 node_modules 下的样式不转换)
})
]
}
后续需要详细配置的话,需要关注的两个属性
selectorBlackList: [],
mediaQuery: false,
- 在App.vue 文件下添加(关键)
function setRootFontSize() {
const screenWidth = document.documentElement.clientWidth;
const rootFontSize = screenWidth / 7.5;
document.documentElement.style.fontSize = `${rootFontSize}px`;
}
setRootFontSize();
window.addEventListener('resize', setRootFontSize);
这里因为我们项目主要是大屏和超大屏工作,所以我针对我们的项目具体应用场景做了一下更改
function setRootFontSize() {
const screenWidth = document.documentElement.clientWidth;
if(screenWidth<1560){
const rootFontSize = screenWidth / 75;
document.documentElement.style.fontSize = `${rootFontSize}px`;
}
}
具体原理搜索rem是什么意思就行了