WordPress钩子机制中add_filter与add_action底层实现的差异解析

WordPress 钩子机制:add_filter 与 add_action 的底层实现差异解析

大家好,今天我们来深入探讨 WordPress 钩子机制中 add_filteradd_action 这两个核心函数的底层实现差异。虽然它们在使用方式上非常相似,但理解它们在内部如何运作,对于编写高效、可维护的 WordPress 代码至关重要。

1. 钩子机制概述

在深入研究 add_filteradd_action 之前,我们先简单回顾一下 WordPress 的钩子机制。钩子机制允许开发者在 WordPress 的核心代码执行流程中插入自定义代码,而无需直接修改核心文件。这极大地提高了 WordPress 的可扩展性和灵活性。

钩子分为两种类型:

  • Action (动作): 允许在特定事件发生时执行自定义代码。例如,在文章发布后、主题初始化时等等。
  • Filter (过滤器): 允许修改数据。例如,修改文章标题、内容、评论等等。

2. 核心数据结构:$wp_filter 全局变量

$wp_filter 是 WordPress 钩子机制的核心数据结构,它是一个全局变量,用于存储所有已注册的钩子。它是一个多维数组,其结构如下:

global $wp_filter;

// 示例结构
$wp_filter = [
    'hook_name' => [ // 钩子名称,例如 'the_title' 或 'wp_head'
        priority => [ // 优先级,整数,数值越小优先级越高
            'unique_id' => [ // 唯一ID,通常是函数名和类名组合
                'function' => 'callback_function', // 回调函数名称
                'accepted_args' => number_of_arguments // 回调函数接受的参数个数
            ],
            // 更多相同优先级的回调函数
        ],
        // 更多不同优先级的回调函数
    ],
    // 更多钩子
];

$wp_filter 数组的每一层含义如下:

  • hook_name: 钩子的名称,例如 the_title (过滤器) 或 wp_head (动作)。
  • priority: 钩子的优先级,数值越小,优先级越高。WordPress 会按照优先级从小到大的顺序执行回调函数。
  • unique_id: 钩子的唯一标识符,通常由函数名或类名生成,用于确保同一个回调函数不会被多次添加到同一个钩子上。
  • function: 回调函数,也就是当钩子被触发时要执行的函数。
  • accepted_args: 回调函数接受的参数个数。这很重要,因为 WordPress 会根据这个值传递相应的参数。

3. add_filter 函数的底层实现

add_filter 函数用于注册一个过滤器钩子。它的基本语法如下:

add_filter( string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1 );
  • $tag: 钩子的名称。
  • $function_to_add: 要执行的回调函数。
  • $priority: 优先级,默认为 10。
  • $accepted_args: 回调函数接受的参数个数,默认为 1。

下面是 add_filter 函数的简化版底层实现(基于 WordPress 6.4.2):

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

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

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

    unset( $merged_filters[ $tag ] ); // 确保下次过滤时重新排序

    return true;
}

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, '' );
    } 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];
    }
    return false;
}

代码解析:

  1. global $wp_filter, $merged_filters, $wp_current_filter;: 声明要使用的全局变量。$wp_filter 用于存储钩子信息,$merged_filters 用于缓存已排序的钩子列表,$wp_current_filter 用于跟踪当前正在执行的过滤器。
  2. $idx = _wp_filter_build_unique_id( $tag, $function_to_add, $priority );: 调用 _wp_filter_build_unique_id 函数生成回调函数的唯一 ID。这个ID用于防止同一个函数被多次添加到同一个钩子上。函数内部对不同类型的回调(字符串函数,对象方法,闭包)做了不同的处理,保证唯一性。
  3. $wp_filter[ $tag ][ $priority ][ $idx ] = array(...): 将回调函数的信息(函数本身和接受的参数个数)存储到 $wp_filter 数组中。以钩子名称 ($tag)、优先级 ($priority) 和唯一 ID ($idx) 作为键。
  4. unset( $merged_filters[ $tag ] ): 清除 $merged_filters 数组中与当前钩子相关的缓存。这是为了确保下次执行 apply_filters 时,钩子列表会被重新排序,以反映最新的优先级。
  5. _wp_filter_build_unique_id: 生成唯一 ID 的函数。它会根据回调函数的类型(字符串函数、对象方法、闭包等)生成不同的 ID。

4. apply_filters 函数的底层实现

apply_filters 函数用于触发一个过滤器钩子,并应用所有已注册的回调函数。它的基本语法如下:

apply_filters( string $tag, mixed $value, mixed ...$args );
  • $tag: 钩子的名称。
  • $value: 要过滤的值。
  • ...$args: 传递给回调函数的额外参数。

下面是 apply_filters 函数的简化版底层实现(基于 WordPress 6.4.2):

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

    $wp_current_filter[] = $tag;

    $args = array();
    $args[] = $value;
    for ( $i = 2; $i < func_num_args(); $i++ ) {
        $args[] = func_get_arg( $i );
    }

    $priority = has_filter( $tag );

    if ( ! $priority ) {
        array_pop( $wp_current_filter );
        return $value;
    }

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

    reset( $wp_filter[ $tag ] );

    do {
        foreach ( (array) current( $wp_filter[ $tag ] ) as $the_ ) {
            if ( ! is_null( $the_['function'] ) ) {
                $args[0] = call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) );
            }
        }
    } while ( next( $wp_filter[ $tag ] ) !== false );

    array_pop( $wp_current_filter );

    return $args[0];
}

代码解析:

  1. global $wp_filter, $merged_filters, $wp_current_filter;: 声明要使用的全局变量。
  2. $wp_current_filter[] = $tag;: 将当前正在执行的过滤器添加到 $wp_current_filter 数组中,用于防止循环调用。
  3. $args = array(); ...: 将传递给 apply_filters 的所有参数存储到 $args 数组中。第一个参数($value)是要过滤的值,后面的参数是传递给回调函数的额外参数。
  4. $priority = has_filter( $tag );: 检查是否存在与 $tag 相关的过滤器。如果不存在,直接返回 $value
  5. if ( ! isset( $merged_filters[ $tag ] ) ) { ... }: 如果 $merged_filters 数组中没有与当前钩子相关的缓存,则对 $wp_filter[$tag] 数组按照优先级进行排序,并将结果缓存到 $merged_filters 数组中。ksort 函数用于按照键(优先级)对数组进行排序。
  6. reset( $wp_filter[ $tag ] );: 将 $wp_filter[$tag] 数组的内部指针重置到第一个元素。
  7. do { ... } while ( next( $wp_filter[ $tag ] ) !== false );: 循环遍历 $wp_filter[$tag] 数组中的所有回调函数,并按照优先级顺序执行它们。
    • foreach ( (array) current( $wp_filter[ $tag ] ) as $the_ ) { ... }: 遍历当前优先级下的所有回调函数。
    • if ( ! is_null( $the_['function'] ) ) { ... }: 检查回调函数是否存在。
    • $args[0] = call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) );: 调用回调函数,并将 $args 数组中的前 accepted_args 个元素作为参数传递给它。call_user_func_array 函数用于动态调用函数。回调函数的返回值会被赋值给 $args[0],也就是 $value,这样就实现了过滤数据的效果。
  8. array_pop( $wp_current_filter );: 从 $wp_current_filter 数组中移除当前过滤器。
  9. return $args[0];: 返回经过所有回调函数过滤后的值。

5. add_action 函数的底层实现

add_action 函数用于注册一个动作钩子。它的基本语法如下:

add_action( string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1 );
  • $tag: 钩子的名称。
  • $function_to_add: 要执行的回调函数。
  • $priority: 优先级,默认为 10。
  • $accepted_args: 回调函数接受的参数个数,默认为 1。

add_action 函数的底层实现与 add_filter 函数非常相似。 实际上,add_action 只是 add_filter 的一个别名或者包装器。

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

核心区别:apply_filters vs do_action

add_actionadd_filter 最大的区别在于它们对应的触发函数:do_actionapply_filters。 虽然 add_action 本身只是 add_filter 的一个别名,但是 do_actionapply_filters 的行为却有着本质的不同。

6. do_action 函数的底层实现

do_action 函数用于触发一个动作钩子,并执行所有已注册的回调函数。它的基本语法如下:

do_action( string $tag, mixed ...$arg );
  • $tag: 钩子的名称。
  • ...$arg: 传递给回调函数的参数。

下面是 do_action 函数的简化版底层实现(基于 WordPress 6.4.2):

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

    $wp_current_filter[] = $tag;

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

    if ( empty( $wp_filter[ $tag ] ) ) {
        array_pop( $wp_current_filter );
        return;
    }

    $args = func_get_args();

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

    reset( $wp_filter[ $tag ] );

    do {
        foreach ( (array) current( $wp_filter[ $tag ] ) as $the_ ) {
            if ( ! is_null( $the_['function'] ) ) {
                call_user_func_array( $the_['function'], array_slice( $args, 1, (int) $the_['accepted_args'] ) );
            }
        }
    } while ( next( $wp_filter[ $tag ] ) !== false );

    array_pop( $wp_current_filter );
}

代码解析:

  1. global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;: 声明要使用的全局变量。$wp_actions 用于记录每个动作被执行的次数。
  2. if ( ! isset( $wp_actions[ $tag ] ) ) { ... }: 记录动作被执行的次数。
  3. if ( empty( $wp_filter[ $tag ] ) ) { ... }: 如果不存在与 $tag 相关的动作,直接返回。
  4. $args = func_get_args();: 将传递给 do_action 的所有参数存储到 $args 数组中。
  5. if ( ! isset( $merged_filters[ $tag ] ) ) { ... }: 如果 $merged_filters 数组中没有与当前钩子相关的缓存,则对 $wp_filter[$tag] 数组按照优先级进行排序,并将结果缓存到 $merged_filters 数组中。
  6. reset( $wp_filter[ $tag ] );: 将 $wp_filter[$tag] 数组的内部指针重置到第一个元素。
  7. do { ... } while ( next( $wp_filter[ $tag ] ) !== false );: 循环遍历 $wp_filter[$tag] 数组中的所有回调函数,并按照优先级顺序执行它们。
    • foreach ( (array) current( $wp_filter[ $tag ] ) as $the_ ) { ... }: 遍历当前优先级下的所有回调函数。
    • if ( ! is_null( $the_['function'] ) ) { ... }: 检查回调函数是否存在。
    • call_user_func_array( $the_['function'], array_slice( $args, 1, (int) $the_['accepted_args'] ) );: 调用回调函数,并将 $args 数组中的第 2 个元素开始的 accepted_args 个元素作为参数传递给它。注意,这里传递的是 $args[1] 开始的参数,因为 $args[0] 是动作的名称 $tag
  8. array_pop( $wp_current_filter );: 从 $wp_current_filter 数组中移除当前动作。

关键区别:

  • apply_filters 返回经过所有回调函数过滤后的值。
  • do_action 不返回任何值,它的目的是执行一系列操作。

7. 差异总结

特性 add_filter add_action
目的 修改数据 执行动作
触发函数 apply_filters do_action
返回值 返回经过所有回调函数处理后的值 无返回值
核心实现 将回调函数注册到 $wp_filter 数组中,以供 apply_filters 调用 将回调函数注册到 $wp_filter 数组中,以供 do_action 调用
本质 添加过滤器 添加动作(实际上是特殊的过滤器)

8. 示例代码

使用 add_filter 修改文章标题:

function my_custom_title( $title ) {
    return 'Custom Title: ' . $title;
}
add_filter( 'the_title', 'my_custom_title' );

// 在模板文件中:
echo apply_filters( 'the_title', get_the_title() ); // 输出:Custom Title: 原始文章标题

使用 add_actionwp_head 钩子上添加自定义代码:

function my_custom_header_code() {
    echo '<meta name="description" content="My custom description">';
}
add_action( 'wp_head', 'my_custom_header_code' );

// WordPress 核心代码中:
do_action( 'wp_head' ); // 执行所有注册到 wp_head 钩子上的函数

9. 理解内部机制才能写出更好的代码

通过上面的分析,我们可以看到 add_filteradd_action 的底层实现非常相似,它们都依赖于 $wp_filter 全局变量来存储钩子信息。 关键的区别在于它们对应的触发函数:apply_filtersdo_actionapply_filters 用于修改数据,而 do_action 用于执行动作。 理解这些差异有助于我们更好地使用 WordPress 的钩子机制,编写更高效、更可维护的代码。

希望今天的讲解对大家有所帮助。 谢谢!

发表回复

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