WordPress 钩子体系:add_action 和 add_filter 到底差在哪
两者共用 add_filter 底层,差别只在返回值去向:do_action 丢弃、apply_filters 传递,外加优先级、参数个数与移除钩子的坑。
很多人用了几年 WordPress,仍然分不清 add_action 和 add_filter。它们底层是同一套机制、同一张 $wp_filter 全局数组,唯一的区别在返回值怎么用。搞懂这一点,纠结就没了。
一、共同点:都是挂在钩子上
两个函数最终都调用 add_filter($tag, $callback, $priority, $accepted_args)。是的,add_action 内部就是 add_filter 的包装:
function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
return add_filter($tag, $function_to_add, $priority, $accepted_args);
}
所以从「能不能挂」的角度讲,二者完全等价——你甚至可以用 add_action('the_content', ...) 挂一个本来属于 filter 的钩子,代码照样跑。区别只在触发方怎么处理你的返回值。
二、区别:do_action 丢弃返回值,apply_filters 拿走返回值
action 的场景
// 定义方
function my_theme_header() {
do_action('my_after_header');
}
// 使用方
add_action('my_after_header', function () {
echo '<div class="banner">广告位</div>';
});
do_action 只负责依次调用回调,回调返回什么它不管。你要产生影响,必须是回调内部的副作用——echo 输出、写数据库、发邮件、改全局变量。
filter 的场景
// 定义方
$title = apply_filters('my_title', $raw_title);
// 使用方
add_filter('my_title', function ($title) {
return $title . ' | 我的站'; // 必须 return
});
apply_filters 把当前值传进第一个回调,接收回调的返回值作为新值,再传给下一个回调,最后返回最终值。这里忘记 return 就是灾难——返回 null,原来那串标题直接变空。
一句话记法:action 靠副作用,filter 靠返回值。
三、优先级和参数个数
第三个参数 $priority 越小越先执行,默认 10。想让自己的回调在别人之后跑,用 20;抢在前面用 5。同一优先级按注册顺序执行。
第四个参数 $accepted_args 老被忽略。回调能接几个参数,由它决定:
// the_content 实际传 1 个参数,这里写 3 也只会拿到 1 个
add_filter('the_content', 'my_filter', 10, 1);
// save_post 传 3 个:$post_id, $post, $update
add_action('save_post', 'my_save', 10, 3);
function my_save($post_id, $post, $update) {
// 三个都能用
}
常见 bug:add_action('save_post', 'my_save', 10, 3) 写成了 4 个参数没声明,回调只能收 1 个,$update 永远是 null。
四、移除钩子必须参数对得上
remove_action / remove_filter 靠「回调 + 优先级」定位,优先级不同就删不掉:
// 注册
add_action('wp_head', 'my_meta', 20);
// 删不掉(优先级不匹配)
remove_action('wp_head', 'my_meta');
// 正确
remove_action('wp_head', 'my_meta', 20);
匿名函数(闭包)注册的钩子基本删不掉,除非你把闭包存进变量再传:
$cb = function () { echo 'x'; };
add_action('wp_head', $cb);
remove_action('wp_head', $cb);
这是做子主题覆盖父主题行为时最常见的翻车点。
五、怎么知道一个钩子存在不存在
has_action($tag) / has_filter($tag) 可以探测(两个函数通用,别被名字骗了)。查看某钩子上挂了什么:
global $wp_filter;
var_dump($wp_filter['the_content']);
输出里能看到每个回调、优先级、accepted_args。调试「为什么我的过滤器没生效」时,先确认它真的被注册了——有时候是条件判断 is_admin() 写反,压根没进注册分支。
六、到底该用哪个:判断标准
| 你的需求 | 用哪个 |
|---|---|
| 在某个位置输出 HTML | action |
| 修改一段已有的文本/数组/对象 | filter |
| 保存文章后做额外操作 | action |
| 改变查询参数 | filter(pre_get_posts) |
| 页面加载完执行 JS | action(wp_footer) |
其实不用记表。看定义方的源码:写 do_action 的地方只能挂 action,写 apply_filters 的地方只能挂 filter。挂错了不报错,但大概率不生效——这是最隐蔽的一类问题。
小结
同源、同表、同参数,唯一分歧在返回值去向:do_action 丢弃,apply_filters 传递。filter 忘记 return 会清空数据,action 里 return 毫无意义。下一篇讲 wp_enqueue_script 的依赖顺序——为什么你的 jQuery 插件总说 $ 未定义。