详解 WordPress `add_action()` 与 `add_filter()` 函数源码:钩子函数如何存储在全局数组。

咳咳,各位同学,咱们今天上课,主题是 WordPress 钩子函数的大揭秘!

啥?你问我啥是钩子函数?简单来说,就是 WordPress 这位大厨在做菜(运行代码)的时候,预留了一些“钩子”给你,你可以用这些钩子来偷偷地加点你自己的调料(运行你自己的代码),改变菜的味道(修改 WordPress 的行为)。

add_action()add_filter() 就是你往这些钩子上挂调料包(函数)的工具!

今天,咱们就来扒一扒这两个函数的源码,看看 WordPress 到底是怎么把这些调料包(函数)存起来,又怎么在关键时刻把它们拿出来用的。准备好了吗?发车!

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

首先,我们要明确一点:add_action()add_filter() 这两个函数,虽然名字不一样,但本质上干的事情差不多。它们都是用来把你的函数注册到某个特定的“钩子”上。

  • add_action() 主要用于执行一些动作,比如在文章发布后发送邮件,或者在页面底部添加广告。它通常不期望你返回任何值。
  • add_filter() 主要用于修改数据,比如修改文章标题,或者过滤评论内容。它期望你返回修改后的数据。

实际上,在 WordPress 的核心代码里,add_action() 最终也是调用了 add_filter() 来实现的。所以,我们重点分析 add_filter(),搞懂了它,add_action() 自然也就明白了。

二、add_filter() 源码剖析:调料包登记处

让我们来看看 add_filter() 的源码(简化版):

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

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

    if ( isset( $wp_filter[ $tag ][ $priority ][ $idx ] ) ) {
        return;
    }

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

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

这段代码看起来有点吓人,但别怕,咱们一步步来分析。

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

    这行代码很重要!它声明了三个全局变量:$wp_filter$wp_actions$merged_filters。其中,$wp_filter 是我们今天的主角!它是一个多维数组,用来存储所有注册的过滤器(包括 actions)。$wp_actions 用来记录 action 触发的次数,$merged_filters 用于缓存已经合并过的过滤器。

    用人话说,$wp_filter 就像一个巨大的调料包登记处,每个钩子($tag)就是一个货架,每个优先级($priority)就是货架上的一层,每个调料包($function_to_add)都有自己的编号($idx)。

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

    这行代码调用了 _wp_filter_build_unique_id() 函数,生成一个唯一的 ID。这个 ID 的作用是防止同一个函数被重复注册到同一个钩子的同一个优先级上。

    _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];
        }
    }
  3. if ( isset( $wp_filter[ $tag ][ $priority ][ $idx ] ) ) { return; }

    这行代码检查一下,这个调料包是不是已经登记过了。如果登记过了,那就直接返回,不重复登记。

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

    这行代码是核心!它把调料包的信息存到了 $wp_filter 数组里。

    • $tag:钩子的名称,也就是货架的名称。
    • $priority:优先级,也就是货架的层数。数字越小,优先级越高,越早被执行。
    • $idx:调料包的唯一 ID,也就是货架上调料包的编号。
    • $function_to_add:你要执行的函数,也就是调料包的内容。
    • $accepted_args:这个函数接受的参数个数。

    所以,这行代码相当于在 $wp_filter 数组里创建了一个三维数组,把你的函数和参数信息存了进去。

  5. unset( $merged_filters[ $tag ] );

    这行代码的作用是清除 $merged_filters 数组中 $tag 对应的缓存。因为我们添加了一个新的过滤器,所以需要清除缓存,以便下次执行 apply_filters() 的时候重新合并过滤器。

三、$wp_filter 数组结构:调料包仓库的蓝图

现在,我们来仔细看看 $wp_filter 数组的结构。它是一个三维数组,结构如下:

$wp_filter = array(
    'hook_name' => array( // 钩子名称,例如 'the_title', 'wp_footer'
        priority => array( // 优先级,数字越小优先级越高,例如 10, 20, 1
            'unique_id' => array( // 函数的唯一 ID
                'function' => 'callable', // 要执行的函数,可以是函数名、数组(对象方法)、闭包
                'accepted_args' => int // 函数接受的参数个数
            ),
            'unique_id2' => array(
                'function' => 'callable',
                'accepted_args' => int
            ),
            // ... 更多函数
        ),
        priority2 => array(
            'unique_id' => array(
                'function' => 'callable',
                'accepted_args' => int
            ),
            // ... 更多函数
        ),
        // ... 更多优先级
    ),
    'hook_name2' => array(
        // ... 更多钩子
    )
);

这个数组结构非常重要,理解了它,你就理解了 WordPress 是如何管理和执行钩子函数的。

四、apply_filters()do_action():调料包的使用说明书

OK,我们已经学会了如何把调料包(函数)放到仓库($wp_filter)里。接下来,我们来看看 WordPress 是如何在需要的时候把这些调料包拿出来用的。

这两个函数分别是 apply_filters()do_action()

  • apply_filters() 用于执行过滤器,并返回修改后的值。
  • do_action() 用于执行动作,不返回值。

实际上,do_action() 内部也是调用了 apply_filters(),只是它忽略了返回值。

让我们来看看 apply_filters() 的源码(简化版):

function apply_filters( $tag, $value ) {
    global $wp_filter, $wp_actions, $merged_filters;

    $args = func_get_args();
    $hook_name = array_shift( $args );
    $value = array_shift( $args );

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

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

    reset( $wp_filter[ $hook_name ] );

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

    return $value;
}

这段代码的作用是:

  1. 获取参数: 获取钩子的名称 $tag 和要过滤的值 $value
  2. 检查钩子是否存在: 如果 $wp_filter 数组中不存在 $tag 对应的钩子,说明没有函数注册到这个钩子上,直接返回原始值 $value
  3. 合并过滤器: 如果 $merged_filters 数组中不存在 $tag 对应的缓存,说明这个钩子的过滤器还没有被合并过,需要对 $wp_filter[ $tag ] 数组按照优先级进行排序(ksort()),然后将 $merged_filters[ $tag ] 设置为 true,表示已经合并过了。
  4. 遍历过滤器: 遍历 $wp_filter[ $tag ] 数组,按照优先级从高到低的顺序,依次执行注册到这个钩子上的函数。
  5. 执行函数: 使用 call_user_func_array() 函数来调用注册的函数。call_user_func_array() 函数可以动态地调用函数,并传递参数。
  6. 更新值: 将函数的返回值作为新的 $value,传递给下一个函数。
  7. 返回最终值: 返回经过所有过滤器处理后的最终值 $value

五、代码示例:调料包的实战演练

光说不练假把式,咱们来写几个代码示例,演示一下 add_filter()apply_filters() 的用法。

示例 1:修改文章标题

// 定义一个函数,用于修改文章标题
function my_custom_title( $title ) {
    return '【重要】' . $title;
}

// 将 my_custom_title 函数注册到 the_title 钩子上,优先级为 10
add_filter( 'the_title', 'my_custom_title', 10 );

// 在模板文件中,使用 apply_filters() 函数来获取修改后的文章标题
$title = get_the_title();
$title = apply_filters( 'the_title', $title );
echo $title;

在这个例子中,我们定义了一个函数 my_custom_title(),用于在文章标题前面添加 【重要】 前缀。然后,我们使用 add_filter() 函数将这个函数注册到 the_title 钩子上,优先级为 10。最后,我们在模板文件中使用 apply_filters() 函数来获取修改后的文章标题,并输出到页面上。

示例 2:过滤评论内容

// 定义一个函数,用于过滤评论内容
function my_custom_comment_content( $content ) {
    $content = str_replace( '敏感词', '***', $content );
    return $content;
}

// 将 my_custom_comment_content 函数注册到 comment_text 钩子上,优先级为 10
add_filter( 'comment_text', 'my_custom_comment_content', 10 );

// 在模板文件中,使用 apply_filters() 函数来获取过滤后的评论内容
$comment_content = get_comment_text();
$comment_content = apply_filters( 'comment_text', $comment_content );
echo $comment_content;

在这个例子中,我们定义了一个函数 my_custom_comment_content(),用于将评论内容中的 敏感词 替换为 ***。然后,我们使用 add_filter() 函数将这个函数注册到 comment_text 钩子上,优先级为 10。最后,我们在模板文件中使用 apply_filters() 函数来获取过滤后的评论内容,并输出到页面上。

六、add_action() 的本质:一个特殊的 add_filter()

前面说过,add_action() 本质上也是调用了 add_filter() 来实现的。让我们来看看 add_action() 的源码:

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

可以看到,add_action() 函数只是简单地调用了 add_filter() 函数,并将参数原封不动地传递给它。

那么,do_action() 又是怎么实现的呢?

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

    if ( is_scalar( $tag ) ) {
        $wp_current_filter[] = $tag;
        if ( isset( $wp_actions[ $tag ] ) ) {
            ++$wp_actions[ $tag ];
        } else {
            $wp_actions[ $tag ] = 1;
        }
    }

    /**
     * Fires functions attached to a specific action hook.
     *
     * @since 1.2.0
     *
     * @param string $tag The name of the action to be fired.
     * @param mixed ...$arg Optional. Additional arguments which are passed on to the
     *                      functions hooked to the action.
     */
    $args = func_get_args();
    $hook_name = array_shift( $args );
    apply_filters( $hook_name, '', $args );

    array_pop( $wp_current_filter );
}

可以看到,do_action() 函数也是调用了 apply_filters() 函数,但是它传递给 apply_filters() 函数的 $value 参数是一个空字符串 ''。这意味着,do_action() 函数不期望过滤器返回任何值,它只关心过滤器是否被执行。

七、总结:调料包的奥秘

通过对 add_filter()apply_filters() 源码的分析,我们了解了 WordPress 钩子函数的实现机制:

  • WordPress 使用全局数组 $wp_filter 来存储所有注册的过滤器(包括 actions)。
  • add_filter() 函数将函数和参数信息存储到 $wp_filter 数组中。
  • apply_filters() 函数遍历 $wp_filter 数组,按照优先级从高到低的顺序执行注册的函数。
  • add_action() 函数只是一个特殊的 add_filter() 函数,它不期望过滤器返回任何值。
  • do_action() 函数只是一个特殊的 apply_filters() 函数,它传递给 apply_filters() 函数的 $value 参数是一个空字符串。

理解了这些,你就可以更好地利用 WordPress 的钩子函数机制,扩展 WordPress 的功能,定制 WordPress 的行为。

八、课后作业:调料包的自由搭配

  1. 尝试使用 add_filter() 函数修改文章内容,例如在文章内容中添加版权信息。
  2. 尝试使用 add_action() 函数在页面底部添加自定义的 HTML 代码。
  3. 研究 WordPress 官方文档,了解更多常用的钩子函数,并尝试使用它们。

好了,今天的课就上到这里,希望大家能够学有所获,早日成为 WordPress 调料包大师!下课!

发表回复

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