深入解读 WordPress `Walker` 类源码:递归渲染树形结构的实现原理。

各位观众,晚上好!今天咱们来聊聊WordPress里一个有点意思的类:Walker。别被它这个名字吓到,它其实就是个“树形结构渲染大师”,专门负责把那些层层叠叠的数据,比如文章分类、菜单结构,给你漂漂亮亮地展示出来。

开场白:认识一下我们的“树形结构渲染大师”

想象一下,你面前有一棵树,树干是根节点,然后分出很多枝干,枝干又分出小枝,小枝再长出叶子。这个结构,在计算机里就叫树形结构。WordPress里有很多地方需要用到这种结构,比如文章分类,你可以有“技术文章”这个大类,下面又有“PHP”、“JavaScript”、“WordPress”这些小类。

Walker类,就是用来把这种树形结构“画”出来的工具。它像一个经验丰富的园丁,知道怎么从根节点开始,一步一步地遍历整棵树,并把每个节点按照你的要求展示出来。

第一幕:Walker类的基本结构

Walker类本身是一个抽象类,这意味着你不能直接用它,而是需要创建一个它的子类,然后重写一些方法,告诉它你想怎么渲染每个节点。

我们先来看看Walker类的基本结构:

abstract class Walker {
    public $tree_type = array( 'post' ); // 默认的树形结构类型,比如 'post'(文章)、'category'(分类)
    public $db_fields = array ('parent' => 'post_parent', 'id' => 'ID'); // 数据库字段的映射关系

    /**
     * Starts the list before the elements are added.
     *
     * @param string $output Used to append additional content (passed by reference).
     * @param int    $depth  Depth of the item being displayed.
     * @param array  $args   An array of arguments.
     */
    public function start_lvl( &$output, $depth = 0, $args = array() ) {}

    /**
     * Ends the list of after the elements are added.
     *
     * @param string $output Used to append additional content (passed by reference).
     * @param int    $depth  Depth of the item being displayed.
     * @param array  $args   An array of arguments.
     */
    public function end_lvl( &$output, $depth = 0, $args = array() ) {}

    /**
     * Starts the element output.
     *
     * @param string $output Used to append additional content (passed by reference).
     * @param object $data_object The data object.
     * @param int    $depth  Depth of the item being displayed.
     * @param array  $args   An array of arguments.
     * @param int    $id     ID of the current item.
     */
    public function start_el( &$output, $data_object, $depth = 0, $args = array(), $id = 0 ) {}

    /**
     * Ends the element output, after the elements and any children have been added.
     *
     * @param string $output Used to append additional content (passed by reference).
     * @param object $data_object The data object.
     * @param int    $depth  Depth of the item being displayed.
     * @param array  $args   An array of arguments.
     */
    public function end_el( &$output, $data_object, $depth = 0, $args = array() ) {}

    /**
     * Display element.
     *
     * @param object $element           Data object.
     * @param array  $children_elements List of the element's children.
     * @param int    $max_depth         Max depth to traverse.
     * @param int    $depth             Depth of the current element.
     * @param array  $args              An array of arguments.
     * @param string $output            Used to append additional content (passed by reference).
     */
    public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {}

    /**
     * Traverse elements to create HTML list.
     *
     * @param array $elements An array of elements.
     * @param int   $max_depth The maximum depth.
     * @return string|void
     */
    public function walk( $elements, $max_depth ) {}

    /**
     * Flat list walker.
     *
     * @param array $elements An array of elements.
     * @param int   $max_depth The maximum depth.
     * @return string|void
     */
    public function paged_walk( $elements, $max_depth ) {}
}

是不是感觉有点多?别慌,我们一个个来看。

  • $tree_type: 这个属性定义了你要处理的树形结构的类型。默认是'post',也就是文章。但你可以根据需要改成'category'(分类)、'nav_menu'(导航菜单)等等。
  • $db_fields: 这个属性定义了数据库字段的映射关系。比如,'parent' => 'post_parent'表示,在你的数据里,哪个字段代表父节点ID。'id' => 'ID'表示,哪个字段代表当前节点ID。
  • start_lvl(): 这个方法在开始渲染一个层级(level)的时候被调用。比如,在开始渲染一个分类的子分类列表之前。你可以在这里输出一些HTML标签,比如<ul>
  • end_lvl(): 这个方法在结束渲染一个层级的时候被调用。比如,在结束渲染一个分类的子分类列表之后。你可以在这里输出一些HTML标签,比如</ul>
  • start_el(): 这个方法在开始渲染一个元素(element)的时候被调用。比如,在开始渲染一个分类的时候。你可以在这里输出一些HTML标签,比如<li>,以及分类的名称。
  • end_el(): 这个方法在结束渲染一个元素的时候被调用。比如,在结束渲染一个分类之后。你可以在这里输出一些HTML标签,比如</li>
  • display_element(): 这个方法用来显示一个元素。它会判断这个元素是否有子元素,如果有,就递归调用walk()方法来渲染子元素。
  • walk(): 这个方法是Walker类的核心方法。它接收一个元素数组和一个最大深度作为参数,然后遍历这个数组,并调用display_element()方法来渲染每个元素。
  • paged_walk(): 这个方法也是用来遍历元素数组的,但是它支持分页。

第二幕:创建一个自定义的Walker

光说不练假把式,咱们来创建一个自定义的Walker类,用来渲染文章分类。

class My_Category_Walker extends Walker {
    public $tree_type = 'category';
    public $db_fields = array ('parent' => 'parent', 'id' => 'term_id');

    public function start_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat( "t", $depth );
        $output .= "n$indent<ul class="children">n";
    }

    public function end_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat( "t", $depth );
        $output .= "$indent</ul>n";
    }

    public function start_el( &$output, $category, $depth = 0, $args = array(), $id = 0 ) {
        $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';

        $cat_name = esc_attr( $category->name );
        $cat_name = apply_filters( 'list_cats', $cat_name, $category );

        $link = '<a href="' . esc_url( get_category_link( $category->term_id ) ) . '" ';
        if ( $args['use_desc_for_title'] == true && ! empty( $category->description ) ) {
            $link .= 'title="' . esc_attr( strip_tags( $category->description ) ) . '"';
        }
        $link .= '>';
        $link .= $cat_name . '</a>';

        if ( ! empty( $args['feed'] ) ) {
            $link .= ' ';

            $link .= '<a href="' . esc_url( get_category_feed_link( $category->term_id, $args['feed_type'] ) ) . '"';

            $link .= '>';
            $link .= ! empty( $args['feed_image'] ) ? '<img src="' . esc_attr( $args['feed_image'] ) . '" alt="rss" />' : esc_html( $args['feed'] );
            $link .= '</a>';
        }

        if ( ! empty( $args['show_count'] ) ) {
            $link .= ' (' . intval( $category->count ) . ')';
        }

        if ( 'list' == $args['style'] ) {
            $output .=  $indent . '<li';
            $css_class = array(
                'cat-item',
                'cat-item-' . $category->term_id,
            );

            if ( ! empty( $args['current_category'] ) ) {
                $_current_category = get_term( $args['current_category'], $this->tree_type );

                if ( $category->term_id == $args['current_category'] ) {
                    $css_class[] = 'current-cat';
                } elseif ( $category->term_id == $_current_category->parent ) {
                    $css_class[] = 'current-cat-parent';
                }
            } elseif ( get_queried_object_id() == $category->term_id ) {
                $css_class[] = 'current-cat';
            }

            $css_class = join( ' ', apply_filters( 'category_css_class', $css_class, $category, $depth, $args ) );

            $output .=  ' class="' . $css_class . '"';
            $output .= ">$linkn";
        } else {
            $output .= "t$link<br />n";
        }
    }

    public function end_el( &$output, $category, $depth = 0, $args = array() ) {
        if ( 'list' == $args['style'] ) {
            $output .= "</li>n";
        }
    }
}

这个类做了以下几件事:

  1. 继承Walker: class My_Category_Walker extends Walker,这表明My_Category_WalkerWalker的一个子类。
  2. 设置$tree_type$db_fields: $tree_type = 'category'告诉Walker,我们要处理的是分类数据。$db_fields定义了分类数据的父节点ID和节点ID的字段名。
  3. 重写start_lvl()end_lvl(): 这两个方法用来输出<ul></ul>标签,用来包裹子分类列表。
  4. 重写start_el()end_el(): 这两个方法用来输出每个分类的链接。start_el()负责输出<li>标签和链接,end_el()负责输出</li>标签。

第三幕:使用自定义的Walker

有了自定义的Walker类,我们就可以用它来渲染分类列表了。

$args = array(
    'walker'         => new My_Category_Walker(), // 使用我们自定义的 Walker 类
    'title_li'       => '', // 不要显示默认的“分类”标题
    'show_count'     => 1, // 显示分类的文章数量
    'hierarchical'   => true, // 显示层级关系
    'style'          => 'list', // 使用列表样式
    'depth'          => 0 // 显示所有层级
);

echo '<ul class="my-category-list">'; // 添加一个外层 ul
wp_list_categories( $args );
echo '</ul>'; // 闭合外层 ul

这段代码做了以下几件事:

  1. 定义参数数组$args: 这个数组用来传递给wp_list_categories()函数。
    • 'walker' => new My_Category_Walker(): 告诉wp_list_categories()函数,使用我们自定义的Walker类。
    • 'title_li' => '': 不要显示默认的“分类”标题。
    • 'show_count' => 1: 显示分类的文章数量。
    • 'hierarchical' => true: 显示层级关系。
    • 'style' => 'list': 使用列表样式。
    • 'depth' => 0: 显示所有层级。
  2. 调用wp_list_categories()函数: 这个函数会根据参数数组,使用我们自定义的Walker类来渲染分类列表。
  3. 输出HTML标签: 我们在调用wp_list_categories()函数前后,分别输出了<ul></ul>标签,用来包裹整个分类列表。

第四幕:深入理解walk()方法的递归原理

walk()方法是Walker类的核心方法,它负责遍历整个树形结构。它的实现原理是递归。

我们来看一下walk()方法的简化版代码:

public function walk( $elements, $max_depth, ...$args ) {
    $output = '';
    $parent_field = $this->db_fields['parent'];
    $id_field = $this->db_fields['id'];

    // 将元素按照父节点ID分组
    $children_elements = array();
    foreach ( $elements as $e ) {
        $parent_id = $e->$parent_field;
        if ( ! isset( $children_elements[$parent_id] ) ) {
            $children_elements[$parent_id] = array();
        }
        $children_elements[$parent_id][] = $e;
    }

    // 从根节点开始遍历
    if ( isset( $children_elements[0] ) ) {
        $this->display_element( $elements[0], $children_elements, $max_depth, 0, $args, $output ); // 假设第一个元素是根节点
    }

    return $output;
}

public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
    if ( ! $element ) {
        return;
    }

    $id_field = $this->db_fields['id'];
    $id = $element->$id_field;

    // 输出元素的开始标签
    $this->start_el( $output, $element, $depth, $args );

    // 如果有子元素,并且深度没有超过最大深度,就递归调用 walk() 方法
    if ( isset( $children_elements[ $id ] ) && ( ( $max_depth === 0 ) || ( $max_depth > $depth + 1 ) ) ) {
        foreach ( $children_elements[ $id ] as $child ) {
            $this->display_element( $child, $children_elements, $max_depth, $depth + 1, $args, $output );
        }

        unset( $children_elements[ $id ] );
    }

    // 输出元素的结束标签
    $this->end_el( $output, $element, $depth, $args );
}

这段代码的执行流程是这样的:

  1. walk()方法:
    • 接收一个元素数组$elements和一个最大深度$max_depth作为参数。
    • 将元素按照父节点ID分组,存储在$children_elements数组中。
    • 从根节点开始遍历,调用display_element()方法来渲染每个元素。
  2. display_element()方法:
    • 接收一个元素$element、子元素数组$children_elements、最大深度$max_depth、当前深度$depth、参数数组$args和一个输出字符串$output作为参数。
    • 调用start_el()方法输出元素的开始标签。
    • 判断元素是否有子元素,并且深度没有超过最大深度。
      • 如果有,就遍历子元素,并递归调用display_element()方法来渲染每个子元素。
    • 调用end_el()方法输出元素的结束标签。

递归的意思是,display_element()方法在执行过程中,会调用自身来渲染子元素。这样,就可以一层一层地遍历整个树形结构,并把每个节点按照你的要求展示出来。

第五幕:Walker类的应用场景

Walker类在WordPress里有很多应用场景,比如:

  • 渲染文章分类: 就像我们前面演示的那样,你可以使用Walker类来渲染文章分类列表。
  • 渲染导航菜单: WordPress的导航菜单也是一个树形结构,你可以使用Walker_Nav_Menu类来渲染导航菜单。
  • 渲染评论列表: 评论列表也可以看作是一个树形结构,你可以使用Walker_Comment类来渲染评论列表。
  • 自定义树形结构: 如果你有自己的树形结构数据,也可以创建一个自定义的Walker类来渲染它。
应用场景 对应的 Walker 类 主要用途
文章分类列表 My_Category_Walker (自定义) 显示文章分类,可以自定义 HTML 结构和样式
导航菜单 Walker_Nav_Menu 生成导航菜单,支持多级菜单,方便用户浏览网站
评论列表 Walker_Comment 显示评论内容,支持嵌套评论,方便用户交流
自定义数据结构 自定义 Walker 类 渲染任何树形结构的数据,具有高度的灵活性

总结:Walker类的精髓

Walker类的精髓在于它提供了一种灵活的方式来渲染树形结构。你可以通过继承Walker类,并重写一些方法,来定制渲染过程。

记住以下几点:

  • Walker类是一个抽象类,你需要创建一个它的子类才能使用。
  • 你需要重写start_lvl()end_lvl()start_el()end_el()这四个方法,来定义渲染过程。
  • walk()方法是核心方法,它负责遍历整个树形结构。
  • display_element()方法负责渲染单个元素,并且递归调用自身来渲染子元素。

掌握了Walker类,你就可以轻松地渲染各种树形结构,让你的WordPress网站更加美观和易用。

今天的分享就到这里,希望大家有所收获! 感谢大家的观看!

发表回复

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