从MVC到React:深入理解虚拟DOM与单向数据流

作者:小齿轮

时间:2026-10-30

浏览:289

本文深入解析React框架的核心原理,对比传统MVC与数据绑定模式的局限性,阐述虚拟DOM如何通过最小化DOM操作提升性能,并解释JSX语法与单向数据流如何简化状态管理。

文章目录

引言:从误解到认可

如果在两个月前有人询问我对React的看法,我的回答很可能是:

  • 我的模板在哪里?JavaScript中处理HTML的方式为何如此怪异?JSX看起来非常别扭!最好直接弃用它。

这是因为当时我并未真正理解它。

但我现在确信,React确实走在正确的技术轨道上,请听我详细解释。

传统MVC架构的局限性

回顾经典的MVC模式

在构建交互式应用程序时,状态管理的复杂性往往是问题的根源。

传统的解决方案是采用MVC架构或其变体。

MVC的核心观点是:模型(Model)是数据的唯一真理来源,所有状态都存储于此。视图(View)由模型派生而来,必须与模型保持同步。当模型发生变化时,视图也必须随之更新。

引言:从误解到认可

当模型数据发生变动时,系统需要对视图进行重新渲染。

这听起来似乎很简单:首先描述视图,即如何将模型状态转换为DOM结构;然后,当用户执行操作时更新模型,并重新渲染整个页面……真的这么简单吗?并非如此。不幸的是,这种做法存在两个主要缺陷:

  • DOM本身具有某种状态,例如文本输入框中的用户输入内容。如果完全销毁并重新渲染DOM,这些输入内容将会丢失。
  • DOM操作(如删除和插入节点)非常耗时。频繁的渲染会导致严重的性能问题。

数据绑定机制及其问题

那么,我们如何在避免上述问题的同时保持模型与视图的同步呢?答案是数据绑定。

在过去三年中,解决这一问题的最常用框架功能就是数据绑定。

数据绑定能够自动保持模型与视图的同步,在JavaScript中通常体现为对象与DOM之间的关联。

它通过声明应用中各模块之间的依赖关系来实现这种同步。当状态发生变化时,这种变化会蔓延至整个应用程序,所有依赖模块都会自动更新。

让我们来看看几个知名框架中数据绑定的实际运作方式。

Knockout框架

Knockout采用MVVM(模型-视图-视图模型)方法,并帮你实现了“视图”部分:


// View (a template)
<p>First name: <input data-bind="value: firstName" /></p> 
<p>Last name: <input data-bind="value: lastName" /></p> 
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
// ViewModel (diplay data... and logic?)
var ViewModel = function(first, last) { 
 this.firstName = ko.observable(first);
 this.lastName = ko.observable(last);
 this.fullName = ko.pureComputed(function() {
   // Knockout tracks dependencies automatically. It knows that fullName depends on firstName and lastName, because these get called when evaluating fullName.
   return this.firstName() + " " + this.lastName();
 }, this);
};

就是这样。无论改变哪个输入值,span中的内容都会随之变化。你无需编写任何绑定代码。这难道不酷吗?

但是等等,模型不是真理的来源吗?这里的视图模型的状态从何而来?它是如何感知模型变化的?这是一个有趣的问题。

Angular框架

Angular描述了保持模型与视图同步的数据绑定方式。其文档如此描述:

引言:从误解到认可

但是……视图应该直接与模型交互吗?这样它们不就紧密耦合了吗?

无论如何,我们还是来义务性地看看hello world示例吧:


// View (a template) 
<div ng-controller="HelloController as hello"> 
 <label>Name:</label>
 <input type="text" ng-model="hello.firstName">
 <input type="text" ng-model="hello.lastName">
 <h1>Hello {{hello.fullName()}}!</h1>
</div>
 
// Controller 
angular.module('helloApp', []) 
.controller('HelloController', function() {
 var hello = this;
 hello.fullName = function() {
  return hello.firstName + hello.lastName;
 };
});

从这个示例中可以看出,控制器似乎拥有状态,并具有类似模型的行为——或者也许是一个视图模型?假设模型位于其他地方,它是如何与控制器保持同步的呢?

我的头开始有点疼了。 数据绑定的问题

数据绑定在小规模示例中运行良好。然而,随着应用规模的扩大,你可能会遇到以下问题。

声明的依赖关系很快会引入循环。

最常处理的问题是应对状态变化的副作用。这张图来自Flux介绍,它解释了依赖关系是如何开始产生问题的:

引言:从误解到认可

你能预见到当一个模型发生变化时随之发生什么改变吗?当依赖关系发生变化时,由于代码可以任意次序执行,你很难推理出问题的起因。 模板与展示逻辑被人为地分离

视图扮演了什么角色呢?它的角色是向用户展示数据。视图模型扮演的角色又是什么呢?它的角色也是向用户展示数据?有何不同?完全没有!

  • 毫无疑问,模板割裂了计数 ~ Pete Hunt

最后,视图组件应该能够操作其数据并以所需的格式展示数据。然而,所有的模板语言本质上都是有缺陷的:它们永远无法达到与代码相同的表现力和功能。

很简单,{{# each}}、ng-repeat和databind="foreach"这些都是针对JavaScript中某些原生和琐碎事务的拙劣替代品。它们不会更进一步提供过滤器或映射等功能。

数据绑定是应重新渲染而生的小技巧

什么是圣杯不再我们的讨论之列。每个人总是想要得到的是,当状态发生变化时能重新对整个应用进行渲染。这样,我们就不用去处理所有麻烦问题的根源了:状态总是会随着时间发生变化——给定任何特定的状态,我们就可以简单的描述出应用回是什么样子的。

React的核心原理:虚拟DOM与JSX

好了,问题清楚了。哥们,我希望某些大公司能组个超能天才开发者团来真正解决这个问题... 拥抱Facebook的React

事实证明他们做到了。React实现了一个虚拟的DOM,一种给我们带来的圣杯的利器. 虚拟的DOM是啥东西呢?

很高兴你能这么问?让我们来看看一个简单React示例.


var Hello = React.createClass({ 
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});
React.render(<Hello name="World" />, document.getElementById('container'));

这就是一个React组件的所有API。你必须要有一个渲染方法。复杂吧,呵呵?

OK, 但是 <div> 是什么意思? 那不是 JavaScript 啊! 对了,它就不是.

你的新伙伴,JSX

这段代码实际上是用 JSX 写的,它是 JavaScript 的一个超集,包含了用于定义组件的语法。上面的代码会被编译成 JavaScript,因此实际上会变成:


var Hello = React.createClass({displayName: "Hello", 
  render: function() {
    return React.createElement("div", null, "Hello ", this.props.name);
  }
});
React.render(React.createElement(Hello, {name: "World"}), document.getElementById('container'));

你明白这段对 createElement 调用的代码么? 这些对象组成了虚拟 DOM 的实现。

很简单 : React 首先在内存中对应用的整个结构进行了组装。然后它会把这个结构装换成实际的 DOM 节点并将其插入浏览器的 DOM 中。

OK,但是用这些奇怪的 createElement 函数编写 HTML 的目的是什么呢? 虚拟的DOM就是快

我们已经讨论过, 操作 DOM 消耗大得离谱,因此它必须以尽可能少的时间完成。

React 的虚拟 DOM 使得两棵 DOM 结构的比对真正快起来,并且能确切的找到它们之间有什么变化. 如此,React 就能计算出更新 DOM 所需要做出的最小变更。

实话说,React 能比对两棵 DOM 树,找出它所要执行的最小操作集。这有两个意义:

  1. 如果一个带有文本的输入框被重新渲染,React 会知道它有的内容, 它不会碰那个碰那个输入框。不会有状态发生丢失的!
  2. 比对虚拟 DOM 开销一点也不昂贵,因此我们想怎么比对都可以。当其准备好要对 DOM 进行实际的修改时,它只会进行最少量的操作。没有额外的拖慢布局之虞!

那我们还要在状态发生变化时记住这两个对整个 app 进行重新渲染的问题么?

这都是过去式了。

React 将状态映射到 DOM

React 中只有虚拟 DOM 的渲染和比对是神奇的部分。其优秀性能是使得我们拥有简化了许多的整理架构的基础。有多简单呢?

React 组件都是幂等(一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同)的函数。它们能在任意一个实时的点来描述你的UI。~ Pete Hunt, React: 对最佳实践的重新思考

简单的幂等函数。

React 组件整个就是这么一个东西,真的。它将当前的应用状态映射到了 DOM。并且你也拥有JavaScript的全部能力去描述你的 UI——循环,函数,作用域,组合,模块 - 不是一个蹩脚的模板语言哦.


var CommentList = React.createClass({ 
 render: function() {
  var commentNodes = this.props.data.map(function (comment) {
   return (    <Comment author={comment.author}>
     {comment.text}    </Comment>
   );
  });
  return (   <div className="commentList">
    {commentNodes}   </div>
  );
 }
});
 
var CommentBox = React.createClass({ 
 render: function() {
  return (   <div className="commentBox">
    <h1>Comments</h1>
    <CommentList data={this.props.data} />
   </div>
  );
 }
});
 
React.render( 
 <CommentBox data={data} />,
 document.getElementById('content')
);

总结与建议

今天就开始使用 React

React 一开始会有点令人生畏。它提出了一个实在是太大了点的模式转变,这总有点令人不舒服。不过,当你开始使用它时其优势会变得清楚起来。

React 文档很优秀. 你应该照着教程对其进行一下尝试。我确信如果你给它一个机会,你肯定会爱上她。

编码快乐!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章