深入解读 WordPress `do_action()` 与 `apply_filters()` 函数源码:钩子执行的具体流程。

大家好,欢迎来到今天的“WordPress 钩子大冒险”讲座!我是你们的导游,老码农一枚,今天带大家深入 WordPress 的核心,扒一扒 do_action()apply_filters() 这两个家伙的底裤,看看它们到底是怎么运作的。

准备好了吗? Let’s go!

第一幕:钩子的前世今生 (概念回顾,务必搞懂)

在开始源码探险之前,我们先快速回顾一下什么是 Action 和 Filter。你可以把 WordPress 想象成一个大型乐高玩具,Action 和 Filter 就是乐高上的连接点,允许你修改或扩展它的功能,而无需修改核心代码。

  • Action (动作钩子): 像一个事件触发器。当 WordPress 执行到 do_action() 函数时,它会执行所有挂载到该 Action 上的函数。 你可以理解为 “当发生 X 事件时,执行 Y 个函数”。
  • Filter (过滤器钩子): 允许你修改数据。 当 WordPress 执行到 apply_filters() 函数时,它会传递一个值,然后所有挂载到该 Filter 上的函数都会接收这个值,并有机会修改它,最后返回修改后的值。 你可以理解为 “用 Y 个函数来修改 X 的值”。

简单来说,Action 是“做事”,Filter 是“改东西”。

第二幕:do_action() 源码解剖 (Action 钩子的执行流程)

好,概念复习完毕,我们直接上代码,看看 do_action() 到底干了些啥:

// wp-includes/plugin.php

/**
 * Execute functions hooked on a specific action hook.
 *
 * @since 0.71
 *
 * @param string $tag The name of the action to be executed.
 * @param mixed  ...$arg Optional. Additional arguments which are passed on to the
 *                       functions hooked to the action.
 * @return null Will return null if $tag does not exist in $wp_actions.
 */
function do_action( $tag, ...$arg ) {
    global $wp_filter, $wp_actions, $wp_current_filter;

    $wp_actions[ $tag ] = isset( $wp_actions[ $tag ] ) ? absint( $wp_actions[ $tag ] ) + 1 : 1;

    // Do nothing if there's no hooks.
    if ( ! isset( $wp_filter[ $tag ] ) ) {
        return;
    }

    if ( empty( $wp_filter[ $tag ]->callbacks ) ) {
        return;
    }

    // Prevents recursion if the action calls itself.
    $wp_current_filter[] = $tag;

    /**
     * Fires functions attached to a specific action hook.
     *
     * @param mixed ...$arg Arguments passed to the functions.
     */
    do_action_ref_array( $tag, $arg );

    array_pop( $wp_current_filter );
}

别害怕,我们一行一行来解读:

  1. global $wp_filter, $wp_actions, $wp_current_filter;: 这行代码声明了三个全局变量。它们是 do_action()apply_filters() 的核心数据结构:

    • $wp_filter: 一个超级数组,存储了所有已注册的 Action 和 Filter 的回调函数(也就是你用 add_action()add_filter() 注册的函数)。它的结构大致是: ['action_name' => WP_Hook object]
    • $wp_actions: 一个简单的数组,记录了每个 Action 被触发的次数。 比如,$wp_actions['wp_head'] 的值可能是 3,表示 wp_head 这个 Action 已经被触发了 3 次。
    • $wp_current_filter: 一个数组,记录了当前正在执行的 Action 或 Filter 的名字。 用于防止递归调用(比如,一个 Action 的回调函数又触发了同一个 Action,就会形成死循环)。
  2. $wp_actions[ $tag ] = isset( $wp_actions[ $tag ] ) ? absint( $wp_actions[ $tag ] ) + 1 : 1;: 这行代码更新了 $wp_actions 数组,记录了当前 Action 被触发的次数。 如果 $wp_actions[$tag] 已经存在,就加 1;否则,就设置为 1。

  3. if ( ! isset( $wp_filter[ $tag ] ) ) { return; }: 这个判断至关重要。它检查 $wp_filter 数组中是否存在以 $tag 为键的元素。 如果不存在,说明没有任何函数挂载到这个 Action 上,所以直接返回,不做任何事情。

  4. if ( empty( $wp_filter[ $tag ]->callbacks ) ) { return; }: 如果存在 $wp_filter[$tag],但其 callbacks 属性为空,说明虽然定义了这个 Action ,但是没有注册任何回调函数。 直接返回。

  5. $wp_current_filter[] = $tag;: 将当前 Action 的名字添加到 $wp_current_filter 数组中,用于防止递归调用。

  6. do_action_ref_array( $tag, $arg );: 这行代码才是真正执行 Action 的地方。 它调用了 do_action_ref_array() 函数,并将 Action 的名字 $tag 和传递给 do_action() 的参数 $arg 传递给它。

  7. array_pop( $wp_current_filter );: 执行完毕后,从 $wp_current_filter 数组中移除当前 Action 的名字。

你看,do_action() 本身并没有做什么复杂的事情。 它主要负责检查 Action 是否存在,以及防止递归调用,然后把真正的执行任务交给 do_action_ref_array()

do_action_ref_array() 源码深度挖掘 (Action 钩子的执行内核)

接下来,我们深入 do_action_ref_array() 的源码:

// wp-includes/plugin.php

/**
 * Execute functions hooked on a specific action hook, specifying arguments in an array.
 *
 * @since 2.1.0
 *
 * @param string $tag The name of the action to be executed.
 * @param array  $args The arguments supplied to the functions hooked to {@see $tag}.
 * @return null
 */
function do_action_ref_array( $tag, $args ) {
    global $wp_filter, $wp_actions, $wp_current_filter;

    if ( ! isset( $wp_filter[ $tag ] ) ) {
        return;
    }

    $wp_filter[ $tag ]->do_action( $args );
}

简直不敢相信,do_action_ref_array() 函数居然这么简单! 它只做了两件事:

  1. if ( ! isset( $wp_filter[ $tag ] ) ) { return; }: 再次检查 Action 是否存在,如果不存在,直接返回。

  2. $wp_filter[ $tag ]->do_action( $args );: 调用 $wp_filter[$tag] 对象的 do_action() 方法,并将参数 $args 传递给它。

等等,$wp_filter[$tag] 是什么? 前面说过,它是 WP_Hook 对象。 所以,真正的 Action 执行逻辑,其实藏在 WP_Hook 类的 do_action() 方法里!

WP_Hook::do_action() 源码探秘 (Action 钩子的终极真相)

现在,我们来到 WP_Hook 类的 do_action() 方法:

// wp-includes/class-wp-hook.php

/**
 * Executes the hook.
 *
 * @param array $args The arguments to pass to the callbacks.
 */
public function do_action( $args ) {
    $res = $this->apply_filters( '', $args );

    // `apply_filters()` always returns a value.
    // We're only using it for its side-effects.
    if ( null !== $res ) {
        return;
    }
}

我没看错吧? WP_Hook::do_action() 竟然调用了 apply_filters() 方法!

等等,这有点绕。 Action 怎么会调用 Filter 的方法呢?

别忘了,Action 的本质是“做事”,而“做事”其实也是一种“修改”——修改了程序的运行状态。 WP_Hook 类把 Action 和 Filter 的底层实现统一起来,用 apply_filters() 来执行回调函数,只是 Action 的 apply_filters() 不会修改任何数据(它传递的是空字符串 '')。

Action 钩子的执行流程总结

为了方便大家理解,我们用一个表格来总结 Action 钩子的执行流程:

步骤 函数/方法 作用
1 do_action() 检查 Action 是否存在,记录 Action 被触发的次数,防止递归调用,然后调用 do_action_ref_array()
2 do_action_ref_array() 再次检查 Action 是否存在,然后调用 $wp_filter[$tag] 对象的 do_action() 方法。
3 WP_Hook::do_action() 调用 apply_filters() 方法,将空字符串 '' 和参数传递给它。
4 WP_Hook::apply_filters() 遍历所有挂载到该 Action 上的回调函数,依次执行它们,并将参数传递给它们。(具体细节我们稍后在 Filter 的部分详细讲解)

第三幕:apply_filters() 源码剖析 (Filter 钩子的执行流程)

了解了 Action 的执行流程,Filter 就简单多了。 我们直接上 apply_filters() 的源码:

// wp-includes/plugin.php

/**
 * Calls the functions that have been added to a filter hook.
 *
 * @since 0.71
 *
 * @param string $tag 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 {@see $tag}.
 * @return mixed The filtered value after all hooked functions are applied to it.
 */
function apply_filters( $tag, $value, ...$arg ) {
    global $wp_filter, $wp_current_filter;

    if ( ! isset( $wp_filter[ $tag ] ) ) {
        return $value;
    }

    if ( empty( $wp_filter[ $tag ]->callbacks ) ) {
        return $value;
    }

    $wp_current_filter[] = $tag;

    $args = array_merge( array( $value ), $arg );

    $filtered = $wp_filter[ $tag ]->apply_filters( $value, $args );

    array_pop( $wp_current_filter );

    return $filtered;
}

是不是感觉似曾相识? apply_filters() 的结构和 do_action() 非常相似:

  1. global $wp_filter, $wp_current_filter;: 声明全局变量,和 do_action() 一样。

  2. if ( ! isset( $wp_filter[ $tag ] ) ) { return $value; }: 检查 Filter 是否存在。如果不存在,直接返回原始值 $value

  3. if ( empty( $wp_filter[ $tag ]->callbacks ) ) { return $value; }: 如果存在 $wp_filter[$tag],但其 callbacks 属性为空,说明虽然定义了这个 Filter ,但是没有注册任何回调函数。 直接返回原始值 $value

  4. $wp_current_filter[] = $tag;: 将当前 Filter 的名字添加到 $wp_current_filter 数组中,用于防止递归调用。

  5. $args = array_merge( array( $value ), $arg );: 将原始值 $value 和其他参数 $arg 合并成一个数组 $args,传递给回调函数。 注意,原始值 $value 始终是数组的第一个元素。

  6. $filtered = $wp_filter[ $tag ]->apply_filters( $value, $args );: 调用 $wp_filter[$tag] 对象的 apply_filters() 方法,并将原始值 $value 和参数数组 $args 传递给它。

  7. array_pop( $wp_current_filter );: 执行完毕后,从 $wp_current_filter 数组中移除当前 Filter 的名字。

  8. return $filtered;: 返回经过所有回调函数过滤后的值 $filtered

你看,apply_filters() 的核心逻辑也是调用 WP_Hook 类的 apply_filters() 方法。

WP_Hook::apply_filters() 源码揭秘 (Filter 钩子的核心算法)

现在,我们来看看 WP_Hook 类的 apply_filters() 方法:

// wp-includes/class-wp-hook.php

/**
 * Runs the functions hooked on a specific filter hook.
 *
 * @param mixed $value The value to filter.
 * @param array $args The array of parameters passed to the functions.
 * @return mixed The filtered value after all hooked functions are applied to it.
 */
public function apply_filters( $value, $args ) {
    $res = $value;

    $this->iterations++;

    foreach ( $this->callbacks as $priority => $functions ) {
        ksort( $functions );

        foreach ( $functions as $function ) {
            $res = call_user_func_array( $function['function'], $args );
        }
    }

    $this->iterations--;

    return $res;
}

这才是 Filter 的核心算法! 我们来仔细分析:

  1. $res = $value;: 将原始值 $value 赋值给变量 $res,用于存储过滤后的值。

  2. $this->iterations++;: 增加迭代次数,用于防止递归调用。

  3. foreach ( $this->callbacks as $priority => $functions ) { ... }: 遍历 $this->callbacks 数组。 $this->callbacks 数组存储了所有挂载到该 Filter 上的回调函数,按照优先级($priority)进行分组。

  4. ksort( $functions );: 对同一优先级的回调函数进行排序。 ksort() 函数按照键名(也就是回调函数的唯一标识符)对数组进行排序,确保同一优先级的回调函数按照注册顺序执行。

  5. foreach ( $functions as $function ) { ... }: 遍历同一优先级的回调函数。

  6. $res = call_user_func_array( $function['function'], $args );: 调用回调函数。 $function['function'] 存储了回调函数的名称或对象方法,$args 存储了传递给回调函数的参数数组。 call_user_func_array() 函数会调用回调函数,并将参数数组传递给它,然后将回调函数的返回值赋值给 $res。 注意,每个回调函数都会接收上一个回调函数的返回值作为输入,这就是 Filter 的核心思想:通过一系列的函数,逐步修改原始值。

  7. $this->iterations--;: 减少迭代次数。

  8. return $res;: 返回经过所有回调函数过滤后的值 $res

Filter 钩子的执行流程总结

同样,我们用一个表格来总结 Filter 钩子的执行流程:

步骤 函数/方法 作用
1 apply_filters() 检查 Filter 是否存在,防止递归调用,将原始值和参数合并成数组,然后调用 $wp_filter[$tag] 对象的 apply_filters() 方法。
2 WP_Hook::apply_filters() 遍历所有挂载到该 Filter 上的回调函数,按照优先级和注册顺序依次执行它们,并将上一个回调函数的返回值作为输入传递给下一个回调函数。 最后,返回经过所有回调函数过滤后的值。

第四幕:实例演示 (用代码说话)

光说不练假把式,我们来写几个简单的例子,加深理解:

Action 示例:

// 注册一个 Action
add_action( 'wp_footer', 'my_footer_function' );

function my_footer_function() {
  echo '<p>This is my custom footer message.</p>';
}

// 在 WordPress 的某个地方触发该 Action
do_action( 'wp_footer' );

这段代码会在页面的 <footer> 标签之前输出一段文字。 当 WordPress 执行到 do_action( 'wp_footer' ) 时,它会找到所有挂载到 wp_footer Action 上的函数,然后依次执行它们。

Filter 示例:

// 注册一个 Filter
add_filter( 'the_content', 'my_content_filter' );

function my_content_filter( $content ) {
  $content .= '<p>This is my custom content message.</p>';
  return $content;
}

// 在 WordPress 的某个地方应用该 Filter
$content = apply_filters( 'the_content', $content );

这段代码会在文章内容的末尾添加一段文字。 当 WordPress 执行到 apply_filters( 'the_content', $content ) 时,它会将文章内容 $content 传递给所有挂载到 the_content Filter 上的函数,然后依次执行它们,并将上一个函数的返回值作为输入传递给下一个函数。 最后,返回经过所有函数过滤后的文章内容。

第五幕:总结与思考 (知识点梳理)

今天我们深入剖析了 WordPress 的 do_action()apply_filters() 函数的源码,了解了它们的执行流程。 希望通过今天的讲座,大家能够对 WordPress 的钩子机制有更深入的理解。

以下是一些关键知识点:

  • do_action() 用于触发 Action,apply_filters() 用于修改数据。
  • $wp_filter 数组存储了所有已注册的 Action 和 Filter 的回调函数。
  • WP_Hook 类是 Action 和 Filter 的核心实现,它负责存储和执行回调函数。
  • Filter 的核心思想是通过一系列的函数,逐步修改原始值。

思考题:

  1. 为什么 Action 的 WP_Hook::do_action() 方法要调用 apply_filters()
  2. 如何利用 Action 和 Filter 来扩展 WordPress 的功能?
  3. 如果多个函数挂载到同一个 Action 或 Filter 上,它们的执行顺序是什么?
  4. 如何防止 Action 或 Filter 形成递归调用?

希望大家能够带着这些问题,继续探索 WordPress 的源码,发现更多有趣的秘密!

今天的讲座就到这里,谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注