剖析 `remove_action()` 和 `remove_filter()` 函数的源码,它们如何从 `Array` 数组中删除钩子?

各位观众,掌声在哪里?! 今天咱们不聊八卦,就聊聊WordPress里面两个看似不起眼,实则非常重要的函数:remove_action()remove_filter()。 别看它们名字简单,背后隐藏的可是WordPress钩子机制的核心逻辑。 咱们今天就来扒一扒它们的源码,看看它们是怎么在浩瀚的钩子数组中,精准定位并移除我们指定的钩子的。

开场白:钩子是个啥?

在深入源码之前,先简单回顾一下钩子是什么。 想象一下,WordPress的代码就像一条河流,而钩子就像河流上的码头。 你可以在特定的码头上(也就是特定的 action 或 filter )停靠你的小船(你的自定义函数),对河流上的船只(WordPress的执行流程)进行一些操作。

add_action()add_filter() 就是负责建造这些码头的工具,而 remove_action()remove_filter() 则是拆除码头的工具。

源码剖析:remove_action()remove_filter()

实际上,remove_action()remove_filter() 本质上做的事情是一样的,它们都调用了同一个核心函数:remove_filter() (是的,你没看错,remove_action也调用remove_filter!)。

为什么呢? 因为在WordPress的底层,action 和 filter 都是通过同一个全局变量 $wp_filter 来管理的。 让我们来看看 remove_action() 的源码:

function remove_action( $tag, $function_to_remove, $priority = 10 ) {
    return remove_filter( $tag, $function_to_remove, $priority );
}

瞧,是不是很简单粗暴? remove_action() 只是 remove_filter() 的一个别名,负责处理 action 类型的钩子移除。

现在,重点来了,让我们深入 remove_filter() 的源码:

function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
    global $wp_filter, $merged_filters, $wp_current_filter;

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

    $r = false;
    if ( isset( $wp_filter[ $tag ][ $priority ][ $idx ] ) ) {
        unset( $wp_filter[ $tag ][ $priority ][ $idx ] );
        $r = true;
        if ( ! empty( $wp_current_filter ) ) {
            $wp_current_filter = array_diff( $wp_current_filter, array( $tag ) );
        }
    }

    return $r;
}

源码精简,但信息量巨大。 让我们逐行分析:

  1. global $wp_filter, $merged_filters, $wp_current_filter;

    • $wp_filter: 这是最重要的变量,它是一个全局数组,存储了所有的 action 和 filter 。它的结构大概是这样的:
    $wp_filter = array(
        'tag_name' => array( // 钩子的名称 (例如:'wp_head', 'the_content')
            priority_number => array( // 优先级 (例如: 10, 20, 5)
                'unique_id' => array( // 钩子的唯一ID
                    'function' => 'callback_function', // 回调函数
                    'accepted_args' => number_of_arguments // 回调函数接受的参数数量
                ),
                'unique_id2' => array(
                    'function' => 'another_callback_function',
                    'accepted_args' => number_of_arguments
                )
            ),
            priority_number2 => array(
                // ... 更多优先级和钩子
            )
        ),
        'another_tag_name' => array(
            // ... 更多钩子
        )
    );
    • $merged_filters: 这个变量用于缓存已经合并过的过滤器。 WordPress会把所有相同 tag 和 priority 的 filter 合并到一个数组中,提高执行效率。
    • $wp_current_filter: 这个变量记录了当前正在执行的 filter 的 tag 名称。 用于避免循环调用。
  2. $idx = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );

    • 这行代码调用了 _wp_filter_build_unique_id() 函数来生成一个唯一的 ID。 这个 ID 是用来在 $wp_filter 数组中精准定位我们要移除的钩子的关键。

    让我们看看 _wp_filter_build_unique_id() 的源码:

    function _wp_filter_build_unique_id( $tag, $function_to_remove, $priority ) {
        static $filter_id_count = 0;
    
        if ( is_string( $function_to_remove ) ) {
            return $function_to_remove;
        }
    
        if ( is_object( $function_to_remove ) ) {
            // Closures are currently implemented as objects
            $function_to_remove = array( $function_to_remove, '' );
        } else {
            $function_to_remove = (array) $function_to_remove;
        }
    
        if ( is_object( $function_to_remove[0] ) ) {
            // Object Class Calling
            return spl_object_hash( $function_to_remove[0] ) . $function_to_remove[1];
        } elseif ( is_string( $function_to_remove[0] ) ) {
            // Static Calling
            return $function_to_remove[0] . '::' . $function_to_remove[1];
        }
    }

    这个函数的作用是根据传入的 $tag (钩子名称), $function_to_remove (要移除的函数), 和 $priority (优先级) 生成一个唯一的 ID。 它的逻辑是这样的:

    • 如果 $function_to_remove 是一个字符串 (函数名),则直接返回这个字符串作为 ID。
    • 如果 $function_to_remove 是一个对象 (例如:闭包),则使用 spl_object_hash() 函数生成一个对象的唯一哈希值,并将其与方法名连接起来作为 ID。
    • 如果 $function_to_remove 是一个数组 (例如:array( $object, 'method' ) 表示对象的方法,或者 array( 'Class', 'staticMethod' ) 表示静态方法),则根据不同的情况生成相应的 ID。

    表格总结 _wp_filter_build_unique_id() 的返回值

    $function_to_remove 类型 返回值
    字符串 (函数名) 函数名
    对象 (闭包) spl_object_hash($object)
    数组 ( array( $object, 'method' ) ) spl_object_hash($object) . 'method'
    数组 ( array( 'Class', 'staticMethod' ) ) 'Class::staticMethod'

    重要提示: 这就是为什么在移除钩子时,你需要提供完全一致的函数名或对象引用。 如果你提供的函数名或对象引用与添加钩子时不一样,_wp_filter_build_unique_id() 生成的 ID 也会不一样,remove_filter() 就无法找到要移除的钩子。

  3. if ( isset( $wp_filter[ $tag ][ $priority ][ $idx ] ) ) { ... }

    • 这行代码检查 $wp_filter 数组中是否存在我们要移除的钩子。 它使用 $tag (钩子名称), $priority (优先级), 和 $idx (唯一ID) 来构建数组的索引,精准定位到要移除的钩子。
  4. unset( $wp_filter[ $tag ][ $priority ][ $idx ] );

    • 如果找到了要移除的钩子,这行代码就使用 unset() 函数将其从 $wp_filter 数组中移除。
  5. $r = true;

    • 设置 $rtrue,表示成功移除了钩子。
  6. if ( ! empty( $wp_current_filter ) ) { $wp_current_filter = array_diff( $wp_current_filter, array( $tag ) ); }

    • 这部分代码用于维护 $wp_current_filter 变量。 如果当前正在执行的 filter 中包含了我们要移除的 $tag,则将其从 $wp_current_filter 数组中移除。 这主要是为了防止循环调用。
  7. return $r;

    • 返回 $r,表示是否成功移除了钩子 (true 表示成功,false 表示失败)。

举例说明:

假设我们有以下代码:

function my_custom_function( $content ) {
    return $content . ' This is added by my custom function.';
}

add_filter( 'the_content', 'my_custom_function', 10 );

这段代码向 the_content 过滤器添加了一个名为 my_custom_function 的函数,优先级为 10。

要移除这个钩子,我们需要使用 remove_filter() 函数:

remove_filter( 'the_content', 'my_custom_function', 10 );

当执行 remove_filter() 时,它会:

  1. 使用 _wp_filter_build_unique_id() 函数生成 ID。 在这个例子中,ID 就是字符串 'my_custom_function'
  2. $wp_filter 数组中查找 ['the_content'][10]['my_custom_function']
  3. 如果找到了这个元素,就使用 unset() 函数将其移除。

容易犯的错误:

  • 函数名不一致: 如果你在添加钩子时使用了 'my_custom_function',但在移除钩子时使用了 'My_Custom_Function' (大小写不一致),remove_filter() 将无法找到要移除的钩子。
  • 对象引用不一致: 如果你在添加钩子时使用了 $object->method,但在移除钩子时创建了一个新的 $object 实例并尝试移除钩子,remove_filter() 也将无法找到要移除的钩子。因为两个 $object 实例的 spl_object_hash() 值不同.
  • 优先级错误: 优先级必须与添加钩子时使用的优先级完全一致。
  • 移除不存在的钩子: 如果你尝试移除一个根本不存在的钩子,remove_filter() 会返回 false,但不会报错。

高级技巧:移除匿名函数 (闭包)

移除匿名函数 (闭包) 稍微复杂一点,因为你不能直接使用函数名来移除。 你需要保存闭包的引用,并在移除钩子时使用相同的引用。

// 保存闭包的引用
$my_anonymous_function = function( $content ) {
    return $content . ' This is added by an anonymous function.';
};

add_filter( 'the_content', $my_anonymous_function, 10 );

// 移除闭包
remove_filter( 'the_content', $my_anonymous_function, 10 );

在这个例子中,我们把闭包赋值给变量 $my_anonymous_function,然后在添加和移除钩子时都使用这个变量。 这样,remove_filter() 才能正确地找到并移除闭包。

表格总结 remove_action()remove_filter() 的使用要点

要点 说明
函数名/对象引用 必须与添加钩子时使用的函数名或对象引用完全一致 (大小写、对象实例等)。
优先级 必须与添加钩子时使用的优先级完全一致
作用域 确保在移除钩子时, $wp_filter 变量已经初始化 (例如:在 plugins_loaded action 之后)。
匿名函数 (闭包) 需要保存闭包的引用,并在添加和移除钩子时使用相同的引用。
返回值 true 表示成功移除了钩子, false 表示没有找到要移除的钩子。
调试 如果移除钩子失败,可以使用 global $wp_filter; var_dump( $wp_filter ); 来查看 $wp_filter 数组的内容,检查是否存在你要移除的钩子,以及函数名、优先级等是否正确。

结论:

remove_action()remove_filter() 是WordPress插件开发中非常重要的工具。 它们允许你移除已经添加的钩子,从而修改 WordPress 的默认行为。 理解它们的源码和使用方法,可以帮助你更有效地控制 WordPress 的执行流程,并编写更灵活、更强大的插件。

希望这次的源码剖析对大家有所帮助! 记住,编程就像侦探破案,细节决定成败! 感谢大家的观看,下次再见!

发表回复

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