分析 `add_action` 和 `do_action` 的源码,它们如何利用全局 `Array` 数组存储和调用钩子函数?

各位观众老爷,大家好!今天给大家带来的节目是《WordPress 钩子背后的秘密:add_actiondo_action 的源码解剖》,保证让各位听完之后,感觉自己也能参与到 WordPress 的内核开发中去!

咱们先来热个身,想想 WordPress 的插件机制为啥这么强大? 核心就是它那灵活的钩子机制,允许我们轻松地在 WordPress 的各个关键点插入自定义代码。 而实现这一切的关键,就是 add_actiondo_action 这两个好基友。

一、钩子机制:WordPress 的灵魂

想象一下,WordPress 是一辆正在高速行驶的汽车,而钩子就是汽车上预留的一些接口。 你可以通过这些接口,连接各种各样的设备,比如导航仪、行车记录仪、甚至是一个能自动播放《忐忑》的音箱(当然,这可能不太受欢迎)。

在 WordPress 中,钩子实际上就是一个名字,一个字符串,用来标记 WordPress 代码中允许你插入自定义代码的位置。 插件和主题可以通过 add_actionadd_filter 函数,将自己的代码“挂”到这些钩子上,然后在 WordPress 执行到 do_actionapply_filters 的时候,这些代码就会被自动执行。

二、add_action:注册你的代码

add_action 函数的作用就是将你的函数(或者方法)注册到指定的钩子上。 简单来说,就是告诉 WordPress:“嘿,当执行到 xxx 这个钩子的时候,记得也执行我的函数 yyy 哦!”

咱们来看看 add_action 的源码(以下代码为简化版,方便大家理解):

function add_action( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
  global $wp_filter;

  // 确保 $wp_filter 是一个全局变量,并且是一个数组
  if ( ! isset( $wp_filter ) || ! is_array( $wp_filter ) ) {
    $wp_filter = array();
  }

  // 将函数信息存储到全局数组中
  $wp_filter[ $tag ][ $priority ][ $function_to_add ] = array(
    'function' => $function_to_add,
    'accepted_args' => $accepted_args
  );

  return true;
}

代码解释:

  1. global $wp_filter;: 这行代码非常重要! 它声明了 $wp_filter 是一个全局变量。 注意,这里用的是 global 关键字,意味着 $wp_filter 可以在函数外部访问和修改。 $wp_filter 实际上就是一个多维数组,它存储了所有注册到钩子上的函数信息。

  2. if ( ! isset( $wp_filter ) || ! is_array( $wp_filter ) ) { ... }: 检查 $wp_filter 是否存在,如果不存在或者不是一个数组,就初始化它。 这是为了确保我们能正常地向 $wp_filter 中添加数据。

  3. $wp_filter[ $tag ][ $priority ][ $function_to_add ] = array( ... );: 这行代码是核心! 它将函数信息存储到 $wp_filter 数组中。

    • $tag: 钩子的名称,比如 'wp_head' 或者 'the_content'
    • $priority: 优先级,数值越小,优先级越高,越早执行。 默认值是 10
    • $function_to_add: 要执行的函数名(或者方法名)。
    • array( ... ): 一个包含函数信息的数组,包括:
      • 'function' => $function_to_add: 函数名。
      • 'accepted_args' => $accepted_args: 函数接受的参数个数。

$wp_filter 数组的结构:

为了更清楚地理解,我们用一个表格来展示 $wp_filter 数组的结构:

键 (Key) 值 (Value)
$tag 一个数组,包含所有注册到该钩子上的函数信息。
$priority 一个数组,包含所有具有相同优先级的函数信息。 优先级数字越小,优先级越高。
$function_to_add 一个数组,包含函数的详细信息,如函数名和接受的参数个数。
'function' 函数名(或者方法名)。
'accepted_args' 函数接受的参数个数。

举个例子:

add_action( 'wp_head', 'my_custom_header_code', 10, 0 );
add_action( 'wp_footer', 'my_custom_footer_code', 20, 1 );

执行完上面的代码后,$wp_filter 数组可能会是这样的(简化版):

$wp_filter = array(
  'wp_head' => array(
    10 => array(
      'my_custom_header_code' => array(
        'function' => 'my_custom_header_code',
        'accepted_args' => 0
      )
    )
  ),
  'wp_footer' => array(
    20 => array(
      'my_custom_footer_code' => array(
        'function' => 'my_custom_footer_code',
        'accepted_args' => 1
      )
    )
  )
);

可以看出,$wp_filter 数组就像一个巨大的索引表,它记录了所有钩子、优先级、以及对应的函数信息。

三、do_action:触发你的代码

do_action 函数的作用就是触发指定钩子上注册的所有函数。 简单来说,就是告诉 WordPress:“执行到这里了,快去 $wp_filter 数组里找找,看看有没有函数注册到这个钩子上,如果有,就执行它们!”

咱们来看看 do_action 的源码(同样是简化版):

function do_action( $tag, ...$args ) {
  global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;

  // 记录 action 被调用的次数
  if ( ! isset( $wp_actions[ $tag ] ) ) {
    $wp_actions[ $tag ] = 1;
  } else {
    ++$wp_actions[ $tag ];
  }

  // 如果没有函数注册到这个钩子上,直接返回
  if ( ! isset( $wp_filter[ $tag ] ) ) {
    return;
  }

  // 排序,确保按照优先级顺序执行
  if ( ! isset( $merged_filters[ $tag ] ) ) {
    ksort( $wp_filter[ $tag ] );
    $merged_filters[ $tag ] = true;
  }

  // 将当前的钩子添加到 $wp_current_filter 数组中
  $wp_current_filter[] = $tag;

  // 遍历所有注册到该钩子上的函数,并执行它们
  foreach ( $wp_filter[ $tag ] as $priority => $functions ) {
    foreach ( $functions as $function ) {
      // 根据函数接受的参数个数,传递不同的参数
      call_user_func_array( $function['function'], array_slice( $args, 0, $function['accepted_args'] ) );
    }
  }

  // 将当前的钩子从 $wp_current_filter 数组中移除
  array_pop( $wp_current_filter );
}

代码解释:

  1. global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;: 声明了几个全局变量。 $wp_filter 我们已经熟悉了,它存储了所有钩子和函数的信息。 $wp_actions 记录了每个 action 被调用的次数。 $merged_filters 记录了是否已经对某个钩子的优先级进行了排序。 $wp_current_filter 记录了当前正在执行的钩子。

  2. if ( ! isset( $wp_actions[ $tag ] ) ) { ... } else { ... }: 记录当前 action 被调用的次数。 这对于调试和性能分析非常有用。

  3. if ( ! isset( $wp_filter[ $tag ] ) ) { return; }: 如果 $wp_filter 数组中不存在当前钩子的信息,说明没有任何函数注册到这个钩子上,直接返回。

  4. if ( ! isset( $merged_filters[ $tag ] ) ) { ... }: 如果还没有对当前钩子的优先级进行排序,就使用 ksort() 函数按照优先级进行排序。 ksort() 函数会按照键(这里是优先级)对数组进行升序排序。

  5. $wp_current_filter[] = $tag;array_pop( $wp_current_filter );: 这两行代码用于记录当前正在执行的钩子。 $wp_current_filter 是一个数组,它记录了当前正在执行的钩子,可以用来防止递归调用。

  6. foreach ( $wp_filter[ $tag ] as $priority => $functions ) { ... }: 遍历所有注册到该钩子上的函数。

  7. call_user_func_array( $function['function'], array_slice( $args, 0, $function['accepted_args'] ) );: 这是执行函数的关键!

    • $function['function']: 要执行的函数名(或者方法名)。
    • $args: 传递给函数的参数。 ...$args 是 PHP 5.6 引入的 "argument unpacking" 语法,可以将多个参数传递给函数。
    • array_slice( $args, 0, $function['accepted_args'] ): 从 $args 数组中提取指定个数的参数。 这样做是为了确保传递给函数的参数个数与函数声明的参数个数一致。
    • call_user_func_array(): PHP 内置函数,用于调用一个用户定义的函数,并将参数传递给它。

四、apply_filters:修改数据

除了 add_actiondo_action 之外,WordPress 还有一个非常重要的函数: apply_filtersapply_filters 的作用与 do_action 类似,但它主要用于修改数据。

apply_filters 会将一个值(通常是一个变量)传递给所有注册到指定钩子上的函数,每个函数都可以对这个值进行修改,并将修改后的值返回。 最终,apply_filters 会返回经过所有函数修改后的值。

apply_filters 的用法如下:

$content = apply_filters( 'the_content', $content );

这行代码会将 $content 变量传递给所有注册到 'the_content' 钩子上的函数,每个函数都可以对 $content 进行修改,并将修改后的 $content 返回。 最终,apply_filters 会返回经过所有函数修改后的 $content,并将其赋值给 $content 变量。

apply_filters 的源码与 do_action 非常相似,主要的区别在于它会返回一个值,并且会将这个值传递给下一个函数。

五、总结:add_actiondo_action 的关系

add_actiondo_action 就像一对好基友,一个负责注册,一个负责触发。

  • add_action 将函数注册到指定的钩子上,并将函数信息存储到全局数组 $wp_filter 中。
  • do_action 触发指定钩子上注册的所有函数,并按照优先级顺序执行它们。

$wp_filter 数组是它们之间沟通的桥梁,add_action 将函数信息写入 $wp_filterdo_action$wp_filter 中读取函数信息并执行它们。

六、实战演练:自定义一个钩子

光说不练假把式,咱们来做一个小实验,自定义一个钩子,并使用 add_actiondo_action 来触发它。

// 定义一个函数,用于修改文章标题
function my_custom_title( $title ) {
  return '【特别推荐】' . $title;
}

// 将函数注册到自定义的钩子 'my_title_filter' 上
add_filter( 'my_title_filter', 'my_custom_title' );

// 在需要修改文章标题的地方,使用 apply_filters 函数
$title = get_the_title(); // 获取文章标题
$title = apply_filters( 'my_title_filter', $title ); // 应用过滤器
echo $title; // 输出修改后的文章标题

在这个例子中,我们首先定义了一个函数 my_custom_title,用于修改文章标题。 然后,我们使用 add_filter 函数将 my_custom_title 函数注册到自定义的钩子 'my_title_filter' 上。 最后,我们在需要修改文章标题的地方,使用 apply_filters 函数,将文章标题传递给 'my_title_filter' 钩子,apply_filters 函数会自动调用 my_custom_title 函数,并将修改后的文章标题返回。

七、高级技巧:移除钩子

有时候,我们需要移除已经注册的钩子。 WordPress 提供了 remove_actionremove_filter 函数来实现这个功能。

remove_actionremove_filter 函数的用法如下:

remove_action( 'wp_head', 'my_custom_header_code' );
remove_filter( 'the_content', 'my_custom_content_filter' );

需要注意的是,要移除一个钩子,你需要知道钩子的名称、函数名、以及优先级。

八、总结与展望

今天我们深入剖析了 add_actiondo_action 的源码,了解了 WordPress 钩子机制的底层实现。 希望通过今天的讲解,大家能够对 WordPress 的插件机制有更深入的理解,并能够灵活运用钩子机制来扩展 WordPress 的功能。

WordPress 的钩子机制非常强大,它为插件和主题开发提供了无限的可能性。 掌握了钩子机制,你就可以像一位魔法师一样,随意地改变 WordPress 的行为,创造出属于你自己的 WordPress 世界!

今天的节目就到这里,感谢大家的收看! 我们下期再见!

发表回复

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