详解 WordPress `remove_action()` 和 `remove_filter()` 函数源码:它们如何从 `$wp_filter` 数组中移除钩子,并解释其在卸载插件时的作用。

咳咳,同学们,老司机我来啦!今天咱们不飙车,咱们来扒一扒 WordPress 源码里两个很重要的函数:remove_action()remove_filter()。这俩兄弟长得像,作用也差不多,都是用来“卸载”挂载在 WordPress 钩子上的自定义函数。

咱们都知道,WordPress 的强大之处在于它的钩子系统。开发者可以通过 add_action()add_filter() 这俩好基友,在 WordPress 的各个关键节点“挂”上自己的函数,从而改变 WordPress 的默认行为。但是,如果想“卸”掉这些挂上去的函数,就需要 remove_action()remove_filter() 这俩兄弟出马了。

这俩函数的核心功能都是从全局变量 $wp_filter 数组中移除相应的钩子。 $wp_filter 是 WordPress 存储所有已注册的 action 和 filter 的大本营,理解了 $wp_filter 的结构,就掌握了 remove_action()remove_filter() 的精髓。

第一部分:$wp_filter 的内部结构

$wp_filter 是一个全局数组,它的结构有点像一个多维哈希表。咱们用表格的形式来更清晰地展示它的结构:

键 (Key) 值 (Value)
钩子名称 (Tag) 一个数组,该数组的键是优先级 (Priority),值又是一个数组。
优先级 (Priority) 一个数组,该数组的键是函数的“标识符” (Function Identifier),值是一个包含钩子函数信息(函数名、接受的参数数量)的数组。
函数标识符 (Function Identifier) 一个数组,包含了以下信息: 'function' => 回调函数 (Callback Function), 'accepted_args' => 接受的参数数量 (Accepted Arguments)

举个例子,假设咱们用 add_action('wp_head', 'my_custom_function', 10, 2) 挂载了一个函数 my_custom_functionwp_head 钩子上,优先级是 10,接受 2 个参数。那么,$wp_filter 数组中就会有类似这样的结构:

$wp_filter['wp_head'][10]['my_custom_function'] = array(
    'function' => 'my_custom_function',
    'accepted_args' => 2,
);

注意,这里的 my_custom_function 只是一个简化的示意,实际上,'my_custom_function' 可能是字符串函数名,也可能是一个数组,包含对象和方法名(比如 array( $this, 'my_method' )),或者是一个闭包函数。关键是,这个键能唯一标识这个钩子函数。

函数标识符的生成:_wp_filter_build_unique_id()

WordPress 使用 _wp_filter_build_unique_id() 函数来生成这个“函数标识符”。这个函数会根据回调函数的类型,生成不同的标识符。

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];
    }
}

这段代码做了什么?

  1. 如果是字符串: 如果 $function_to_add 就是一个简单的函数名(字符串),那么直接返回这个函数名。
  2. 如果是对象: 如果 $function_to_add 是一个对象(通常是闭包函数),把它转换成数组形式 array( $object, '' )
  3. 如果是数组: 如果 $function_to_add 是一个数组,检查数组的第一个元素是否是对象或字符串。
    • 如果是对象: 使用 spl_object_hash() 获取对象的唯一哈希值,然后和数组的第二个元素(通常是方法名)拼接起来。
    • 如果是字符串: 将数组的第一个元素(类名)和第二个元素(方法名)用 :: 连接起来。

总之,_wp_filter_build_unique_id() 的目标就是生成一个唯一的字符串,用来标识这个钩子函数。

第二部分:remove_action()remove_filter() 的源码剖析

咱们先来看 remove_action() 的源码(remove_filter() 的逻辑几乎一样,只是名字不同):

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

你看,remove_action() 实际上就是调用了 remove_filter()。这俩函数在 WordPress 内部是用一套代码实现的。

现在,咱们来看 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 ] );
        if ( empty( $wp_filter[ $tag ][ $priority ] ) ) {
            unset( $wp_filter[ $tag ][ $priority ] );
        }
        if ( empty( $wp_filter[ $tag ] ) ) {
            unset( $wp_filter[ $tag ] );
        }
        $r = true;
    }

    return $r;
}

这段代码做了什么?

  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_remove, $priority ); 使用 _wp_filter_build_unique_id() 函数,根据传入的 $tag(钩子名称),$function_to_remove(要移除的函数),$priority(优先级)生成函数标识符 $idx
  3. 检查是否存在: if ( isset( $wp_filter[ $tag ][ $priority ][ $idx ] ) ) { ... } 检查 $wp_filter 数组中是否存在要移除的钩子。
  4. 移除钩子:
    • unset( $wp_filter[ $tag ][ $priority ][ $idx ] ); 如果存在,就用 unset() 函数从 $wp_filter 数组中移除这个钩子。
    • if ( empty( $wp_filter[ $tag ][ $priority ] ) ) { unset( $wp_filter[ $tag ][ $priority ] ); } 如果移除后,这个优先级下没有任何钩子了,就把这个优先级也从 $wp_filter 数组中移除。
    • if ( empty( $wp_filter[ $tag ] ) ) { unset( $wp_filter[ $tag ] ); } 如果移除后,这个钩子名称下没有任何优先级了,就把这个钩子名称也从 $wp_filter 数组中移除。
  5. 返回结果: $r = true; 设置 $rtrue,表示成功移除了钩子。
  6. 返回移除结果: return $r; 返回 $r,告诉调用者是否成功移除了钩子。

重点:函数标识符必须匹配!

remove_action()remove_filter() 能否成功移除钩子,关键在于你提供的 $function_to_remove 参数,经过 _wp_filter_build_unique_id() 函数处理后,生成的标识符必须和 add_action()add_filter() 时生成的标识符完全一致!

这意味着:

  • 如果 add_action()add_filter() 时,$function_to_add 是一个简单的函数名(字符串),那么 remove_action()remove_filter() 时,$function_to_remove 也必须是相同的函数名(字符串)。
  • 如果 add_action()add_filter() 时,$function_to_add 是一个数组(比如 array( $this, 'my_method' )),那么 remove_action()remove_filter() 时,$function_to_remove 也必须是完全相同的数组。
  • 如果 add_action()add_filter() 时,$function_to_add 是一个闭包函数,那么 remove_action()remove_filter() 几乎不可能移除它!因为闭包函数的对象哈希值是动态生成的,你很难在移除的时候获得相同的哈希值。

第三部分:remove_action()remove_filter() 在插件卸载时的作用

当一个插件被卸载时,它应该清理掉它注册的所有 action 和 filter,以避免对 WordPress 的其他部分产生影响。这就是 remove_action()remove_filter() 发挥作用的地方。

插件通常会在卸载函数中调用 remove_action()remove_filter(),将插件注册的所有钩子移除。

例如,如果你的插件在 wp_head 钩子上挂载了一个函数 my_plugin_function,那么在插件的卸载函数中,你应该这样写:

function my_plugin_uninstall() {
    remove_action( 'wp_head', 'my_plugin_function' );
}
register_uninstall_hook( __FILE__, 'my_plugin_uninstall' );

register_uninstall_hook() 函数是 WordPress 提供的一个函数,用于在插件卸载时执行指定的函数。

为什么插件卸载时需要移除钩子?

如果不移除钩子,即使插件被卸载了,挂载在钩子上的函数仍然会执行!这会导致:

  • 错误: 如果钩子函数依赖于插件中的代码,而插件已经被卸载了,那么钩子函数就会报错。
  • 性能问题: 即使钩子函数没有报错,它仍然会占用服务器资源,影响 WordPress 的性能。
  • 安全问题: 在某些情况下,残留的钩子函数可能会被恶意利用,导致安全问题。

第四部分:remove_all_actions()remove_all_filters()

WordPress 还提供了 remove_all_actions()remove_all_filters() 函数,用于移除指定钩子上的所有 action 和 filter。这两个函数比较暴力,慎用!

咱们来看 remove_all_actions() 的源码:

function remove_all_actions( $tag, $priority = false ) {
    global $wp_filter;

    if ( isset( $wp_filter[ $tag ] ) ) {
        if ( false !== $priority && isset( $wp_filter[ $tag ][ $priority ] ) ) {
            unset( $wp_filter[ $tag ][ $priority ] );
        } else {
            unset( $wp_filter[ $tag ] );
        }
    }
}

这段代码做了什么?

  1. 检查钩子是否存在: if ( isset( $wp_filter[ $tag ] ) ) { ... } 检查 $wp_filter 数组中是否存在指定的钩子。
  2. 移除所有 action:
    • 如果指定了优先级: if ( false !== $priority && isset( $wp_filter[ $tag ][ $priority ] ) ) { unset( $wp_filter[ $tag ][ $priority ] ); } 如果指定了优先级,就只移除该优先级下的所有 action。
    • 如果没有指定优先级: else { unset( $wp_filter[ $tag ] ); } 如果没有指定优先级,就移除该钩子下的所有 action(包括所有优先级)。

remove_all_filters() 函数的逻辑和 remove_all_actions() 几乎一样,只是名字不同。

总结

remove_action()remove_filter() 是 WordPress 中用于移除钩子的重要函数。理解 $wp_filter 数组的结构,以及 _wp_filter_build_unique_id() 函数如何生成函数标识符,是掌握这两个函数的关键。在插件卸载时,务必使用 remove_action()remove_filter() 清理掉插件注册的所有钩子,以避免对 WordPress 产生不良影响。

记住,移除钩子的时候,一定要确保提供的参数(特别是 $function_to_remove)和 add_action()add_filter() 时使用的参数完全一致。否则,你就白费力气了!

好了,今天的课就上到这里。同学们,下课!

发表回复

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