blog

ビュー

nextTickを使用すると、次のDOM更新ループが終了した後に、更新されたDOMを取得するための遅延コールバックを実行できます。 これはVue 2.4でも使用されていましたが、優先順位が高すぎるため...

Mar 25, 2020 · 22 min. read
シェア

目次

NextTick 原則的な分析

nextTick を使用すると、次の DOM 更新ループが終了した後に遅延コールバックを実行し、更新された DOM を取得することができます。

Vue 2.4では、microtasksが使用されていましたが、microtasksの優先順位が高すぎて、場合によってはイベントバブリングよりも高速になることがありましたが、macrotasksを使用するとレンダリングでパフォーマンスの問題が発生することがありました。そのため新しいバージョンでは、デフォルトでマイクロタスクが使用されますが、v-onのような特殊なケースではマクロタスクが使用されます。

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
 macroTimerFunc = () => {
 setImmediate(flushCallbacks)
 }
} else if (
 typeof MessageChannel !== 'undefined' &&
 (isNative(MessageChannel) ||
 // PhantomJS
 MessageChannel.toString() === '[object MessageChannelConstructor]')
) {
 const channel = new MessageChannel()
 const port = channel.port2
 channel.port1.onmessage = flushCallbacks
 macroTimerFunc = () => {
 port.postMessage(1)
 }
} else {
 /* istanbul ignore next */
 macroTimerFunc = () => {
 setTimeout(flushCallbacks, 0)
 }
}

nextTickはPromiseの使用もサポートしており、Promiseを実装しているかどうかを判断します。

export function nextTick(cb?: Function, ctx?: Object) {
 let _resolve
 // コールバック関数を配列に統合する。
 callbacks.push(() => {
 if (cb) {
 try {
 cb.call(ctx)
 } catch (e) {
 handleError(e, ctx, 'nextTick')
 }
 } else if (_resolve) {
 _resolve(ctx)
 }
 })
 if (!pending) {
 pending = true
 if (useMacroTask) {
 macroTimerFunc()
 } else {
 microTimerFunc()
 }
 }
 // Promiseが使えるかどうかを判断する
 // できれば_resolve  
 // こうすることで、コールバック関数をプロミスとして呼び出すことができる。
 if (!cb && typeof Promise !== 'undefined') {
 return new Promise(resolve => {
 _resolve = resolve
 })
 }
}

ライフサイクルの分析

ライフサイクル関数は、コンポーネントが初期化されたときやデータが更新されたときにトリガーされるフック関数です。

初期化中に以下のコードが呼び出され、ライフサイクルはcallHookを介して呼び出されるものです。

Vue.prototype._init = function(options) {
 initLifecycle(vm)
 initEvents(vm)
 initRender(vm)
 callHook(vm, 'beforeCreate') // プロップスデータを取得できない
 initInjections(vm) 
 initState(vm)
 initProvide(vm)
 callHook(vm, 'created')
}

上記のコードでは、beforeCreateが呼び出されたとき、propsやdataのデータを取得できないことに気づくでしょう。

次に、マウント関数が実行されます。

export function mountComponent {
 callHook(vm, 'beforeMount')
 // ...
 if (vm.$vnode == null) {
 vm._isMounted = true
 callHook(vm, 'mounted')
 }
}

beforeMountはマウントの前に実行され、VDOMの作成と実際のDOMへの置き換えを開始し、最後にマウントされたフックを実行します。ここで判定ロジックがあり、外部のnew Vue({})であれば$vnodeは存在せず、マウントフックが直接実行されます。サブコンポーネントがある場合は、サブコンポーネントが再帰的にマウントされ、すべてのサブコンポーネントがマウントされた時点で、ルートコンポーネントのマウントフックが実行されます。

次に、データが更新されたときに呼び出されるフック関数です。

function flushSchedulerQueue() {
 // ...
 for (index = 0; index < queue.length; index++) {
 watcher = queue[index]
 if (watcher.before) {
 watcher.before() // beforeUpdateを呼び出す
 }
 id = watcher.id
 has[id] = null
 watcher.run()
 // in dev build, check and stop circular updates.
 if (process.env.NODE_ENV !== 'production' && has[id] != null) {
 circular[id] = (circular[id] || 0) + 1
 if (circular[id] > MAX_UPDATE_COUNT) {
 warn(
 'You may have an infinite update loop ' +
 (watcher.user
 ? `in watcher with expression "${watcher.expression}"`
 : `in a component render function.`),
 watcher.vm
 )
 break
 }
 }
 }
 callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks(queue) {
 let i = queue.length
 while (i--) {
 const watcher = queue[i]
 const vm = watcher.vm
 if (vm._watcher === watcher && vm._isMounted) {
 callHook(vm, 'updated')
 }
 }
}

上の図では、activatedとdeactivatedという2つのライフサイクルが省かれていますが、これらのフック関数はkeep-aliveコンポーネント独自のものです。keep-aliveでラップされたコンポーネントは切り替え時に破棄されず、メモリにキャッシュされ、非アクティブ化されたフックが実行され、アクティブ化されたフックはキャッシュレンダリングにヒットした後に実行されます。

最後に、コンポーネントを破棄するフック関数があります。

Vue.prototype.$destroy = function() {
 // ...
 callHook(vm, 'beforeDestroy')
 vm._isBeingDestroyed = true
 // remove self from parent
 const parent = vm.$parent
 if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
 remove(parent.$children, vm)
 }
 // teardown watchers
 if (vm._watcher) {
 vm._watcher.teardown()
 }
 let i = vm._watchers.length
 while (i--) {
 vm._watchers[i].teardown()
 }
 // remove reference from data ob
 // frozen object may not have observer.
 if (vm._data.__ob__) {
 vm._data.__ob__.vmCount--
 }
 // call the last hook...
 vm._isDestroyed = true
 // invoke destroy hooks on current rendered tree
 vm.__patch__(vm._vnode, null)
 // fire destroyed hook
 callHook(vm, 'destroyed')
 // turn off all instance listeners.
 vm.$off()
 // remove __vue__ reference
 if (vm.$el) {
 vm.$el.__vue__ = null
 }
 // release circular reference (#6759)
 if (vm.$vnode) {
 vm.$vnode.parent = null
 }
}

beforeDestroyフック関数は、破壊操作が実行される前に呼び出され、その後、一連の破壊操作が実行されます。 サブコンポーネントがある場合、それらは再帰的に破壊され、ルートコンポーネントの破壊フック関数は、すべてのサブコンポーネントが破壊された後にのみ実行されます。

VueRouter ソースコードの解析

重要な機能 マインドマップ

次のマインドマップは、ソースコード内の重要な機能の一部をリストアップしたものです。

ルート登録

最初のうちは、ソースコードのコピーをクローンして読み返すことをお勧めします。長いし、関数間のジャンプが多いからです。

ルートを使用するには、Vue.use(VueRouter)を呼び出す必要があります。

export function initUse (Vue: GlobalAPI) {
 Vue.use = function (plugin: Function | Object) {
 // プラグインの重複インストールを判断する
 const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
 if (installedPlugins.indexOf(plugin) > -1) {
 return this
 }
 const args = toArray(arguments, 1)
 // Vueを挿入する
 args.unshift(this)
 // 一般的にプラグインは、インストール関数
 // この関数により、プラグインはVue
 if (typeof plugin.install === 'function') {
 plugin.install.apply(plugin, args)
 } else if (typeof plugin === 'function') {
 plugin.apply(null, args)
 }
 installedPlugins.push(plugin)
 return this
 }
}

インストール関数の部分的な実装を見てみましょう。

export function install (Vue) {
 // installが一度呼び出されることを確認する
 if (install.installed && _Vue === Vue) return
 install.installed = true
 // グローバル変数にVueを割り当てる
 _Vue = Vue
 const registerInstance = (vm, callVal) => {
 let i = vm.$options._parentVnode
 if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
 i(vm, callVal)
 }
 }
 // 各コンポーネントのフック関数の実装を混ぜる
 // これは`beforeCreate` フックが実行されると
 // ルートを初期化する
 Vue.mixin({
 beforeCreate () {
 // コンポーネントにルーターオブジェクトがあるかどうかを判断する。ルーターオブジェクトはルートコンポーネントにのみ存在する。
 if (isDef(this.$options.router)) {
 // ルートルートはそれ自身に設定される
 this._routerRoot = this
 this._router = this.$options.router
 // ルートを初期化する
 this._router.init(this)
 // 重要なのは_route 双方向バインディングのプロパティ
 // コンポーネントレンダリングのトリガー
 Vue.util.defineReactive(this, '_route', this._router.history.current)
 } else {
 // ルーター・ビューの階層判定
 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
 }
 registerInstance(this, this)
 },
 destroyed () {
 registerInstance(this)
 }
 })
 // グローバル登録コンポーネント router-link と router-view
 Vue.component('RouterView', View)
 Vue.component('RouterLink', Link)
}

ルート登録のために、コアはVue.use(VueRouter)を呼び出してVueRouterをVueで利用可能にし、VueRouterのinstall関数をVue経由で呼び出します。その関数の中で、コンポーネントのフック関数を混ぜて、2つのルーティングコンポーネントをグローバルに登録するのがコアです。

VueRouter

プラグインをインストールした後、VueRouterをインスタンス化します。

const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 3. Create the router
const router = new VueRouter({
 mode: 'hash',
 base: __dirname,
 routes: [
 { path: '/', component: Home }, // all paths are defined without the hash.
 { path: '/foo', component: Foo },
 { path: '/bar', component: Bar }
 ]
})

VueRouterコンストラクタを見てみましょう。

constructor(options: RouterOptions = {}) {
 // ...
 // ルートマッチングオブジェクト
 this.matcher = createMatcher(options.routes || [], this)
 // モードに応じて、さまざまなルーティング方法を取る
 let mode = options.mode || 'hash'
 this.fallback =
 mode === 'history' && !supportsPushState && options.fallback !== false
 if (this.fallback) {
 mode = 'hash'
 }
 if (!inBrowser) {
 mode = 'abstract'
 }
 this.mode = mode
 switch (mode) {
 case 'history':
 this.history = new HTML5History(this, options.base)
 break
 case 'hash':
 this.history = new HashHistory(this, options.base, this.fallback)
 break
 case 'abstract':
 this.history = new AbstractHistory(this, options.base)
 break
 default:
 if (process.env.NODE_ENV !== 'production') {
 assert(false, `invalid mode: ${mode}`)
 }
 }
 }

VueRouterをインスタンス化するプロセスの核心は、ルートマッチングオブジェクトを作成し、モードに応じて異なるルートを取ることです。

ルート・マッチ・オブジェクトの作成

export function createMatcher (
 routes: Array<RouteConfig>,
 router: VueRouter
): Matcher {
 // ルートマッピングテーブルを作成する
 const { pathList, pathMap, nameMap } = createRouteMap(routes)
 
 function addRoutes (routes) {
 createRouteMap(routes, pathList, pathMap, nameMap)
 }
 // ルートマッチング
 function match (
 raw: RawLocation,
 currentRoute?: Route,
 redirectedFrom?: Location
 ): Route {
 //...
 }
 return {
 match,
 addRoutes
 }
}

createMatcher関数の目的は、ルート・マッピング・テーブルを作成し、addRoutes関数とmatch関数がクロージャによってルート・マッピング・テーブルから複数のオブジェクトを使用できるようにし、最後にMatcherオブジェクトを返すことです。

次に、createMatcher 関数がマッピング テーブルを作成する方法を見てみましょう。

export function createRouteMap (
 routes: Array<RouteConfig>,
 oldPathList?: Array<string>,
 oldPathMap?: Dictionary<RouteRecord>,
 oldNameMap?: Dictionary<RouteRecord>
): {
 pathList: Array<string>;
 pathMap: Dictionary<RouteRecord>;
 nameMap: Dictionary<RouteRecord>;
} {
 // マッピングテーブルの作成
 const pathList: Array<string> = oldPathList || []
 const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
 const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
 // ルートコンフィギュレーションを繰り返し、コンフィギュレーションごとにルートレコードを追加する。
 routes.forEach(route => {
 addRouteRecord(pathList, pathMap, nameMap, route)
 })
 // ワイルドカードが
 for (let i = 0, l = pathList.length; i < l; i++) {
 if (pathList[i] === '*') {
 pathList.push(pathList.splice(i, 1)[0])
 l--
 i--
 }
 }
 return {
 pathList,
 pathMap,
 nameMap
 }
}
// ルートレコードを追加する
function addRouteRecord (
 pathList: Array<string>,
 pathMap: Dictionary<RouteRecord>,
 nameMap: Dictionary<RouteRecord>,
 route: RouteConfig,
 parent?: RouteRecord,
 matchAs?: string
) {
 // ルート設定のプロパティを取得する
 const { path, name } = route
 const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
 // URLのフォーマット、/の置き換え
 const normalizedPath = normalizePath(
 path,
 parent,
 pathToRegexpOptions.strict
 )
 // レコードオブジェクトを生成する
 const record: RouteRecord = {
 path: normalizedPath,
 regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
 components: route.components || { default: route.component },
 instances: {},
 name,
 parent,
 matchAs,
 redirect: route.redirect,
 beforeEnter: route.beforeEnter,
 meta: route.meta || {},
 props: route.props == null
 ? {}
 : route.components
 ? route.props
 : { default: route.props }
 }
 if (route.children) {
 // 再帰ルート設定のchildrenプロパティは、ルートレコードを追加する。
 route.children.forEach(child => {
 const childMatchAs = matchAs
 ? cleanPath(`${matchAs}/${child.path}`)
 : undefined
 addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
 })
 }
 // もしルートにエイリアス
 // エイリアスにルートレコードを追加する
 if (route.alias !== undefined) {
 const aliases = Array.isArray(route.alias)
 ? route.alias
 : [route.alias]
 aliases.forEach(alias => {
 const aliasRoute = {
 path: alias,
 children: route.children
 }
 addRouteRecord(
 pathList,
 pathMap,
 nameMap,
 aliasRoute,
 parent,
 record.path || '/' // matchAs
 )
 })
 }
 // マッピングテーブルの更新
 if (!pathMap[record.path]) {
 pathList.push(record.path)
 pathMap[record.path] = record
 }
 // 名前付きルートのレコードを追加する
 if (name) {
 if (!nameMap[name]) {
 nameMap[name] = record
 } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
 warn(
 false,
 `Duplicate named routes definition: ` +
 `{ name: "${name}", path: "${record.path}" }`
 )
 }
 }
}

上記は、ルートマッチングオブジェクトを作成し、ユーザが設定したルーティングルールによって対応するルートマッピングテーブルを作成する全体のプロセスです。

ルートの初期化

ルート・コンポーネントがbeforeCreateフック関数を呼び出すと、以下のコードが実行されます。

beforeCreate () {
// ルートコンポーネントだけがルーター属性を持っているので、ルートコンポーネントが初期化されると、ルートが初期化される。
 if (isDef(this.$options.router)) {
 this._routerRoot = this
 this._router = this.$options.router
 this._router.init(this)
 Vue.util.defineReactive(this, '_route', this._router.history.current)
 } else {
 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
 }
 registerInstance(this, this)
}

次に、ルートの初期化が何をするのかを見てみましょう

init(app: any /* Vue component instance */) {
 // コンポーネントの例を保存する
 this.apps.push(app)
 // ルートコンポーネントがすでに存在する場合は
 if (this.app) {
 return
 }
 this.app = app
 // 割り当てルーティングパターン
 const history = this.history
 // ハッシュパターンを例に、ルートパターンを決定する
 if (history instanceof HTML5History) {
 history.transitionTo(history.getCurrentLocation())
 } else if (history instanceof HashHistory) {
 // hashchangeリスナーを追加する
 const setupHashListener = () => {
 history.setupListeners()
 }
 // ルートジャンプ
 history.transitionTo(
 history.getCurrentLocation(),
 setupHashListener,
 setupHashListener
 )
 }
 // コールバックはtransitionToで呼ばれる。
 // そして最後にURLの変更とコンポーネントのレンダリングを完了する。_route コンポーネント・レンダリングのトリガーにプロパティを割り当てる
 history.listen(route => {
 this.apps.forEach(app => {
 app._route = route
 })
 })
 }

ルートが初期化されるとき、処理の中心はルートホップを行い、URLを変更し、対応するコンポーネントをレンダリングすることです。ルーティングの仕組みを見てみましょう。

ルートジャンプ

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
 // 一致するルート情報を取得する
 const route = this.router.match(location, this.current)
 // ルート切り替えの確認
 this.confirmTransition(route, () => {
 // 以下は、ルート切り替えが成功した場合と失敗した場合のコールバックである。
 // コンポーネントのルート情報を更新する_route コンポーネント・レンダリングのトリガーにプロパティを割り当てる
 // afterHooksでフックを呼び出す
 this.updateRoute(route)
 // hashchangeリスナーを追加する
 onComplete && onComplete(route)
 // URLを更新する
 this.ensureURL()
 // readyコールバックを一度だけ実行する
 if (!this.ready) {
 this.ready = true
 this.readyCbs.forEach(cb => { cb(route) })
 }
 }, err => {
 // エラー処理
 if (onAbort) {
 onAbort(err)
 }
 if (err && !this.ready) {
 this.ready = true
 this.readyErrorCbs.forEach(cb => { cb(err) })
 }
 })
}

ルートホッピングでは、まずマッチするルート情報を取得する必要があります。

function match (
 raw: RawLocation,
 currentRoute?: Route,
 redirectedFrom?: Location
): Route {
 // URLをシリアライズする
 // 例えば、/abcというURLの場合?foo=bar&baz=qux#hello
 // はルートを/abcとしてシリアライズする
 // ハッシュは#hello
 // パラメータはfoo: 'bar', baz: 'qux'
 const location = normalizeLocation(raw, currentRoute, false, router)
 const { name } = location
 // 名前付きルートであれば、名前付きルートコンフィギュレーションがレコードに存在するかどうかを判断する。
 if (name) {
 const record = nameMap[name]
 // 見つからないとは、一致するルートがないことを意味する
 if (!record) return _createRoute(null, location)
 const paramNames = record.regex.keys
 .filter(key => !key.optional)
 .map(key => key.name)
 // パラメータ処理
 if (typeof location.params !== 'object') {
 location.params = {}
 }
 if (currentRoute && typeof currentRoute.params === 'object') {
 for (const key in currentRoute.params) {
 if (!(key in location.params) && paramNames.indexOf(key) > -1) {
 location.params[key] = currentRoute.params[key]
 }
 }
 }
 if (record) {
 location.path = fillParams(record.path, location.params, `named route "${name}"`)
 return _createRoute(record, location, redirectedFrom)
 }
 } else if (location.path) {
 // 名前なしルート処理
 location.params = {}
 for (let i = 0; i < pathList.length; i++) {
 // レコードを見つける
 const path = pathList[i]
 const record = pathMap[path]
 // ルートが一致すれば、ルートが作成される
 if (matchRoute(record.regex, location.path, location.params)) {
 return _createRoute(record, location, redirectedFrom)
 }
 }
 }
 // 一致するルートがない
 return _createRoute(null, location)
}

次に、ルートを作成する方法を見てみましょう。

// 条件に基づいて異なるルートを作成する
function _createRoute(
 record: ?RouteRecord,
 location: Location,
 redirectedFrom?: Location
): Route {
 if (record && record.redirect) {
 return redirect(record, redirectedFrom || location)
 }
 if (record && record.matchAs) {
 return alias(record, location, record.matchAs)
 }
 return createRoute(record, location, redirectedFrom, router)
}
export function createRoute (
 record: ?RouteRecord,
 location: Location,
 redirectedFrom?: ?Location,
 router?: VueRouter
): Route {
 const stringifyQuery = router && router.options.stringifyQuery
 // パラメータをクローンする
 let query: any = location.query || {}
 try {
 query = clone(query)
 } catch (e) {}
 // ルートオブジェクトの作成
 const route: Route = {
 name: location.name || (record && record.name),
 meta: (record && record.meta) || {},
 path: location.path || '/',
 hash: location.hash || '',
 query,
 params: location.params || {},
 fullPath: getFullPath(location, stringifyQuery),
 matched: record ? formatMatch(record) : []
 }
 if (redirectedFrom) {
 route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
 }
 // ルートオブジェクトを変更不可能にする
 return Object.freeze(route)
}
// 現在のルートのすべてのネストされたパスフラグメントを含むルートレコードを取得する。
// ルートルートから現在のルートまで、一致するレコードを上から順に含む。
function formatMatch(record: ?RouteRecord): Array<RouteRecord> {
 const res = []
 while (record) {
 res.unshift(record)
 record = record.parent
 }
 return res
}

これでマッチングルートが完了したので、transitionTo関数に戻ってtransitionを確認します。

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
 // ルート切り替えの確認
 this.confirmTransition(route, () => {}
}
confirmTransition(route: Route, onComplete: Function, onAbort?: Function) {
 const current = this.current
 // ルートジャンプ関数を中断する
 const abort = err => {
 if (isError(err)) {
 if (this.errorCbs.length) {
 this.errorCbs.forEach(cb => {
 cb(err)
 })
 } else {
 warn(false, 'uncaught error during route navigation:')
 console.error(err)
 }
 }
 onAbort && onAbort(err)
 }
 // 同じルートならジャンプしない。
 if (
 isSameRoute(route, current) &&
 route.matched.length === current.matched.length
 ) {
 this.ensureURL()
 return abort()
 }
 // ルートを比較することで、再利用可能なコンポーネント、レンダリングが必要なコンポーネント、非アクティブ化されたコンポーネントを分析する。
 const { updated, deactivated, activated } = resolveQueue(
 this.current.matched,
 route.matched
 )
 
 function resolveQueue(
 current: Array<RouteRecord>,
 next: Array<RouteRecord>
 ): {
 updated: Array<RouteRecord>,
 activated: Array<RouteRecord>,
 deactivated: Array<RouteRecord>
 } {
 let i
 const max = Math.max(current.length, next.length)
 for (i = 0; i < max; i++) {
 // 現在のルートパスとジャンプルートのパスが異なる場合、トラバーサルはジャンプアウトする。
 if (current[i] !== next[i]) {
 break
 }
 }
 return {
 // 再利用可能なコンポーネント対応ルート
 updated: next.slice(0, i),
 // レンダリングされるコンポーネントの対応するルート
 activated: next.slice(i),
 // ルートに対応する非アクティブ化コンポーネント
 deactivated: current.slice(i)
 }
 }
 // ナビゲーションガード配列
 const queue: Array<?NavigationGuard> = [].concat(
 // 非アクティブ化されたコンポーネントフック
 extractLeaveGuards(deactivated),
 // グローバルbeforeEachフック
 this.router.beforeHooks,
 // 現在のルートが変更され、コンポーネントが再利用される場合、URLの変更とコンポーネントのレンダリングを完了するために
 extractUpdateHooks(updated),
 // コンポーネントがガードフックに入るのをレンダリングする必要がある
 activated.map(m => m.beforeEnter),
 // 非同期ルーティングコンポーネントの解析
 resolveAsyncComponents(activated)
 )
 // ルートを保存する
 this.pending = route
 // ナビゲーションガードフックをキューで実行するためのイテレータ
 const iterator = (hook: NavigationGuard, next) => {
 // ルートが等しくない場合はジャンプしない
 if (this.pending !== route) {
 return abort()
 }
 try {
 // 実行フック
 hook(route, current, (to: any) => {
 // フック関数nextの実行後にのみ、次のフック関数の実行が続く。
 // そうでない場合は、ジャンプを一時停止する
 // 次のロジックは、next()でパスを決定するために使用される。
 if (to === false || isError(to)) {
 // next(false) 
 this.ensureURL(true)
 abort(to)
 } else if (
 typeof to === 'string' ||
 (typeof to === 'object' &&
 (typeof to.path === 'string' || typeof to.name === 'string'))
 ) {
 // next('/') あるいはnext({ path: '/' }) ->  
 abort()
 if (typeof to === 'object' && to.replace) {
 this.replace(to)
 } else {
 this.push(to)
 }
 } else {
 // ここでは、次の
 // つまり、step(index)内の以下の関数runQueueである。+ 1)
 next(to)
 }
 })
 } catch (e) {
 abort(e)
 }
 }
 // 非同期関数の古典的な同期実行
 runQueue(queue, iterator, () => {
 const postEnterCbs = []
 const isValid = () => this.current === route
 // すべての非同期コンポーネントがロードされると、ここでのコールバック、つまりrunQueueのcb()が実行される。
 // 次のステップは、レンダリングが必要なコンポーネントのナビゲーションガードフックを実行することである。
 const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
 const queue = enterGuards.concat(this.router.resolveHooks)
 runQueue(queue, iterator, () => {
 // ジャンプの完了
 if (this.pending !== route) {
 return abort()
 }
 this.pending = null
 onComplete(route)
 if (this.router.app) {
 this.router.app.$nextTick(() => {
 postEnterCbs.forEach(cb => {
 cb()
 })
 })
 }
 })
 })
}
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
 const step = index => {
 // 関数のキューが実行され、次にコールバック関数が実行される。
 if (index >= queue.length) {
 cb()
 } else {
 if (queue[index]) {
 // イテレータを実行するために、ユーザーはフック関数のnext()コールバックを実行する。
 // コールバックでは、渡されたパラメータを判定し、問題がなければ、fn関数の2番目のパラメータであるnext()を実行する。
 fn(queue[index], () => {
 step(index + 1)
 })
 } else {
 step(index + 1)
 }
 }
 }
 // キューの最初のフック関数を取る
 step(0)
}

ナビゲーショナル・ガードの導入

const queue: Array<?NavigationGuard> = [].concat(
 // 非アクティブ化されたコンポーネントフック
 extractLeaveGuards(deactivated),
 // グローバルbeforeEachフック
 this.router.beforeHooks,
 // 現在のルートが変更され、コンポーネントが再利用される場合、URLの変更とコンポーネントのレンダリングを完了するために
 extractUpdateHooks(updated),
 // コンポーネントがガードフックに入るのをレンダリングする必要がある
 activated.map(m => m.beforeEnter),
 // 非同期ルーティングコンポーネントの解析
 resolveAsyncComponents(activated)
)

最初のステップは、非アクティブ化されたコンポーネントのフック関数を実行することです。

function extractLeaveGuards(deactivated: Array<RouteRecord>): Array<?Function> {
// 実行するフック関数の名前を渡す
 return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractGuards(
 records: Array<RouteRecord>,
 name: string,
 bind: Function,
 reverse?: boolean
): Array<?Function> {
 const guards = flatMapComponents(records, (def, instance, match, key) => {
 // コンポーネント内の対応するフック関数を見つける
 const guard = extractGuard(def, name)
 if (guard) {
 // コンポーネント自体のために、各フック関数にコンテキストオブジェクトを追加する。
 return Array.isArray(guard)
 ? guard.map(guard => bind(guard, instance, match, key))
 : bind(guard, instance, match, key)
 }
 })
 // 配列のサイズを縮小し、反転する必要があるかどうかを判断する
 // いくつかのフック関数は子から親へ実行する必要があるからだ。
 return flatten(reverse ? guards.reverse() : guards)
}
export function flatMapComponents (
 matched: Array<RouteRecord>,
 fn: Function
): Array<?Function> {
// 配列の次元削減
 return flatten(matched.map(m => {
 // コンポーネントオブジェクトをコールバック関数に渡して、フック関数の配列を取得する。
 return Object.keys(m.components).map(key => fn(
 m.components[key],
 m.instances[key],
 m, key
 ))
 }))
}

2番目のステップは、グローバルなbeforeEachフック関数を実行します。

beforeEach(fn: Function): Function {
 return registerHook(this.beforeHooks, fn)
}
function registerHook(list: Array<any>, fn: Function): Function {
 list.push(fn)
 return () => {
 const i = list.indexOf(fn)
 if (i > -1) list.splice(i, 1)
 }
}

beforeRouteUpdateフック関数は、渡される関数名が異なることと、この関数内でthisオブジェクトにアクセスできることを除けば、最初のステップと同じ方法で呼び出されます。

4番目のステップでは、beforeEnterフック関数を実行します。これはルート専用のフック関数です。

5つ目のステップは、非同期コンポーネントの解析です。

export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
 return (to, from, next) => {
 let hasAsync = false
 let pending = 0
 let error = null
 // 関数の役割はすでに紹介した
 flatMapComponents(matched, (def, _, match, key) => {
 // コンポーネントが非同期かどうかを判断する
 if (typeof def === 'function' && def.cid === undefined) {
 hasAsync = true
 pending++
 // 成功したコールバック
 // once 非同期コンポーネントが一度だけロードされるようにする関数
 const resolve = once(resolvedDef => {
 if (isESModule(resolvedDef)) {
 resolvedDef = resolvedDef.default
 }
 // ルートがコンストラクタかどうかを判断する
 // 存在しない場合は、Vue.Constructorを通してコンポーネントのコンストラクタを生成することができる。
 def.resolved = typeof resolvedDef === 'function'
 ? resolvedDef
 : _Vue.extend(resolvedDef)
 // コンポーネントを割り当てる
 // コンポーネントがすべて解析されたら、次のステップに進む。
 match.components[key] = resolvedDef
 pending--
 if (pending <= 0) {
 next()
 }
 })
 // 失敗コールバック
 const reject = once(reason => {
 const msg = `Failed to resolve async component ${key}: ${reason}`
 process.env.NODE_ENV !== 'production' && warn(false, msg)
 if (!error) {
 error = isError(reason)
 ? reason
 : new Error(msg)
 next(error)
 }
 })
 let res
 try {
 // 非同期コンポーネント関数の実行
 res = def(resolve, reject)
 } catch (e) {
 reject(e)
 }
 if (res) {
 // ダウンロードが完了するとコールバックが実行される。
 if (typeof res.then === 'function') {
 res.then(resolve, reject)
 } else {
 const comp = res.component
 if (comp && typeof comp.then === 'function') {
 comp.then(resolve, reject)
 }
 }
 }
 }
 })
 // 非同期コンポーネントではなく、直接次の
 if (!hasAsync) next()
 }
}

上記が最初のrunQueueのロジックで、最初のrunQueueのコールバック関数は5ステップ目以降に実行されます。

// このコールバックは`beforeRouteEnter` フックのコールバック関数
const postEnterCbs = []
const isValid = () => this.current === route
// beforeRouteEnter ナビゲーションガードフック
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
// beforeResolve ナビゲーションガードフック
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
 if (this.pending !== route) {
 return abort()
 }
 this.pending = null
 // afterEachナビゲーションガードフックはここで実行される。
 onComplete(route)
 if (this.router.app) {
 this.router.app.$nextTick(() => {
 postEnterCbs.forEach(cb => {
 cb()
 })
 })
 }
})

beforeRouteEnterフックは、ナビゲーションが確認される前に呼び出されるため、thisオブジェクトにアクセスすることができません。コンポーネントのためにレンダリングされる必要があるコンポーネントはまだ作成されていないからです。しかし、このフック関数はルート確認時に実行されるコールバックで this オブジェクトへのアクセスをサポートする唯一の関数です。

beforeRouteEnter (to, from, next) {
 next(vm => {
 //  `vm` コンポーネントの例にアクセスする
 })
}

コールバックでthisオブジェクトを取得する方法は以下の通りです。

function extractEnterGuards(
 activated: Array<RouteRecord>,
 cbs: Array<Function>,
 isValid: () => boolean
): Array<?Function> {
// ナビゲーションガードは基本的に同じである。
 return extractGuards(
 activated,
 'beforeRouteEnter',
 (guard, _, match, key) => {
 return bindEnterGuard(guard, match, key, cbs, isValid)
 }
 )
}
function bindEnterGuard(
 guard: NavigationGuard,
 match: RouteRecord,
 key: string,
 cbs: Array<Function>,
 isValid: () => boolean
): NavigationGuard {
 return function routeEnterGuard(to, from, next) {
 return guard(to, from, cb => {
 // cbが関数かどうかを判断する
 // 存在する場合は、それをpostEnterCbsにプッシュする。
 next(cb)
 if (typeof cb === 'function') {
 cbs.push(() => {
 // コンポーネントインスタンスを取得するまでループする
 poll(cb, match.instances, key, isValid)
 })
 }
 })
 }
}
// この関数は、issusを解決するために設計されている。#750
// モードout-inのトランジション・コンポーネントがルーター・ビューにラップされると、次のようになる。
// コンポーネントが最初にナビゲートするときは、コンポーネントインスタンスオブジェクトは取得されない。
function poll(
 cb: any, // somehow flow cannot infer this is a function
 instances: Object,
 key: string,
 isValid: () => boolean
) {
 if (
 instances[key] &&
 !instances[key]._isBeingDestroyed // do not reuse being destroyed instance
 ) {
 cb(instances[key])
 } else if (isValid()) {
 // setTimeout 16ms roleとnextTickは基本的に同じである。
 setTimeout(() => {
 poll(cb, instances, key, isValid)
 }, 16)
 }
}

7番目のステップは、beforeResolveナビゲーションガードフックを実行することです。beforeResolveフックが登録されていれば、グローバルbeforeResolveフックが実行されます。

ステップ8はナビゲーションの確認で、ガードフックをナビゲートするためにafterEachを呼び出します。

上記のすべてが実行されると、コンポーネントのレンダリングが開始されます。

history.listen(route => {
 this.apps.forEach(app => {
 app._route = route
 })
})

上記のコールバックは updateRoute で呼び出されます。

updateRoute(route: Route) {
 const prev = this.current
 this.current = route
 this.cb && this.cb(route)
 this.router.afterHooks.forEach(hook => {
 hook && hook(route, prev)
 })
}

この時点で、ルートジャンプは分析されました。ジャンプするルートがレコードに存在するかどうかを判断し、さまざまなナビゲーションガード機能を実行し、最後にURLの変更とコンポーネントのレンダリングを完了することが核心です。

Read next

偉大なキラーjstatは、もはやJVMの癇癪を恐れていない!

あなたの人生をコーディングし始め、コーディング王になりましょう!\n\n\n序文\n\nJVMのEden、Survivor、Elderly、その他のJVM内のデータをリアルタイムで簡単に表示し、JVMがどのように実行されているかを理解するのに役立つ良いツールです。

Mar 25, 2020 · 3 min read