如何Debug?

Debug一直是RxJS的难题,原因是当我们使用RxJS后,程式码就会变得高度抽象化;实际上抽象并不是什么坏事,抽象会让程式码显得简洁、干净,但同时也带来了除错上的困难。

在撰写程式时,我们都会希望程式码是简洁且可读的。但当我们用简洁的程式码来处理复杂的问题,就表示我们的程式码会变得高度抽象!其实人类在思考复杂的问题都会偏好用抽象的方式来处理,例如说在下围棋时,常常说的棋形或是黑白哪一边的比较好,这都是在抽象化处理问题。

抽象化程式

高度抽象化的程式码不好除错!

当我们在为程式码除错时,通常会希望能够一步一步的了解错误在哪里发生,但如果我们用了很多抽象的方式来撰写程式码后,就会没有办法细部的一步一步去看错误在哪,程式就会变得不好除错。

这不是RxJS 独有的问题,而是任何抽象化的程式码都会面临的困难,像是从以前用jQuery 直接对DOM 操作到现行的前端框架(React, Vue, NG)都是对资料做操作然后生成画面,这就是一种抽象化。

这里我们用jQuery 跟Vue 做范例

  • jQuery
<div>
    <p id="message"></p>
    <input type="text" id="input">
</div>
$('#input').on('input', function(event) {
    $('#message').text(event.target.value)
})

JSBin | JSFiddle

  • Vue
<div id="app">
    <p>{{message}}</p>
    <input type="text" v-model="message">
</div>
const app = new Vue({
  el: '#app',
  data: { message: '' }
})

JSBin | JSFiddle

这虽然是一个很简单的例子,但相信读者应该都能看出来Vue 的程式码相对jQuery 是抽象很多的。只是目前的例子太单纯了,看不出Vue 的优势,一旦需求变得复杂后,Vue 的程式码就会相对jQuery 少很多;但如果这两段程式码都发生了错误,大家认为哪个会比较好除错呢?

答案肯定会是jQuery 比较好除错,因为jQuery 撰写出来的程式相对详细,我们能一步步的找出错误,但Vue 的程式码较为抽象,当发生错误时就可能需要靠一些工具或方法来帮助我们。

这就是为什么高度抽象化的程式码不好除错,而我们会需要更好的除错技巧与工具。

RxJS 如何除错?

do

在RxJS的世界中,有一个Operator叫作do,它不会对元素产生任何影响,在实务上很常用来做错误的追踪,如下

const source = Rx.Observable.interval(1000).take(3);

const example = source
                .do(x => console.log('do log: ' + x))
                .map(x => x + 1);

example.subscribe((x) => {
    console.log('subscription log: ' + x)
})

// do log: 0
// subscription log: 1
// do log: 1
// subscription log: 2
// do log: 2
// subscription log: 3

JSBin | JSFiddle

从上面的例子可以看出来,我们可以传入一个callback function给do,我们可以在do的内部对元素作任何操作(像是log),但不会对元素产生影响。这很适合用在检测每一步送出的元素是否符合我们的预期。

do(...)的行为跟map(x => { ... return x; })本质上是一样的

Observable 间的关联图

当程式有点复杂时,我们最好是能先画出Observable 与Observable 之间的关联,在厘清各个Observable 间的关系后,我们就能更轻易地找出问题在哪。范例如下

const addButton = document.getElementById('addButton');
const minusButton = document.getElementById('minusButton');
const state = document.getElementById('state');

const addClick = Rx.Observable.fromEvent(addButton, 'click');
const minusClick = Rx.Observable.fromEvent(minusButton, 'click');
const initialState = Rx.Observable.of(0);

const numberState = initialState
    .merge(
        addClick.mapTo(1), 
        minusClick.mapTo(-1)
    )
    .scan((origin, next) => origin + next)

numberState
  .subscribe({
    next: (value) => { state.innerHTML = value;},
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
  });

JSBin | JSFiddle

上面这段程式码,我们可以把关联图画成以下的样子

--------------        --------------        --------------
'            '        '            '        '            ' 
'initialState'        '  addClcik  '        ' minusClick '
'            '        '            '        '            '
--------------        --------------        --------------
      |                     |                      |
      |                     |  mapTo(1)            | mapTo(-1)
merge | ____________________|                      |
      | \__________________________________________|
      |                      
     \|/
      |
      | scan((origin, next) => origin + next)
      |
     \|/
-------------
'           '
'numberState'  
'           '
-------------

把每个一observable 物件都框起来,并画出之间的关联,以及中间使用到的Operators,这样一来我们就能够很清楚的了解这段程式码在做什么,以及如何运作。最后我们只要在每一个环节去确认送出的元素就能找出错误出现在哪里。

Marble Diagram

在厘清每个observable之间的关系并找出问题出现在哪个环节后,我们只要画出该环节的Marble Diagram前后变化就能清楚地知道问题是如何发生。接续上面的例子,如果今天问题出在merge()之后,那我们就把merge()前后的Marble Diagram画出来

initialState: 0|
addClick    : ----------1---------1--1-------
minusClick  : -----(-1)---(-1)---------------

                       merge(...)

            : 0----(-1)-1-(-1)----1--1-------

           scan((origin, next) => origin +next)

numberState : 0----(-1)-0-(-1)----0--1-------

到这里我们应该就能清楚地知道问题出在哪,最后就只要想如何解决问题就行了。

如果还是不知道问题在哪,很有可能是Marble Diagram画错,可以再利用do进行检查

只要照着以上三个步骤做除错,基本上就不用担心会有解决不了的错误,但是这三个步骤仍然显得太过繁琐,或许我们应该做一个工具来简化这整个流程!

RxJS Devtools

RxJS Devtools是我跟我的好友Jerry Lin共同开发的Chrome Extension,目前还在preview阶段,很多feature还没有实作但基本的功能已经能动了,使用方式很简单如下

Observable.prototype.debug = window.rxDevTool(Observable);

首先我们的extension会在window底下塞入一个方法叫rxDevTool,所以开发者只要传入Observable并把这个rxDevTool的回传值塞到Observable.prototype.debug就能使用debug了。

Observable.interval(1000).take(5)
    .debug('source1')
    .map(x => x + 1)
    .debug('source2')
    .subscribe(function() {
        //...
    })

这个debug()do()一样,不会对元素造成任何影响,但不同的是debug()要传入的参数是开发者自订的名称,代表当前的observable,这时在Chrome的开发者工具中切到RxJS的tab页就能看到自动画出Marble Diagram,如下图

送出元素是物件也行喔!

目前RxJS Devtools 已经能够自动画出Marble Diagram,也能做到类似do 的功能(放在第二个参数),之后会希望能够自动画出observable 之间的关联图,这样一来我们在做RxJS 的除错时就会方便非常多!

等到RxJS Devtools 正式release 后,会在专门写一篇文章介绍如何使用。

结语

这篇文章主要在讲述我们使用RxJS 后要如何进行除错,基本上只要照着以下三个步骤就能找出问题

  • 善用do()检查送出的元素
  • 画出observable 之间的关联图
  • 画出关键环节前后的Marble Diagram

最后简单的介绍了RxJS Devtools 的使用方式与功能,也请期待RxJS Devtools 的正式释出。

results matching ""

    No results matching ""