阐述 WordPress `add_action()` 和 `add_filter()` 函数源码:优先级的源码实现与执行顺序。

各位朋友们,晚上好!我是老码农,今天咱们不聊风花雪月,就聊聊WordPress里两个看似简单,实则暗藏玄机的函数:add_action()add_filter()。 这俩函数,就像是WordPress这艘大船上的铆钉,看似不起眼,却决定了整艘船的稳定性。 咱们今天就来拆解它们,看看它们是怎么运作的,尤其是关于优先级的实现和执行顺序。

第一部分:开胃小菜 – add_action()add_filter() 的基本概念

首先,我们要明白,add_action()add_filter()都是用来挂载钩子的。 啥是钩子?你可以把它想象成代码里的预留位置,允许你在特定的时间点或者事件发生时,执行你自定义的代码。

  • add_action() 用于在某个动作发生时执行你的函数。 比如,当文章发布时,你可以使用add_action('publish_post', 'my_custom_function');来执行my_custom_function。这个my_custom_function通常用于执行一些副作用,比如发送邮件、更新缓存等等。

  • add_filter() 用于修改某个数据。 比如,你想修改文章的内容,可以使用add_filter('the_content', 'my_content_filter');来执行my_content_filter。这个my_content_filter接收文章内容作为参数,修改后返回新的内容。

简单来说,add_action()是“做事情”,add_filter()是“改东西”。

第二部分:源码探秘 – add_action()add_filter() 的真面目

现在,让我们深入到WordPress的源码中,看看add_action()add_filter()到底是怎么实现的。 其实,它们最终都会调用同一个函数:add_filter()

// wp-includes/plugin.php

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()的一个马甲而已。 真正的核心逻辑都在add_filter()里。

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

这段代码看起来有点吓人,但别怕,我们一步一步来拆解它。

  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 );: 这行代码生成一个唯一的ID,用于标识这个特定的钩子。 这个ID的生成方式很重要,因为它决定了相同的函数是否可以多次添加到同一个钩子上。

  3. $wp_filter[ $tag ][ $priority ][ $idx ] = array(...);: 这行代码是关键。它将你的函数信息存储到$wp_filter数组中。 数组的结构是这样的:

    • $wp_filter: 一个关联数组,键是钩子的名称($tag)。
    • $wp_filter[$tag]: 一个关联数组,键是优先级($priority)。
    • $wp_filter[$tag][$priority]: 一个关联数组,键是唯一的ID($idx)。
    • $wp_filter[$tag][$priority][$idx]: 一个包含函数信息的数组,包括要执行的函数(function)和接受的参数个数(accepted_args)。
  4. unset( $merged_filters[ $tag ] );: 这行代码清除了$merged_filters中对应于当前钩子的缓存。 这是因为你刚刚添加了一个新的过滤器,所以需要重新合并过滤器列表。

  5. return true;: 函数执行成功,返回true

第三部分:优先级的奥秘 – 源码中的排序机制

重点来了! 优先级到底是怎么实现的? 答案就在$wp_filter数组的结构中。

WordPress在执行钩子时,会按照优先级从小到大遍历$wp_filter[$tag]数组。 也就是说,优先级数字越小,函数执行的越早。

让我们用一个表格来更直观地展示这个结构:

钩子名称 ($tag) 优先级 ($priority) 唯一ID ($idx) 函数信息
the_content 5 unique_id_1 array('function' => 'my_content_filter_early', 'accepted_args' => 1)
the_content 10 unique_id_2 array('function' => 'my_content_filter_normal', 'accepted_args' => 1)
the_content 10 unique_id_3 array('function' => 'my_content_filter_another', 'accepted_args' => 1)
the_content 15 unique_id_4 array('function' => 'my_content_filter_late', 'accepted_args' => 1)

在这个例子中,当执行the_content钩子时,函数会按照以下顺序执行:

  1. my_content_filter_early (优先级 5)
  2. my_content_filter_normal (优先级 10)
  3. my_content_filter_another (优先级 10)
  4. my_content_filter_late (优先级 15)

划重点: 如果多个函数具有相同的优先级,它们将按照添加的顺序执行。 这就是为什么my_content_filter_normal会在my_content_filter_another之前执行的原因。

第四部分:执行流程 – do_action()apply_filters() 的登场

既然我们已经了解了钩子的存储方式,那么接下来就要看看WordPress是如何执行这些钩子的。 这就要用到do_action()apply_filters()这两个函数。

  • do_action( $tag, ...$arg ) 用于触发一个动作。它会执行所有挂载到指定动作上的函数。

  • apply_filters( $tag, $value, ...$arg ) 用于应用过滤器。 它会遍历所有挂载到指定过滤器上的函数,并将 $value 作为参数传递给它们,最终返回修改后的 $value

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

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

    $wp_did_filter[ $tag ] ++;

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

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

    reset( $wp_filter[ $tag ] );

    if ( empty( $args ) ) {
        $args = array( $value );
    } else {
        array_unshift( $args, $value );
    }

    do {
        foreach ( (array) current( $wp_filter[ $tag ] ) as $the_ ) {
            if ( ! is_callable( $the_['function'] ) ) {
                continue;
            }

            $wp_current_filter[] = $tag;

            $args[0] = call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) );

            array_pop( $wp_current_filter );
        }

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

    return $args[0];
}

这段代码稍微有点长,我们挑几个关键部分来分析:

  1. if ( ! isset( $wp_filter[ $tag ] ) ) { return $value; }: 如果没有任何函数挂载到这个钩子上,直接返回原始值。

  2. if ( ! isset( $merged_filters[ $tag ] ) ) { ksort( $wp_filter[ $tag ] ); $merged_filters[ $tag ] = true; }: 如果这个钩子还没有被合并过,就按照优先级对$wp_filter[$tag]数组进行排序。 ksort()函数会按照键(也就是优先级)对数组进行排序。 排序完成后,将$merged_filters[$tag]设置为true,表示这个钩子已经被合并过了。

  3. reset( $wp_filter[ $tag ] );: 将数组的内部指针重置到第一个元素,以便从头开始遍历。

  4. array_unshift( $args, $value );: 将原始值 $value 添加到参数列表的开头。 这是因为过滤器函数通常需要接收原始值作为第一个参数。

  5. do { ... } while ( next( $wp_filter[ $tag ] ) !== false );: 循环遍历所有挂载到这个钩子上的函数。

  6. $args[0] = call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) );: 调用过滤器函数。 call_user_func_array()函数允许你使用一个数组作为参数来调用一个函数。 array_slice()函数用于提取参数数组的一部分,确保传递给过滤器函数的参数数量不超过其声明的accepted_args。 重点! 函数执行的结果会覆盖$args[0],也就是原始值 $value, 这样就实现了值的传递和修改。

  7. return $args[0];: 返回最终修改后的值。

do_action()的源码和apply_filters()类似,但它不会返回值,而是直接执行挂载的函数。

第五部分:实战演练 – 代码示例和最佳实践

理论讲了一大堆,现在让我们来几个实际的例子,看看如何使用add_action()add_filter()

示例 1:在文章发布后发送邮件

function send_email_after_publish( $post_id ) {
    $post = get_post( $post_id );
    $title = $post->post_title;
    $permalink = get_permalink( $post_id );

    $to = '[email protected]';
    $subject = 'New post published: ' . $title;
    $message = 'A new post has been published: ' . $title . "n" . $permalink;

    wp_mail( $to, $subject, $message );
}
add_action( 'publish_post', 'send_email_after_publish', 10, 1 );

这段代码会在文章发布后发送一封邮件给管理员。 publish_post是一个WordPress内置的动作钩子,当文章状态变为“已发布”时会被触发。

示例 2:修改文章标题

function modify_post_title( $title ) {
    return '【重要】' . $title;
}
add_filter( 'the_title', 'modify_post_title', 10, 1 );

这段代码会在文章标题前面加上“【重要】”前缀。 the_title是一个WordPress内置的过滤器钩子,用于修改文章标题。

最佳实践:

  • 使用命名空间和类: 为了避免函数名冲突,最好使用命名空间和类来组织你的代码。
  • 小心优先级: 选择合适的优先级,确保你的函数在正确的时间执行。
  • 考虑参数个数: 正确设置accepted_args参数,避免传递不必要的参数。
  • 移除钩子: 如果不再需要某个钩子,可以使用remove_action()remove_filter()来移除它。
  • 调试技巧: 可以使用has_action()has_filter()来检查某个钩子上是否挂载了函数。

第六部分:高级技巧 – 移除钩子和条件判断

有时候,你可能需要移除某个已经存在的钩子,或者根据一些条件来决定是否执行某个钩子。

移除钩子:

remove_action( 'publish_post', 'send_email_after_publish', 10 );
remove_filter( 'the_title', 'modify_post_title', 10 );

注意,移除钩子时,你需要提供相同的钩子名称、函数名和优先级。

条件判断:

function maybe_modify_content( $content ) {
    if ( is_single() ) {
        return $content . '<p>This is a single post.</p>';
    }
    return $content;
}
add_filter( 'the_content', 'maybe_modify_content', 10, 1 );

这段代码只有在文章是单篇文章页面时才会修改文章内容。 is_single()是一个WordPress提供的条件判断函数。

第七部分:总结 – add_action()add_filter() 的威力

add_action()add_filter()是WordPress插件开发的核心。 它们允许你以一种灵活的方式扩展和修改WordPress的功能。

记住以下几点:

  • add_action()用于执行动作,add_filter()用于修改数据。
  • 优先级决定了函数的执行顺序。
  • 使用do_action()apply_filters()来触发钩子。
  • 合理使用命名空间、类、移除钩子和条件判断。

掌握了这些知识,你就可以像一个真正的WordPress大师一样,驾驭钩子的力量,打造出强大的插件和主题!

好了,今天的讲座就到这里。 希望这些内容能帮助你更深入地理解add_action()add_filter()。 如果有任何问题,欢迎随时提问。 谢谢大家!

发表回复

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