簡單聊一下 One-way data flow、Two-way data binding 與前端框架

新手在學完JavaScript基本知識、離開新手村之後,很快就必須面對前端框架。

這些框架常會號稱是one-way data flow或是two-way data binding。

這兩個名詞究竟代表什麼呢?一定要有框架才能做到嗎?一個框架只能是one-way或是two-way嗎?

這篇文章會一次回答這些觀念問題,並且分別檢視一次Backbone、Angular、React三套框架。

用詞澄清

在開始之前,先針對用詞做兩點澄清。

澄清一:「data flow」跟「data binding」指的是同一件事。

換句話說,one-way data flow就是one-way data binding;two-way data binding就是two-way data flow。

這件事其實Facebook的React官方文件就有提到:

React’s one-way data flow (also called one-way binding) keeps everything modular and fast.

澄清二:這兩個詞是指稱某種行為,而不是指稱具備某種功能。

舉例來說,下面這種句子是不嚴謹、充滿誤會的:

* 某某框架「是」一個 one-way data binding 框架。 → 不嚴謹的句型,盡量少用。

正確的描述方法是這樣:

* 某某框架讓人「很容易做到」one-way data binding。 → 正確句型。

溝通上應該避免用「是」句型;用「很容易做到」句型才正確。

使用正確的句型有助於理解這兩個詞。這部份後面會再解釋。

Data model

data binding指的是UI元件和data model之間的互動。

在談data binding之前,我們先從data model的觀念開始談起。

沒有data model觀念的JavaScript/jQuery寫法

新手最直覺的作法,就是用JavaScript/jQuery不斷操作HTML元件,讓畫面上顯示他要的效果。

不同元件之間需要溝通時,就每次都去觀察HTML元件的內容,將需要的資料找到,接著處理。

舉例來說,做一個TODO list時,就用ul元件包住一堆li元件,每個li元件代表一個待辦事項。

需要增加/減少/修改事項時,就用JavaScript/jQuery想辦法找到目標li元件,然後處理它。

那麼要把所有事項內容用alert跳出來,或是用HTTP POST丟給後端怎麼辦?

那就用JavaScript/jQuery把li的內容分別抓出來,整理成字串丟給alert顯示,或是另組成form元件submit出去(用成Ajax丟出也可以)。

簡單來說,就是data本身存在於UI元件(HTML元件)之中。每次需要data就去分析一次UI元件。

這種作法簡單、直覺,在UI單純時可以這樣寫,但當頁面上的UI元件多、互動又複雜時,程式碼很快就會變成一團混亂。

有data model觀念的JavaScript/jQuery寫法

有經驗的工程師很快就會想到separation of concerns原則,將data本身獨立成自己的模型(本文稱為data model),每次顯示就根據那個data model去render出UI元件即可。

例如說像這樣(todos陣列就是data model):

    然後在button元件或是li元件的onclick事件裡面操作todos陣列,就算是操作data model了。

    此外,記得在所有關於data model的操作內呼叫renderTodoList函式。

     
    
    

    像這樣任何操作都從data model出發,最後再render的作法,可以讓data model跟UI元件保持某種對應關係,讓程式碼更好維護。

    有經驗的前端工程師,光靠這種技巧,再搭配Handlebars之類的模板系統,就可以寫出十分漂亮的程式碼。

    例如這份:jQuery TodoMVC source code

    可以反過來寫嗎?

    這個時候,就有人疑惑了:要讓data model跟UI保持對應關係,可不只這種方法。

    為何要從data model出發再render出UI?何不從UI出發再generate出data model?

    每次變動過HTML都去更新data model不可以嗎?

    其實可以,那就會像這樣:

    • Exercise
    • Learn JavaScript
    • Write a blog

    而清空列表的程式碼會變這樣:

     
    
    

    任何操作都從HTML(UI)出發,最後再generateDataModel的作法,一樣可以讓data model跟UI元件保持某種對應關係。

    Data binding

    有了data model獨立於UI的觀念之後,我們會發現上面兩種作法代表兩種資料的流向(data flow):

    * 從data model出發,每次更新就同步更新UI(甲方向)

    * 從UI出發,每次更新就同步更新data model(乙方向)

    前端框架與data binding的關係,就是框架本身能否讓人很容易就做出甲方向或是乙方向的效果。

    能輕易做出其中一個方向,就算是達成one-way data binding。

    能輕易同時做出兩方向,就算是達成two-way data binding。

    以前面Todo list的兩個範例來說,其實都做到one-way data binding了,但是我需要去多寫一個陣列、幾個函式、用了onclick事件、手動去loop過每個待辦事項、還要記得每次去呼叫函式,花了點功夫(共做5件事)才做到one-way data binding。

    所以我們不會說JavaScript本身有data binding機制,也不會說jQuery是一個讓人很容易做到one-way data binding的套件。

    這也是最前面「澄清二」提到的避免使用「是」句型的原因:

    * 說「jQuery是一個提供one-way data binding的套件」是錯誤的,因為做起來不太容易。

    * 說「jQuery不是一個提供one-way data binding的套件」也是錯誤的,因為還是做得到。

    * 說「jQuery要做到one-way data binding不太容易」就沒問題,邏輯上正確。

    如果每次更新data model就call renderTodoList,再加上,每次更新UI就call generateDataModel,那甚至做到了two-way data binding。(實務上大概不會有人這麼做。)

    說明完這兩個詞為什麼是在指稱行為而非特性,然後解釋了句型上的正確用法之後,讓我們來解讀各大前端框架的特性,以及用正確的句子評論它們。

    Backbone.js

    參考這份程式碼:http://jsfiddle.net/Xm5eH/9/

    甲方向data binding

    設定data model更新時要call render函式:

    this.model.on("change", this.render, this);
    

    運用Underscore.js提供的模板系統:

    template: _.template($("#say-template").html()),
    

    在render函式用jQuery正式將模板系統的輸出塞進HTML裡面;

    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
    }
    

    官方提供的幾個機制就達成data binding了。

    這也是為什麼很多人說「Backbone.js是一個one-way data binding框架」的原因。

    (其實正確說法應該是:「Backbone.js是一個讓人很容易做到 one-way data binding的框架」。)

    乙方向data binding

    先去監聽UI元件的變化,一變化就call update函式:

    events: {
        "change #input": "update"
    },
    

    在update函式去更新data model:

    update: function(e) {
        this.model.set("text", $(e.target).val());
        this.model.set("message", "Model value 'text' changed to '" + this.model.get('text') + "'");
    },
    

    評論

    乙方向data binding很有爭議,也造成Backbone.js社群的一些誤會與爭論。

    在某些人看來,這樣夠輕易就達成乙方向data binding了,因此「Backbone.js是一個two-way data binding框架」。

    在另外某些人看來,這只是去監聽UI內容的變化然後手動更新data model而已,因此「Backbone.js不是一個two-way data binding框架」。

    在最後某些人看來,上面甲乙兩個方向都是一堆手工設定達成,需要更優雅的去綁定兩邊才算數,所以又做了Epoxy.js這樣的套件。

    這也再次說明了為什麼要避免使用「是」句型:它帶給人們混亂、誤會與雞同鴨講。

    就說你認為「Backbone.js容不容易做到one/two-way data binding」就好了。

    因為「容不容易」是一個主觀問題,聽的人各自判斷,誰也不需要說服誰。

    Angular

    參考這份程式碼:http://plnkr.co/edit/GxqBiOoNFuECn55R4uJZ?p=preview

    甲方向data binding

    把資料設在$scope底下:

    app.controller('MainCtrl', function($scope) {
       $scope.firstName = 'John'; 
    });
    

    使用{{}}的模板syntax:

    First name: {{firstName}}

    乙方向data binding

    利用ng-model這directive就完成了:

    
    

    評論

    甲乙兩方向都很容易做到,Angular也一直以此為賣點。

    這就是一談到two-way data binding,人們就想到Angular的原因。

    話雖如此,還是老話一句:不要說「Angular是一個two-way data binding的框架」。

    說「Angular讓人很容易做到two-way data binding」比較好。

    React

    參考這份程式碼:https://jsfiddle.net/reactjs/n47gckhr/

    甲方向data binding

    用props的方式設定資料:

    var PRODUCTS = [
        {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
        {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
        {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
        {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
        {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
        {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
    ];
    
    ReactDOM.render(
        ,
        document.getElementById('container')
    );
    

    或是用state的方式設定資料:

    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },
    

    然後在render函式裡面寫獨特又優雅的jsx語法:

    render: function() {
      var rows = [];
      // 省略資料整理過程
      return (
        {rows}
    Name Price
    ); }

    一律由state或是props一層一層將data往子元件傳,超級優雅的甲方向one-way data binding寫法。

    這就是一談到one-way data flow,人們就想到React的原因。

    乙方向data binding

    寫一個監聽變化的函式,一監聽到就去更新data model:

    handleChange: function(event) {
      this.setState({message: event.target.value});
    },
    

    然後設定好onChange這個prop即可:

    render: function() {
      var message = this.state.message;
      return ;
    }
    

    評論

    乙方向data binding做得到,官方甚至提供Helpers使用,還用Two-Way Binding Helpers描述它。

    但是實務上通常會採取某種程度的Flux架構(永遠經手action和store去更新UI),因此不會使用這種Helpers。

    所以大家都說「React是one-way data flow」而不會說「React是two-way data binding」。

    話雖如此,換個方式,說「React會強迫做到one-way data flow」比較好。

    至於React跟two-way data binding的關係就不重要了。

    不過就是個做得到但實務上不常用的手法。你覺得是就是,你覺得容易就容易吧。

    結論

    透過這篇文章的脈絡,你會發現隨便套個模板系統,然後在每次data model更新時用下面任一方式重新render出UI,都算是甲方向data binding:

    * 全手動(jQuery範例的呼叫render)
    * 半手動(Backbone範例的設定模板與model on事件)
    * 自動(Angular與React)

    至於乙方向data binding就更多元了:有的看起來像是全手動更新,有的看起來像是半手動監聽事件然後update model,有的看起來像是宣告bindings即可,有的寫個ng-model的directive就完成。

    光是甲或乙方向data binding的定義就如此鬆散,自然one-way或是two-way的爭論會應運而生。

    其實,在挑選框架的時候,不管是標榜one-way或two-way的框架,背後終究都只是資料傳過來傳過去而已,差別不過在於框架本身替你做掉多少事情。

    然而,框架替你做多做少都不是重點,畢竟做多本身也代表入門越困難、convention越多。

    只要整體用起來順手、好理解、好維護,那麼任何一種寫法都是最好的寫法。

    (完)


    參考資料

    http://stackoverflow.com/questions/13504906/what-is-two-way-binding

    http://stackoverflow.com/questions/30590170/simple-practical-example-for-two-way-data-binding-in-angularjs

    (Photo via TheGiantVermin, CC licensed.)