深入理解 `do_action()` 和 `apply_filters()` 函数的源码,解释它们在执行钩子时的具体流程。

大家好!今天咱们来聊聊 WordPress 钩子背后的秘密:do_action()apply_filters() 的源码大揭秘!

各位,有没有觉得 WordPress 插件和主题就像乐高积木一样,可以随意组合、扩展? 这都得归功于它强大的钩子(Hooks)机制。而 do_action()apply_filters() 就是操控这些钩子的关键“指挥官”。

今天,咱们就深入源码,看看这两位指挥官是如何“发号施令”,让各种函数像训练有素的士兵一样,在特定时刻执行任务的。 别怕,我会用最通俗易懂的方式,加上实战代码,保证你听完之后,也能成为钩子大师!

一、 钩子的概念:代码中的“预留插槽”

在深入源码之前,我们先温习一下钩子的概念。 可以把钩子想象成代码中的“预留插槽”, 允许插件或主题在不修改核心代码的情况下,插入自己的功能。

钩子分为两种类型:

  • 动作(Action): 允许你执行一些操作。 比如,在文章发布后发送邮件通知,或者在页面底部添加自定义内容。
  • 过滤器(Filter): 允许你修改数据。 比如,修改文章标题,或者过滤评论内容。

do_action() 用于触发动作,而 apply_filters() 用于应用过滤器。

二、 do_action() 的源码解析: 动作的“发令枪”

do_action() 函数的作用是执行与特定动作钩子关联的所有函数。 让我们一起看看它的源码(简化版):

/**
 * Execute functions hooked on a specific action hook.
 *
 * @since 1.5.0
 *
 * @param string $hook_name The name of the action to execute.
 * @param mixed  ...$arg    Optional. Additional arguments which are passed on to the
 *                           functions hooked to the action.
 */
function do_action( $hook_name, ...$arg ) {
    global $wp_filter, $wp_actions, $wp_current_filter;

    if ( ! isset( $wp_actions[ $hook_name ] ) ) {
        $wp_actions[ $hook_name ] = 1;
    } else {
        ++$wp_actions[ $hook_name ];
    }

    // Do 'all' actions first.
    if ( isset( $wp_filter['all'] ) ) {
        $wp_current_filter[] = $hook_name;
        _wp_call_all_hook( $arg );
    }

    if ( ! isset( $wp_filter[ $hook_name ] ) ) {
        if ( isset( $wp_filter['all'] ) ) {
            array_pop( $wp_current_filter );
        }
        return;
    }

    if ( ! is_array( $wp_filter[ $hook_name ]->callbacks ) ) {
        return;
    }

    if ( isset( $wp_filter['all'] ) ) {
        array_pop( $wp_current_filter );
    }

    reset( $wp_filter[ $hook_name ]->callbacks );

    foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $functions ) {
        ksort( $functions );
        foreach ( $functions as $function ) {
            $args = $arg;
            $num_args = count( $arg );

            if ( is_null( $function['function'] ) ) {
                continue;
            }

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

代码解读:

  1. 全局变量: $wp_filter 是一个全局数组,存储了所有已注册的钩子和它们关联的函数。 $wp_actions 记录了每个动作被触发的次数。 $wp_current_filter 是一个堆栈,记录了当前正在执行的钩子,用于防止循环调用。

  2. 统计动作触发次数:

    if ( ! isset( $wp_actions[ $hook_name ] ) ) {
        $wp_actions[ $hook_name ] = 1;
    } else {
        ++$wp_actions[ $hook_name ];
    }

    这段代码用于统计特定动作 $hook_name 被触发的次数,并存储在全局数组 $wp_actions 中。

    • if ( ! isset( $wp_actions[ $hook_name ] ) ): 检查 $wp_actions 数组中是否已经存在以 $hook_name 为键的元素。如果不存在,表示该动作第一次被触发。
    • $wp_actions[ $hook_name ] = 1;: 如果动作第一次被触发,则在 $wp_actions 数组中创建一个以 $hook_name 为键的元素,并将其值设置为 1,表示该动作被触发了一次。
    • else { ++$wp_actions[ $hook_name ]; }: 如果 $wp_actions 数组中已经存在以 $hook_name 为键的元素,则表示该动作之前已经被触发过。此时,将该键对应的值加 1,表示该动作又被触发了一次。

    这段代码的主要作用是记录每个动作被触发的次数,方便后续的调试和分析。 例如,可以用来判断某个动作是否被意外地多次触发,或者用于性能分析,找出触发次数最多的动作。

  3. 执行 ‘all’ 动作:

    if ( isset( $wp_filter['all'] ) ) {
        $wp_current_filter[] = $hook_name;
        _wp_call_all_hook( $arg );
    }

    这段代码检查是否存在名为 'all' 的钩子,如果存在,则将当前钩子 $hook_name 添加到 $wp_current_filter 堆栈中,然后调用 _wp_call_all_hook() 函数来执行与 'all' 钩子关联的所有函数。

    • if ( isset( $wp_filter['all'] ) ): 检查 $wp_filter 数组中是否存在以 'all' 为键的元素。如果存在,表示已经注册了与 'all' 钩子关联的函数。
    • $wp_current_filter[] = $hook_name;: 将当前正在执行的钩子 $hook_name 添加到 $wp_current_filter 堆栈中。$wp_current_filter 用于跟踪当前正在执行的钩子,防止循环调用。
    • _wp_call_all_hook( $arg );: 调用 _wp_call_all_hook() 函数来执行与 'all' 钩子关联的所有函数。$arg 是传递给这些函数的参数。

    'all' 钩子是一个特殊的钩子,任何动作被触发时,都会执行与 'all' 钩子关联的函数。这提供了一种全局监听所有动作的方式,可以用于日志记录、调试等目的。

    _wp_call_all_hook() 函数源码如下:

    /**
     * Calls the 'all' hook, which passes the arguments from the current filter.
     *
     * @since 2.5.0
     * @access private
     *
     * @param array $args The array of arguments passed into the filter.
     */
    function _wp_call_all_hook( $args ) {
        global $wp_filter;
    
        reset( $wp_filter['all']->callbacks );
    
        foreach ( $wp_filter['all']->callbacks as $priority => $functions ) {
            ksort( $functions );
            foreach ( $functions as $function ) {
                call_user_func_array( $function['function'], $args );
            }
        }
    }

    _wp_call_all_hook() 函数遍历与 'all' 钩子关联的所有函数,并使用 call_user_func_array() 函数来调用它们,并将 $args 作为参数传递给这些函数。

  4. 检查动作是否存在:

    if ( ! isset( $wp_filter[ $hook_name ] ) ) {
        if ( isset( $wp_filter['all'] ) ) {
            array_pop( $wp_current_filter );
        }
        return;
    }

    这段代码检查是否存在与 $hook_name 关联的函数。如果不存在,则表示没有函数注册到该动作,直接返回。

    • if ( ! isset( $wp_filter[ $hook_name ] ) ): 检查 $wp_filter 数组中是否存在以 $hook_name 为键的元素。如果不存在,表示没有函数注册到该动作。
    • if ( isset( $wp_filter['all'] ) ) { array_pop( $wp_current_filter ); }: 如果设置了’all’ 钩子,则将当前正在执行的钩子 $hook_name$wp_current_filter 堆栈中移除,因为已经确定没有与 $hook_name 关联的函数需要执行。
    • return;: 直接返回,不执行任何操作。
  5. 检查 $wp_filter[ $hook_name ]->callbacks 是否是数组

    if ( ! is_array( $wp_filter[ $hook_name ]->callbacks ) ) {
        return;
    }

    这段代码检查 $wp_filter[ $hook_name ]->callbacks 是否是数组。如果不是数组,直接返回。

  6. 遍历并执行函数:

    foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $functions ) {
        ksort( $functions );
        foreach ( $functions as $function ) {
            $args = $arg;
            $num_args = count( $arg );
            if ( is_null( $function['function'] ) ) {
                continue;
            }
            call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );
        }
    }

    这段代码是 do_action() 函数的核心部分,它遍历与 $hook_name 关联的所有函数,并按照优先级顺序执行它们。

    • foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $functions ): 遍历 $wp_filter[ $hook_name ]->callbacks 数组。这个数组的键是优先级($priority),值是具有相同优先级的函数数组($functions)。
    • ksort( $functions );: 对具有相同优先级的函数数组 $functions 按照键进行排序。这是为了确保在相同优先级下,函数按照它们被添加的顺序执行。
    • foreach ( $functions as $function ): 遍历具有相同优先级的函数数组 $functions
    • $args = $arg;: 将传递给 do_action() 函数的参数 $arg 复制到 $args 变量中。
    • $num_args = count( $arg );: 计算传递给 do_action() 函数的参数的数量。
    • if ( is_null( $function['function'] ) ) { continue; }: 检查 $function['function'] 是否为 null。如果是 null,表示该函数已被移除,跳过本次循环。
    • call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );: 使用 call_user_func_array() 函数来调用函数 $function['function'],并将 $args 作为参数传递给它。array_slice( $args, 0, (int) $function['accepted_args'] ) 用于截取 $args 数组,只传递函数 $function['function'] 声明接受的参数数量。$function['accepted_args'] 存储了函数 $function['function'] 声明接受的参数数量。

    call_user_func_array() 是一个 PHP 函数,它可以调用一个回调函数,并将一个数组作为参数传递给它。

    优先级(Priority): 钩子函数可以设置优先级,数值越小,优先级越高,越先执行。 默认优先级是 10。
    接受的参数数量(accepted_args): 指定钩子函数接受的参数数量。 这样可以避免传递过多的参数,提高性能。

流程总结:

  1. do_action() 接收动作名称和参数。
  2. 它检查是否存在与该动作关联的函数。
  3. 如果存在,它会按照优先级顺序遍历这些函数。
  4. 对于每个函数,它会调用 call_user_func_array() 函数来执行该函数,并将参数传递给它。
  5. 如果注册了 ‘all’ 动作,也会执行 ‘all’ 动作关联的所有函数

三、 apply_filters() 的源码解析: 过滤器的“数据处理器”

apply_filters() 函数的作用是应用与特定过滤器钩子关联的所有函数,以修改数据。 让我们看看它的源码(简化版):

/**
 * Call the functions added to a filter hook.
 *
 * @since 0.71
 *
 * @param string $hook_name The name of the filter hook.
 * @param mixed  $value     The value to filter.
 * @param mixed  ...$arg    Optional. Additional parameters to pass to the functions hooked to `$hook_name`.
 *
 * @return mixed The filtered value after all hooked functions are applied to it.
 */
function apply_filters( $hook_name, $value, ...$arg ) {
    global $wp_filter, $wp_current_filter;

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

    if ( ! is_array( $wp_filter[ $hook_name ]->callbacks ) ) {
        return $value;
    }

    $wp_current_filter[] = $hook_name;

    reset( $wp_filter[ $hook_name ]->callbacks );

    do {
        foreach ( (array) current( $wp_filter[ $hook_name ]->callbacks ) as $the_ ) {
            foreach ( (array) $the_ as $priority => $function ) {
                $args = array_merge( array( $value ), $arg );
                $num_args = count( $args );

                if ( is_null( $function['function'] ) ) {
                    continue;
                }

                $value = call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );
            }
        }
    } while ( next( $wp_filter[ $hook_name ]->callbacks ) !== false );

    array_pop( $wp_current_filter );

    return $value;
}

代码解读:

  1. 全局变量:do_action() 类似,apply_filters() 也使用全局变量 $wp_filter$wp_current_filter 来存储钩子信息和防止循环调用。

  2. 检查过滤器是否存在:

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

    这段代码检查是否存在与 $hook_name 关联的函数。如果不存在,则直接返回原始值 $value

  3. 遍历并应用函数:

    do {
        foreach ( (array) current( $wp_filter[ $hook_name ]->callbacks ) as $the_ ) {
            foreach ( (array) $the_ as $priority => $function ) {
                $args = array_merge( array( $value ), $arg );
                $num_args = count( $args );
    
                if ( is_null( $function['function'] ) ) {
                    continue;
                }
    
                $value = call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );
            }
        }
    } while ( next( $wp_filter[ $hook_name ]->callbacks ) !== false );

    这段代码是 apply_filters() 函数的核心部分,它遍历与 $hook_name 关联的所有函数,并按照优先级顺序应用它们,以修改传入的值 $value

    • do { ... } while ( next( $wp_filter[ $hook_name ]->callbacks ) !== false );: 使用 do...while 循环来遍历 $wp_filter[ $hook_name ]->callbacks 数组。next() 函数用于将数组的内部指针移动到下一个元素。当 next() 函数返回 false 时,表示已经遍历完所有元素,循环结束。
    • foreach ( (array) current( $wp_filter[ $hook_name ]->callbacks ) as $the_ ): 获取当前优先级的所有函数。current() 函数返回数组中当前指针指向的元素。
    • foreach ( (array) $the_ as $priority => $function ): 遍历当前优先级的所有函数。
    • $args = array_merge( array( $value ), $arg );: 将原始值 $value 和传递给 apply_filters() 函数的额外参数 $arg 合并到一个数组 $args 中。array_merge() 函数用于合并数组。
    • $num_args = count( $args );: 计算合并后的参数数量。
    • if ( is_null( $function['function'] ) ) { continue; }: 检查 $function['function'] 是否为 null。如果是 null,表示该函数已被移除,跳过本次循环。
    • $value = call_user_func_array( $function['function'], array_slice( $args, 0, (int) $function['accepted_args'] ) );: 使用 call_user_func_array() 函数来调用函数 $function['function'],并将 $args 作为参数传递给它。array_slice( $args, 0, (int) $function['accepted_args'] ) 用于截取 $args 数组,只传递函数 $function['function'] 声明接受的参数数量。$function['accepted_args'] 存储了函数 $function['function'] 声明接受的参数数量。注意,每次函数执行后,其返回值会更新 $value,作为下一个函数的输入。
  4. 返回最终值:

    return $value;

    在应用所有过滤器函数后,apply_filters() 函数返回最终修改后的值 $value

流程总结:

  1. apply_filters() 接收过滤器名称、初始值和参数。
  2. 它检查是否存在与该过滤器关联的函数。
  3. 如果存在,它会按照优先级顺序遍历这些函数。
  4. 对于每个函数,它会调用 call_user_func_array() 函数来执行该函数,并将当前值和参数传递给它。
  5. 函数的返回值会更新当前值,作为下一个函数的输入。
  6. 最终,apply_filters() 返回经过所有过滤器函数处理后的值。

四、 注册钩子: add_action()add_filter()

要让 do_action()apply_filters() 能够正常工作,我们需要先使用 add_action()add_filter() 函数来注册钩子。

add_action() 用于注册动作钩子,add_filter() 用于注册过滤器钩子。

它们的原型如下:

add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): bool
add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): bool
  • $hook_name: 钩子的名称。
  • $callback: 要执行的函数。
  • $priority: 优先级,数值越小,优先级越高。
  • $accepted_args: 函数接受的参数数量。

这两个函数的内部实现非常相似, 它们都将钩子名称、函数和优先级存储到全局数组 $wp_filter 中。

五、 示例演示:

为了更好地理解 do_action()apply_filters() 的工作方式,我们来看几个示例。

示例 1:使用 do_action() 添加自定义内容到文章底部

// 注册一个动作钩子
add_action( 'the_content', 'add_custom_content' );

function add_custom_content( $content ) {
    $custom_content = '<div class="custom-content">This is custom content added by a hook!</div>';
    return $content . $custom_content;
}

// 在文章内容输出的地方触发动作
do_action( 'the_content', $content );

在这个示例中,我们使用 add_action() 函数注册了一个名为 the_content 的动作钩子,并将 add_custom_content() 函数与之关联。 当 WordPress 在文章内容输出的地方调用 do_action( 'the_content', $content ) 时, add_custom_content() 函数会被执行,并将自定义内容添加到文章底部。

示例 2:使用 apply_filters() 修改文章标题

// 注册一个过滤器钩子
add_filter( 'the_title', 'prefix_title' );

function prefix_title( $title ) {
    return 'Prefix: ' . $title;
}

// 在文章标题输出的地方应用过滤器
$title = apply_filters( 'the_title', $title );
echo $title;

在这个示例中,我们使用 add_filter() 函数注册了一个名为 the_title 的过滤器钩子,并将 prefix_title() 函数与之关联。 当 WordPress 在文章标题输出的地方调用 apply_filters( 'the_title', $title ) 时, prefix_title() 函数会被执行,并将 "Prefix: " 添加到文章标题的前面。

示例 3:使用优先级控制钩子函数的执行顺序

add_action( 'my_action', 'function_one', 10 ); // 默认优先级
add_action( 'my_action', 'function_two', 5 );  // 优先级更高,先执行
add_action( 'my_action', 'function_three', 15 ); // 优先级更低,后执行

function function_one() {
    echo 'Function One<br>';
}

function function_two() {
    echo 'Function Two<br>';
}

function function_three() {
    echo 'Function Three<br>';
}

do_action( 'my_action' ); // 输出:Function Two<br>Function One<br>Function Three<br>

在这个示例中,我们注册了三个与 my_action 动作钩子关联的函数,并分别设置了不同的优先级。 优先级数值越小,函数越先执行。 因此,function_two() 会先执行,然后是 function_one(),最后是 function_three()

示例 4:传递多个参数给钩子函数

add_action( 'my_action', 'my_function', 10, 2 ); // 声明接受 2 个参数

function my_function( $arg1, $arg2 ) {
    echo 'Arg1: ' . $arg1 . '<br>';
    echo 'Arg2: ' . $arg2 . '<br>';
}

do_action( 'my_action', 'Hello', 'World' ); // 输出:Arg1: Hello<br>Arg2: World<br>

在这个示例中,我们使用 add_action() 函数的第四个参数 $accepted_args 声明 my_function() 函数接受 2 个参数。 当调用 do_action( 'my_action', 'Hello', 'World' ) 时,'Hello''World' 这两个参数会被传递给 my_function() 函数。

示例 5:使用 remove_action()remove_filter() 移除钩子函数

// 先添加一个 action
add_action( 'wp_footer', 'my_footer_function' );

function my_footer_function() {
    echo '<p>This is my footer function.</p>';
}

// 然后移除这个 action
remove_action( 'wp_footer', 'my_footer_function' );

// footer 不再输出内容

示例 6: 使用 has_action()has_filter() 检查钩子是否存在

// 检查是否存在名为 'wp_footer' 且绑定 'my_footer_function' 的 action
if ( has_action( 'wp_footer', 'my_footer_function' ) ) {
    echo "The 'my_footer_function' is hooked to 'wp_footer'.";
} else {
    echo "The 'my_footer_function' is not hooked to 'wp_footer'.";
}

六、 总结: 钩子的强大之处

通过深入了解 do_action()apply_filters() 的源码,我们可以更好地理解 WordPress 钩子机制的强大之处。

  • 解耦: 钩子机制允许插件和主题在不修改核心代码的情况下,扩展 WordPress 的功能。
  • 灵活性: 开发者可以根据需要,注册自己的钩子函数,并在特定的时刻执行。
  • 可扩展性: 钩子机制使得 WordPress 具有很强的可扩展性,可以满足各种不同的需求。

掌握了钩子机制,你就可以像一位经验丰富的建筑师一样,利用 WordPress 提供的各种“积木”,搭建出功能强大的网站。

希望今天的讲座对大家有所帮助! 咱们下期再见!

发表回复

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