• 浅析MV*模式


      做客户端开发、前端开发对MVC、MVP、MVVM这些名词不了解也应该大致听过,都是为了解决图形界面应用程序复杂性管理问题而产生的应用架构模式。网上很多文章关于这方面的讨论比较杂乱,各种MV*模式之间的区别分不清,甚至有些描述都是错误的。

      本文我们将来讨论下三个非常重要的架构模式 - MVC (Model(模型)-View(视图)-Controller(控制器))、MVP (Model(模型)-View(视图)-Presenter(中介者)) 以及 MVVM (Model(模型)-View(视图)-ViewModel(视图模型))。过去,这些模式已经被大量用于构建桌面和服务器端的应用程序,但它只是在最近几年才被应用到JavaScript。我们首先来了解下基础知识。

    一、GUI程序所面临的问题

      图形界面的应用程序提供给用户可视化的操作界面,这个界面提供数据和信息。用户输入行为(键盘,鼠标等)会执行一些应用逻辑,应用逻辑(application logic)可能会触发一定的业务逻辑(business logic)对应用程序数据的变更,数据的变更自然需要用户界面的同步变更以提供最准确的信息。例如用户对一个电子表格重新排序的操作,应用程序需要响应用户操作,对数据进行排序,然后需要同步到界面上。

      在开发应用程序的时候,以求更好的管理应用程序的复杂性,基于“职责分离(Speration of Duties)”的思想都会对应用程序进行分层。在开发图形界面应用程序的时候,会把管理用户界面的层次称为View,应用程序的数据为Model(注意这里的Model指的是Domain Model,这个应用程序对需要解决的问题的数据抽象,不包含应用的状态,可以简单理解为对象)。Model提供数据操作的接口,执行相应的业务逻辑。

      有了View和Model的分层,那么问题就来了:View如何同步Model的变更,View和Model之间如何粘合在一起。

      带着这个问题开始探索MV*模式,会发现这些模式之间的差异可以归纳为对这个问题处理的方式的不同。而几乎所有的MV模式都是经典的Smalltalk-80 MVC的修改版。

    二、Smalltalk-80 MVC

      早在上个世纪70年代,美国的施乐公司(Xerox)的工程师研发了Smalltalk编程语言,并且开始用它编写图形界面的应用程序。而在Smalltalk-80这个版本的时候,一位叫Trygve Reenskaug的工程师设计了MVC图形应用程序的架构模式,极大地降低了图形应用程序的管理难度。在1995年的“设计模式: 面向对象软件中的可复用元素” (著名的"GoF"的书)中,MVC被进一步深入的描述,该书对MVC的流行使用起到了关键作用。

      了解一下最初的MVC模式打算解决什么问题是很重要的,因为自从诞生之日起它已经发生了很大的改变。回到70年代,图形用户界面还很稀少,一个被称为分离展示的概念开始被用来清晰的划分下面两种对象:

      (1)领域对象,它对现实世界里的概念进行建模(比如一张照片,一个人),

      (2)还有展示对象,它被渲染到用户屏幕上进行展示。

      Smalltalk-80作为MVC的实现,把这一概念进一步发展,产生这样一个观点,即把应用逻辑从用户界面中分离开来。这种想法使得应用的各个部分之间得以解耦,也允许该应用中的其它界面对模型进行复用。关于Smalltalk-80的MVC架构,有几点很有趣,值得注意一下:

    • 模型表现了领域特定的数据,并且不用考虑用户界面(视图和控制器),当一个模型有所改变的时候,它会通知它的观察者。
    • 视图表现了一个模型的当前状态,观察者模式被用来让视图在任何时候都知晓模型已经被更新了或者被改变了。
    • 展现受到视图的照管,但是不仅仅只有一个单独的视图或者控制器——每一个在屏幕上展现的部分或者元素都需要一个视图-控制器对。
    • 控制器在这个视图-控制器对中扮演着处理用户交互的角色(比如按键或者点击动作),做出对视图的选择。

      开发者有时候会惊奇于他们了解到的观察者模式(如今已经被普遍的作为发布/订阅的变异实现了)已经在几十年以前被作为MVC架构的一部分包含进去了。在Smalltalk-80的 MVC中,视图观察着模型:如上面要点中所提到的,模型在任何时候发生了改变,视图就会做出响应。一个简单的示例就是一个由股票市场数据支撑的应用程序——为了应用程序的实用性,任何对于我们模型中数据的改变都应该导致视图中的结果实时的刷新。

    三、MVC模式

      我们知道MVC由三个核心部分组成:

    1、Models

      Models管理一个业务应用的数据。它们既与用户界面无关也与表现层无关,相反的它们代表了一个业务应用所需要的形式唯一的数据。当一个model改变时(比如当它被更新时),它通常会通知它的观察者(比如我们很快会介绍的views)一个改变已经发生了,以便观察者采取相应的反应。

      为了更深的理解models,让我们假设我们有一个JavaScript的相册应用。在一个相册中,照片这个概念配得上拥有一个自己的model, 因为它代表了特定领域数据的一个独特类型。这样一个model可以包含一些相关的属性,比如标题,图片来源和额外的元数据。一张特定的照片可以存储到model的一个实例中,而且一个model也可以被复用。下面我们可以看到一个用Backbone实现的被简化的model例子。

    var Photo = Backbone.Model.extend({
        // 照片的默认属性
        defaults: {
          src: "placeholder.jpg",
          caption: "A default image",
          viewed: false
        },
        // 确保每一个被创建的照片都有一个`src`.
        initialize: function() {
           this.set( { "src": this.defaults.src} );
        }
    });

      不同的框架其内置的模型的能力有所不同,然而他们对于属性验证的支持还是相当普遍的,属性展现了模型的特征,比如一个模型标识符。当在一个真实的世界使用模型的时候,我们一般也希望模型能够持久。持久化允许我们用最近的状态对模型进行编辑和更新,这一状态会存储在内存、用户的本地数据存储区或者一个同步的数据库中。

      另外,模型可能也会被多个视图观察着。如果说,我们的照片模型包含了一些元数据,比如它的位置(经纬度),照片中所展现的好友(一个标识符的列表)和一个标签的列表,开发者也许会选择为这三个方面的每一个提供一个单独的视图。

    2、视图:视图观察模型

      视图是模型的可视化表示,提供了一个当前状态的经过过滤的视图。Smaltalk的视图是关于绘制和操作位图的,而JavaScript的视图是关于构建和操作DOM元素的。

      一个视图通常是模型的观察者,当模型改变的时候,视图得到通知,因此使得视图可以更新自身。用设计模式的语言可以称视图为“哑巴”,因为在应用程序中是它们关于模型和控制器的了解是受到限制的。

      用户可以和视图进行交互,包括读和编辑模型的能力(例如,获取或者设置模型的属性值)。因为视图是表示层,我们通常以用户友好的方式提供编辑和更新的能力。例如,在之前我们讨论的照片库应用中,模型编辑可以通过“编辑”视图来进行,这个视图里面,用户可以选择一个特定的图片,接着编辑它的元数据。

      而实际更新模型的任务落到了控制器上面(我们很快就会讲这个东西)。这个架构的好处是每个组件在应用工作的时候都扮演着必要的独立的角色

    3、模板

      在支持 MVC/MV* 的JavaScript框架的下,有必要简略的讨论一下JavaScript的模板以及它们与视图之间的关系。

      历史已经证明在内存中通过字符串拼接来构建大块的HTML标记是一种糟糕的性能实践。开发者这样做,就会深受其害。遍历数据,将其封装成嵌套的div,使用例如document.write 这样过时的技术将"模板"注入到DOM中。这样通常意味着校本化的标记将会嵌套在我们标准的标记中,很快就变得很难阅读了,更重要的是,维护这样的代码将是一场灾难,尤其是在构建大型应用的时候。

      JavaScript 模板解决方案(例如Handlebars.js 和Mustache)通常用于为视图定义模板作为标记(要么存储在外部,要么存储在脚本标签里面,使用自定义的类型例如text/template),标记中包含有模板变量。变量可以使用变化的语法来分割(例如{{name}}),框架通常也足够只能接受JSON格式的数据(模型可以转化成JSON格式),这样我们只需要关心如何维护干净的模型和干净的模板。人们遭遇的绝大多数的苦差事都被框架本身所处理了。这样做有大量的好处,尤其选择是将模板存储在外部的时候,这样在构建大型应用的时候可以是模板按照需要动态加载。

      下面我们可以看到两个HTMP模板的例子。一个使用流行的Handlebar.js框架实现,一个使用Underscore模板实现

    // Handlebars.js
    <li class="photo">
      <h2>{{caption}}</h2>
      <img class="source" src="{{src}}"/>
      <div class="meta-data">
        {{metadata}}
      </div>
    </li>
    
    // Underscore.js Microtemplates
    <li class="photo">
      <h2><%= caption %></h2>
      <img class="source" src="<%= src %>"/>
      <div class="meta-data">
        <%= metadata %>
      </div>
    </li><span style="line-height:1.5;font-size:10pt;"></span>

      请注意模板并不是它们自身的视图,来自于Struts Model 2 架构的开发者可能会感觉模板就是一个视图,但并不是这样的。视图是一个观察着模型的对象,并且让可视的展现保持最新模板也许是用一种声明的方式指定部分甚至所有的视图对象,因此它可能是从模板定制文档生成的。

      在经典的web开发中,在单独的视图之间进行导航需要利用到页面刷新,然而也并不值得这样做。而在单页面Javascript应用程序中,一旦数据通过ajax从服务器端获取到了,并不需要任何这样必要的刷新,就可以简单的在同一个页面渲染出一个新的视图。

      这里导航就降级为了“路由”的角色,用来辅助管理应用程序状态(例如,允许用户用书签标记它们已经浏览到的视图)。然而,路由既不是MVC的一部分,也不在每一个类MVC框架中展现出来,在这一节中我将不深入详细的讨论它们。

      总而言之,视图是对我们的数据的一种可视化展现;而模板就是用声明的方式指定部分视图对象。

    4、MVC的依赖关系

      MVC除了把应用程序分成View、Model层,还额外的加了一个Controller层,它的职责为进行Model和View之间的协作(路由、输入预处理等)的应用逻辑(application logic);Model进行处理业务逻辑。Model、View、Controller三个层次的依赖关系如下:

      Controller和View都依赖Model层,Controller和View可以互相依赖。在一些网上的资料Controller和View之间的依赖关系可能不一样,有些是单向依赖,有些是双向依赖,这个其实关系不大,后面会看到它们的依赖关系都是为了把处理用户行为触发的事件处理权交给Controller。

    5、控制器:

      控制器是模型和视图之间的中介,典型的职责是当用户操作视图的时候同步更新模型

      在我们的照片廊应用程序中,控制器会负责处理用户通过对一个特定照片的视图进行编辑所造成改变,当用户完成编辑后,就更新一个特定的照片模型。

    6、MVC的调用关系

    (1)用户对View操作以后,View捕获到这个操作,会把处理的权利交移给Controller(Pass calls);

    (2)Controller会对来自View数据进行预处理、决定调用哪个Model的接口;

    (3)然后由Model执行相关的业务逻辑;

    (4)当Model变更了以后,会通过观察者模式(Observer Pattern)通知View;View通过观察者模式收到Model变更的消息以后,会向Model请求最新的数据,然后重新更新界面。如下图:

      看似没有什么特别的地方,但是有几个需要特别关注的关键点:

    1. View是把控制权交移给Controller,Controller执行应用程序相关的应用逻辑(对来自View数据进行预处理、决定调用哪个Model的接口等等)。
    2. Controller操作Model,Model执行业务逻辑对数据进行处理。但不会直接操作View,可以说它是对View无知的。
    3. View和Model的同步消息是通过观察者模式进行,而同步操作是由View自己请求Model的数据然后对视图进行更新。

      需要特别注意的是MVC模式的精髓在于第三点:Model的更新是通过观察者模式告知View的,具体表现形式可以是Pub/Sub或者是触发Events,而网上很多对于MVC的描述都没有强调这一点。

      如我们所讨论的,模型代表应用程序的数据,而视图则是用户在屏幕上看到的被展现出来的东西。如此,MVC它的一些核心的通讯就要依赖于观察者模式(令人惊奇的是,一些相关的内容在许多关于MVC模式的书籍并没有被涵盖到)。当模型被改变时,它会通知观察者(视图)一些东西已经被更新了—— 这也许是MVC中最重要的关系。观察者的这一特性也是实现将多个视图连结到同一个模型的基础

      这一模式的目标之一就是帮助去实现一个主体(数据对象)和它的观察者之间的一对多关系的定义。当一个主体发生改变的时候,它的观察者也会被更新。视图和控制器有一种稍微不同的关系,控制器协助视图对不同的用户输入做出响应,这也是一个策略模式的例子。

      通过观察者模式的好处就是:不同的MVC三角关系可能会有共同的Model,一个MVC三角中的Controller操作了Model以后,两个MVC三角的View都会接受到通知,然后更新自己。保持了依赖同一块Model的不同View显示数据的实时性和准确性。我们每天都在用的观察者模式,在几十年前就已经被大神们整合到MVC的架构当中。

    7、MVC的优缺点

      优点:

    (1)把业务逻辑和展示逻辑分离,模块化程度高。且当应用逻辑需要变更的时候,不需要变更业务逻辑和展示逻辑,只需要Controller换成另外一个Controller就行了(Swappable Controller)。

    (2)观察者模式可以做到多视图同时更新。

      缺点:

    (1)Controller测试困难。因为视图同步操作是由View自己执行,而View只能在有UI的环境下运行。在没有UI环境下对Controller进行单元测试的时候,应用逻辑正确性是无法验证的:Model更新的时候,无法对View的更新操作进行断言。

    (2)View无法组件化。View是强依赖特定的Model的,如果需要把这个View抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的Domain Model是不一样的

    四、MVP模式

      模型-视图-展示器(MVP)是MVC设计模式的一个衍生模式,它专注于提升展现逻辑。它来自于上个世纪九十年代早期的一个叫做Taligent的公司,当时他们正工作于一个基于C++ CommonPoint环境的模型。而MVC和MVP的目标都直指对整个多组件关注点的分离,它们之间有一些基础上的不同。

      MVP模式有两种:

    (1)Passive View

    (2)Supervising Controller

      而大多数情况下讨论的都是Passive View模式。我们会对PV模式进行较为详细的介绍,而SC模式则简单提及。

    1、模型、视图、展示器

      MVP中的P代表展示器,它是一个包含视图的用户界面逻辑的组件(不像MVC),来自视图的调用被委派给了展示器,它是从视图中解耦出来的,并且转而通过一个接口来同它进行对话,这允许所有类型的有用的东西,比如在单元测试中模拟视图的调用。

      对MVP最通常的实现是使用一个被动视图(Passive View 一种对所有动机和目的保持静默的视图),包含很少甚至与没有任何逻辑。如果MVC和MVP是不同的,那是因为其C和P干了不同的事情。

      在MVP中,P观察着模型并且当模型发生改变的时候对视图进行更新,P切实的将模型绑定到了视图。

      通过视图发送请求,展示者执行所有和用户请求相关的工作,并且把数据返回给视图。从这个方面来讲,它们获取数据,操作数据,然后决定数据如何在视图上面展示。在一些实现当中,展示者同时和一个服务层交互,用于持久化数据(模型)。模型可以触发事件,但是是由展示者扮演这个角色,用于订阅这些事件,从而来更新视图。在这个被动体系架构下,我们没有直接数据绑定的概念。视图暴露setter ,而展示者使用这些setter 来设置数据。

      相较于MVC模式的这个改变所带来的好处是,增强了我们应用的可测试性,并且提供了一个更加干净的视图和模型之间的隔离。但是在这个模式里面伴随着缺乏数据绑定支持的缺陷,这就意味着必须对这个任务做另外的处理。

      尽管被动视图实现起来普遍都是为视图和实现一个接口,但在它之上还是有差异的,包括可以更多的把视图从展示器解耦的事件的使用。由于在Javascript中我们并没有接口的构造,我们这里更多的是使用一种约定而不是一个明确的接口。技术上看它仍然是一个接口,而从那个角度对于我们而言把它作为一个接口引用可能更加说得过去一些。

    2、MVP(Passive View)的依赖关系

      MVP模式把MVC模式中的Controller换成了Presenter。MVP层次之间的依赖关系如下:

      MVP打破了View原来对于Model的依赖,其余的依赖关系和MVC模式一致。

    3、MVP(Passive View)的调用关系

      既然View对Model的依赖被打破了,那View如何同步Model的变更?看看MVP的调用关系:

      和MVC模式一样,用户对View的操作都会从View交移给Presenter。Presenter会执行相应的应用程序逻辑,并且对Model进行相应的操作;而这时候Model执行完业务逻辑以后,也是通过观察者模式把自己变更的消息传递出去,但是是传给Presenter而不是View。Presenter获取到Model变更的消息以后,通过View提供的接口更新界面

      关键点:

    (1)View不再负责同步的逻辑,而是由Presenter负责。Presenter中既有应用程序逻辑也有同步逻辑。

    (2)View需要提供操作界面的接口给Presenter进行调用。(关键)

      对比在MVC中,Controller是不能操作View的,View也没有提供相应的接口;而在MVP当中,Presenter可以操作View,View需要提供一组对界面操作的接口给Presenter进行调用;Model仍然通过事件广播自己的变更,但由Presenter监听而不是View。

    4、MVP(Passive View)的优缺点

      优点:

    (1)便于测试。Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter应用逻辑的正确性。这里根据上面的例子给出了Presenter的单元测试样例

    (2)View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的View组件。

      缺点:

    (1)Presenter中除了应用逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。

    5、MVP(Supervising Controller模式)

      上面讲的是MVP的Passive View模式,该模式下View非常Passive,它几乎什么都不知道,Presenter让它干什么它就干什么。而Supervising Controller模式中,Presenter会把一部分简单的同步逻辑交给View自己去做,Presenter只负责比较复杂的、高层次的UI操作,所以可以把它看成一个Supervising Controller。

      Supervising Controller模式下的依赖和调用关系:

      因为Supervising Controller用得比较少,对它的讨论就到这里为止。

    6、MVP还是MVC?

      MVP一般最常使用在企业级应用程序中,这样的程序中有必要对展现逻辑尽可能的重用。

      带有非常复杂的逻辑和大量用户交互的应用程序中,我们也许会发现MVC相对来说并不怎么满足需求,因为要解决这个问题可能意味着对多重控制器的重度依赖。在MVP中,所有这些复杂的逻辑能够被封装到一个展示器中,它可以显著的简化维护工作量。

      由于MVP的视图是通过一个接口来被定义的,而这个接口在技术上唯一的要点只是系统和视图(展示器除外)之间接触,这一模式也允许开发者不需要等待设计师为应用程序制作出布局和图形,就可以开始编写展现逻辑。

      根据其实现,MVP也比MVC更加容易进行自动的单元测试。为此常常被提及的理由是展示器可以被当做用户接口的完全模拟来使用,而因此它能够独立于其它组件接受单元测试。

    五、MVVM模式

      MVVM(Model View ViewModel)是一种基于MVC和MVP的架构模式,它试图将用户界面(UI)从业务逻辑和行为中更加清晰地分离出来。为了这个目的,很多例子使用声明变量绑定来把View层的工作从其他层分离出来。这促进了UI和开发工作在同一代码库中的同步进行。UI开发者用他们的文档标记(HTML)绑定到ViewModel,在这个地方Model和ViewModel由负责逻辑的开发人员维护。

      MVVM可以看作是一种特殊的MVP(Passive View)模式,或者说是对MVP模式的一种改良。

      MVVM模式最早是微软公司提出,并且了大量使用在.NET的WPF和Sliverlight中。2005年微软工程师John Gossman在自己的博客上首次公布了MVVM模式。

    1、ViewModel

      MVVM代表的是Model-View-ViewModel,这里需要解释一下什么是ViewModel。

      ViewModel的含义就是 "Model of View",视图的模型。它的含义包含了领域模型(Domain Model)和视图的状态(State)。

      在图形界面应用程序当中,界面所提供的信息可能不仅仅包含应用程序的领域模型,还可能包含一些领域模型不包含的视图状态,例如电子表格程序上需要显示当前排序的状态是顺序的还是逆序的,而这是Domain Model所不包含的,但也是需要显示的信息。

      可以简单把ViewModel理解为页面上所显示内容的数据抽象,和Domain Model不一样,ViewModel更适合用来描述View。

    2、MVVM的依赖

      MVVM的依赖关系和MVP依赖,只不过是把P换成了VM。

    3、MVVM的调用关系

      MVVM的调用关系和MVP一样。但是,在ViewModel当中会有一个叫Binder,或者是Data-binding engine的东西。以前全部由Presenter负责的View和Model之间数据同步操作交由给Binder处理。你只需要在View的模版语法当中,指令式地声明View上的显示的内容是和Model的哪一块数据绑定的。当ViewModel对进行Model更新的时候,Binder会自动把数据更新到View上去,当用户对View进行操作(例如表单输入),Binder也会自动把数据更新到Model上去。这种方式称为:Two-way data-binding,双向数据绑定。可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。

      也就是说,MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交由框架所提供的Binder进行负责。只需要告诉Binder,View显示的数据对应的是Model哪一部分即可。

    4、MVVM的优缺点

      优点:

    (1)提高可维护性。解决了MVP大量的手动View和Model同步的问题,提供双向绑定机制,提高了代码的可维护性。

    (2)简化测试。因为同步逻辑是交由Binder做的,View跟着Model同时变更,所以只需要保证Model的正确性,View就正确。大大减少了对View同步更新的测试。

      缺点:

    (1)过于简单的图形界面不适用,或说牛刀杀鸡。

    (2)对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高。

    (3)数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的。

    六、总结

      MV*模式主要解决的问题就是 View代码难以维护的问题。MV*模式将View中的逻辑分离出去,形成一个弱逻辑的易于维护的视图。MV*中的*是Model和View的桥梁,负责保持Model和View的同步。这主要是基于分层的目的,让彼此的职责分开

    1、MVC模式(Model - View - Controler)

      Controler负责视图逻辑,数据流向为: View -> Controler,Controler -> Model,Model -> View,三种呈环形结构。

      可以有2种触发view同步改变的方式:界面触发Controler、用户直接触发Controler。因为Controler去改变Model,而View是Model的观察者,Model改变就会触发View同步更新。

    2、MVP模式(Model、View、Presenter)

      Presenter负责视图逻辑,数据流向为: View <-> Presentrer, Presenter <-> Model。

      Persenter成为View和Model的中介,不允许Model和View直接通信,MVP一定程度上解决了MVC的问题,但是Presenter将会非常复杂

    3、MVVM模式(Model、View、ViewModel)

      ViewModel负责视图逻辑,数据流向为: View <-> ViewModel, ViewModel <-> Model,进化版的MVP模式,将Presenter改为ViewModel。

      其中的Model为纯数据,不包括视图元素状态(显示/隐藏,高亮,禁用/启用等)。而多数的MVVM框架,ViewModel包含Model,ViewModel是数据和业务逻辑的集合体View和ViewModel的同步是采用data-binding的形式。实际上,形成如下演变模式, View <-> ViewModel,说是VVM模式也许更贴切。

      相比MVC模式或MVP模式,MVVM只是把MVC模式的Controller或MVP模式中的Presenter改成了ViewModel。View的变化自动更新到ViewModel,ViewModel的变化也会自动同步到View上显示。

      这种自动同步是因为ViewModel中的属性实现了Observer,当属性变更时都能触发对应的操作。目前这种方式被推崇为前端领域的最佳实践。

    参考于文章:界面之下:还原真实的MV*模式

  • 相关阅读:
    图片懒加载DEMO
    手写offset函数
    DOM
    jQuery笔记
    children和 childNodes辨析
    运算符...典型的三种用处
    Python中的数据结构---栈,队列
    手写call方法
    移动零元素--leetcode题解总结
    剑指 Offer 36. 二叉搜索树与双向链表
  • 原文地址:https://www.cnblogs.com/goloving/p/14649406.html
Copyright © 2020-2023  润新知