尤川豪   ·  6年前
445 貼文  ·  275 留言

簡單聊一下 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):

<ul id='todo-list'></ul> 
<script> 
    var todos = [ 
        {text: 'Exercise.'}, 
        {text: 'Learn JavaScript.'}, 
        {text: 'Write a blog.'}, 
    ]; 

    function renderTodoList() { 
        $('#todo-list').empty(); 

        todos.map(function(el){ 
            $('#todo-list').append(new $('<li>' + el.text + '</li>')) 
        }) 
    } 

    renderTodoList(); 
</script>

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

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

<button id='clear' onclick='clearAllTodo()'>Clear all</button> 
<script> 
    function clearAllTodo() { 
        todos = []; 

        renderTodoList(); 
    } 
</script>

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

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

例如這份:jQuery TodoMVCsource code

可以反過來寫嗎?

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

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

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

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

<ul id='todo-list'> 
    <li>Exercise</li> 
    <li>Learn JavaScript</li> 
    <li>Write a blog</li> 
</ul> 
<script> 
    var todos = []; 

    function generateDataModel() { 
        todos = []; 
        $('#todo-list li').each(function(i, el){ 
            todos.push({text: el.innerText}); 
        }) 
    } 

    generateDataModel(); 
</script> 

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

<button id='clear' onclick='clearAllTodo()'>Clear all</button> 
<script> 
    function clearAllTodo() { 
        $('#todo-list').empty(); 

        generateDataModel(); 
    } 
</script>

任何操作都從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:

<span>First name:</span> {{firstName}}<br />

乙方向data binding

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

<input type="text" ng-model="firstName"/>

評論

甲乙兩方向都很容易做到,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(
    <FilterableProductTable products={PRODUCTS} />,
    document.getElementById('container')
);

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

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

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

render: function() {
  var rows = [];
  // 省略資料整理過程
  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

一律由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 <input type="text" value={message} onChange={this.handleChange} />;
}

評論

乙方向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越多。

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

(完)


refs:

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

  分享   共 15,557 次點閱
按了喜歡:
共有 5 則留言
尤川豪   ·  6年前
445 貼文  ·  275 留言

這是我自己在 2016 年寫的文章 跟各位分享

http://blog.turn.tw/?p=2948

 
按了喜歡:
Eklipsorz   ·  3年前
0 貼文  ·  1 留言

這篇文章真的易懂,感謝大大寫了這篇,讓我更加了解它們XD

 
按了喜歡:
caesar.minfeng.tsai   ·  3年前
0 貼文  ·  2 留言

非常清楚 請問這篇文章提到的 data model 應該想成 MVVM 哪個部份呢? 我自己感覺是 VM

 
尤川豪   ·  3年前
445 貼文  ·  275 留言

我不知道 😅

我不熟悉 MVVM 的觀念 😅😅

應該是 model 或 VM 吧 😅😅😅

   ·  1年前
0 貼文  ·  1 留言

謝謝大大的文章,非常清楚易懂。 我讀完後的感覺是 每一個框架(e.g. Angular, React, Vue, etc), 其實都可以在自身框架裡做到 one-way data binding 或 two-way data binding, 只是看開發者要不要這樣做而已 😅 。

 
您的留言
尤川豪
445 貼文  ·  275 留言

Devs.tw 是讓工程師寫筆記、網誌的平台。隨手紀錄、寫作,方便日後搜尋!

歡迎您一起加入寫作與分享的行列!

查看所有文章