用 Virtual DOM 加速开发

简聊(by Teambition)产品前端中使用了 React,最初开发时使用的 Backbone 搭配 doT.js 模版渲染界面,实践下来效果提升了很多。我们希望能吸引更多同学能够运用 Virtual DOM 改进前端开发,所以这篇文章会主要介绍 React 当中 Virtual DOM 相关的知识。

传统的 HTML 模版引擎

在这之间先看下传统模版引擎的优劣。前端模版大多是跟服务端模板一致,如果底层语言是 JavaScript 的话,比如 EJS,那么模版引擎可以在前端和后端共用代码。模版的写法比如 Handlebars.js 类似 HTML 的语法,在其中增加 {{}} 作为插值的语法:

    <div class="entry">  
      <h1>{{title}}</h1>
      <div class="body">
        {{body}}
      </div>
    </div>  

然而前端不像后端那样具备访问文件的能力,因此在模版的组件化方面会有不足。比如 include header.html 的写法,对于前端来说会有一些难度。组件化对于代码重用来说还是非常必要的,所以我们经常对浏览器端模版要采取一些手段,大到 HTML 片段嵌入进行重用的目的,比如:

将子模版生成的 HTML 作为数据嵌入到更大的模版当中 使用 DOM 操作的手法,将子模板生成 DOM,再插入到父节点 修改前端模版引擎的实现,支持模版的嵌套,比如我们的 mejs 还有一些细节,因为模版引引擎使用的是字符串,导致效果不好。比如最初使用 doT.js 时模版当中的换行缩进经常会在生成的 HTML 中形成空白,干扰到图标文字细节的效果。同时,我们还要提防模版当中的 JavaScript 注入,如果模板引擎处理得不好,细节就带来麻烦。基于字符串的模板引擎,它的实现常常是字符串到 JavaScript 简单的编译,并不能达到特别好的效果。在我们看来,模版引擎不是信服的解决方案。

React 的 JSX

这篇文章主要是关于 React 的,所以直接开始介绍 JSX。如果你对 React 了解不多,可以先看看阮一峰写的入门教程,你可以看到 React 在 JavaScript 代码当中嵌入了类似 XML 的写法,其中还有 {value} 这样花括号的插值。通过这样的写法,React 实现了 DOM 的渲染以及组件化。

var HelloWorld = React.createClass({  
  render: function() {
    return (
      <p>
        Hello, <input type="text" placeholder="Your name here" />!
        It is {this.props.date.toTimeString()}
      </p>
    );
  }
});

setInterval(function() {  
  React.render(
    <HelloWorld date={new Date()} />,
    document.getElementById('example')
  );
}, 500);

JSX 规范是 Facebook 提出的一套 ECMAScript 的扩展,为了写 ES6 代码中自由插入 XML 标签用于生成 DOM。同时也明确承认 JSX 并不是为了进入 ECMAScript 当中,而是希望由各种 JavaScript 预编译器来处理,比如 Babel 目前已经支持 JSX 的编译,而 Facebook 未来也很可能弃用旧的方案转而使用 Babel 编译 JSX。除了编译工具,其他比如编辑器支持,打包工具,整个 JSX 的生态基本已经成熟。

JSX 生成的 Virtual DOM

我们可以参照官方文档熟悉一下 JSX 是怎样编译成 JavaScript 代码的。比如下面的代码是个例子:

var Nav;  
// Input (JSX):
var app = <Nav color="blue" />;  
// Output (JS):
var app = React.createElement(Nav, {color:"blue"});  

JSX 中的元素会编译为 createElement 函数方法调用,第一个参数是标签名,采用首字母大写与浏览器原生的标签做区分,第二个参数是个对象,表示元素的属性。其实还可以写更多的参数,作为生成元素的子节点,比如这样:

var child1 = React.createElement('li', null, 'First Text Content');  
var child2 = React.createElement('li', null, 'Second Text Content');  
var root = React.createElement('ul', { className: 'my-list' }, child1, child2);  
React.render(root, document.getElementById('example'));  

同时由于存在编译过程,实际上 JSX 中的标签并不是和 HTML 完全一致的,比如 className 和 htmlFor 的写法遵循 JavaScript,而不是 HTML 字符串形式中的 class 和 for。同时属性会被 React 过滤,比如事件绑定,只有给定的一些属性才会完成绑定,这中间有兼容性的考虑,事实上 React 也是对 DOM 的事件系统做了一些封装和 Polyfill 的,不过这篇文章不能展开讲了。

JSX 一些细节

同时 React 提供了一个函数 createFactory 用来更方便地创建元素:

ReactElement.createFactory = function(type) {  
  var factory = ReactElement.createElement.bind(null, type);
  factory.type = type;
  return factory;
};

这样在创建元素时写法可以更灵活一些,比如说创建 div 元素,就可以不用每次都使用 createElement 函数,而是直接用下面这样的写法进行创建:

var Factory = React.createFactory(ComponentClass);  
var root = Factory({ custom: 'prop' });  
React.render(root, document.getElementById('example'));  

这样的写法在某些场景可以带来方便,特别是绕过 JSX 转而使用 CoffeeScrpt 编写 React 组件的时候,可以帮你写出简洁明了的代码:

React = require 'react'

div = React.createFactory 'div'

module.exports = React.createClass  
  displayName: 'loading-indicator'

  render: ->
    div className: 'loading-indicator',
      div className: 'loader-dot'
      div className: 'loader-dot'
      div className: 'loader-dot'

当你从 Node.js 环境的命令行去查看 render 函数中生成的对象,会发现这仅仅是一个 JSON 结构的对象,并不是 DOM 对象,也不是 HTML 字符串,或者更像我们想要说的 Virtual DOM,其中的细节会是这样的:

> a = React.createElement('div', {})
{ type: 'div',
  key: null,
  ref: null,
  _owner: null,
  _context: {},
  _store: { props: {}, originalProps: {} } }
> b = React.createElement('div', {}, a)
{ type: 'div',
  key: null,
  ref: null,
  _owner: null,
  _context: {},
  _store:
   { props: { children: [Object] },
     originalProps: { children: [Object] } } }

灵活性, 应用性

在 React 中 Virtual DOM 是具体实现当中的核心部分。React 的组件会渲染成为 Virtual DOM,就像在上边的例子上看到的一棵 JSON 对象的树一样,其中不仅包含了元素,也包含了 React Component 的节点。React 组件当中的 props 或者 state 发生改变之后,Virtual DOM 会被重新生成,随后,React 会对 Virtual DOM 进行 Diff,找到与上一次 DOM 更新时相比树的差别,最后在真实的 DOM Tree 上进行高效的操作让新的 DOM Tree 与数据保持一致。具体可以看关于 DOM Diff 算法的文章。

从 Virtual DOM 生成 DOM 带来了一些好处,可以和前面说的字符串模版引擎做一些对比:

  • 标签之间的空格从一开始就被排除,不会带来意外的空白
  • 元素的字符串被自动转义,不会那么容易导致代码注入
  • JSON 的 Tree 很容易组合,组件化很自然就实现出来了
  • 同时,JavaScript 可以在 Node.js 和浏览器同时运行,能够共享代码
  • 同时得益于 DOM 会自动完成 Diff 和 Patch,DOM 树就能够自动随着数据改变进行更新,更新 DOM 不再像是基于字符串模板引擎的应用那么麻烦了。无论是使用双向绑定还是 Virtual DOM,或者其他混搭风格的方案,我们终于可以摆脱 jQuery 那样的手动更新 DOM 的烦恼了。就像是 MVC 描述的那样,Model 被更新,View 就跟着更新,就这么简单。

Virtual DOM 也激发了一些很棒的新想法,比如最近火热的 React Native,Virtual DOM 被 Objective-C 底层代码转义为 iOS 环境中的 View,从前让 JavaScript 能够编写 iOS 应用。除了 iOS,还有 Canvas,还有 WebGL,比如 react-canvas,react-three,react-pixi,react-art,react-famous。很有可能未来引发更加激动人心的方案浮现。

总结

了解到 Virtual DOM 带来的前端 MVC 应用开发的方便,简聊选择了 React 作为前端的 View 框架。通过编写声明式的 JSX 元素的结构,就像是模板引擎,通过编写组件,并且将组件不断进行组合,最终拼装成整个应用。相对于模板引擎而言,省去了很多很多琐碎的操作,应用的结构也更加明确。也很开心看到 React 被 Strikingly,被天猫这样的技术团队认定作为支撑产品的重要方案。

除了 React,社区还有其他的 Virtual DOM的实现,比如:Om, mercury, Riot.js, deku,尝试从不同方面尝试对 Virtual DOM 的实现进行改进,我们也会继续保持关注,也希望更多同学加入我们一起改善前端开发的生态。