9
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React Routerは何をしているのか? ~history編~

Posted at

history

  • React Routerのroutingに関するUtil Library
  • 公式サイト
  • React Router内のhisotryはこのLibraryのこと

listen

createBrowserHistory
const createBrowserHistory = (options = {}) => {
  ...,

  const listen = (listener) =>
    startListener(listener, false)
}

listenerをstartListenerに渡しているだけのようです。
startListenerを見に行きます。

startListener
  const startListener = (listener, before) => {
    if (++listenerCount === 1)
      stopListener = BrowserProtocol.startListener(
        history.transitionTo
      )

    const unlisten = before
      ? history.listenBefore(listener)
      : history.listen(listener)

    return () => {
      unlisten()

      if (--listenerCount === 0)
        stopListener()
    }
  }

初回は++listenerCount === 1なのでstopListenerが作成されます。
まずは、BrowserProtocolを見てみます。

BrowserProtocol
export const startListener = (listener) => {
  const handlePopState = (event) => {
    if (event.state !== undefined) // Ignore extraneous popstate events in WebKit
      listener(_createLocation(event.state))
  }

  addEventListener(window, PopStateEvent, handlePopState)

  return () =>
    removeEventListener(window, PopStateEvent, handlePopState)
}

htmlのpopstateイベントにlistenerを登録しています。
まずは、listenerとして渡された、history.transitionToを見てみます。
その前に、historyとは何なのかを確認します。

history
  const Protocol = useRefresh ? RefreshProtocol : BrowserProtocol

  const {
    getUserConfirmation,
    getCurrentLocation,
    pushLocation,
    replaceLocation,
    go
  } = Protocol

  const history = createHistory({
    getUserConfirmation, // User may override in options
    ...options,
    getCurrentLocation,
    pushLocation,
    replaceLocation,
    go
  })

ProtocolはBrowserProtocolと考えて進めます。
BrowserProtocolで定義されているmethodを取得後に、createHistoryしています。

createHistory
const createHistory = (options = {}) => {
  const {
    getCurrentLocation,
    getUserConfirmation,
    pushLocation,
    replaceLocation,
    go,
    keyLength
  } = options

  ...,

  return {
    getCurrentLocation,
    listenBefore,
    listen,
    transitionTo,
    push,
    replace,
    go,
    goBack,
    goForward,
    createKey,
    createPath,
    createHref,
    createLocation
  }
}

定義したmethodをまとめたObjectのようです。
transitionToがあるのでこれを確認します。

transitionTo
  const transitionTo = (nextLocation) => {
    if (
      (currentLocation && locationsAreEqual(currentLocation, nextLocation)) ||
      (pendingLocation && locationsAreEqual(pendingLocation, nextLocation))
    )
      return // Nothing to do

    ...,

    confirmTransitionTo(nextLocation, (ok) => {
      if (pendingLocation !== nextLocation)
        return // Transition was interrupted during confirmation

      pendingLocation = null

      if (ok) {
        if (nextLocation.action === PUSH) {
          const prevPath = createPath(currentLocation)
          const nextPath = createPath(nextLocation)

          if (nextPath === prevPath && deepEqual(currentLocation.state, nextLocation.state))
            nextLocation.action = REPLACE

        if (nextLocation.action === POP) {
          updateLocation(nextLocation)
        } else if (nextLocation.action === PUSH) {
          if (pushLocation(nextLocation) !== false)
            updateLocation(nextLocation)
        } else if (nextLocation.action === REPLACE) {
          if (replaceLocation(nextLocation) !== false)
            updateLocation(nextLocation)
        }
      } else if (currentLocation && nextLocation.action === POP) {
        const prevIndex = allKeys.indexOf(currentLocation.key)
        const nextIndex = allKeys.indexOf(nextLocation.key)

        if (prevIndex !== -1 && nextIndex !== -1)
          go(prevIndex - nextIndex) // Restore the URL
      }
    })
  }

Locationのactionによって処理を変更しています。
処理内のconfirmTransitionToを見てみます。

confirmTransitionTo
  const confirmTransitionTo = (location, callback) => {
    loopAsync(
      beforeListeners.length,
      (index, next, done) => {
        runTransitionHook(beforeListeners[index], location, (result) =>
          result != null ? done(result) : next()
        )
      },
      (message) => {
        if (getUserConfirmation && typeof message === 'string') {
          getUserConfirmation(message, (ok) => callback(ok !== false))
        } else {
          callback(message !== false)
        }
      }
    )
  }

laodAsyncに3つの引数が渡されています。
loadAsyncが何をしているかを確認します。

loadAsync
export const loopAsync = (turns, work, callback) => {
  let currentTurn = 0, isDone = false
  let isSync = false, hasNext = false, doneArgs

  const done = (...args) => {
    ...,
  }

  const next = () => {
    ...,
  }

  next()
}

ここまででlistenerが分かりました。
listenerの引数である_createLocationを見に行きます。

_createLocation
const _createLocation = (historyState) => {
  const key = historyState && historyState.key

  return createLocation({
    pathname: window.location.pathname,
    search: window.location.search,
    hash: window.location.hash,
    state: (key ? readState(key) : undefined)
  }, undefined, key)
}

event.state.keyを取得して、createLocationしています。
stateのreadStateを見てみます。

readState
export const readState = (key) => {
  let json
  try {
    json = window.sessionStorage.getItem(createKey(key))
  } catch (error) {
    if (error.name === SecurityError) {
      // Blocking cookies in Chrome/Firefox/Safari throws SecurityError on any
      // attempt to access window.sessionStorage.
      warning(
        false,
        '[history] Unable to read state; sessionStorage is not available due to security settings'
      )

      return undefined
    }
  }

  if (json) {
    try {
      return JSON.parse(json)
    } catch (error) {
      // Ignore invalid JSON.
    }
  }

  return undefined
}

まずは、createKeyを見ていきます。

createKey
const KeyPrefix = '@@History/'

const createKey = (key) =>
  KeyPrefix + key

このkeyをwindow.sessionStorage.getItemに渡してjsonを取得しています。
おそらく、browserが持つsessionの内、履歴についてのものでしょうか。

createLocation
export const createLocation = (input = '/', action = POP, key = null) => {
  const object = typeof input === 'string' ? parsePath(input) : input

  const pathname = object.pathname || '/'
  const search = object.search || ''
  const hash = object.hash || ''
  const state = object.state

  return {
    pathname,
    search,
    hash,
    state,
    action,
    key
  }
}

input={...}のObjectを渡しています。
actionにはundefinedが渡されていますが、undefinedの場合はdefault値が適用されるので、action=POPとなります。
結果は、LocationのObjectを返しています。
inputを引数に取るparsePathを見に行きます。

parsePath
export const parsePath = (path) => {
  let pathname = extractPath(path)
  let search = ''
  let hash = ''

  ...,

  const hashIndex = pathname.indexOf('#')
  if (hashIndex !== -1) {
    hash = pathname.substring(hashIndex)
    pathname = pathname.substring(0, hashIndex)
  }

  const searchIndex = pathname.indexOf('?')
  if (searchIndex !== -1) {
    search = pathname.substring(searchIndex)
    pathname = pathname.substring(0, searchIndex)
  }

  if (pathname === '')
    pathname = '/'

  return {
    pathname,
    search,
    hash
  }
}

pathnameを返す、extractPathを見てみます。

extractPath
const extractPath = (string) => {
  const match = string.match(/^https?:\/\/[^\/]*/)
  return match == null ? string : string.substring(match[0].length)
}

inputのstringに対して正規表現で抜き出しています。
https://hogehoge/barhttp://hogehoge/fooのようなものはmatchしますが、それ以外はstringそのままを返します。
parsePathに戻って、次にhashstringとquerystringが含まれていればそこまでの文字列をpathnameとしています。
https://hogehoge/#bar -> { pathname: https://hogehoge/, hash: #bar }となり、https://hogehoge/foo?item=bar -> { pathname: https://hogehoge/foo, search: ?item=bar }となります。

以上からまとめると、createBrowserHistory.listen(listener)が実行されると次のように処理が走ることがわかりました。
window.popstate(browserのurlが変更される)のイベントが発生->現在のstateからlocationを作成->loadAsync内の処理でok=trueならば、actionはPOPなのでupdateLocationが実行される

これでReact Routerを読み進めることができます。

9
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?