设计理念与生态体系

React 设计理念:专注于视图层的组件库

2013 年,Facebook 在 Github 上开源了 React,一个专注于解决视图层的库,为当时很多苦于 Angular 1 性能问题的开发者指明了另一条路,也成为了近十年来前端领域发生的最大变革之一。React 提供了一些新颖的概念、库和编程原则让你能够同时在服务端和客户端编写快速、紧凑、漂亮的代码来构建 Web 应用。从笔者数年来深入实践 React 技术栈的感受而言,React 除却本身将用户界面抽象为组件树之外更大的意义在于将 ES6、模块化与打包、组件化等等一系列工程化所需要的必要因素引入了前端领域,使前端领域从刀耕火种的原始时代慢慢过渡到各种工具百花齐放的时代。在 React 学习的过程中,我们可能会接触到如下技术概念或者原则,希望阅读完本书的读者能够对这些概念都形成自己的理解。

  • ES6 React
  • Virtual DOM:虚拟 DOM
  • Component-Driven Development:组件驱动开发
  • Immutability:不变性
  • Top-down Rendering:自上而下的渲染
  • 渲染路径与优化
  • 打包工具、构建请求、调试、路由等
  • Isomorphic Application:同构应用

Here are a few reasons why React has become so popular so quickly:

  • Working with the DOM API is hard. React basically gives developers the ability to work with a virtual browser that is faster and more friendly than the real browser. React’s virtual browser acts like an agent between the developer and the real browser.
  • React enables developers to declaratively describe their User Interfaces and model the state of those interfaces. This means instead of coming up with steps to describe transactions on interfaces, developers just describe the interfaces in terms of a final state (like a function). When transactions happen to that state, React takes care of updating the User Interfaces based on that.
  • React is just JavaScript, there is a very small API to learn, just a few functions and how to use them. After that, your JavaScript skills are what make you a better React developer. There are no barriers to entry. A JavaScript developer can become a productive React developer in an hour or so.

But there’s a lot more to it than just that. Let’s attempt to cover all the reasons behind React’s rising popularity. One reason is its Virtual DOM (React’s reconciliation algorithm). We’ll work through an example to show the actual practical value of having such an algorithm at your command.

小而美的视图层

React 与 VueJS 都是所谓小而美的视图层 Library,而不是 Angular 2 这样兼容并包的 Frameworks。任何一个编程生态都会经历三个阶段,第一个是原始时期,由于需要在语言与基础的 API 上进行扩充,这个阶段会催生大量的 Tools。第二个阶段,随着做的东西的复杂化,需要更多的组织,会引入大量的设计模式啊,架构模式的概念,这个阶段会催生大量的 Frameworks。第三个阶段,随着需求的进一步复杂与团队的扩充,就进入了工程化的阶段,各类分层 MVC,MVP,MVVM 之类,可视化开发,自动化测试,团队协同系统。这个阶段会出现大量的小而美的 Library。

React 并没有提供很多复杂的概念与繁琐的 API,而是以最少化为目标,专注于提供清晰简洁而抽象的视图层解决方案,同时对于复杂的应用场景提供了灵活的扩展方案,典型的譬如根据不同的应用需求引入 MobX/Redux 这样的状态管理工具。React 在保证较好的扩展性、对于进阶研究学习所需要的基础知识完备度以及整个应用分层可测试性方面更胜一筹。不过很多人对 React 的意见在于其陡峭的学习曲线与较高的上手门槛,特别是 JSX 以及大量的 ES6 语法的引入使得很多的传统的习惯了 jQuery 语法的前端开发者感觉学习成本可能会大于开发成本。与之相比 Vue 则是典型的所谓渐进式库,即可以按需渐进地引入各种依赖,学习相关地语法知识。比较直观的感受是我们可以在项目初期直接从 CDN 中下载 Vue 库,使用熟悉的脚本方式插入到 HTML 中,然后直接在 script 标签中使用 Vue 来渲染数据。随着时间的推移与项目复杂度的增加,我们可以逐步引入路由、状态管理、HTTP 请求抽象以及可以在最后引入整体打包工具。这种渐进式的特点允许我们可以根据项目的复杂度而自由搭配不同的解决方案,譬如在典型的活动页中,使用 Vue 能够兼具开发速度与高性能的优势。不过这种自由也是有利有弊,所谓磨刀不误砍材工,React 相对较严格的规范对团队内部的代码样式风格的统一、代码质量保障等会有很好的加成。一言蔽之,笔者个人觉得 Vue 会更容易被纯粹的前端开发者的接受,毕竟从直接以 HTML 布局与 jQuery 进行数据操作切换到指令式的支持双向数据绑定的 Vue 代价会更小一点,特别是对现有代码库的改造需求更少,重构代价更低。而 React 及其相对严格的规范可能会更容易被后端转来的开发者接受,可能在初学的时候会被一大堆概念弄混,但是熟练之后这种严谨的组件类与成员变量/方法的操作会更顺手一点。便如 Dan Abramov 所述,Facebook 推出 React 的初衷是为了能够在他们数以百计的跨平台子产品持续的迭代中保证组件的一致性与可复用性。

声明式组件

在 React 中,应用利用 State 与 Props 对象实现单向数据流的传递。换言之,在一个多组件的架构中,某个父类组件只会负责响应自身的 State,并且通过 Props 在链中传递给自己的子元素。

命令式编程与声明式编程

命令式编程(Imperative Programming)着眼于控制流,主要的代码会用于描述达成目标所需的特定操作步骤,即是直观地表现如何去做。声明式编程(Declarative Programming)则是将控制流抽象出来,主要的代码会用于描述某些独立的操作,即是表现做什么,不同操作之间往往不会强耦合。举例而言,我们希望将某个数组中的数值乘以 2,并且返回新的数组,使用命令式编程的做法如下:

const doubleMap = (numbers) => {
  const doubled = [];
  for (let i = 0; i < numbers.length; i++) {
    doubled.push(numbers[i] * 2);
  }
  return doubled;
};

console.log(doubleMap([2, 3, 4])); // [4, 6, 8]

而使用声明式编程来实现相同的操作时,我们首先会将变换的过程抽象出来,以更清晰的方式描述这个过程:

const doubleMap = (numbers) => numbers.map((n) => n * 2);

console.log(doubleMap([2, 3, 4])); // [4, 6, 8]

命令式编程中我们会频繁地使用声明语句(Statements),每个声明语句都是执行某些操作的代码片,常用的声明语句包括forifswitchthrow这些。而函数式编程更加依赖于表达式(Expressions),每个表达式都会用于计算某些值,常见的表达式包括函数调用、函数组合等计算某些值的过程。典型的表达式的例子包括:

2 * 2;
doubleMap([2, 3, 4]);
Math.max(4, 3, 2);

jQuery

jQuery 作为了影响一代前端开发者的框架,是前端工具的典型代表,它留下了璀璨的痕迹与无法磨灭的脚印。笔者在这里以 jQuery 作为一个符号,来代表以 DOM 节点的操作为核心的一代的前端开发风格。那个年代里,要插入数据或者更改数据,都是直接操作 DOM 节点,或者手工的构造 DOM 节点。譬如从服务端获得一个用户列表之后,会通过构造<i>节点的方式将数据插入到 DOM 树中。jQuery 这个框架本身非常的优秀并且在不断的完善中,但是它本身的定位,作为早期的跨浏览器的工具类屏蔽层在今天这个浏览器 API 逐步统一并且完善的今天,逐渐不是那么关键。因此,笔者认为 jQuery 会逐渐隐去的原因可能为:

  • 现代浏览器的发展与逐步统一的原生 API:由于浏览器的历史原因,曾经的前端开发为了兼容不同浏览器怪癖,需要增加很多成本。jQuery 由于提供了非常易用的 API,屏蔽了浏览器差异,极大地提高了开发效率。这也导致很多前端只懂 jQuery。其实这几年浏览器更新很快,也借鉴了很多 jQuery 的 API,如 querySelectorquerySelectorAll 和 jQuery 选择器同样好用,而且性能更优。
  • 前端由以 DOM 为中心到以数据/状态为中心:jQuery 代表着传统的以 DOM 为中心的开发模式,但现在复杂页面开发流行的是以 React 为代表的以数据/状态为中心的开发模式。应用复杂后,直接操作 DOM 意味着手动维护状态,当状态复杂后,变得不可控。React 以状态为中心,自动帮我们渲染出 DOM,同时通过高效的 DOM Diff 算法,也能保证性能。
  • 不支持同构渲染与跨平台渲染:React Native 中不支持 jQuery。同构就是前后端运行同一份代码,后端也可以渲染出页面,这对 SEO 要求高的场景非常合适。由于 React 等流行框架天然支持,已经具有可行性。当我们在尝试把现有应用改成同构时,因为代码要运行在服务器端,但服务器端没有 DOM,所以引用 jQuery 就会报错。这也是要移除 jQuery 的迫切原因。同时不但要移除 jQuery,在很多场合也要避免直接操作 DOM。
  • 性能缺陷:jQuery 的性能已经不止一次被诟病了,在移动端兴起的初期,就出现了 Zepto 这样的轻量级框架,Angular 1 也内置了 jqlite 这样的小工具。前端开发一般不需要考虑性能问题,但你想在性能上追求极致的话,一定要知道 jQuery 性能很差。原生 API 选择器相比 jQuery 丰富很多,如 document.getElementsByClassName 性能是 $(classSelector) 的 50 多倍!

对于很多初学 React 的开发者可能会疑问,到底是否应该继续使用 jQuery?笔者虽然不提倡使用 jQuery,但是也不完全否定可以使用 jQuery,还是需要根据具体的应用场景与控件需求来决定。

声明式组件

我们在上文中提到过,声明式编程的核心理念在于描述做什么,通过声明式的方式我们能够以链式方法调用的形式对于输入的数据流进行一系列的变换处理,本部分我们还是以 jQuery 为例,阐述命令式编程与声明式编程在 Web 前端开发中的实际应用对比。譬如我们以 jQuery 开发简单的登录界面:

jQuery(function ($) {
  var username = "";
  var password = ""; // Disable the button at start

  $("#signup-button").attr("disabled", true); // Email field

  $("#email-field").on("blur", function () {
    username = $(this).val();
    if (username == "") {
      $("#email-error").html("Please enter email address");
      $("#signup-button").attr("disabled", true);
    } else {
      checkValues();
    }
  }); // Password field

  $("#password-field").on("blur", function () {
    password = $(this).val();
    if (password == "") {
      $("#password-error").html("Please enter password");
      $("#signup-button").attr("disabled", true);
    } else {
      checkValues();
    }
  }); // Both fields

  function checkValues() {
    if (username != "" && password != "") {
      $("#email-error").html("");
      $("#password-error").html("");
      $("#signup-button").attr("disabled", false);
    }
  }
});

上述代码中我们以符合平时思维逻辑的、声明语句形式的方式描述了整个业务逻辑,这就是典型的命令式编程思想。不过这种方式也显而易见的存在很多的代码冗余,导致整体的可读性与重构性降低,譬如邮箱与密码这两个输入域都会在失去焦点时进行验证,并且判断是否设置按钮失效。而在声明式编程中,我们可以将公用的部分业务逻辑代码,即是偏向于计算的、表达式形式的代码剥离出来,可以得到如下的封装:

jQuery(function ($) {
  function checkIfEmpty(e) {
    return !e.target.value;
  }
  function checkIfBothEmpty(noEmail, noPass) {
    return noEmail || noPass;
  }

  function getEmailMessage(noEmail) {
    return noEmail ? "Please enter email address." : "";
  }

  function getPasswordMessage(noPassword) {
    return noPassword ? "Please enter password." : "";
  } // Email field

  var email = $("#email-field").asEventStream("blur").map(checkIfEmpty);
  email.map(getEmailMessage).assign($("#email-error"), "html"); // Password field

  var password = $("#password-field").asEventStream("blur").map(checkIfEmpty);
  password.map(getPasswordMessage).assign($("#password-error"), "html"); // Both fields

  Bacon.combineWith(checkIfBothEmpty, email, password).assign(
    $("#signup-button"),
    "attr",
    "disabled"
  );
});

代码更加清晰易懂,并且对于空判断这些公共逻辑代码的提出也方便了我们进行重构或者对于业务逻辑的变化进行快速响应。未来如果我们需要添加 Checkbox、DialogBox 等控件时,声明式的代码增加会远小于命令式,并且我们也只是需要创建新的数据流而已。具体到 React 中,我们的任何组件都可以以声明式的语法表述,譬如我们要写包含多个选项的选择控件时:

<select value={this.state.value} onChange={this.handleChange}>
  {somearray.map((element) => (
    <option value={element.value}>{element.text}</option>
  ))}
</select>

我们并不需要手动地使用for循环来控制元素生成,这也就是声明式的精髓。

JSX

很多人第一次学习 React 的时候都会觉得 JSX 语法看上去非常怪异,这种背离传统的 HTML 模板开发方式真的靠谱吗?(在 2.0 版本中 Vue 也引入了 JSX 语法支持)。我们并不能单纯地将 JSX 与传统的 HTML 模板相提并论,JSX 本质上是对于React.createElement函数的抽象,而该函数主要的作用是将朴素的 JavaScript 中的对象映射为某个 DOM 表示。其大概思想图示如下:

在现代浏览器中,对于 JavaScript 的计算速度远快于对 DOM 进行操作,特别是在涉及到重绘与重渲染的情况下。并且以 JavaScript 对象代替与平台强相关的 DOM,也保证了多平台的支持,譬如在 ReactNative 的协助下我们很方便地可以将一套代码运行于 iOS、Android 等多平台。总结而言,JSX 本质上还是 JavaScript,因此我们在保留了 JavaScript 函数本身在组合、语法检查、调试方面优势的同时又能得到类似于 HTML 这样声明式用法的便利与较好的可读性。

Virtual DOM

如我们所知,在浏览器渲染网页的过程中,加载到 HTML 文档后,会将文档解析并构建 DOM 树,然后将其与解析 CSS 生成的 CSSOM 树一起结合产生爱的结晶——RenderObject 树,然后将 RenderObject 树渲染成页面(当然中间可能会有一些优化,比如 RenderLayer 树)。这些过程都存在与渲染引擎之中,渲染引擎在浏览器中是于 JavaScript 引擎(JavaScriptCore 也好 V8 也好)分离开的,但为了方便 JS 操作 DOM 结构,渲染引擎会暴露一些接口供 JavaScript 调用。由于这两块相互分离,通信是需要付出代价的,因此 JavaScript 调用 DOM 提供的接口性能不咋地。各种性能优化的最佳实践也都在尽可能的减少 DOM 操作次数。而虚拟 DOM 干了什么?它直接用 JavaScript 实现了 DOM 树(大致上)。组件的 HTML 结构并不会直接生成 DOM,而是映射生成虚拟的 JavaScript DOM 结构,React 又通过在这个虚拟 DOM 上实现了一个 diff  算法找出最小变更,再把这些变更写入实际的 DOM 中。这个虚拟 DOM 以 JS 结构的形式存在,计算性能会比较好,而且由于减少了实际 DOM 操作次数,性能会有较大提升。React 渲染出来的 HTML 标记都包含了data-reactid属性,这有助于 React 中追踪 DOM 节点。

上一页