阐述 WordPress `add_action()` 和 `add_filter()` 函数源码:分析 `$wp_filter` 全局数组的数据结构,特别是如何存储优先级和参数数量。

咳咳,各位同学,早上好!我是你们今天的WordPress源码小讲师,咱们今天来聊聊WordPress的add_action()add_filter()这两个神器的内部机制。

咱们都知道,WordPress 的插件机制非常强大,很大程度上归功于 action 和 filter 这两个钩子。它们允许我们在不修改 WordPress 核心代码的情况下,插入我们自己的代码,改变程序的行为。而 add_action()add_filter() 就是用来注册这些钩子的函数。

今天咱们就扒开它们的源码,看看它们到底是怎么工作的,特别是那个神秘的 $wp_filter 全局数组。

1. add_action()add_filter():表面兄弟,实则一家

首先,我们来看看add_action()add_filter()的源码(为了方便阅读,我简化了一些错误处理和注释):

// wp-includes/plugin.php

function add_action( $hook, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    return add_filter( $hook, $function_to_add, $priority, $accepted_args );
}

function add_filter( $hook, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    global $wp_filter, $wp_actions, $merged_filters;

    $idx = _wp_filter_build_unique_id( $hook, $function_to_add, $priority );

    $wp_filter[ $hook ][ $priority ][ $idx ] = array(
        'function' => $function_to_add,
        'accepted_args' => $accepted_args
    );

    unset( $merged_filters[ $hook ] );
    return true;
}

看到没? add_action() 其实就是 add_filter() 的一个马甲!它们最终都调用了 add_filter() 函数。add_action() 存在的意义,仅仅是为了语义上的区分,让开发者更清楚地知道自己是在添加一个动作(action)还是一个过滤器(filter)。

它们接收的参数也很相似:

  • $hook: 钩子的名称,也就是你要挂载代码的地方。
  • $function_to_add: 你要执行的函数或方法。
  • $priority: 优先级,数字越小,优先级越高,越先执行。默认是 10。
  • $accepted_args: 你的函数期望接收的参数数量。默认是 1。

2. 神秘的 $wp_filter 全局数组:钩子的藏身之所

现在,我们来重点关注 $wp_filter 这个全局数组。它是 WordPress 存储所有已注册钩子的核心数据结构。 说它神秘,是因为它是个多维数组,层层嵌套,刚开始看容易头晕。 咱们来拆解一下它的结构:

  • 第一层 (Key: $hook): 以钩子的名称作为键名。比如 'wp_head', 'the_content', 'save_post' 等等。 每个钩子名称对应的值,是一个数组,包含了所有注册到该钩子的函数信息。
  • 第二层 (Key: $priority): 以优先级作为键名。比如 1, 5, 10, 15 等等。 每个优先级对应的值,又是一个数组,包含了所有具有相同优先级的函数信息。
  • 第三层 (Key: $idx): 这是一个唯一的ID,用于标识注册的函数。这个ID是通过 _wp_filter_build_unique_id() 函数生成的,确保即使同一个函数多次注册到同一个钩子,也能被区分开。
  • 第四层 (Value: Array): 这是一个数组,包含了两个关键信息:
    • 'function': 你要执行的函数或方法。
    • 'accepted_args': 你的函数期望接收的参数数量。

用表格来表示,会更清晰:

层次 键 (Key) 值 (Value)
第一层 $hook Array (包含所有注册到该钩子的函数信息)
第二层 $priority Array (包含所有具有相同优先级的函数信息)
第三层 $idx Array (包含了函数信息)
第四层 'function' callable (要执行的函数或方法)
第四层 'accepted_args' int (函数期望接收的参数数量)

3. _wp_filter_build_unique_id():生成唯一ID的幕后英雄

我们再来看看_wp_filter_build_unique_id()这个函数,它负责为每个注册的函数生成一个唯一的ID。

// wp-includes/plugin.php

function _wp_filter_build_unique_id( $hook_name, $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, '' );
    } 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];
    } elseif ( is_string( $function_to_add[0] ) ) {
        return $function_to_add[0] . '::' . $function_to_add[1];
    }
}

这个函数的作用是:

  • 如果 $function_to_add 是一个字符串(函数名),直接返回函数名作为ID。
  • 如果 $function_to_add 是一个对象(通常是闭包),使用 spl_object_hash() 函数生成一个唯一的哈希值作为ID。
  • 如果 $function_to_add 是一个数组(通常是类的方法),将类名和方法名拼接起来作为ID。

4. 案例分析:理解 $wp_filter 的数据结构

为了更好地理解 $wp_filter 的数据结构,我们来看一个例子。假设我们有以下代码:

add_action( 'wp_head', 'my_wp_head_function', 10 );
add_action( 'wp_head', 'another_wp_head_function', 5 );
add_action( 'wp_footer', 'my_wp_footer_function', 10 );

执行完这些代码后,$wp_filter 数组的结构大概是这样的(简化版):

$wp_filter = array(
    'wp_head' => array(
        5 => array(
            'another_wp_head_function' => array(
                'function' => 'another_wp_head_function',
                'accepted_args' => 1
            )
        ),
        10 => array(
            'my_wp_head_function' => array(
                'function' => 'my_wp_head_function',
                'accepted_args' => 1
            )
        )
    ),
    'wp_footer' => array(
        10 => array(
            'my_wp_footer_function' => array(
                'function' => 'my_wp_footer_function',
                'accepted_args' => 1
            )
        )
    )
);

可以看到,$wp_filter 数组的键名是钩子的名称('wp_head', 'wp_footer'),每个钩子名称对应的值是一个数组,包含了所有注册到该钩子的函数信息。在 'wp_head' 钩子下,我们有两个优先级:510。优先级 5 对应的是 another_wp_head_function 函数,优先级 10 对应的是 my_wp_head_function 函数。

5. 参数数量 $accepted_args 的作用

$accepted_args 参数指定了你的函数期望接收的参数数量。 这个参数在 WordPress 调用你的函数时会用到。 WordPress 会根据这个值,传递相应数量的参数给你的函数。

例如,如果你注册一个 filter 如下:

add_filter( 'the_content', 'my_content_filter', 10, 2 );

function my_content_filter( $content, $post_id ) {
  // ...
  return $content;
}

那么,WordPress 在调用 my_content_filter 函数时,会传递两个参数:$content$post_id

如果 $accepted_args 的值大于实际传递给钩子的参数数量,你的函数将收到 NULL 值作为额外的参数。 如果 $accepted_args 的值小于实际传递给钩子的参数数量,你的函数将忽略多余的参数。

6. 如何利用 $wp_filter 调试

了解了 $wp_filter 的结构,我们就可以利用它来进行调试,查看某个钩子上注册了哪些函数,以及它们的优先级。

你可以使用以下代码来打印 $wp_filter 数组:

global $wp_filter;
echo '<pre>';
print_r( $wp_filter );
echo '</pre>';

将这段代码放在你的主题或插件中,刷新页面,你就可以看到 $wp_filter 数组的内容了。

7. remove_action()remove_filter():钩子的卸载工

既然有注册钩子的函数,自然也有卸载钩子的函数,那就是remove_action()remove_filter()

function remove_action( $hook_name, $function_to_be_removed, $priority = 10 ) {
    return remove_filter( $hook_name, $function_to_be_removed, $priority );
}

function remove_filter( $hook_name, $function_to_be_removed, $priority = 10 ) {
    global $wp_filter, $merged_filters;

    $idx = _wp_filter_build_unique_id( $hook_name, $function_to_be_removed, $priority );

    unset( $wp_filter[ $hook_name ][ $priority ][ $idx ] );

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

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

    unset( $merged_filters[ $hook_name ] );

    return true;
}

这两个函数的作用是从 $wp_filter 数组中移除指定的函数。它们接收的参数和 add_action()add_filter() 类似:

  • $hook_name: 要移除的钩子的名称。
  • $function_to_be_removed: 要移除的函数或方法。
  • $priority: 优先级。

remove_filter() 函数会根据这些参数,在 $wp_filter 数组中找到对应的函数,并将其移除。

8. has_action()has_filter():钩子的侦察兵

有时候,我们需要知道某个钩子上是否已经注册了函数,这时就可以使用 has_action()has_filter() 函数。

function has_action( $hook_name, $function_to_check = false ) {
    return has_filter( $hook_name, $function_to_check );
}

function has_filter( $hook_name, $function_to_check = false ) {
    global $wp_filter;

    $has = false;

    if ( isset( $wp_filter[ $hook_name ] ) ) {
        if ( false === $function_to_check ) {
            $has = ! empty( $wp_filter[ $hook_name ] );
        } else {
            $idx = _wp_filter_build_unique_id( $hook_name, $function_to_check, false );
            foreach ( (array) array_keys( $wp_filter[ $hook_name ] ) as $priority ) {
                if ( isset( $wp_filter[ $hook_name ][ $priority ][ $idx ] ) ) {
                    $has = true;
                    break;
                }
            }
        }
    }

    return $has;
}

这两个函数的作用是:

  • 如果只传递 $hook_name 参数,它们会检查该钩子上是否注册了任何函数。
  • 如果传递了 $hook_name$function_to_check 参数,它们会检查该钩子上是否注册了指定的函数。

9. do_action()apply_filters():钩子的执行者

注册了钩子,最终目的是要执行它们。 do_action()apply_filters() 就是负责执行钩子的函数。

// wp-includes/plugin.php

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

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

    // Do 'all' actions first
    if ( isset( $wp_filter['all'] ) ) {
        $wp_current_filter[] = $hook;
        _wp_call_all_hook( $args );
    }

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

    if ( ! isset( $merged_filters[ $hook ] ) ) {
        ksort( $wp_filter[ $hook ] );
        $merged_filters[ $hook ] = true;
    }

    reset( $wp_filter[ $hook ] );

    if ( empty( $args ) ) {
        $args = array('');
    }

    do {
        foreach ( (array) current( $wp_filter[ $hook ] ) as $the_ )
            if ( !is_null( $the_['function'] ) )
                call_user_func_array( $the_['function'], $args );

    } while ( next( $wp_filter[ $hook ] ) !== false );

    array_pop( $wp_current_filter );
}

function apply_filters( $hook, $value, ...$args ) {
    global $wp_filter, $merged_filters, $wp_current_filter;

    $wp_current_filter[] = $hook;

    if ( isset( $wp_filter['all'] ) ) {
        $wp_current_filter[] = $hook;
        _wp_call_all_hook( array_merge( array( $value ), $args ) );
    }

    if ( ! isset( $wp_filter[ $hook ] ) ) {
        array_pop( $wp_current_filter );
        return $value;
    }

    if ( ! isset( $merged_filters[ $hook ] ) ) {
        ksort( $wp_filter[ $hook ] );
        $merged_filters[ $hook ] = true;
    }

    reset( $wp_filter[ $hook ] );

    do {
        foreach ( (array) current( $wp_filter[ $hook ] ) as $the_ ) {
            if ( !is_null( $the_['function'] ) ) {
                $value = call_user_func_array( $the_['function'], array_merge( array( $value ), $args ) );
            }
        }
    } while ( next( $wp_filter[ $hook ] ) !== false );

    array_pop( $wp_current_filter );

    return $value;
}

它们的主要区别是:

  • do_action(): 执行一个动作,不返回任何值。通常用于在某个事件发生时,执行一些操作。
  • apply_filters(): 应用一个过滤器,返回修改后的值。通常用于修改某个变量的值。

这两个函数都会从 $wp_filter 数组中获取所有注册到指定钩子的函数,并按照优先级顺序执行它们。

10. 总结:钩子的本质

总的来说,WordPress 的 action 和 filter 机制,本质上是一种事件驱动的设计模式。 通过 add_action()add_filter(),我们可以将自己的代码注册到 WordPress 的特定事件(钩子)上。 当这些事件发生时,WordPress 会自动执行我们注册的代码。

$wp_filter 数组是这个机制的核心数据结构,它存储了所有已注册的钩子和函数信息。 理解了 $wp_filter 的结构,我们就能更好地理解 WordPress 的插件机制,编写更高效、更可靠的插件。

好了,今天的讲座就到这里。希望大家对 WordPress 的 add_action()add_filter() 函数有了更深入的了解。 课后记得复习,下次考试要考的哦!

(鞠躬)

发表回复

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