スクロール位置に応じたフェード効果付きの表示・非表示の制御

# 概要
- setTimeoutで実行をずらしていると、意図しない結果になるタイミングがある。
- 中断条件を入れることでそれを防ぐ。

# 具体例
※便宜上jQueryを使用。

- [実行サンプル](http://codepen.io/yama-t/pen/xgLqRw)
- 200pxスクロールしたら表示する。
- 一番上までスクロールしたら消えている。

```html:HTML
<div class="wrap"></div>
```

```css:CSS
body {
  height: 1000px;
}

.wrap {
  position: fixed;
  background-color: #CCC;
  left: 10px;
  bottom: 0;
  opacity: 0;
  width: 100px;
  height: 100px;
  transition: opacity 0.3s;
}

.wrap.is-show {
  opacity: 1;
}

.wrap.is-hide {
  display: none;
}
```

# とりあえず作成
- だいたいOKだが、勢い良く一番上までスクロールした時に消えてくれない場合がある。

```js
class Sample {
  constructor(){
    this.SHOW_CLASS = 'is-show';
    this.HIDE_CLASS = 'is-hide';
    this.THRESHOLD = 200;
    this.$window = $(window);
    this.$wrap = $('.wrap');
    
    this.bindEvent();
  }
  bindEvent(){
    this.$window.on('scroll', () => this.toggle());
    
    this.$wrap.on('transitionend', e => this.out(e));
  }
  toggle() {
    const scroll = this.$window.scrollTop();
    
    if(scroll < this.THRESHOLD){
      this.hide();
      return;
    }
    
    this.show();
  }
  isShow() {
    return this.$wrap.hasClass(this.SHOW_CLASS);
  }
  show() {
    this.$wrap.removeClass(this.HIDE_CLASS);
    
    setTimeout(() => this.$wrap.addClass(this.SHOW_CLASS), 100);
  }
  hide() {
    this.$wrap.removeClass(this.SHOW_CLASS);
  }
  out(e) {
    const isOpacity = e.originalEvent.propertyName === 'opacity';
    
    if(!isOpacity || this.isShow()){
       return;
    }
    
    this.$wrap.addClass(this.HIDE_CLASS);
  }
};

new Sample();
```

## 消えない理由
- 一番上までスクロールしたときに下記の状態になっている。

```html
<div class="wrap is-show"></div>
``` 

表示上は消えているが、下記のような状態になるケースも。

```html
<div class="wrap is-show is-hide"></div>
```

### setTimeoutの問題
上に向かって勢い良くスクロールした場合、

200px以下の位置にいる時にshowメソッドが実行され、setTimeoutの待機状態に入る。

```js
setTimeout(() => this.$wrap.addClass(this.SHOW_CLASS), 100);
```

その後、200pxより上に来た際にhideメソッドが実行され、is-showクラスが外れる。

```js
this.$wrap.removeClass(this.SHOW_CLASS);
```

待機していたsetTimeoutの処理が実行され、is-showクラスが付与される。

```js
this.$wrap.addClass(this.SHOW_CLASS);
```

このような順番で処理が行われる結果、一番上までスクロールしたはずなのに is-showのクラスが付与されてしまう。

# 対処法
- show, hideそれぞれに中断条件を付与する。
- is-showクラスの有無で判定する。

# 完成コード
```js:JS
class Sample {
  constructor(){
    this.SHOW_CLASS = 'is-show';
    this.HIDE_CLASS = 'is-hide';
    this.THRESHOLD = 200;
    this.$window = $(window);
    this.$wrap = $('.wrap');
    
    this.bindEvent();
  }
  bindEvent(){
    this.$window.on('scroll', () => this.toggle());
    
    this.$wrap.on('transitionend', e => this.out(e));
  }
  toggle() {
    const scroll = this.$window.scrollTop(),
          isShow = this.isShow();
    
    if(scroll < this.THRESHOLD){
      this.hide(isShow);
      return;
    }
    
    this.show(isShow);
  }
  isShow() {
    return this.$wrap.hasClass(this.SHOW_CLASS);
  }
  show(isShow) {
  // 既に表示されている場合は処理中断
    if(isShow){
       return;
    }
    this.$wrap.removeClass(this.HIDE_CLASS);
    
    setTimeout(() => this.$wrap.addClass(this.SHOW_CLASS), 100);
  }
  hide(isShow) {
   // 既に非表示の場合は処理中断
    if(!isShow){
       return;
    }
    this.$wrap.removeClass(this.SHOW_CLASS);
  }
  out(e) {
    const isOpacity = e.originalEvent.propertyName === 'opacity';
    
    if(!isOpacity || this.isShow()){
       return;
    }
    
    this.$wrap.addClass(this.HIDE_CLASS);
  }
};

new Sample();
```

# まとめ
- scrollに関する処理をsetTimeoutでずらすと、処理が前後することがある。
- 適切に処理を中断・間引きする必要がある。

# 備考
- toggleの関数内にaddClass、removeClassを書いておくより、別メソッドとして独立させておいた方が、今回のケースのような中断条件を追加しやすく、読みやすいコードになる。