簡聊(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 的實現進行改進,我們也會繼續保持關注,也希望更多同學加入我們一起改善前端開發的生態。