分析 WordPress `has_action()` 和 `has_filter()` 函数源码:如何高效地检查特定钩子是否被注册。

各位同学们,晚上好!我是你们的老朋友,今晚咱们来聊聊WordPress里两个重量级的函数:has_action()has_filter()。它们就像侦探一样,专门用来调查某个钩子(Hook)上有没有注册过函数。别看它们名字简单,内部的实现却藏着一些小秘密,高效使用它们能让你的代码更优雅、性能更好。

今天,咱们就来扒一扒这两个函数的源码,看看它们是如何高效地完成任务的。准备好了吗?Let’s dive in!

一、钩子(Hook):WordPress的灵魂

在深入has_action()has_filter()之前,我们先简单回顾一下什么是钩子。钩子是WordPress插件和主题开发的核心机制,它允许开发者在不修改WordPress核心代码的情况下,插入或修改WordPress的行为。

钩子分为两种:

  • 动作(Action): 允许你执行一些代码。比如,在文章发布后发送邮件通知。
  • 过滤器(Filter): 允许你修改数据。比如,修改文章标题的显示格式。

二、has_action():侦查动作钩子的利器

has_action()函数用于检查某个动作钩子是否被注册过任何函数,或者检查某个特定的函数是否被注册到该动作钩子上。

1. has_action()的函数签名:

/**
 * Checks if any action has been registered for a given hook.
 *
 * @since 2.5.0
 *
 * @global array $wp_actions Storage for all of the registered actions.
 *
 * @param string   $hook_name The name of the action hook.
 * @param callable $function_to_check Optional. The name of the function to check for.
 *                                     Default false.
 * @return int|false If the hook has actions registered, it returns the number of actions registered.
 *                   If $function_to_check is specified, it returns the priority of that hook when found,
 *                   or false if not found.
 */
function has_action( $hook_name, $function_to_check = false ) {
    global $wp_actions, $wp_filter;

    $has = false;

    if ( isset( $wp_actions[ $hook_name ] ) ) {
        $has = $wp_actions[ $hook_name ];
    }

    if ( false === $function_to_check ) {
        return $has;
    }

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

    if ( ! is_callable( $function_to_check ) ) {
        return false;
    }

    $callback = _wp_filter_build_unique_id( $hook_name, $function_to_check, false );

    if ( ! empty( $wp_filter[ $hook_name ]->callbacks ) ) {
        foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $functions ) {
            if ( isset( $functions[ $callback ] ) ) {
                return $priority;
            }
        }
    }

    return false;
}

2. 源码解读:

  • 全局变量的利用:$wp_actions$wp_filter

    WordPress使用全局变量$wp_actions$wp_filter来存储已注册的动作和过滤器。$wp_actions是一个数组,记录了每个动作钩子被触发的次数。$wp_filter则是一个更复杂的数据结构,存储了每个钩子上注册的所有函数及其优先级。

    这两个全局变量是性能的关键。通过直接访问它们,has_action()避免了遍历所有已注册的钩子,从而提高了效率。

  • 快速检查:isset( $wp_actions[ $hook_name ] )

    首先,has_action()检查$wp_actions数组中是否存在以$hook_name为键的元素。如果存在,说明该动作钩子至少被触发过一次,即有函数注册到该钩子上。这一步非常快,因为它只是一个简单的数组键值查找。

  • 精确查找:$function_to_check

    如果提供了$function_to_check参数,has_action()会进一步检查指定的函数是否被注册到该动作钩子上。

    • _wp_filter_build_unique_id():这个函数用于生成一个唯一的ID,代表特定的函数和钩子的组合。这是因为同一个函数可能会被注册到多个钩子上,我们需要一个唯一标识符来区分它们。

    • 遍历回调函数:has_action()会遍历$wp_filter数组中$hook_name对应的回调函数,查找是否存在与$callback匹配的函数。如果找到,返回该函数的优先级;否则,返回false

3. 性能分析:

  • 最佳情况: 当只需要检查钩子是否被注册过(不提供$function_to_check参数)时,has_action()的性能非常高,因为它只需要访问$wp_actions数组。这是一个O(1)的操作。

  • 最坏情况: 当需要检查特定的函数是否被注册到钩子上时,has_action()需要遍历$wp_filter数组中该钩子对应的所有回调函数。这是一个O(n)的操作,其中n是注册到该钩子上的函数数量。但是,由于WordPress会尽量减少不必要的函数注册,所以n通常不会太大。

4. 使用示例:

// 检查 'wp_head' 动作钩子是否被注册过任何函数
if ( has_action( 'wp_head' ) ) {
    echo "wp_head 动作钩子已经被注册过函数了!";
} else {
    echo "wp_head 动作钩子还没有被注册过函数!";
}

// 检查 'wp_head' 动作钩子上是否注册了名为 'my_custom_function' 的函数
if ( has_action( 'wp_head', 'my_custom_function' ) ) {
    echo "my_custom_function 已经注册到 wp_head 动作钩子上了!优先级是:" . has_action( 'wp_head', 'my_custom_function' );
} else {
    echo "my_custom_function 还没有注册到 wp_head 动作钩子上!";
}

// 假设你有这样一个函数:
function my_custom_function() {
  echo "Hello from my_custom_function!";
}

// 你可以这样注册它:
add_action( 'wp_head', 'my_custom_function', 10 ); // 优先级为10

三、has_filter():过滤器钩子的侦查专家

has_filter()函数与has_action()类似,但它是专门用于检查过滤器钩子的。

1. has_filter()的函数签名:

/**
 * Checks if any filter has been registered for a given hook.
 *
 * @since 2.5.0
 *
 * @global array $wp_filter Storage for all of the registered filters.
 *
 * @param string   $hook_name         The name of the filter hook.
 * @param callable $function_to_check Optional. The name of the function to check for.
 *                                     Default false.
 * @return int|false If the hook has filters registered, it returns the number of filters registered.
 *                   If $function_to_check is specified, it returns the priority of that hook when found,
 *                   or false if not found.
 */
function has_filter( $hook_name, $function_to_check = false ) {
    global $wp_filter;

    $has = ! empty( $wp_filter[ $hook_name ] );

    if ( false === $function_to_check ) {
        return $has;
    }

    if ( ! $has ) {
        return false;
    }

    if ( ! is_callable( $function_to_check ) ) {
        return false;
    }

    $callback = _wp_filter_build_unique_id( $hook_name, $function_to_check, false );

    if ( ! empty( $wp_filter[ $hook_name ]->callbacks ) ) {
        foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $functions ) {
            if ( isset( $functions[ $callback ] ) ) {
                return $priority;
            }
        }
    }

    return false;
}

2. 源码解读:

  • 全局变量的利用:$wp_filter

    has_action()类似,has_filter()也依赖于全局变量$wp_filter来存储已注册的过滤器函数。

  • 快速检查:! empty( $wp_filter[ $hook_name ] )

    has_filter()首先检查$wp_filter数组中是否存在以$hook_name为键的元素,并且该元素不为空。如果存在且不为空,说明该过滤器钩子已经被注册过函数。这一步同样非常快。

  • 精确查找:$function_to_check

    如果提供了$function_to_check参数,has_filter()会进一步检查指定的函数是否被注册到该过滤器钩子上。

    • _wp_filter_build_unique_id():作用与has_action()相同,生成一个唯一的ID,代表特定的函数和钩子的组合。

    • 遍历回调函数:has_filter()会遍历$wp_filter数组中$hook_name对应的回调函数,查找是否存在与$callback匹配的函数。如果找到,返回该函数的优先级;否则,返回false

3. 性能分析:

  • 最佳情况: 当只需要检查钩子是否被注册过(不提供$function_to_check参数)时,has_filter()的性能非常高,因为它只需要访问$wp_filter数组。这是一个O(1)的操作。

  • 最坏情况: 当需要检查特定的函数是否被注册到钩子上时,has_filter()需要遍历$wp_filter数组中该钩子对应的所有回调函数。这是一个O(n)的操作,其中n是注册到该钩子上的函数数量。

4. 使用示例:

// 检查 'the_title' 过滤器钩子是否被注册过任何函数
if ( has_filter( 'the_title' ) ) {
    echo "the_title 过滤器钩子已经被注册过函数了!";
} else {
    echo "the_title 过滤器钩子还没有被注册过函数!";
}

// 检查 'the_title' 过滤器钩子上是否注册了名为 'my_title_filter' 的函数
if ( has_filter( 'the_title', 'my_title_filter' ) ) {
    echo "my_title_filter 已经注册到 the_title 过滤器钩子上了!优先级是:" . has_filter( 'the_title', 'my_title_filter' );
} else {
    echo "my_title_filter 还没有注册到 the_title 过滤器钩子上!";
}

// 假设你有这样一个函数:
function my_title_filter( $title ) {
  return 'Custom Title: ' . $title;
}

// 你可以这样注册它:
add_filter( 'the_title', 'my_title_filter', 10 ); // 优先级为10

四、_wp_filter_build_unique_id():构建唯一ID的关键

_wp_filter_build_unique_id() 函数是 has_action()has_filter() 的幕后英雄,它负责为每个钩子和回调函数的组合生成唯一的 ID。这个函数保证了即使同一个函数被多次添加到同一个钩子上,或者被添加到多个钩子上,也能被正确区分。

1. 函数签名:

/**
 * Build Unique ID for storage and lookup of filters.
 *
 * The unique ID is a string comprised of a few parts:
 *  - The filter name
 *  - The class/function name
 *  - The function arguments
 *
 * @ignore
 * @since 2.2.3
 *
 * @global array $wp_filter Storage for all of the filters.
 *
 * @param string   $tag      Filter name.
 * @param string|array|object $function_to_add Function to be added.
 * @param int|bool $priority The priority at which the function should be executed.
 * @return string Unique ID for usage in filters array.
 */
function _wp_filter_build_unique_id( $tag, $function_to_add, $priority ) {
    static $filter_id_count = 0;

    if ( is_string( $function_to_add ) ) {
        return $function_to_add;
    }

    if ( is_object( $function_to_add ) ) {
        // Closures are currently implemented as objects
        $function_to_add = array( $function_to_add, '__invoke' );
    } else {
        $function_to_add = (array) $function_to_add;

        if ( is_object( $function_to_add[0] ) ) {
            return spl_object_hash( $function_to_add[0] ) . $function_to_add[1];
        }
    }

    if ( is_string( $function_to_add[0] ) ) {
        return $function_to_add[0] . '::' . $function_to_add[1];
    }

    return false;
}

2. 源码解读:

  • 处理不同类型的回调函数:
    这个函数可以处理不同类型的回调函数,包括字符串(函数名)、数组(对象方法)和闭包。

  • 为对象方法生成唯一 ID:
    对于对象方法,它使用 spl_object_hash() 函数生成对象的哈希值,并将其与方法名连接起来,作为唯一 ID。

  • 为静态方法生成唯一 ID:
    对于静态方法,它使用类名和方法名,用 "::" 连接,作为唯一 ID。

五、高效使用has_action()has_filter()的技巧

  • 避免不必要的调用: 只有在真正需要知道钩子是否被注册时才调用has_action()has_filter()。频繁的调用会降低性能。
  • 利用返回值: has_action()has_filter()的返回值不仅可以告诉你钩子是否被注册,还可以告诉你特定函数的优先级。善用这些信息,可以避免重复查找。
  • 考虑使用条件语句: 在某些情况下,可以使用条件语句来避免调用has_action()has_filter()。例如,如果你的代码只在某个插件激活时才执行,可以使用is_plugin_active()函数来判断插件是否激活,而不是使用has_action()来检查某个钩子是否被注册。
  • 缓存结果: 如果你需要多次检查同一个钩子是否被注册,可以考虑将结果缓存起来,避免重复调用has_action()has_filter()

六、总结

函数 作用 依赖的全局变量 性能特点
has_action() 检查动作钩子是否被注册过函数,或检查特定函数是否被注册到该动作钩子上。 $wp_actions, $wp_filter 最佳情况O(1),最坏情况O(n),n为注册到该钩子上的函数数量。
has_filter() 检查过滤器钩子是否被注册过函数,或检查特定函数是否被注册到该过滤器钩子上。 $wp_filter 最佳情况O(1),最坏情况O(n),n为注册到该钩子上的函数数量。
_wp_filter_build_unique_id() 为钩子和回调函数的组合生成唯一的 ID。 取决于回调函数的类型。

has_action()has_filter() 是 WordPress 开发中非常实用的工具。理解它们的源码和使用技巧,可以帮助你编写更高效、更健壮的代码。 记住,良好的代码习惯就像优秀的侦探一样,需要细致的观察和巧妙的推理。希望今天的分享对大家有所帮助!

好了,今天的讲座就到这里,大家有什么问题可以随时提问。下课!

发表回复

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