Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

jQueryでむりやりFluxをするとどうなるのか?

結論: 非常に辛い。なぜならFluxの恩恵はコンポーネント化によってもたらされるから。

jQueryのコードが汚くてつらい

こちらをご覧ください。jQueryで作ったカンタンなアプリのコードになります。

<!DOCTYPE html>
<title>jQuery</title>
<script src="https://pandanoir.net/js/jquery.slim.min.js"></script>
<script src="socket.io/socket.io.js"></script>
<script>
 jQuery($ => {
     const socket = io();
     let existsFire = false;
     let extinguishTime = 0;
     let maxRecord = 0;
     $('#explosion').html($('<img>').prop('src','http://ws.pandanoir.net/maki.png'));
     setInterval(() => {
         const rest = Math.max(0|(extinguishTime - new Date())/1000, 0);
         $('#rest-time').text(`残り${rest}秒`);
     }, 100);
     setInterval(() => {
         $('#record').text(`只今の最高記録: ${0|maxRecord}秒`);
     }, 1000);

     socket.on('info', (data) => {
         existsFire = data.existsFire;
         extinguishTime = data.extinguishTime;
         maxRecord = data.max;

         console.log(`connect [sessid = ${socket.id}]`);
     });
     socket.on('fire_extinguished', ({record}) => {
         console.log(`鎮火しました。只今の記録は${record}秒でした`);
         $('#mes').text($('#mes').text() + (`鎮火しました。只今の記録は${record}秒でした`));
         $('#explosion').html($('<img>').prop('src','http://ws.pandanoir.net/maki.png'));
     });
     socket.on('fire', (data) => {
         existsFire = true;
         extinguishTime = data.extinguishTime;
         $('#explosion').html($('<img>').prop('src','http://ws.pandanoir.net/takibi.png'));
     });
     socket.on('add_fuel', (data) => {
         existsFire = true;
         extinguishTime = data.extinguishTime;
     });
     socket.on('disconnect', () => console.log('exit !!'));
     $('#fuel').on('click', () => {
         socket.emit('add_fuel');
     });
     $('#fire').on('click', () => {
         socket.emit('fire');
     });
 });
</script>

<div id="rest-time"></div>
<div id="record"></div>
<div id="explosion"></div>
薪を足す<input type="button" id="fuel" value="fuel">
着火する<input type="button" id="fire" value="fire">
<div id="mes"></div>

…非常につらい!つらい理由はいくつかあります。

  • イベントを中心に据えているせいで、たとえば#explosion要素が変化する部分がコード中に散らばっていて把握しづらい
  • イベントハンドラーと実際の要素が離れているせいで意味を把握しづらい

ようするに、コードにコンポーネントという意味的まとまりが存在していないため読みづらくなっています。

コレを(無理やり)コンポーネント単位にまとめてみます。

<!DOCTYPE html>
<title>jQuery with component</title>
<script src="https://pandanoir.net/js/jquery.slim.min.js"></script>
<script src="socket.io/socket.io.js"></script>
<script>
 {
     // Dispatcherに相当
     const _ = jQuery('<span>');
     jQuery.extend(jQuery, {
         on: _.on.bind(_),
         off: _.off.bind(_),
         trigger: _.trigger.bind(_),
     });
 }
 const createStore = () => {
     // Dispatcherからイベントを受け取って状態を変更し、
     // storeChangedイベントを発行してViewを変更する
     const store = {
         existsFire: false,
         extinguishTime: 0,
         startTime: Infinity,
         maxRecord: 0,
     };
     $.on(`${action.addFuel} ${action.loadRemoteData}`, (_, {payload}) => {
         Object.assign(store, payload);
         store.trigger(action.storeChanged);
     });
     $.on(action.fire, (_, {payload}) => {
         Object.assign(store, payload);
         store.existsFire = true;
         store.trigger(action.storeChanged);
     });
     $.on(action.fireExtinguished, (_, {payload}) => {
         store.startTime = Infinity;
         store.maxRecord = Math.max(store.maxRecord, payload.record);
         store.trigger(action.storeChanged);
     });
     const _ = jQuery('<span>');
     jQuery.extend(store, {
         on: _.on.bind(_),
         off: _.off.bind(_),
         trigger: _.trigger.bind(_),
     });
     return store;
 };
 const action = {
     storeChanged: 'STORE.CHANGED',
     addFuel: 'ADD_FUEL',
     fire: 'FIRE',
     fireExtinguished: 'FIRE_EXTINGUISHED',
     loadRemoteData: 'LOAD_REMOTE_DATA',
 };
 const createSocket = () => {
     const socket = io.connect('http://localhost:3000');
     const trigger = (eventName) => (data) => $.trigger(eventName, {payload: data});

     socket.on('info', trigger(action.loadRemoteData));
     socket.on('fire_extinguished', trigger(action.fireExtinguished));
     socket.on('fire', trigger(action.fire));
     socket.on('add_fuel', trigger(action.addFuel));
     socket.on('disconnect', () => console.log('exit !!'));
     return socket;
 };
 jQuery($ => {
     $('<img>').prop('src','http://ws.pandanoir.net/takibi.png');
     $('<img>').prop('src','http://ws.pandanoir.net/maki.png');
     const socket = createSocket();
     const store = createStore();

     // コンポーネントたちを定義する
     // #explosion
     const explosionImageComponent = ((store) => {
         const $elm = $('<div>');
         const $img = $('<img>').appendTo($elm).prop('src','http://ws.pandanoir.net/maki.png');
         store.on(action.storeChanged, () => {
             if (store.existsFire)
                 $img.prop('src','http://ws.pandanoir.net/takibi.png');
             else
                 $img.prop('src','http://ws.pandanoir.net/maki.png');
         });
         return $elm;
     })(store);

     const restTimeComponent = ((store) => {
         const $elm = $('<div>');
         setInterval(() => {
             const rest = Math.max(Math.ceil((store.extinguishTime - new Date())/1000), 0);
             const remain = Math.max(Math.floor((+(new Date()) - store.startTime)/1000), 0);

             $elm.html(`残り${rest}秒<br>現在${remain}秒続いています`);
         }, 100);
         return $elm;
     })(store);

     const recordComponent = ((store) => {
         const $elm = $('<div>');
         store.on(action.storeChanged, () => {
             $elm.text(`只今の最高記録: ${0|store.maxRecord}秒`);
         });
         return $elm;
     })(store);

     const messageComponent = (() => {
         const $elm = $('<div>');
         const state = {
             text: []
         };
         $.on(action.fireExtinguished, (_, {payload: {record}}) => {
             state.text.push(`鎮火しました。只今の記録は${record}秒でした`);
             $elm.html(state.text.join('<br>'));
         });
         return $elm;
     })();

     const fuelButtonComponent = (() => {
         const $elm = $('<span>');
         const $button = $('<input>')
             .prop('type', 'button')
             .val('薪を足す')
             .on('click', () => socket.emit('add_fuel'));
         $elm.append($button);
         return $elm;
     })();
     const fireButtonComponent = (() => {
         const $elm = $('<span>');
         const $button = $('<input>')
             .prop('type', 'button')
             .val('着火する')
             .on('click', () => socket.emit('fire'));
         $elm.append($button);
         return $elm;
     })();

     $('body').append(
         restTimeComponent,
         recordComponent,
         explosionImageComponent,
         fuelButtonComponent,
         fireButtonComponent,
         messageComponent
     );
 });
</script>

先程と比べると、各コンポーネントの動作が明確になっています。しかし、VueやReactと比べると、どういうHTML構造になるのか分かりづらいです。やはりjQueryでFluxをするのはつらい…