主循环与 wp_reset_postdata:为什么改不干净

· wordpress · 阅读 26
摘要

主循环操作的是全局 $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,并把当前文章对象写进全局 $post
  • the_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 迁过来时最容易写错的地方。