深入理解 WordPress `do_action()` 函数源码:它是如何遍历 `$wp_filter` 数组,并通过 `call_user_func_array()` 调用钩子函数的。

各位观众老爷们,早上好!我是你们的老朋友,今天咱们来聊聊 WordPress 里的一个神奇函数——do_action()。这玩意儿啊,看起来不起眼,但却是 WordPress 插件和主题开发的灵魂所在。它就像一个交通枢纽,把不同的功能模块连接起来,让你的代码像乐高积木一样灵活组合。

准备好了吗?咱们这就开讲!

1. do_action() 究竟是个啥?

简单来说,do_action() 就是一个“钩子”函数。它会在 WordPress 执行的某个特定时刻,触发你预先定义好的函数。这些预先定义好的函数,我们称之为“钩子函数”。

你可以把它想象成一个“事件发布者”。WordPress 在执行代码的时候,会时不时地喊一声:“嘿,有没有人想在这个时候做点啥?” do_action() 就负责喊这一嗓子。如果你之前注册了一个钩子函数,说:“嘿,老子想在这个时候执行!”,那么 do_action() 就会找到你,执行你的函数。

2. do_action() 的基本用法

do_action() 接受至少一个参数:钩子的名称。

do_action( 'my_custom_action' );

上面这行代码的意思是:WordPress 在执行到这里的时候,会触发名为 my_custom_action 的钩子。

你还可以传递额外的参数给钩子函数:

do_action( 'my_custom_action', $arg1, $arg2, $arg3 );

这些参数会被传递给所有注册到 my_custom_action 上的钩子函数。

3. add_action():注册你的钩子函数

光有 do_action() 还不够,你还需要使用 add_action() 函数来注册你的钩子函数。add_action() 告诉 WordPress:“嘿,当 do_action()my_custom_action 的时候,执行我这个函数!”

function my_custom_function( $arg1, $arg2, $arg3 ) {
  // 在这里写你的代码
  echo "Arg1: " . $arg1 . "<br>";
  echo "Arg2: " . $arg2 . "<br>";
  echo "Arg3: " . $arg3 . "<br>";
}

add_action( 'my_custom_action', 'my_custom_function', 10, 3 );
  • 'my_custom_action':钩子的名称,和 do_action() 里的名字对应。
  • 'my_custom_function':你的钩子函数的名称。
  • 10:优先级,数字越小,优先级越高。
  • 3:钩子函数接受的参数个数。必须和 do_action() 传递的参数个数一致。

4. 源码剖析:do_action() 的内部运作

好了,铺垫了这么多,终于要进入正题了。咱们来扒一扒 do_action() 的源码,看看它是怎么工作的。

do_action() 函数定义在 wp-includes/plugin.php 文件中。它的简化版代码如下:

function do_action( $hook_name, ...$args ) {
  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( $args );
  }

  if ( ! isset( $wp_filter[ $hook_name ] ) ) {
    do_action_ref_array( $hook_name, $args );
    return;
  }

  if ( ! in_array( $hook_name, $wp_current_filter, true ) ) {
    $wp_current_filter[] = $hook_name;
  }

  // Sort.
  if ( $wp_filter[ $hook_name ] instanceof WP_Hook ) {
    $wp_filter[ $hook_name ]->do_action( $args );
  } else {
    ksort( $wp_filter[ $hook_name ] );
    do_action_ref_array( $hook_name, $args );
  }

  array_pop( $wp_current_filter );
}

看起来有点复杂,别怕,咱们一步一步来。

  • global $wp_filter, $wp_actions, $wp_current_filter;

    这行代码声明了三个全局变量:$wp_filter$wp_actions$wp_current_filter

    • $wp_filter:这是一个超级重要的数组,它存储了所有注册的钩子函数。它的结构是这样的:

      $wp_filter = array(
        'hook_name' => array(
          priority => array(
            'function_name' => array(
              'function' => 'the_function_to_execute',
              'accepted_args' => 'number_of_accepted_arguments'
            ),
            'function_name2' => array(
              'function' => 'another_function_to_execute',
              'accepted_args' => 'number_of_accepted_arguments'
            )
          ),
          priority2 => array(
            // ... more functions for this priority
          )
        ),
        'another_hook_name' => array(
          // ... functions for another hook
        )
      );

      简单来说,$wp_filter 是一个多维数组,第一层键是钩子的名称,第二层键是优先级,第三层键是钩子函数的名称,值是一个包含了函数名和接受参数个数的数组。

    • $wp_actions:这是一个计数器,记录了每个钩子被触发的次数。

    • $wp_current_filter:这是一个数组,记录了当前正在执行的钩子。用于防止循环调用。

  • if ( ! isset( $wp_actions[ $hook_name ] ) ) { ... } else { ... }

    这段代码用于增加 $wp_actions 中对应钩子的计数器。

  • if ( isset( $wp_filter['all'] ) ) { ... }

    这是一个特殊的情况,如果注册了名为 all 的钩子,那么在执行任何其他钩子之前,会先执行 all 钩子上的函数。

  • if ( ! isset( $wp_filter[ $hook_name ] ) ) { ... }

    如果没有任何函数注册到这个钩子上,就直接返回。

  • if ( ! in_array( $hook_name, $wp_current_filter, true ) ) { ... }

    防止循环调用。如果当前钩子已经在 $wp_current_filter 中,说明已经执行过了,就跳过。

  • if ( $wp_filter[ $hook_name ] instanceof WP_Hook ) { ... } else { ... }

    这是 WordPress 5.1 引入的 WP_Hook 类,用于更高效地管理钩子函数。如果 $wp_filter[ $hook_name ] 是一个 WP_Hook 对象,就调用它的 do_action() 方法。否则,就使用 ksort() 函数对 $wp_filter[ $hook_name ] 数组按照优先级进行排序,然后调用 do_action_ref_array() 函数。

  • array_pop( $wp_current_filter );

    执行完当前钩子后,从 $wp_current_filter 中移除。

5. do_action_ref_array():真正执行钩子函数

do_action() 函数本身并没有直接执行钩子函数,而是调用了 do_action_ref_array() 函数。do_action_ref_array() 才是真正负责执行钩子函数的核心。

它的简化版代码如下:

function do_action_ref_array( $hook_name, $args ) {
    global $wp_filter, $wp_actions, $wp_current_filter;

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

    if ( empty( $wp_filter[ $hook_name ] ) ) {
        return;
    }

    foreach ( (array) current( $wp_filter[ $hook_name ] ) as $the_ ) {
        if ( ! is_array( $the_ ) ) {
            continue;
        }

        foreach ( $the_ as $key => $function ) {
            if ( is_string( $function['function'] ) ) {
                $hook_name_t = explode( '::', $function['function'] );
                if ( isset( $hook_name_t[1] ) ) {
                    $function['function'] = array( $hook_name_t[0], $hook_name_t[1] );
                }
            }

            $accepted_args = (int) $function['accepted_args'];

            if ( $accepted_args ) {
                $args = array_slice( $args, 0, $accepted_args );
            }

            $value = call_user_func_array( $function['function'], $args );

            if ( false === $value ) {
                return;
            }
        }
    }
}
  • if ( ! isset( $wp_filter[ $hook_name ] ) ) { ... }

    do_action() 一样,如果没有任何函数注册到这个钩子上,就直接返回。

  • foreach ( (array) current( $wp_filter[ $hook_name ] ) as $the_ ) { ... }

    这个循环遍历 $wp_filter[ $hook_name ] 数组中的所有优先级。current() 函数用于获取数组的第一个元素(即优先级最低的函数列表)。

  • foreach ( $the_ as $key => $function ) { ... }

    这个循环遍历当前优先级下的所有钩子函数。

  • if ( is_string( $function['function'] ) ) { ... }

    如果钩子函数是一个字符串,就尝试将其解析为类方法。例如,'My_Class::my_method'

  • $accepted_args = (int) $function['accepted_args'];

    获取钩子函数接受的参数个数。

  • if ( $accepted_args ) { ... }

    如果钩子函数接受参数,就使用 array_slice() 函数截取 do_action() 传递的参数。

  • $value = call_user_func_array( $function['function'], $args );

    这就是关键的一行代码! 使用 call_user_func_array() 函数调用钩子函数。call_user_func_array() 允许你使用一个数组作为参数列表来调用一个函数。

    • $function['function']:要调用的函数名。
    • $args:传递给函数的参数数组。

    call_user_func_array() 会根据 $function['function'] 的值,找到对应的函数,然后将 $args 数组中的元素作为参数传递给这个函数。

  • if ( false === $value ) { ... }

    如果钩子函数返回 false,就停止执行后续的钩子函数。

6. 总结:do_action() 的工作流程

咱们来总结一下 do_action() 的工作流程:

  1. do_action() 被调用,传递钩子的名称和参数。
  2. do_action() 检查 $wp_filter 数组,看是否有函数注册到这个钩子上。
  3. 如果没有函数注册,就直接返回。
  4. 如果有函数注册,就对 $wp_filter 数组按照优先级进行排序。
  5. do_action() 调用 do_action_ref_array() 函数。
  6. do_action_ref_array() 遍历 $wp_filter 数组,找到所有注册到这个钩子上的函数。
  7. 对于每一个找到的函数,do_action_ref_array() 使用 call_user_func_array() 函数调用它,并将 do_action() 传递的参数传递给它。
  8. 如果钩子函数返回 false,就停止执行后续的钩子函数。

7. 举个栗子:主题的 wp_head 钩子

WordPress 主题的 wp_head 钩子是一个非常常用的钩子。它允许你在 <head> 标签中插入自定义的代码,例如 CSS 样式、JavaScript 脚本等。

add_action( 'wp_head', 'my_custom_head_function' );

function my_custom_head_function() {
  echo '<style>body { background-color: #f0f0f0; }</style>';
  echo '<script>console.log("Hello from wp_head!");</script>';
}

这段代码会在 <head> 标签中插入一段 CSS 样式和一个 JavaScript 脚本。

当 WordPress 执行到 wp_head 钩子时,它会调用 do_action( 'wp_head' );do_action() 函数会找到所有注册到 wp_head 钩子上的函数,并使用 call_user_func_array() 函数调用它们。

8. 表格总结

为了方便大家理解,咱们用一个表格来总结一下:

函数 作用 参数 返回值
do_action() 触发一个钩子,执行所有注册到该钩子上的函数。 钩子名称 (string), 传递给钩子函数的参数 (mixed)
add_action() 注册一个函数到指定的钩子上。 钩子名称 (string), 钩子函数名 (string), 优先级 (int, 默认为 10), 接受的参数个数 (int, 默认为 1)
call_user_func_array() 调用一个函数,并将一个数组作为参数传递给它。 函数名 (string), 参数数组 (array) 函数的返回值
$wp_filter 全局变量,存储了所有注册的钩子函数。 数组

9. 总结的总结:灵魂画风

总而言之,do_action() 就像一个媒婆,负责把 WordPress 的各个功能模块连接起来。它通过 $wp_filter 数组找到所有想要参与某个“事件”的函数,然后使用 call_user_func_array() 函数把它们“撮合”在一起,让它们协同工作。

希望今天的讲座能帮助大家更深入地理解 do_action() 函数。掌握了它,你就能更好地开发 WordPress 插件和主题,让你的代码更加灵活和强大!

下次再见!

发表回复

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