Help us understand the problem. What is going on with this article?

【JavaScript】プラグイン「Jump.js」のコードを読んでみた 

はじめに

スキルアップのためにGithubに公開されているプラグインのコードを読み、そこで学んだことをまとめました。
想定する読者は基本的な構文などを学んだけど次に学習する内容や学習方法に悩んでいる初学者の方になります。
実践的なコードの書き方や学習方法、次に作るもの等の参考にしていただければ幸いです。

読んだコードはcallmecavsさんの「Jump.js」というプラグインで、webサイトでスクロールアニメーションを実現できます。

基本的な使い方

基本的な使い方から見ていきます。
第一引数にスクロールのターゲットになる要素を指定することで、その要素の位置までスクロールさせることができます。指定する要素はノードかセレクタを渡すことができ、セレクタから複数の要素が見つかった場合は1つ目の要素がターゲットになります。また、数値を渡すことで現在のスクロール位置から相対の位置にスクロールします。

Jump(node)
Jump('.target')
Jump(100)

  
第二引数にはオプションを指定することができ、以下のように記述します。

Jump('.target', {
  duration: 1000, // スクロール時間
  offset: 0, // ターゲット要素の位置にoffsetの値を加えた数値がスクロール終了位置になります
  callback: undefined, // スクロール終了後に呼び出されるコールバック関数
  easing: easeInOutQuad, // スクロールに使用されるイージング関数
  a11y: false // trueを指定するとスクロール終了後にターゲット要素のfocus属性をtrueにします
})

  
使用例:
ページ内リンクがクリックされた時にhref属性の値をターゲットに指定してスクロールさせる

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Test Jump.js</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jump.js/1.0.2/jump.js"></script>

</head>
<body>
  <a href="#target">trigger</a>
  <div id="target" style="margin-top: 600px; margin-bottom: 2000px;">target</div>
  <script src="script.js"></script>
</body>
</html>

script.js

window.addEventListener('load', function() {
  const trigger = document.querySelectorAll('a[href^="#"]')
  for (let i = 0; i < trigger.length; i++) {
    trigger[i].addEventListener('click', function(e) {
      const targt = e.target.getAttribute('href')
      Jump(target)
    })
  }
})

全体的なコードの内容

まずはざっくりとコードの内容を見ていきます。
関数内の具体的な処理は省略しています。

import easeInOutQuad from './easing.js'

const jumper = () => {
  // private variable cache
  // no variables are created during a jump, preventing memory leaks

  let element         // element to scroll to                   (node)

  let start           // where scroll starts                    (px)
  let stop            // where scroll stops                     (px)

  let offset          // adjustment from the stop position      (px)
  let easing          // easing function                        (function)
  let a11y            // accessibility support flag             (boolean)

  let distance        // distance of scroll                     (px)
  let duration        // scroll duration                        (ms)

  let timeStart       // time scroll started                    (ms)
  let timeElapsed     // time spent scrolling thus far          (ms)

  let next            // next scroll position                   (px)

  let callback        // to call when done scrolling            (function)

  // scroll position helper
  function location () {/* ・・(中略)・・*/}

  // element offset helper
  function top (element) {/* ・・(中略)・・*/}

  // rAF loop helper
  function loop (){/* ・・(中略)・・*/}

  // scroll finished helper
  function done (){/* ・・(中略)・・*/}

  // API
  function jump (target, options = {}) {/* ・・(中略)・・*/}

  // expose only the jump method
  return jump
}

// export singleton
const singleton = jumper()

export default singleton

いろいろと省略しましたが、大まかな内容としてはjumperという関数内で
1.処理に必要な変数を宣言
2.関数jump内で使用するヘルパー関数を定義
3.ユーザーが実際に使うことになるメインの関数jumpを定義
といったことをしています。
そして、最後に関数jumperを実行して、返り値の関数jumpをエクスポートしています。

イージング関数

はじめにインポートされているイージング関数は滑らかなアニメーションを実現するために使用されます。
以下を引数にあてることで、経過時間からアニメーション位置を求めることができます。
・アニメーションの経過時間
・アニメーションさせたい値の初期値
・アニメーションさせたい値が初期値から変動する値
・アニメーションにかかる時間

変数宣言

変数の宣言の箇所ではコメントに
no variables are created during a jump, preventing memory leaks
「ジャンプ中に変数が作成されないため、メモリリークが防止されます」と書かれています。
繰り返し呼び出される処理の外に変数を定義することで、処理の度に変数が宣言されないように工夫されていました。
また、変数ごとにコメントが書かれていてますが、用途や単位が整列されていて読みやすく、コメントの書き方として非常に参考になりました。

ヘルパー関数

具体的な処理の内容にはここでは触れず、コメントから大まかな用途を把握します。

location

アニメーション開始時点のスクロール位置を取得

top

スクロール目的地となる要素のトップ位置を取得

loop

アニメーション時に繰り返し実行される処理

done

スクロール終了時に実行される処理

エクスポート

最後のほうでは、jumperを実行して返り値のjumpオブジェクトをsingletonという変数に代入してエクスポートしています。jumpをそのままエクスポートせずにわざわざjumperで処理を包んでいるのは、変数やヘルパー関数をスコープ内に閉じ込め、jumpだけを返すことで、余計な変数や関数を外部からアクセスできないようにするためです。
シングルトンという聞きなれない言葉が変数名で使用されていますが、Wikpediaでは以下のように説明されています。
そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのインスタンスが1つしか生成されないことを保証するデザインパターン
つまり、使用するときにnew Jump()というような、新しくインスタンスを作成する記述が不要な関数です。
以下の記事で詳しい実装例などが説明されていて参考になります。
【JSでデザインパターン】シングルトン編

jump関数

処理の流れを把握するため、処理のメインとなるjump関数を見ていきます。
おおまかな流れとしては以下になります。

  1. オプションに値が設定されていない項目にはデフォルト値を設定
  2. location関数でスタート位置を取得してstartに代入
  3. 第一引数のtargetから終了位置を取得してstopに代入
  4. スタート位置から終了位置までの距離を取得してdistanceに代入
  5. loop関数を実行してスクロールアニメーションを開始
  function jump (target, options = {}) {
    // resolve options, or use defaults
    duration = options.duration || 1000
    offset = options.offset || 0
    callback = options.callback                       // "undefined" is a suitable default, and won't be called
    easing = options.easing || easeInOutQuad
    a11y = options.a11y || false

    // cache starting position
    start = location()

    // resolve target
    switch (typeof target) {
      // scroll from current position
      case 'number':
        element = undefined           // no element to scroll to
        a11y = false                  // make sure accessibility is off
        stop = start + target
        break

      // scroll to element (node)
      // bounding rect is relative to the viewport
      case 'object':
        element = target
        stop = top(element)
        break

      // scroll to element (selector)
      // bounding rect is relative to the viewport
      case 'string':
        element = document.querySelector(target)
        stop = top(element)
        break
    }

    // resolve scroll distance, accounting for offset
    distance = stop - start + offset

    // resolve duration
    switch (typeof options.duration) {
      // number in ms
      case 'number':
        duration = options.duration
        break

      // function passed the distance of the scroll
      case 'function':
        duration = options.duration(distance)
        break
    }

    // start the loop
    window.requestAnimationFrame(loop)
  }

jump関数の第二引数にoptions = {}と記述されています。
これは引数が指定されなかった場合に適応されるデフォルト値を設定しています。
第二引数が指定されなかった場合は空のオブジェクトがoptionsに代入されます。

ヘルパー関数

ヘルパー関数の具体的な処理を一つずつ見ていきます。

location

現在のスクロール位置を取得する関数になります。

  // scroll position helper

  function location () {
    return window.scrollY || window.pageYOffset
  }

window.scrollYwindow.pageYOffsetはどちらも現在のスクロール位置を示します。
window.scrollYがブラウザで対応されていない場合はwindow.pageYOffsetが返ります。
それぞれのブラウザの対応状況ですが、window.scrollYはIE以外のブラウザで対応されて、widnow.pageYOffsetはIEを含む全てのブラウザで対応されています。
pageYOffsetのほうがscrollYより対応範囲が広く、わざわざscrollYを優先して使用する理由については調べても明確な答えが出てきませんでしたが、こちらのサイトhttps://codeday.me/jp/qa/20190204/216459.htmlでは、「scrollYpageYOffsetのエイリアスであり、読みやすさを優先してscrollYが使用されているのでは」と説明されていました。

top

引数で指定した要素のトップの位置を取得します。

  // element offset helper

  function top (element) {
    return element.getBoundingClientRect().top + start
  }

startはスクロール開始時のスクロール位置が代入されます。
getBoundingRectで要素のビューポートからの相対位置を取得し、windowのスクロール量を加算することで、要素の位置をwindowトップからの絶対位置で取得することができます。

loop

スクロールアニメーション時に繰り返し呼び出される関数です。

  function loop (timeCurrent) {
    // store time scroll started, if not started already
    if (!timeStart) {
      timeStart = timeCurrent
    }

    // determine time spent scrolling so far
    timeElapsed = timeCurrent - timeStart

    // calculate next scroll position
    next = easing(timeElapsed, start, distance, duration)

    // scroll to it
    window.scrollTo(0, next)

    // check progress
    timeElapsed < duration
      ? window.requestAnimationFrame(loop)       // continue scroll loop
      : done()                                   // scrolling is done
  }

スクロールアニメーション時にrequestAnimationFrameのコールバック関数として繰り返し実行されます。
アニメーション開始時からの経過時間でスクロール位置を求めてスクロールさせる処理を、requestAnimationFrameで高速に繰り返すことで滑らかなアニメーションを実現しています。

引数にはミリ秒単位で計測された現在時刻の値を受け取ります。
初回実行時に受け取った現在時刻をグローバル変数timeStartに代入して、二回目以降に呼び出された際の時刻と比較することでアニメーションの経過時間を求めることができます。経過時間はtimeElapsedという変数に代入されています。

その後、イージング関数でスクロール位置を求めてスクロールさせ、アニメーション経過時間がdurationに設定した時間を経過していれば後述するdone関数を呼び出し、経過していなければ再度loop関数を実行します。

done

スクロール終了後に呼び出される関数です。

  function done () {
    // account for rAF time rounding inaccuracies
    window.scrollTo(0, start + distance)

    // if scrolling to an element, and accessibility is enabled
    if (element && a11y) {
      // add tabindex indicating programmatic focus
      element.setAttribute('tabindex', '-1')

      // focus the element
      element.focus()
    }

    // if it exists, fire the callback
    if (typeof callback === 'function') {
      callback()
    }

    // reset time for next jump
    timeStart = false
  }

はじめにWindow.scrollToでスクロール終了位置までスクロールしています。
これは、requestAnimationFrameで繰り返し実行する間隔が処理する端末の処理能力などに依存していて、ぴったしdurationの値とおなじタイミングでスクロールアニメーションが終了するとは限らず、微妙にスクロール終了位置がずれてしまうからです。
そのずれを修正するために改めてターゲット要素の位置を指定してスクロールさせています。

例:durationの値が5000の場合
N回目のloop実行
 timeElapsedの値 4999 → 再度loopを実行
N+1回目のloop実行
 timeElapsedの値 5004 → doneを実行 
結果4ミリ秒分の終了位置がずれる

次に、オプションでa11yをtrueに設定した場合の処理が記述されています。アクセシビリティを向上させるためにスクロール後、ターゲット要素をフォーカスしています。
要素にtabindex属性を指定することで、input要素やtextarea以外の要素でもフォーカスできるようになります。0以上の値をしていするとタブキーで順番にフォーカスされ、マイナスの値を指定するとタブキーではフォーカスされなくます。

次に、オプションでコールバック関数を設定して場合は、その関数を実行しています。
コールバック関数を使用することで、Jump.jsの利用者はアニメーション終了時に様々な処理を差し込むことができます。

最後に、次にJump関数が呼びだれた時のためにtimeStartを初期化して終了です。

まとめ

変数名やコメントのつけ方、ブラウザ対応の方法など、書籍だけでは習得しづらい知見が多々あり勉強になりました。
オープンソースのコードは数えきれないほどあるので、これからも理解できるところ勉強していこうと思います。
勉強法に悩んでいる方にもおすすめです!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした