设计理念与生态体系

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

2013年,FacebookGithub上开源了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.

小而美的视图层

ReactVueJS都是所谓小而美的视图层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中,应用利用StateProps对象实现单向数据流的传递。换言之,在一个多组件的架构中,某个父类组件只会负责响应自身的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。其实这几年浏览器更新很快,也借鉴了很多jQueryAPI,如 querySelectorquerySelectorAlljQuery选择器同样好用,而且性能更优。
  • 前端由以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"
  );
});

代码更加清晰易懂,并且对于空判断这些公共逻辑代码的提出也方便了我们进行重构或者对于业务逻辑的变化进行快速响应。未来如果我们需要添加CheckboxDialogBox等控件时,声明式的代码增加会远小于命令式,并且我们也只是需要创建新的数据流而已。具体到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的协助下我们很方便地可以将一套代码运行于iOSAndroid等多平台。总结而言,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中。这个虚拟DOMJS结构的形式存在,计算性能会比较好,而且由于减少了实际DOM操作次数,性能会有较大提升。React渲染出来的HTML标记都包含了data-reactid属性,这有助于React中追踪DOM节点。

上一页