รีแอคทีฟขั้นสูง

บทแทรก 11.5

แปลไปแล้ว

ในบทนี้ คุณจะได้

  • เรียนรู้ว่าจะสร้างแหล่งข้อมูลแบบรีแอคทีฟได้อย่างไรใน Meteor
  • สร้างตัวอย่างง่ายๆของแหล่งข้อมูลรีแอคทีฟ
  • ดูว่า Tracker ต่างจาก AngularJS อย่างไร
  • มันดูเหมือนไม่ค่อยจำเป็นเท่าไหร่นักที่คุณจะเขียนโค้ดแบบที่มีการติดตามความเชื่อมโยง (dependency tracking) ด้วยตัวคุณเอง แต่มันจะมีประโยชน์แน่ ถ้าทำความเข้าใจมัน ด้วยการแกะรอยวิธีการจัดการความเชื่อมโยงเหล่านั้น

    ลองนึกดูว่า ถ้าเราต้องการติดตามว่า มีเพื่อนเฟซบุ๊คของเรากี่คนที่กด “liked” ในแต่ละข่าวของ Microscope และสมมุติว่า เราได้จัดการเรื่องวิธีการตรวจสอบตัวตนกับเฟซบุ๊ค เรียกใช้ API และแปลงข้อมูลที่เกี่ยวข้องได้แล้ว โดยทำออกมาเป็นฟังก์ชันบนไคลเอนต์แบบ asynchronous ที่จะคืนค่าจำนวน likes มาให้ ชื่อ getFacevookLikeCount(user, url, callback) เรียบร้อยแล้ว

    สิ่งสำคัญที่ต้องจำไว้เกี่ยวกับฟังก์ชันแบบนี้ก็คือ มันทำงานแบบ ไม่เป็นรีแอคทีฟ และ ไม่ใช่เรียลไทม์ แน่ๆ ที่มันทำก็คือ ติดต่อเข้าเฟซบุ๊คทาง HTTP รับข้อมูลบางอย่าง และส่งต่อให้แอปพลิเคชันผ่านฟังก์ชัน callback แบบ asynchronous แต่ฟังก์ชันนั้น จะไม่รันตัวเองซ้ำเมื่อจำนวน liked ที่เฟซบุ๊ค เปลี่ยนไป และ UI ของเราก็ไม่เปลี่ยนแปลงตามข้อมูลช้างหลังด้วย

    เพื่อแก้ไขปัญหานี้ เราจะเริ่มด้วยการใช้ฟังก์ชัน setInterval เพื่อเรียกใช้ฟังก์ชันของเราทุกๆ ห้าวินาที

    currentLikeCount = 0;
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId).url, 
          function(err, count) {
            if (!err)
              currentLikeCount = count;
          });
      }
    }, 5 * 1000);
    

    ไม่ว่าตอนไหนก็ตามที่เราเช็คค่าตัวแปร currentLikeCount เราก็คาดว่าจะได้ตัวเลขที่ถูกต้องทุกช่วงห้าวินาที และเราก็สามารถใช้ตัวแปรนี้กับตัวช่วยได้ดังนี้

    Template.postItem.likeCount = function() {
      return currentLikeCount;
    }
    

    อย่างไรก็ตาม ไม่มีอะไรมาบอกให้เทมเพลทของเราแสดงข้อมูลใหม่ เมื่อ currentLikeCount เปลี่ยนไป ถึงแม้ว่าตัวแปรที่ใช้ตอนนี้ดูเหมือนว่าจะเป็นเรียลไทม์แบบเทียมๆแล้ว เพราะว่ามันสามารถเปลี่ยนค่าเองได้ แต่มันก็ยัง ไม่ใช่รีแอคทีฟ อยู่ดี ทำให้มันไม่สามารถสื่อสารกับส่วนอื่นๆในระบบของ Meteor ได้อย่างถูกต้อง

    แกะรอยการทำงานแบบรีแอคทีฟ (ส่วนประมวลผล)

    การทำงานแบบรีแอคทีฟของ Meteor ใช้ตัวกลางที่เรียกว่า ความเชื่อมโยง (dependencies) ซึ่งเป็นโครงสร้างข้อมูลที่ใช้ติดตามส่วนประมวลผล

    จากที่เราเห็นในบทแทรกตอนต้นๆ เรื่องรีแอคทีฟ ส่วนประมวลผลก็คือ ส่วนของโค้ดที่ใช้ข้อมูลรีแอคทีฟ ในกรณีของเรา ส่วนประมวลผลได้ถูกสร้างขึ้นสำหรับเทมเพลท postitem และทุกๆตัวช่วยของตัวจัดการเทมเพลทก็มีส่วนประมวลผลของตัวเอง เช่นกัน

    เราอาจมองว่า ส่วนประมวลผลก็คือ ส่วนของโค้ดที่ เฝ้าดู ข้อมูลรีแอคทีฟ เมื่อข้อมูลนั้นเปลี่ยนไป ส่วนประมวลผลนี้ก็จะถูกแจ้ง (ผ่านฟังก์ชัน invalidate()) และตัวส่วนประมวลผลเองก็จะตัดสินใจว่า จะต้องทำอะไร

    เปลี่ยนตัวแปรให้กลายเป็นฟังก์ชันแบบรีแอคทีฟ

    การที่จะเปลี่ยนตัวแปร currentLikeCount ให้เป็นแหล่งข้อมูลแบบรีแอคทีฟได้นั้น เราจำเป็นต้องเก็บข้อมูลการติดตามส่วนประมวลผลทั้งหมดที่เรียกใช้มันเอาไว้ในตัวแปรความเชื่อมโยง (dependency) และต้องเปลี่ยนมันจากที่เป็นตัวแปรให้กลายเป็นฟังก์ชั่น (ที่คืนค่ากลับมา)

    var _currentLikeCount = 0;
    var _currentLikeCountListeners = new Tracker.Dependency();
    
    currentLikeCount = function() {
      _currentLikeCountListeners.depend();
      return _currentLikeCount;
    }
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
          function(err, count) {
            if (!err && count !== _currentLikeCount) {
              _currentLikeCount = count;
              _currentLikeCountListeners.changed();
            }
          });
      }
    }, 5 * 1000);
    

    ที่เราทำไปคือ สร้างตัวแปรความเชื่อมโยงชื่อ _currentLikeCountListeners ให้ทำหน้าที่ติดตามส่วนประมวลผลทั้งหมดที่มี currentLikeCount() ใช้อยู่ เมื่อเราเปลี่ยนค่าของ _currentLikeCount เราก็เรียกใช้ฟังก์ชัน changed() กับตัวแปรความเชื่อมโยงนั้นด้วย เพื่อแจ้งให้ส่วนประมวลผลที่ถูกติดตามอยู่ทั้งหมดรับทราบ

    ซึ่งส่วนประมวลผลเหล่านี้ ก็สามารถทำงานต่อและจัดกับการเปลี่ยนแปลงได้ตามแต่ละกรณีไป

    ถ้าดูแล้วรู้สึกว่า จะต้องใช้โค้ดต้นแบบ (boilerplate) จำนวนมาก เพื่อสร้างแหล่งข้อมูลรีแอคทีฟแบบง่ายๆ คุณก็เข้าใจถูกแล้ว ซึ่งตัว Meteor เอง ก็มีเครื่องมือที่ช่วยให้งานนี้ง่ายขึ้น (เปรียบเหมือนกับที่คุณไม่จำเป็นต้องเรียกใช้ส่วนประมวลผลโดยตรง แต่ใช้ autoruns แทน) เป็นแพ็คเกจที่ติดมาเรียกว่า reactive-var ทำงานเหมือนกับที่ฟังก์ชัน currentLikeCount() ทำได้ โดยเราสามารถเพิ่มมันเข้าไป ด้วยคำสั่งนี้

    meteor add reactive-var
    

    แล้วเราก็ใช้มันเพื่อให้เขียนโค้ดได้ง่ายขึ้นดังนี้

    var currentLikeCount = new ReactiveVar();
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
          function(err, count) {
            if (!err) {
              currentLikeCount.set(count);
            }
          });
      }
    }, 5 * 1000);
    

    ตอนนี้ถ้าเราจะใช้งาน เราก็ต้องเรียก currentLikeCount.get() จากในตัวช่วย และมันก็จะทำงานได้เหมือนเดิม นอกจากนี้ยังมีอีกแพ็คเกจนึงชื่อ reactive-dict ซึ่งจะมีข้อมูลรีแอคทีฟแบบ key-val มาให้ (เกือบจะเหมือนกับ Session) ที่ใช้ประโยชน์ได้เช่นกัน

    เปรียบเทียบ Tracker กับ Angular

    Angular เป็นไลบรารี่แบบรีแอคทีฟบนฝั่งไคลเอนต์ตัวหนึ่ง ถูกพัฒนาโดยโปรแกรมเมอร์ที่ Google โดยการเปรียบเทียบระหว่าง วิธีติดตามความเชื่อมโยง (dependency tracking) ของ Meteor กับของ Angular นั้นจะเห็นได้ชัดเจน เนื่องจากทั้งสองใช้แนวทางที่แตกต่างกัน

    เราเห็นแล้วว่า โมเดลของ Meteor นั้นใช้บล็อกของโค้ดที่เรียกว่า ส่วนประมวลผล โดยส่วนประมวลผลเหล่านี้จะถูกติดตามการใช้งานจากแหล่งข้อมูล “รีแอคทีฟ” แบบพิเศษ (ฟังก์ชัน) ซึ่งคอย invalidate พวกมันตามความเหมาะสม จากนั้นตัวแหล่งข้อมูลก็จะแจ้ง ตรง ไปยังส่วนประมวลผลที่เชื่อมโยงกันอยู่ (dependencies) เมื่อพวกมันจำเป็นต้องเรียกใช้ invalidate() สังเกตุด้วยว่า แม้โดยทั่วไปเรื่องพวกนี้จะเกิดขึ้นตอนที่ข้อมูลเปลี่ยนแปลง แต่แหล่งข้อมูลก็สามารถตัดสินใจสั่งให้มีการ invalidate ได้ด้วยเหตุผลอื่นเช่นกัน

    เพื่มเติมอีกนิด ถึงแม้ว่าส่วนประมวลผลมักจะรันตัวเองใหม่เมื่อถูก invalidate คุณก็สามารถกำหนดให้มันทำตามสิ่งที่คุณต้องการได้ นั่นทำให้เราสามารถควบคุมการทำงานแบบรีแอคทีฟได้มากขึ้น

    ใน Angular, การทำงานแบบรีแอคทีฟ จะทำผ่านตัวกลางคือ อ็อบเจกต์ scope ตัว scope อาจมองได้ว่าเป็น อ็อบเจกต์จาวาสคริปต์ตัวนึง ที่มีเมธอดพิเศษชุดหนึ่งติดมาด้วย

    เมื่อคุณต้องการให้การทำงานแบบรีแอคทีฟขึ้นอยู่กับค่าใน scope คุณต้องเรียกใช้ scope.$watch ตามด้วยนิพจน์ (expression) ที่คุณสนใจอยู่ (เช่น ส่วนของ scope ที่คุณสนใจ) และฟังก์ชัน listener ที่จะทำงานทุกครั้งเมื่อนิพจน์เปลี่ยนแปลง จากนั้นคุณก็ระบุโค้ดลงไปว่าต้องการให้ทำอะไรทุกครั้งที่นิพจน์มีค่าเปลี่ยนไป

    ย้อนกลับไปที่ตัวอย่างเฟซบุ๊คของเรา เราก็สามารถเขียนได้ว่า

    $rootScope.$watch('currentLikeCount', function(likeCount) {
      console.log('Current like count is ' + likeCount);
    });
    

    อันที่จริง ก็เหมือนกับตอนที่เราสร้างหน่วยประมวลผลใน Meteor คุณมักไม่ค่อยจะได้ใช้ $watch ตรงๆกันมากนักใน Angular ถ้าเทียบกับคำสั่ง ng-model และ {{expressions}} ที่จะสร้างคำสั่ง watch ให้เองโดยอัตโนมัติ เพื่อจัดการเรื่องการแสดงผลใหม่เมื่อมีการเปลี่ยนแปลงเกิดขึ้น

    เมื่อข้อมูลที่เป็นรีแอคทีฟถูกเปลี่ยนค่า scope.$apply() ก็จะถูกเรียกใช้ ทำให้ watcher ทุกตัวใน scope ถูกประมวลผลใหม่อีกครั้ง แต่จะเรียกใช้ฟังก์ชัน listener ของ watcher เฉพาะตัวที่ค่านิพจน์ เปลี่ยนแปลง เท่านั้น

    นั่นทำให้ scope.$apply() คล้ายกับ dependency.changed() เว้นเสียแต่ว่า มันทำงานในระดับของ scope มากกว่าที่จะยอมให้คุณควบคุมว่า ฟังก์ชัน listener ตัวไหนจะถูกประมวลผลใหม่ จากที่กล่าวมาจะเห็นได้ว่า การลดการควบคุมลง ทำให้ Angular ต้องมีความฉลาดมากขึ้น และต้องมีประสิทธิภาพดีพอที่จะเลือกได้ว่าฟังก์ชัน listener ตัวไหนจะถูกเรียกให้ประมวลผล

    ถ้าเราใช้วิธีแบบ Angular โค้ดในฟังก์ชัน getFacebookLikeCount() ก็จะมีหน้าตาคล้ายๆแบบนี้

    Meteor.setInterval(function() {
      getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
        function(err, count) {
          if (!err) {
            $rootScope.currentLikeCount = count;
            $rootScope.$apply();
          }
        });
    }, 5 * 1000);
    

    ก็ต้องยอมรับว่า Meteor ช่วยเราจัดการส่วนที่ยากที่สุด และทำให้เราใช้ประโยชน์จากความเป็นรีแอคทีฟโดยไม่ต้องทำอะไรในส่วนของเรามากนัก ที่หวังไว้ก็คือ การเรียนรู้รูปแบบพวกนี้จะมีประโยชน์มากทีเดียว ถ้าคุณจำเป็นต้องทำอะไรที่มากไปกว่านี้