大家好!今天咱们来聊聊 WordPress 钩子背后的秘密:do_action()
和 apply_filters()
的源码大揭秘!
各位,有没有觉得 WordPress 插件和主题就像乐高积木一样,可以随意组合、扩展? 这都得归功于它强大的钩子(Hooks)机制。而 do_action()
和 apply_filters()
就是操控这些钩子的关键“指挥官”。
今天,咱们就深入源码,看看这两位指挥官是如何“发号施令”,让各种函数像训练有素的士兵一样,在特定时刻执行任务的。 别怕,我会用最通俗易懂的方式,加上实战代码,保证你听完之后,也能成为钩子大师!
一、 钩子的概念:代码中的“预留插槽”
在深入源码之前,我们先温习一下钩子的概念。 可以把钩子想象成代码中的“预留插槽”, 允许插件或主题在不修改核心代码的情况下,插入自己的功能。
钩子分为两种类型:
- 动作(Action): 允许你执行一些操作。 比如,在文章发布后发送邮件通知,或者在页面底部添加自定义内容。
- 过滤器(Filter): 允许你修改数据。 比如,修改文章标题,或者过滤评论内容。
do_action()
用于触发动作,而 apply_filters()
用于应用过滤器。
二、 do_action()
的源码解析: 动作的“发令枪”
do_action()
函数的作用是执行与特定动作钩子关联的所有函数。 让我们一起看看它的源码(简化版):
/**
* Execute functions hooked on a specific action hook.
*
* @since 1.5.0
*
* @param string $hook_name The name of the action to execute.
* @param mixed ...$arg Optional. Additional arguments which are passed on to the
* functions hooked to the action.
*/
function do_action( $hook_name, ...$arg ) {
global $wp_filter, $wp_actions, $wp_current_filter;
if ( ! isset( $wp_actions[ $hook_name ] ) ) {
$wp_actions[ $hook_name ] = 1;
} else {
++$wp_actions[ $hook_name ];
}
// Do 'all' actions first.
if ( isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $hook_name;
_wp_call_all_hook( $arg );
}
if ( ! isset( $wp_filter[ $hook_name ] ) ) {
if ( isset( $wp_filter['all'] ) ) {
array_pop( $wp_current_filter );
}
return;
}
if ( ! is_array( $wp_filter[ $hook_name ]->callbacks ) ) {
return;
}
if ( isset( $wp_filter['all'] ) ) {
array_pop( $wp_current_filter );
}
reset( $wp_filter[ $hook_name ]->callbacks );
foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $functions ) {
ksort( $functions );
foreach ( $functions as $function ) {
$args = $arg;
$num_args = count( $arg );
if ( is_null( $function['function'] ) ) {
continue;
}
call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );
}
}
}
代码解读:
-
全局变量:
$wp_filter
是一个全局数组,存储了所有已注册的钩子和它们关联的函数。$wp_actions
记录了每个动作被触发的次数。$wp_current_filter
是一个堆栈,记录了当前正在执行的钩子,用于防止循环调用。 -
统计动作触发次数:
if ( ! isset( $wp_actions[ $hook_name ] ) ) { $wp_actions[ $hook_name ] = 1; } else { ++$wp_actions[ $hook_name ]; }
这段代码用于统计特定动作
$hook_name
被触发的次数,并存储在全局数组$wp_actions
中。if ( ! isset( $wp_actions[ $hook_name ] ) )
: 检查$wp_actions
数组中是否已经存在以$hook_name
为键的元素。如果不存在,表示该动作第一次被触发。$wp_actions[ $hook_name ] = 1;
: 如果动作第一次被触发,则在$wp_actions
数组中创建一个以$hook_name
为键的元素,并将其值设置为 1,表示该动作被触发了一次。else { ++$wp_actions[ $hook_name ]; }
: 如果$wp_actions
数组中已经存在以$hook_name
为键的元素,则表示该动作之前已经被触发过。此时,将该键对应的值加 1,表示该动作又被触发了一次。
这段代码的主要作用是记录每个动作被触发的次数,方便后续的调试和分析。 例如,可以用来判断某个动作是否被意外地多次触发,或者用于性能分析,找出触发次数最多的动作。
-
执行 ‘all’ 动作:
if ( isset( $wp_filter['all'] ) ) { $wp_current_filter[] = $hook_name; _wp_call_all_hook( $arg ); }
这段代码检查是否存在名为
'all'
的钩子,如果存在,则将当前钩子$hook_name
添加到$wp_current_filter
堆栈中,然后调用_wp_call_all_hook()
函数来执行与'all'
钩子关联的所有函数。if ( isset( $wp_filter['all'] ) )
: 检查$wp_filter
数组中是否存在以'all'
为键的元素。如果存在,表示已经注册了与'all'
钩子关联的函数。$wp_current_filter[] = $hook_name;
: 将当前正在执行的钩子$hook_name
添加到$wp_current_filter
堆栈中。$wp_current_filter
用于跟踪当前正在执行的钩子,防止循环调用。_wp_call_all_hook( $arg );
: 调用_wp_call_all_hook()
函数来执行与'all'
钩子关联的所有函数。$arg
是传递给这些函数的参数。
'all'
钩子是一个特殊的钩子,任何动作被触发时,都会执行与'all'
钩子关联的函数。这提供了一种全局监听所有动作的方式,可以用于日志记录、调试等目的。_wp_call_all_hook()
函数源码如下:/** * Calls the 'all' hook, which passes the arguments from the current filter. * * @since 2.5.0 * @access private * * @param array $args The array of arguments passed into the filter. */ function _wp_call_all_hook( $args ) { global $wp_filter; reset( $wp_filter['all']->callbacks ); foreach ( $wp_filter['all']->callbacks as $priority => $functions ) { ksort( $functions ); foreach ( $functions as $function ) { call_user_func_array( $function['function'], $args ); } } }
_wp_call_all_hook()
函数遍历与'all'
钩子关联的所有函数,并使用call_user_func_array()
函数来调用它们,并将$args
作为参数传递给这些函数。 -
检查动作是否存在:
if ( ! isset( $wp_filter[ $hook_name ] ) ) { if ( isset( $wp_filter['all'] ) ) { array_pop( $wp_current_filter ); } return; }
这段代码检查是否存在与
$hook_name
关联的函数。如果不存在,则表示没有函数注册到该动作,直接返回。if ( ! isset( $wp_filter[ $hook_name ] ) )
: 检查$wp_filter
数组中是否存在以$hook_name
为键的元素。如果不存在,表示没有函数注册到该动作。if ( isset( $wp_filter['all'] ) ) { array_pop( $wp_current_filter ); }
: 如果设置了’all’ 钩子,则将当前正在执行的钩子$hook_name
从$wp_current_filter
堆栈中移除,因为已经确定没有与$hook_name
关联的函数需要执行。return;
: 直接返回,不执行任何操作。
-
检查
$wp_filter[ $hook_name ]->callbacks
是否是数组if ( ! is_array( $wp_filter[ $hook_name ]->callbacks ) ) { return; }
这段代码检查
$wp_filter[ $hook_name ]->callbacks
是否是数组。如果不是数组,直接返回。 -
遍历并执行函数:
foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $functions ) { ksort( $functions ); foreach ( $functions as $function ) { $args = $arg; $num_args = count( $arg ); if ( is_null( $function['function'] ) ) { continue; } call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) ); } }
这段代码是
do_action()
函数的核心部分,它遍历与$hook_name
关联的所有函数,并按照优先级顺序执行它们。foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $functions )
: 遍历$wp_filter[ $hook_name ]->callbacks
数组。这个数组的键是优先级($priority
),值是具有相同优先级的函数数组($functions
)。ksort( $functions );
: 对具有相同优先级的函数数组$functions
按照键进行排序。这是为了确保在相同优先级下,函数按照它们被添加的顺序执行。foreach ( $functions as $function )
: 遍历具有相同优先级的函数数组$functions
。$args = $arg;
: 将传递给do_action()
函数的参数$arg
复制到$args
变量中。$num_args = count( $arg );
: 计算传递给do_action()
函数的参数的数量。if ( is_null( $function['function'] ) ) { continue; }
: 检查$function['function']
是否为null
。如果是null
,表示该函数已被移除,跳过本次循环。call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );
: 使用call_user_func_array()
函数来调用函数$function['function']
,并将$args
作为参数传递给它。array_slice( $args, 0, (int) $function['accepted_args'] )
用于截取$args
数组,只传递函数$function['function']
声明接受的参数数量。$function['accepted_args']
存储了函数$function['function']
声明接受的参数数量。
call_user_func_array()
是一个 PHP 函数,它可以调用一个回调函数,并将一个数组作为参数传递给它。优先级(Priority): 钩子函数可以设置优先级,数值越小,优先级越高,越先执行。 默认优先级是 10。
接受的参数数量(accepted_args): 指定钩子函数接受的参数数量。 这样可以避免传递过多的参数,提高性能。
流程总结:
do_action()
接收动作名称和参数。- 它检查是否存在与该动作关联的函数。
- 如果存在,它会按照优先级顺序遍历这些函数。
- 对于每个函数,它会调用
call_user_func_array()
函数来执行该函数,并将参数传递给它。 - 如果注册了 ‘all’ 动作,也会执行 ‘all’ 动作关联的所有函数
三、 apply_filters()
的源码解析: 过滤器的“数据处理器”
apply_filters()
函数的作用是应用与特定过滤器钩子关联的所有函数,以修改数据。 让我们看看它的源码(简化版):
/**
* Call the functions added to a filter hook.
*
* @since 0.71
*
* @param string $hook_name The name of the filter hook.
* @param mixed $value The value to filter.
* @param mixed ...$arg Optional. Additional parameters to pass to the functions hooked to `$hook_name`.
*
* @return mixed The filtered value after all hooked functions are applied to it.
*/
function apply_filters( $hook_name, $value, ...$arg ) {
global $wp_filter, $wp_current_filter;
if ( ! isset( $wp_filter[ $hook_name ] ) ) {
return $value;
}
if ( ! is_array( $wp_filter[ $hook_name ]->callbacks ) ) {
return $value;
}
$wp_current_filter[] = $hook_name;
reset( $wp_filter[ $hook_name ]->callbacks );
do {
foreach ( (array) current( $wp_filter[ $hook_name ]->callbacks ) as $the_ ) {
foreach ( (array) $the_ as $priority => $function ) {
$args = array_merge( array( $value ), $arg );
$num_args = count( $args );
if ( is_null( $function['function'] ) ) {
continue;
}
$value = call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );
}
}
} while ( next( $wp_filter[ $hook_name ]->callbacks ) !== false );
array_pop( $wp_current_filter );
return $value;
}
代码解读:
-
全局变量: 与
do_action()
类似,apply_filters()
也使用全局变量$wp_filter
和$wp_current_filter
来存储钩子信息和防止循环调用。 -
检查过滤器是否存在:
if ( ! isset( $wp_filter[ $hook_name ] ) ) { return $value; }
这段代码检查是否存在与
$hook_name
关联的函数。如果不存在,则直接返回原始值$value
。 -
遍历并应用函数:
do { foreach ( (array) current( $wp_filter[ $hook_name ]->callbacks ) as $the_ ) { foreach ( (array) $the_ as $priority => $function ) { $args = array_merge( array( $value ), $arg ); $num_args = count( $args ); if ( is_null( $function['function'] ) ) { continue; } $value = call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) ); } } } while ( next( $wp_filter[ $hook_name ]->callbacks ) !== false );
这段代码是
apply_filters()
函数的核心部分,它遍历与$hook_name
关联的所有函数,并按照优先级顺序应用它们,以修改传入的值$value
。do { ... } while ( next( $wp_filter[ $hook_name ]->callbacks ) !== false );
: 使用do...while
循环来遍历$wp_filter[ $hook_name ]->callbacks
数组。next()
函数用于将数组的内部指针移动到下一个元素。当next()
函数返回false
时,表示已经遍历完所有元素,循环结束。foreach ( (array) current( $wp_filter[ $hook_name ]->callbacks ) as $the_ )
: 获取当前优先级的所有函数。current()
函数返回数组中当前指针指向的元素。foreach ( (array) $the_ as $priority => $function )
: 遍历当前优先级的所有函数。$args = array_merge( array( $value ), $arg );
: 将原始值$value
和传递给apply_filters()
函数的额外参数$arg
合并到一个数组$args
中。array_merge()
函数用于合并数组。$num_args = count( $args );
: 计算合并后的参数数量。if ( is_null( $function['function'] ) ) { continue; }
: 检查$function['function']
是否为null
。如果是null
,表示该函数已被移除,跳过本次循环。$value = call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );
: 使用call_user_func_array()
函数来调用函数$function['function']
,并将$args
作为参数传递给它。array_slice( $args, 0, (int) $function['accepted_args'] )
用于截取$args
数组,只传递函数$function['function']
声明接受的参数数量。$function['accepted_args']
存储了函数$function['function']
声明接受的参数数量。注意,每次函数执行后,其返回值会更新$value
,作为下一个函数的输入。
-
返回最终值:
return $value;
在应用所有过滤器函数后,
apply_filters()
函数返回最终修改后的值$value
。
流程总结:
apply_filters()
接收过滤器名称、初始值和参数。- 它检查是否存在与该过滤器关联的函数。
- 如果存在,它会按照优先级顺序遍历这些函数。
- 对于每个函数,它会调用
call_user_func_array()
函数来执行该函数,并将当前值和参数传递给它。 - 函数的返回值会更新当前值,作为下一个函数的输入。
- 最终,
apply_filters()
返回经过所有过滤器函数处理后的值。
四、 注册钩子: add_action()
和 add_filter()
要让 do_action()
和 apply_filters()
能够正常工作,我们需要先使用 add_action()
和 add_filter()
函数来注册钩子。
add_action()
用于注册动作钩子,add_filter()
用于注册过滤器钩子。
它们的原型如下:
add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): bool
add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): bool
$hook_name
: 钩子的名称。$callback
: 要执行的函数。$priority
: 优先级,数值越小,优先级越高。$accepted_args
: 函数接受的参数数量。
这两个函数的内部实现非常相似, 它们都将钩子名称、函数和优先级存储到全局数组 $wp_filter
中。
五、 示例演示:
为了更好地理解 do_action()
和 apply_filters()
的工作方式,我们来看几个示例。
示例 1:使用 do_action()
添加自定义内容到文章底部
// 注册一个动作钩子
add_action( 'the_content', 'add_custom_content' );
function add_custom_content( $content ) {
$custom_content = '<div class="custom-content">This is custom content added by a hook!</div>';
return $content . $custom_content;
}
// 在文章内容输出的地方触发动作
do_action( 'the_content', $content );
在这个示例中,我们使用 add_action()
函数注册了一个名为 the_content
的动作钩子,并将 add_custom_content()
函数与之关联。 当 WordPress 在文章内容输出的地方调用 do_action( 'the_content', $content )
时, add_custom_content()
函数会被执行,并将自定义内容添加到文章底部。
示例 2:使用 apply_filters()
修改文章标题
// 注册一个过滤器钩子
add_filter( 'the_title', 'prefix_title' );
function prefix_title( $title ) {
return 'Prefix: ' . $title;
}
// 在文章标题输出的地方应用过滤器
$title = apply_filters( 'the_title', $title );
echo $title;
在这个示例中,我们使用 add_filter()
函数注册了一个名为 the_title
的过滤器钩子,并将 prefix_title()
函数与之关联。 当 WordPress 在文章标题输出的地方调用 apply_filters( 'the_title', $title )
时, prefix_title()
函数会被执行,并将 "Prefix: " 添加到文章标题的前面。
示例 3:使用优先级控制钩子函数的执行顺序
add_action( 'my_action', 'function_one', 10 ); // 默认优先级
add_action( 'my_action', 'function_two', 5 ); // 优先级更高,先执行
add_action( 'my_action', 'function_three', 15 ); // 优先级更低,后执行
function function_one() {
echo 'Function One<br>';
}
function function_two() {
echo 'Function Two<br>';
}
function function_three() {
echo 'Function Three<br>';
}
do_action( 'my_action' ); // 输出:Function Two<br>Function One<br>Function Three<br>
在这个示例中,我们注册了三个与 my_action
动作钩子关联的函数,并分别设置了不同的优先级。 优先级数值越小,函数越先执行。 因此,function_two()
会先执行,然后是 function_one()
,最后是 function_three()
。
示例 4:传递多个参数给钩子函数
add_action( 'my_action', 'my_function', 10, 2 ); // 声明接受 2 个参数
function my_function( $arg1, $arg2 ) {
echo 'Arg1: ' . $arg1 . '<br>';
echo 'Arg2: ' . $arg2 . '<br>';
}
do_action( 'my_action', 'Hello', 'World' ); // 输出:Arg1: Hello<br>Arg2: World<br>
在这个示例中,我们使用 add_action()
函数的第四个参数 $accepted_args
声明 my_function()
函数接受 2 个参数。 当调用 do_action( 'my_action', 'Hello', 'World' )
时,'Hello'
和 'World'
这两个参数会被传递给 my_function()
函数。
示例 5:使用 remove_action()
和 remove_filter()
移除钩子函数
// 先添加一个 action
add_action( 'wp_footer', 'my_footer_function' );
function my_footer_function() {
echo '<p>This is my footer function.</p>';
}
// 然后移除这个 action
remove_action( 'wp_footer', 'my_footer_function' );
// footer 不再输出内容
示例 6: 使用 has_action()
和 has_filter()
检查钩子是否存在
// 检查是否存在名为 'wp_footer' 且绑定 'my_footer_function' 的 action
if ( has_action( 'wp_footer', 'my_footer_function' ) ) {
echo "The 'my_footer_function' is hooked to 'wp_footer'.";
} else {
echo "The 'my_footer_function' is not hooked to 'wp_footer'.";
}
六、 总结: 钩子的强大之处
通过深入了解 do_action()
和 apply_filters()
的源码,我们可以更好地理解 WordPress 钩子机制的强大之处。
- 解耦: 钩子机制允许插件和主题在不修改核心代码的情况下,扩展 WordPress 的功能。
- 灵活性: 开发者可以根据需要,注册自己的钩子函数,并在特定的时刻执行。
- 可扩展性: 钩子机制使得 WordPress 具有很强的可扩展性,可以满足各种不同的需求。
掌握了钩子机制,你就可以像一位经验丰富的建筑师一样,利用 WordPress 提供的各种“积木”,搭建出功能强大的网站。
希望今天的讲座对大家有所帮助! 咱们下期再见!