KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
02 · 一页被判成静态:`next build` 表里那个符号是怎么来的 — keel 龙骨
这一章回答:○ 和 ƒ 到底在说什么、哪些代码算"碰了请求"、哪些不算——以及为什么你在渲染函数里写了一句 new Date(),Next 照样把这一页标成"静态"。
这一章回答:○ 和 ƒ 到底在说什么、哪些代码算"碰了请求"、哪些不算——以及为什么你在渲染函数里写了一句 new Date(),Next 照样把这一页标成"静态"。
现场:我明明取了当前时间,构建表却给了我一个圈
手上有一个页面,渲染函数第一行就是取当前时间:
export default function Page() {
const now = new Date().toISOString();
return <b data-probe="now">{now}</b>;
}
按直觉,这页"每次打开都应该显示一个新的时刻",所以你在跑完 next build、翻到它那一行时,心里准备看到的是动态标记。实际躺在那儿的是:
├ ○ /dynamic-time
一个空心圈。再往下两行,另一个页面只做了一件更"无害"的事——把 URL 上的 ?q= 读出来打印——它拿到的却是实心 ƒ:
├ ƒ /search
先别往下翻,给出你的预测: 决定一个页面是 ○ 还是 ƒ 的,是"这段代码算出来的值会不会每次都变",还是别的什么东西?如果赌前者,你就输在 new Date() 这一页上;如果赌后者,那这个"别的"具体是什么,你能一句话说出来吗?
好在 next build 每次都会把答案直接印出来。本课整套实验在同一台 Windows 机器上跑,Next.js 16.3.8(Turbopack 是默认打包器,构建日志首行写着 ▲ Next.js 16.3.8 (Turbopack))、react / react-dom 19.3.0、Node 22.22.2。实验台是一个只有 12 条业务路由的小 app/,另外单独起了一个 mock 源监听 127.0.0.1:3999,它每被打到一次就 hits += 1,把全部命中时刻记在 /log 里。构建表长这样:
Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /api/revalidate
├ ○ /client-boundary
├ ƒ /cookies-read
├ ○ /dynamic-time
├ ○ /fetch-default
├ ƒ /fetch-force
├ ƒ /fetch-nostore
├ ƒ /fetch-revalidate
├ ○ /hydration-mismatch
├ ƒ /search
├ ○ /static-time
└ ƒ /stream
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
14 行,7 个 ○、7 个 ƒ——这个配比没有玄机,它就是本实验台摆出来的形状。
这一章就顺着这张表往下:先看清这两个符号的分界线,再逐条对照"哪些写法把页面推到了哪一边"。 上一章(01 首屏是谁渲染的)讲的是四种渲染方式凭什么分开,这一章把镜头收到最窄的一处:构建期到底读了什么,才敢给一页盖上"静态"的章。
一、○ 和 ƒ 是同一张表上的两个字
先读表尾那两行注释,它们就是官方定义:
○ (Static) prerendered as static content—— 这一页的内容在构建期就被渲染成了成品,打到请求时直接回放;ƒ (Dynamic) server-rendered on demand—— 这一页不在构建期定稿,每次请求来了才在服务端渲染一遍。
这张表不是配置文件,也不是某个开关的读数,它是构建期把每条路由都"预演"一遍之后落下来的判定结果。也就是说,你在表里看到的那个字符,是"构建这台机器当时真的试着去渲染了这页、然后发现它能不能提前定稿"的结论。它可读——这是这一章所有方法的立足点:你不用去猜 Next 的心事,你有一个每跑一次就重印一遍的判定清单。
把这两个符号记成一句话:○ = 构建期就能把 HTML 写死;ƒ = 构建期写不死,只能等到请求。 后面所有反直觉的例子,都是在这句话上做加减法。
二、/dynamic-time:渲染函数里取当前时间,它还是 ○
/dynamic-time 是整章第一个反例,源码短到不能再短——渲染函数里取一次时钟,别的什么都不做,既没有 force-dynamic,也没有读任何请求:
// app/dynamic-time/page.jsx
export default function Page() {
const now = new Date().toISOString();
return <b data-probe="now">{now}</b>;
}
判定结果我们已经看到了:○ Static。所以真正的判据不是"值会不会变",而是别的东西。new Date()、Math.random() 这一类和"当前时钟、随机源"有关的调用,Next 一概不认它们是动态 API。 直觉在这里翻车的机制很朴素:这两个函数读的是 JS 运行时自己的时钟和随机源,它们跟"这次请求带来了什么"完全无关。而 Next 的判定问的恰恰是后一个问题——这次渲染的输出,依赖不依赖外部请求携带的信息? 依赖,就是 ƒ;不依赖,就是 ○。
跑起来看,这个"不依赖"落到什么程度。用运行时探针把这一页请求两次,第二次拿到的值还是同一个:
/data-probe="now" 的值: 2026-10-06T12:22:12.641Z
刷新任意多次: 不变
本路由区间内 mock 源命中: +0
x-nextjs-cache: HIT
cache-control: s-maxage=31536000
那个时间戳 2026-10-06T12:22:12.641Z 不是"你请求它的时刻",是"构建它的时刻"。这一刻就是这份 HTML 被生产出来的那一秒,之后无论刷新多少次,浏览器拿到的都是这份写死了的字节。值在构建期被冻结了。
把 /dynamic-time 和隔壁的 /static-time 摆一起看,这条结论会更清楚。/static-time 只是在模块顶层取了一次时间当常量:
const RENDERED_AT = new Date().toISOString();
两者判定一样是 ○,运行时证据也一样(区间 +0、x-nextjs-cache: HIT、s-maxage=31536000)。区别只在"取值的时机":一个在模块求值期取,一个在渲染函数里取——但对 Next 的判定来说,这两处都发生在"构建期把页面渲染一遍"的同一次过程里,取到的都是那一刻的时钟。所以"我把 new Date() 从模块顶挪进渲染函数,它就该变成动态了吧"这个念头,在这里是错的。
这条事实有一个很实际的下游后果:在自托管(next start)的部署里,用户在页面上看到的那个"当前时间",永远是"你部署那一刻的时间"。 它不随请求变、不随刷新变,只有重新构建才会动。你要的是"每次请求都新的时间",那这页就必须碰一个请求(比如读 cookies()),否则构建这台机器会一直替你回放旧值。
三、什么算"碰了请求"
把反例放一边,来看被判定成 ƒ 的两条入口——它们都把"输出依赖请求"这件事摆得很直白。
第一条:读 cookies()。
import { cookies } from "next/headers";
export default async function Page() {
const c = await cookies();
const all = c.getAll().map((x) => `${x.name}=${x.value}`).join("; ") || "(无 cookie)";
return <code data-probe="cookies">{all}</code>;
}
/cookies-read 在表里是 ƒ Dynamic。原因没有任何争议:cookie 是每个访问者各自带上来的,同一份 HTML 不可能同时满足所有访问者,构建期当然写不死。这条几乎所有人第一次就能猜对。
第二条:只读 searchParams。这条才是真正扎人的。
export default async function Page({ searchParams }) {
const sp = await searchParams;
return <b data-probe="q">{String(sp.q ?? "(空)")}</b>;
}
/search 在表里是 ƒ Dynamic。请注意这一页做了什么、又没做什么:它只是把 URL 上的查询串读出来打印,没有读 cookie、没有读身份、没有 fetch、没有任何"这个人是谁"的信息。即便如此,只读查询串,就足以让整页退化成动态。 逻辑和 cookie 那条同源——?q=abc 和 ?q=xyz 要给出两份不同的输出,这份输出就没法在构建期一次定稿。
这两条合起来给出判定的分水岭:分水岭是"渲染时有没有碰请求",不是"代码里有没有时间或随机数"。 碰了请求(cookie、查询串、请求头、no-store 的 fetch……),ƒ;没碰,哪怕你满页 Math.random(),也还是 ○。
补一句边界:和 cookies() 同一族的 headers() 在本实验台没有单独取证,理由与量法都记在 ## 生产边界,正文不替它下结论。
四、force-dynamic 的作用面是整页
前面 /stream、/fetch-force、/fetch-revalidate 三条路由都带了同一行:
export const dynamic = "force-dynamic";
这一行是路由级的显式声明,它的作用是直接告诉判定:"别替我算,这页就是动态的。" /stream 在表里是 ƒ,声明生效。要看清它的作用面——它压的是整页的判定,不是某一次 fetch、也不是某一个组件。加了它,这一页整体离开静态那一列。
force-dynamic 不是唯一能把页面推成 ƒ 的东西。看 /fetch-nostore:
const res = await fetch(SRC, { cache: "no-store" });
它没有写 force-dynamic,只是给 fetch 挂了一个 cache: "no-store"。结果 /fetch-nostore 在表里也是 ƒ Dynamic。这条值得单独记:fetch 级的 no-store 会把整页拖成动态。 你只改了 fetch 的一个选项,倒下的却是整条路由的判定。(它和 force-dynamic 是两个不同的触发器、同一个落点,这条在 03 fetch 的四种写法 里会拆到底。)
于是可以提炼出一条页级的规律:一个页面里只要有一处碰了请求,整页就是 ƒ。 判定从来不下到组件粒度——它不是"这页的这部分静态、那部分动态",而是"这页能不能在构建期整体定稿"。你可以把 ○ 理解成"整页通过了构建期的定稿检查",把 ƒ 理解成"没通过,交给请求时处理"。
五、静态页和动态页在响应头里长得完全不一样
判定不只是构建日志里那一个字,它在运行时响应头上留着可以直接读的痕迹。把两类路由各请求两次,把 x-nextjs-cache 和 cache-control 并排摆出来:
| 路由 | 第1次页面 hits | 第2次页面 hits | 本路由区间内源命中 | x-nextjs-cache |
cache-control |
|---|---|---|---|---|---|
/static-time |
— | — | +0 | HIT |
s-maxage=31536000 |
/dynamic-time |
— | — | +0 | HIT |
s-maxage=31536000 |
/fetch-default |
2 | 2 | +0 | HIT |
s-maxage=31536000 |
/client-boundary |
— | — | +0 | HIT |
s-maxage=31536000 |
/fetch-nostore |
3 | 4 | +2(每次请求都打源) | (无) | private, no-cache, no-store, max-age=0, must-revalidate |
/fetch-force |
5 | 5 | +1(只第一次打源) | (无) | private, no-cache, no-store, max-age=0, must-revalidate |
/search?q=abc |
— | — | +0 | (无) | private, no-cache, no-store, max-age=0, must-revalidate |
/cookies-read |
— | — | +0 | (无) | private, no-cache, no-store, max-age=0, must-revalidate |
横着读这张表,静态那一组和动态那一组是两副面孔:
- 静态页带
x-nextjs-cache: HIT——这个头是在说"这一次应答命中了构建产物、没有现渲染";cache-control是s-maxage=31536000,那是一年(秒数),意思是"这份东西允许在共享缓存里躺很久"。 - 动态页没有
x-nextjs-cache这个头,cache-control是private, no-cache, no-store, max-age=0, must-revalidate——一串"别缓存、别复用、每次都要回源"的指令。
这两组头是一眼可判的判据:拿到一个页面的响应,先看它有没有 x-nextjs-cache: HIT、再看 cache-control 是 s-maxage=… 还是 no-store 那一串,你基本就能反推出它构建时是 ○ 还是 ƒ。动手段会把这条变成一条你能自己敲的命令。
有一点必须先记住,否则第五节的表会被误读:这张业务表里的 +0 / +1 / +2 是"mock 源在本次两次请求区间内被打到几次",不是页面判定的直接读数。 判定看第 4 列那个字,命中数只是佐证——它告诉你这份产物到底是"回放"还是"现打源"。(这张表的另一半含义属于下一章,这里只用它读响应头。)
六、有客户端组件的页面,照样是 ○
这是最容易被"想当然"划过的一条。/client-boundary 里引了一个文件头写着 "use client" 的子组件:
// app/client-boundary/counter.jsx
"use client";
import { useState } from "react";
export default function Counter({ label }) {
const [n, setN] = useState(0);
return (
<p data-probe="client-counter">
<button onClick={() => setN((x) => x + 1)}>{label}: {n}</button>
</p>
);
}
它有一个按钮、有 useState、有 onClick——"一看就是要在浏览器里跑的东西"。很多人由此推出"这页是动态的"。但表里 /client-boundary 是 ○ Static,/hydration-mismatch(同样带 "use client")也是 ○ Static。
原因还是那一句分水岭:判定只看"渲染这一页时有没有碰请求",而客户端组件的存在不改变这一点。 "use client" 决定的是这段代码被打进哪份 bundle、在哪里执行水合,它不决定首屏的 HTML 长什么样。客户端组件的首屏输出照样在构建期被预渲染进 HTML——服务端先把它的初始状态(n = 0)渲染成 HTML 发给浏览器,同时把那段组件代码单独切进客户端 chunk,等浏览器到了再现收拾(水合)。所以你会看到一个静态回放的页面,里面的按钮却在浏览器里能正常点、点两次真的加到 2。
一句话总结这一节:"use client" 不是动态标记。 有客户端组件的页面可以完完全全是 ○ Static,这是本实验台的实测,不是推测。客户端组件与水合这条线的细节属于 05 服务端 / 客户端边界 与 06 线上的水合,这里只需要记住它对判定的影响是零。
七、fetch 不带选项:整页被预渲染,构建期真的打了一次源
最后回到那张表里最不起眼的一行:
// app/fetch-default/page.jsx
const SRC = "http://127.0.0.1:3999/tick";
export default async function Page() {
const res = await fetch(SRC);
const tick = await res.json();
return <b data-probe="hits">{tick.hits}</b>;
}
它 fetch 了一个外部地址,没传任何选项,也没读 cookie / headers / searchParams。判定结果:○ Static。这就意味着——整个页面在构建期就被渲染定稿了,连那次 fetch 也是在构建期执行的。
这不是推理,mock 源留下了直接证据。它的 hits 计数在实验里的轨迹是:
手工 curl 一次 → hits = 1
next build 期间 → hits = 2
第二条命中时刻 → 2026-10-06T12:22:12.676Z
(落在构建窗口 Generating static pages ... 11/11 期间)
hits 从 1 涨到 2,多出来的那一次,时间戳 12:22:12.676Z 紧挨着 /dynamic-time 被写死的 12:22:12.641Z——两件事发生在同一秒里,这就是"构建这台机器在生成静态页的过程中,真的替这页打了一次源"的铁证。
而一旦定稿,后面就再也不打源了:运行时探针请求两次,页面里的 hits 两次都是 2(构建期的那个值),本路由区间内源命中 +0,响应头是 x-nextjs-cache: HIT + s-maxage=31536000。页面级缓存直接回放构建产物,fetch 一次都没有再发生。
这一页是通往下一章的桥:同样是 fetch,把选项换个写法,判定和命中数会走上完全不同的路。这一章只需要带走结论——"fetch 不带选项"的结果,是"整页在构建期定稿、构建时打一次源、之后一直回放"。 至于另外三种写法把结果存在哪里、存多久、什么时候失效,是 03 章 的全部内容。
本章脉络
flowchart TD
A["next build 扫一遍 app/"] --> B{"渲染这一页时<br/>有没有碰请求?"}
B -->|"没碰"| C["○ Static<br/>prerendered as static content"]
B -->|"碰了"| D["ƒ Dynamic<br/>server-rendered on demand"]
C --> E["产物里有一份 .html<br/>x-nextjs-cache HIT<br/>cache-control s-maxage=31536000"]
D --> F["没有 .html<br/>每次请求现渲染<br/>private, no-cache, no-store, max-age=0, must-revalidate"]
A --> G["不算碰请求"]
G -->|"new Date / Math.random"| C
G -->|"模块级常量"| C
G -->|"fetch 不带选项"| G2["整页在构建期定稿<br/>构建时打源一次"]
G2 --> C
G -->|"页面里 use client 子组件"| C
A --> H["算碰请求"]
H -->|"cookies()"| D
H -->|"只读 searchParams"| D
H -->|"fetch cache no-store"| D
A --> I["export const dynamic = force-dynamic"]
I --> D
图里在说什么。 顶上那个菱形是本章唯一的分岔口——判定只问一句"渲染时有没有碰请求",不问别的。左边一整列是"看着像动态、其实不算"的东西:new Date() / Math.random()(读的是运行时自己的时钟与随机源)、模块级常量、以及页面里带 "use client" 子组件——它们的落点全是 ○。中间那条 fetch 不带选项 有点特别:它自己也不碰请求,所以整页在构建期定稿,而且构建期真的打了一次源,之后一路回放。右边才是真会推到 ƒ 的东西:读 cookies()、只读 searchParams、fetch 级 no-store,以及显式声明 force-dynamic。整张图的关键是:所有分岔都发生在构建期,页面打开时这一支已经定死,运行时改不回来。
生产边界
- 本课只量了自托管
next start。 所有响应头(x-nextjs-cache: HIT、s-maxage=31536000、no-store那一串)都是从本机 3000 端口直连读到的。把同一份产物放到 CDN 或平台缓存后面,s-maxage会被谁尊重、会不会被改写成别的值、静态页会不会在边缘节点先被拦一道——本课一个都没量,不能拿这里的头去推断 CDN 行为。 - 没有量多实例部署。 实验台始终是单进程
next start,"缓存是进程内的还是跨实例共享的"这一问题在本课范围内没有对照组。多副本部署下各实例的缓存是否独立,本课不下结论。 - 没有量
use cache指令。 它是更新的一层显式缓存写法,本实验台里根本没有验证过它,所以它和本章○/ƒ判定怎么互动,正文里一个字都不写——不许猜。 - 没有量 PPR(Partial Prerendering)、
dynamicIO、cacheComponents这类开关。 本实验台的next.config.mjs里只有reactStrictMode: true,故意不开任何优化开关,全部用默认值取证。任何"开启 PPR 之后判定会怎样"的推论,都超出了本课证据。 headers()没有单独取证。 正文只写了实测过的两条"碰请求"入口:cookies()和searchParams。和它们同族的headers()逻辑上属于同一类,但本课没量,所以不把它写成已证事实。- 没有量开发模式(
next dev)下的判定。 本章所有符号都来自next build的输出。dev 模式的判定是否与生产构建完全一致,本课没有采集对照。 - 表里的 7 个
○/ 7 个ƒ是本实验台的形状,不是经验值。 真实项目里静态页占多大比例,取决于你的页面读了多少请求,请不要把这 7:7 当基准。 - 记的是 Next.js 16.3.8 的判定。 判定的形状(碰请求即动、时间与随机数不算)可以带走,但具体哪些写法在哪个版本落到哪一边,请在你自己的版本上重跑一遍
next build复核。
动手:可观察结果
把下面五件事各做一遍。每一步的答案都必须来自命令输出,不能来自记忆。
- 把表数一遍。 跑一次
next build,在输出里找到Route (app)那张表,数出○和ƒ各几行。再去app/里逐条核对:被标ƒ的路由,是不是都碰了 cookie / 查询串 /no-store/force-dynamic里的至少一项?有反例就记下来。 - 看静态页的头。 起
next start,然后curl -sI http://127.0.0.1:3000/static-time(-I只取响应头)。找x-nextjs-cache和cache-control两行。再去请求/cookies-read,看同一个位置变成了什么。两串头的差别,就是第五节的表。 - 刷新一个"取当前时间"的页面。 反复刷新
/dynamic-time,用curl或浏览器读那个now字段。它会变吗?然后对着构建日志里那一刻,回答:这个值是哪一秒定下来的? - 读 mock 源的流水账。 请求
http://127.0.0.1:3999/log,把hits和log打印出来。/fetch-default那一页在构建时留下的那条记录,时间戳是不是落在Generating static pages的窗口里?之后你请求它几次,hits涨过吗? - 给客户端组件那一页再确认一次。 打开
/client-boundary,按两下那个按钮。它是静态回放的页面,按钮却能加到 2——这就是"有客户端组件 ≠ 动态"最直观的一帧。
完成标志:你能对着任意一条路由,不看构建表就先说出"它多半是 ○ 还是 ƒ",并且给出理由("它读了 cookie" / "它只取了个时间");再跑一次 next build 验证,对得上。
故障注入
五种注入,每一种都会让某个路由的判定翻面。先写下预测,再去跑 next build 看表。
| 注入 | 怎么做 | 观察什么 | 期望行为 |
|---|---|---|---|
| 给"取时间"的页加一次读请求 | 在 /dynamic-time 里加 import { cookies } from "next/headers" 并 await cookies(),其余不动 |
构建表里这一行的符号 | 从 ○ Static 翻成 ƒ Dynamic——同样一段 new Date(),只是多碰了一次请求 |
| 把读 cookie 的那一页"腾空" | 删掉 /cookies-read 里的 cookies(),只留一段静态文本 |
构建表那一行的符号 | 从 ƒ Dynamic 翻回 ○ Static——不再碰请求,构建期就能定稿 |
| 把查询串写死 | 把 /search 改成不读 searchParams、直接打印一个常量 q |
构建表那一行的符号 | 从 ƒ Dynamic 翻回 ○ Static——只读查询串足以让整页动态,不读了就回来 |
| 给纯静态页加一句显式声明 | 在 /static-time 顶上加 export const dynamic = "force-dynamic" |
构建表那一行的符号 | 从 ○ 翻成 ƒ——显式声明直接压过"它本来能静态" |
把默认 fetch 换成 no-store |
把 /fetch-default 的 fetch(SRC) 改成 fetch(SRC, { cache: "no-store" }) |
构建表的符号 + 两次请求的源命中区间 | 从 ○ 翻成 ƒ;区间从 +0 变成每次请求 +1(本实验台两次请求记 +2)——只动 fetch 的一个选项,整页被拖走 |
第五种注入跑完之后别急着改回去。再请求这一页两次,把 mock 的 /log 拉开看:no-store 那一页是每次请求都真的打了一次源(区间 +2),而原来的 fetch 不带选项那一页是一次都不再打(区间 +0)。这两条之间的差别,是整个下一章的起点。
自测题
- 一个页面在渲染函数里写了
Math.random(),没有读任何请求。它会被判成○还是ƒ?请给出判据(不是结论),并说明为什么"值每次都变"不是理由。 /static-time和/dynamic-time的判定完全一样(都是○),运行时证据也一样(区间+0、HIT、s-maxage=31536000)。这两页之间唯一的区别是什么?这个区别对"线上用户看到的时间"意味着什么?- 为什么只读
searchParams就足以让整页判成ƒ?请用"构建期能不能定稿"这条标准解释,而不是背结论。 /client-boundary里有一个useState+onClick的按钮,它却是○ Static。请说清"use client"到底决定了什么、又没有决定什么;以及这个按钮在浏览器里为什么还能点。force-dynamic和fetch级no-store都能把页面推成ƒ。它们的作用面为什么都是整页、而不是某一个fetch或某一个组件?如果一页里只有一个小角落读了 cookie,整页会变成什么?- 你拿到一个线上页面的响应头,上面有
x-nextjs-cache: HIT且cache-control: s-maxage=31536000。它构建时最可能是哪个符号?如果换成private, no-cache, no-store, max-age=0, must-revalidate且没有x-nextjs-cache,又是哪个? /fetch-default在构建期的 mockhits从 1 涨到 2,之后所有请求区间都+0。这两条合起来,证明了这个页面的fetch分别在什么时候、几次执行?- 有同事说:"我把
new Date()从模块顶层挪进了渲染函数,这样它每次请求都应该刷新了吧?"请按本章的判定规则指出他错在哪,并给出两种能让这页真的每次都取新时间的改法(提示:其中一种是加一次读请求,另一种是显式声明)。
现在能解释什么
- 为什么"我写了
new Date()/Math.random()却是静态"——它们不是动态 API,值在构建期就被冻结,线上的时间等于部署那一刻; - 为什么"只读了个查询串,整页怎么变动态了"——判定的分水岭是碰没碰请求,
searchParams和cookies()在同一侧; - 为什么"加了
force-dynamic就好了"、以及"我只是给fetch加了个no-store怎么整页都变慢了"——这两个触发器都作用在整页判定上,不针对单个fetch; - 为什么"我明明用了
use client,构建表还是给了我一个圈"——"use client"不是动态标记,首屏输出照样在构建期预渲染进 HTML; - 为什么"这页的
hits一直不动、刷新也白搭"——fetch不带选项时整页在构建期定稿,构建期打了一次源,之后一路回放; - 为什么可以不看配置、只读响应头就反推判定——静态页带
x-nextjs-cache: HIT+s-maxage=31536000,动态页是no-store那一串。
下一步:03 章 · fetch 的四种写法,结果各自存在哪里——这一章说的是"整页要不要在构建期定稿",下一章把问题拆细一层:同一个页面里,fetch 的结果被存在哪个层、存多久、到期后给你的到底是新值还是旧值。