主循环与 wp_reset_postdata:为什么改不干净
主循环操作的是全局 $wp_query 和 $post,子查询里的 the_post() 会把 $post 换掉,走完不还原就污染后面所有输出。本文对照三种写法、query_posts 为什么不该用、pre_get_posts 的两个必备判断,以及两个 reset 函数的区别。
在分类页底部加了个「相关内容」区块,用了 WP_Query。上线后发现两件事:翻到第二页,主循环只剩三篇;点进文章,标题显示的是上一篇文章的标题。
查了两个小时,原因就一行——子循环结束后没调 wp_reset_postdata()。
一、主循环到底在循环什么
模板里那段最熟悉的代码:
if ( have_posts() ) :
while ( have_posts() ) : the_post();
the_title();
the_content();
endwhile;
endif;
三个函数看着像在操作「当前页面」,实际上操作的都是全局变量 $wp_query:
have_posts()→ 判断$wp_query->current_post还没走到底the_post()→ 游标 +1,并把当前文章对象写进全局$postthe_title()等模板函数 → 全部读全局$post
所以「主循环」不是什么魔法,就是在遍历一个全局对象,顺手改写另一个全局变量。
二、二次查询为什么会污染主循环
常见的写法:
$related = new WP_Query( array(
'post__not_in' => array( get_the_ID() ),
'posts_per_page' => 5,
'category__in' => wp_get_post_categories( get_the_ID() ),
) );
if ( $related->have_posts() ) {
while ( $related->have_posts() ) {
$related->the_post();
the_title(); // ← 这里已经改写了全局 $post
}
}
$related 本身是个独立对象,不影响全局 $wp_query——但 $related->the_post() 是同一个方法,它内部执行的是 global $post; $post = $this->next_post;。
也就是说:子循环把全局 $post 换成了它自己的文章,走完之后 $post 停在相关的最后一篇上。
于是主循环继续往下跑时:
the_title()读到的是残留的那篇文章is_single()、get_the_ID()这类条件判断跟着错- 如果子循环在 while 里被调用,主循环的游标还会被带着乱动,出现「翻页只剩三篇」
三、三种写法的对照
| 写法 | 是否污染全局 $post | 要不要 reset |
|---|---|---|
WP_Query + the_post() | 会 | 必须 wp_reset_postdata() |
WP_Query + foreach ( $q->posts as $p ) | 不会 | 不需要,但要 setup_postdata( $p ) 才能用模板函数 |
get_posts() | 不会 | 同上 |
query_posts() | 直接改主循环 | 别用,见下一节 |
最稳的收尾:
wp_reset_postdata(); // 把 $post 恢复到主查询的当前文章
放在 endif; 之后、wp_reset_postdata(); 之前别再输出任何东西——顺序错了等于没写。
四、query_posts() 为什么不该用
它和 new WP_Query 的差别只有一点:它直接改写全局 $wp_query。后果是:
- 主循环的分页计算基于新查询,页码和总数对不上
- 模板里所有
is_category()、is_home()判断失效——因为全局查询被换了 - 要恢复得调
wp_reset_query(),但恢复时机稍晚一点,后面输出的页头页脚已经错了
官方文档里自己写着「不推荐,仅供主题开发时临时使用」。改主循环的正确入口是 pre_get_posts:
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_category( 'zblog' ) ) {
$query->set( 'posts_per_page', 20 );
}
} );
两个判断缺一不可:is_admin() 不加会把后台列表也改掉,is_main_query() 不加会把你自己的子查询一起改了。
五、wp_reset_query 和 wp_reset_postdata 的区别
名字很像,作用不同:
wp_reset_postdata(); // 只恢复全局 $post,回到当前主查询的文章
wp_reset_query(); // 恢复整个全局 $wp_query(连查询条件一起回滚)
用 new WP_Query 时只需前者。用 query_posts() 才需要后者。现在正常开发里后者基本见不到了。
六、嵌套循环:先把主查询存下来
两层子循环(比如「相关文章」里再取每篇的缩略图附件查询)容易把状态搞乱。省事的做法是在模板开头备份:
$main_query = $wp_query;
// ...任意多次子查询...
$wp_query = $main_query;
wp_reset_postdata();
注意这里的 $wp_query 要写成 global $wp_query; 之后赋值,否则只改了局部变量。
七、怎么确认真的错了
别靠肉眼比对标题,直接看查询:
global $wp_query;
echo '<pre>';
var_dump( $wp_query->request );
echo '</pre>';
输出的是实际执行的 SQL。如果 LIMIT 后的数字、WHERE 里的分类条件和你预期不符,问题在查询构造;如果 SQL 正确但输出错,问题就是 $post 被污染——直接去找漏掉的 wp_reset_postdata()。
小结
一条经验:只要在模板里写了 the_post(),函数结尾就必须有 wp_reset_postdata()。把它当成括号一样配对,能省掉绝大多数「数据串了」的调试时间。
下一篇讲钩子体系:add_action 和 add_filter 名字差一个词,语义差在返回值上——这也是从 Z-Blog 的 Filter 迁过来时最容易写错的地方。