剖析 WordPress `wp_enqueue_script()` 函数源码:如何处理依赖关系和 `in_footer` 参数。

嘿,各位代码猎人!今天咱们不聊八卦,就来扒一扒WordPress的wp_enqueue_script()这个函数,看看它到底是怎么把各种JavaScript文件玩转于股掌之间的,特别是它处理依赖关系和in_footer参数的那些小秘密。准备好了吗?Let’s dive in!

开场白:wp_enqueue_script()是啥?为啥要了解它?

简单来说,wp_enqueue_script()是WordPress用来加载JavaScript文件的官方姿势。你可能会问,直接在主题或者插件里用<script>标签不香吗?香是香,但不够优雅,不够灵活,也不够“WordPress”。

使用wp_enqueue_script()有以下几个好处:

  • 依赖管理: 它可以帮你自动加载依赖的JavaScript文件,避免手动处理顺序的痛苦。
  • 版本控制: 可以指定脚本的版本号,方便缓存更新。
  • 位置控制: 可以控制脚本加载的位置(header或footer),优化页面加载速度。
  • 避免冲突: WordPress会检查脚本是否已经被加载,避免重复加载导致冲突。

总之,wp_enqueue_script()是构建健壮、高效的WordPress主题和插件的基石之一。

正文:深入wp_enqueue_script()的源码世界

要理解wp_enqueue_script()是如何处理依赖关系和in_footer参数的,最好的方法就是直接看源码。不过,别担心,咱们不会直接把一大坨代码扔给你,而是会逐步拆解,用更容易理解的方式来讲解。

首先,wp_enqueue_script()实际上只是WP_Scripts类的一个方法。WP_Scripts类负责管理所有的JavaScript脚本。所以,咱们得先找到WP_Scripts类。

1. 找到WP_Scripts

WP_Scripts类位于wp-includes/class.wp-scripts.php文件中。你可以用你喜欢的代码编辑器打开它,或者直接在WordPress的源码库里找到它。

2. wp_enqueue_script()函数(实际上是WP_Scripts::add()

WP_Scripts类中,没有直接叫做wp_enqueue_script()的函数,但它最终会调用WP_Scripts::add()方法。wp_enqueue_script()只是一个wrapper function, 最终调用wp_enqueue_script()实际上是调用了WP_Scripts对象的add方法。add方法才是真正定义了如何注册脚本的。

/**
 * Registers a script.
 *
 * Registers the script if $src provided (does NOT mean enqueue).
 *
 * @since 2.6.0
 *
 * @param string           $handle Name of the script. Should be unique.
 * @param string|bool      $src    Path to the script from WordPress root directory. Example: '/js/myscript.js'.
 *                                 Default empty.
 * @param string[]         $deps   An array of registered script handles this script depends on. Default empty array.
 * @param string|bool|null $ver    String specifying script version number, if it has one, which is added to the URL
 *                                 as a query string for cache busting purposes. If version is set to false, a version
 *                                 is automatically added equal to current installed WordPress version.
 *                                 If version is set to null, no version is added.
 * @param bool             $in_footer Whether to enqueue the script before </body> instead of in the <head>.
 *                                 Default 'false'.
 */
public function add( $handle, $src = false, $deps = array(), $ver = false, $in_footer = false ) {
    if ( is_array( $src ) || is_object( $src ) ) {
        _doing_it_wrong( __FUNCTION__, sprintf( __( 'The script source must not be an array or object, "%s" given.' ), gettype( $src ) ), '5.3.0' );
        return;
    }

    $this->add_data( $handle, 'group', (bool) $in_footer );

    return parent::add( $handle, $src, $deps, $ver );
}

可以看到,add方法接受以下参数:

  • $handle: 脚本的唯一标识符(string)。
  • $src: 脚本的URL(string)。
  • $deps: 脚本依赖的其他脚本的handle数组(array)。
  • $ver: 脚本的版本号(string)。
  • $in_footer: 是否在footer加载(boolean)。

注意到了吗?add方法先调用了$this->add_data( $handle, 'group', (bool) $in_footer ); 这行代码。 这行代码将 $in_footer 参数的值存储到脚本的数据中,键名为 ‘group’。 这个’group’的值最终决定了脚本是在header里还是footer里输出。

然后调用了parent::addWP_Scripts类继承自WP_Dependencies类,而WP_Dependencies::add()方法才是真正负责将脚本信息存储起来的地方。

3. WP_Dependencies::add():存储脚本信息

public function add( $handle, $src, $deps = array(), $ver = false, $args = null ) {
    if ( isset( $this->registered[ $handle ] ) ) {
        return false;
    }

    $this->registered[ $handle ] = (object) array(
        'handle' => $handle,
        'src'    => $src,
        'deps'   => $deps,
        'ver'    => $ver,
        'args'   => $args,
        'extra'  => array(),
    );

    return true;
}

WP_Dependencies::add()方法将脚本的handle、URL、依赖、版本号等信息存储到$this->registered数组中。$this->registered是一个关联数组,key是脚本的handle,value是一个包含脚本信息的对象。

4. wp_enqueue_scripts() action 和 WP_Scripts::enqueue()

wp_enqueue_scripts是一个 WordPress action,它在 WordPress 准备输出页面时被触发。 开发者可以在这个 action 中使用 wp_enqueue_script() 函数来注册和排队脚本。

真正的enqueue逻辑在WP_Scripts::enqueue()中。

public function enqueue( $handles ) {
    $handles = (array) $handles;

    foreach ( $handles as $handle ) {
        if ( ! isset( $this->registered[ $handle ] ) ) {
            continue;
        }

        $this->queue[] = $handle;
    }

    $this->queue = array_unique( $this->queue );

    return $this->queue;
}

这个函数将需要加载的脚本的handle添加到$this->queue数组中。$this->queue数组存储了所有需要加载的脚本的handle。

5. WP_Scripts::do_items():输出脚本

public function do_items( $handles = false, $group = false ) {
    if ( false === $handles ) {
        $handles = $this->queue;
    }

    $this->all_deps( $handles );

    $this->done = array_unique( array_merge( $this->done, $handles ) );

    return $this->print_scripts( $this->done, $group );
}

WP_Scripts::do_items()方法负责输出脚本。它接受两个参数:

  • $handles: 要输出的脚本的handle数组。如果为false,则输出所有排队的脚本。
  • $group: 脚本的group。0表示在header中输出,1表示在footer中输出。

这个函数首先调用$this->all_deps( $handles )方法来处理依赖关系。然后,它调用$this->print_scripts( $this->done, $group )方法来输出脚本。

6. WP_Scripts::all_deps():处理依赖关系

public function all_deps( $handles, $recursion = false, $group = false ) {
    $r = true;
    $this->to_do = array();
    $this->done = array();

    foreach ( (array) $handles as $handle ) {
        $r = $this->do_item( $handle, $recursion, $group ) && $r;
    }

    return $r;
}

WP_Scripts::all_deps()方法遍历所有要加载的脚本,并调用$this->do_item()方法来处理每个脚本的依赖关系。

7. WP_Scripts::do_item():递归处理依赖

public function do_item( $handle, $recursion = false, $group = false ) {
    if ( isset( $this->done[ $handle ] ) ) {
        return true;
    }

    if ( isset( $this->doing[ $handle ] ) ) {
        return false;
    }

    if ( ! isset( $this->registered[ $handle ] ) ) {
        return false;
    }

    $this->doing[ $handle ] = true;

    $deps = $this->registered[ $handle ]->deps;

    if ( ! empty( $deps ) ) {
        $r = true;

        foreach ( $deps as $dep ) {
            if ( ! isset( $this->registered[ $dep ] ) ) {
                _doing_it_wrong(
                    __FUNCTION__,
                    sprintf(
                        /* translators: %s: Script handle. */
                        __( 'Script "%s" depends on a script that is not registered.' ),
                        $handle
                    ),
                    '3.3.0'
                );
                continue;
            }
            $r = $this->do_item( $dep, $recursion, $group ) && $r;
        }
    }

    if ( isset( $this->doing[ $handle ] ) ) {
        unset( $this->doing[ $handle ] );
    }

    $this->to_do[] = $handle;
    $this->done[ $handle ] = true;

    return true;
}

WP_Scripts::do_item()方法是处理依赖关系的核心。它做了以下几件事:

  • 检查脚本是否已经加载: 如果脚本已经在$this->done数组中,说明已经加载过,直接返回true。
  • 检查是否存在循环依赖: 如果脚本已经在$this->doing数组中,说明存在循环依赖,返回false。
  • 递归处理依赖: 遍历脚本的依赖列表,递归调用$this->do_item()方法来处理每个依赖的依赖关系。
  • 将脚本添加到$this->to_do数组: 将脚本添加到$this->to_do数组中,表示这个脚本可以加载了。
  • 将脚本添加到$this->done数组: 将脚本添加到$this->done数组中,表示这个脚本已经处理过了。

这个方法使用递归的方式来处理依赖关系,确保所有依赖的脚本都按照正确的顺序加载。

8. WP_Scripts::print_scripts():输出HTML代码

public function print_scripts( $handles = false, $group = false ) {
    global $wp_scripts, $concatenate_scripts;

    if ( ! $handles ) {
        return;
    }

    $handles = array_unique( $handles );

    $scripts = array();
    $sm_handles = array();

    foreach ( $handles as $handle ) {
        if ( ! isset( $this->registered[ $handle ] ) ) {
            continue;
        }

        if ( ( $this->registered[ $handle ]->args && $group ) || ( ! $this->registered[ $handle ]->args && ! $group ) ) {
            $scripts[] = $handle;
        } else {
            $sm_handles[] = $handle;
        }
    }

    if ( $concatenate_scripts ) {
        do_action( 'wp_print_scripts', $scripts );
        $this->concat( $scripts );
    } else {
        do_action( 'wp_print_scripts', $sm_handles );
        $this->print_html( $sm_handles, $group );
    }

    return $sm_handles;
}

WP_Scripts::print_scripts()方法负责生成HTML代码来加载脚本。它做了以下几件事:

  • 遍历所有要输出的脚本: 遍历$handles数组,获取每个脚本的信息。
  • 根据$in_footer参数过滤脚本: 根据脚本的in_footer参数,将脚本分为两组:一组是在header中加载的,一组是在footer中加载的。
  • 输出HTML代码: 对于在header中加载的脚本,直接输出<script>标签。对于在footer中加载的脚本,输出<?php wp_print_footer_scripts(); ?>

依赖关系的处理流程总结

  1. wp_enqueue_script()将脚本信息存储到$this->registered数组中。
  2. WP_Scripts::enqueue()将需要加载的脚本的handle添加到$this->queue数组中。
  3. WP_Scripts::do_items()调用$this->all_deps()方法来处理依赖关系。
  4. WP_Scripts::all_deps()方法遍历所有要加载的脚本,并调用$this->do_item()方法来处理每个脚本的依赖关系。
  5. WP_Scripts::do_item()方法递归处理依赖,确保所有依赖的脚本都按照正确的顺序加载。
  6. WP_Scripts::print_scripts()方法根据$in_footer参数生成HTML代码来加载脚本。

in_footer参数的处理流程总结

  1. wp_enqueue_script()函数的$in_footer参数传递给WP_Scripts::add()方法。
  2. WP_Scripts::add()方法将$in_footer参数的值存储到脚本的数据中,键名为group
  3. WP_Scripts::print_scripts()方法根据脚本的group值来决定脚本是在header中加载还是在footer中加载。

代码示例:wp_enqueue_script()的用法

下面是一些wp_enqueue_script()的用法示例:

<?php
function my_theme_enqueue_scripts() {
  // 注册一个名为'my-script'的脚本,依赖于'jquery'。
  wp_register_script( 'my-script', get_template_directory_uri() . '/js/my-script.js', array( 'jquery' ), '1.0', true );

  // Enqueue (排队) 脚本.  只有enqueue了,才会最终输出到html中。
  wp_enqueue_script( 'my-script' );

  // 注册并排队另一个脚本,不依赖于任何其他脚本,版本号为'1.1',在header中加载。
  wp_enqueue_script( 'another-script', get_template_directory_uri() . '/js/another-script.js', array(), '1.1', false );

  // 注册并排队一个有多个依赖的脚本。
  wp_enqueue_script( 'complex-script', get_template_directory_uri() . '/js/complex-script.js', array( 'jquery', 'my-script', 'another-script' ), '1.2', true );

}
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_scripts' );
?>

在这个例子中,my-script.js会依赖jquery,并且会在footer中加载。another-script.js不会依赖任何脚本,并且会在header中加载。complex-script.js会依赖jquerymy-scriptanother-script, 并且会在footer中加载。WordPress会自动处理这些依赖关系,确保脚本按照正确的顺序加载。

表格总结:wp_enqueue_script()参数详解

参数 类型 描述 默认值
$handle string 脚本的唯一标识符。
$src string 脚本的URL。 空字符串
$deps array 脚本依赖的其他脚本的handle数组。 空数组
$ver string 脚本的版本号。 false
$in_footer boolean 是否在footer加载。true表示在footer中加载,false表示在header中加载。 false

结束语:成为wp_enqueue_script()大师

通过今天的讲解,相信你已经对wp_enqueue_script()函数有了更深入的了解。掌握了它的原理和用法,你就可以更灵活、更高效地管理WordPress主题和插件中的JavaScript文件,构建出更健壮、更快速的网站。

记住,代码的世界没有捷径,只有不断学习和实践。下次遇到wp_enqueue_script()的问题,不妨再来回顾一下今天的讲解,相信你一定能找到答案!祝你编程愉快!

发表回复

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