【vue-router 源码03】matcher & history

145 阅读1分钟

this.matcher = createMatcher(options.routes || [], this)

matcher

src/create-matcher.js

export function createMatcher (routes: Array<RouteConfig>, router: VueRouter): Matcher {
    const { pathList, pathMap, nameMap } = createRouteMap(routes)
    
    function addRoutes (routes) {
        createRouteMap(routes, pathList, pathMap, nameMap)
    }
    
    function addRoute(parentOrRoute, route){ ... }
    
    function getRoutes () {
        return pathList.map(path => pathMap[path])
    }
    
    function match (
        raw: RawLocation,
        currentRoute?: Route,
        redirectedFrom?: Location
    ): Route { ... }
    
    function alias (
        record: RouteRecord,
        location: Location,
        matchAs: string
    ): Route { ... }
    
    function _createRoute (
        record: ?RouteRecord,
        location: Location,
        redirectedFrom?: Location
    ): Route { ... }
    
    return {
        match,
        addRoute,
        getRoutes,
        addRoutes
    }
}

src/create-route-map.js

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>,
  parentRoute?: RouteRecord
){
    const pathList: Array<string> = oldPathList || []
    const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
    const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
    // 数据创建(用户)或内在引用(开发者),map使用无原型对象hash
    
    routes.forEach(route => {
        addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
    })
    // 遍历生成记录
    
    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 || {}
    // 2.6.0+ 编译正则的选项 
    const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
    
    if (typeof route.caseSensitive === 'boolean') {
        pathToRegexpOptions.sensitive = route.caseSensitive
    }
    // 2.6.0+ 大小写敏感
    
    const record: RouteRecord = {
        path: normalizedPath,
        regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
        components: route.components || { default: route.component },
        alias: route.alias
          ? typeof route.alias === 'string'
            ? [route.alias]
            : route.alias
          : [],
        instances: {},
        enteredCbs: {},
        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) {
        route.children.forEach(child => {
            const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined
            addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
        })
    }
    // 子级递归
    
    if (!pathMap[record.path]) {
        pathList.push(record.path)
        pathMap[record.path] = record
    }
    // 存放 pathList、pathMap
    
    if (route.alias !== undefined) {
        const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
        for (let i = 0; i < aliases.length; ++i) {
            const alias = aliases[i]
            const aliasRoute = {
                path: alias,
                children: route.children
            }
            addRouteRecord(
                pathList,
                pathMap,
                nameMap,
                aliasRoute,
                parent,
                record.path || '/' // matchAs
            )
        }
    }
    // 递归别名
    
    if (name) {
        if (!nameMap[name]) {
            nameMap[name] = record
        }
    }
    // 存放 nameMap
}

function normalizePath (
  path: string,
  parent?: RouteRecord,
  strict?: boolean
): string {
  if (!strict) path = path.replace(/\/$/, '') // 去尾 '/'
  if (path[0] === '/') return path // 非嵌套路由
  if (parent == null) return path // 无父级返回
  return cleanPath(`${parent.path}/${path}`) // '/' 去重
}

function compileRouteRegex (
  path: string,
  pathToRegexpOptions: PathToRegexpOptions
): RouteRegExp {
  const regex = Regexp(path, [], pathToRegexpOptions) // => 'path-to-regexp'
  return regex
}

history

VueRouter.init & history
if (history instanceof HTML5History || history instanceof HashHistory) {
    const handleInitialScroll = routeOrError => {
        const from = history.current
        const expectScroll = this.options.scrollBehavior
        const supportsScroll = supportsPushState && expectScroll

        if (supportsScroll && 'fullPath' in routeOrError) {
                handleScroll(this, routeOrError, from, false)
        }
    }
    const setupListeners = routeOrError => {
        history.setupListeners() // 添加单次 hashchange或popstate 监听
        handleInitialScroll(routeOrError)
    }
    history.transitionTo(history.getCurrentLocation(), setupListeners, setupListeners)
}

// 对应history.update(),同步更新根实例记录当前尾处路由
history.listen(route => {
    this.apps.forEach(app => {
        app._route = route
    })
})

src/history/html5.js

export class HTML5History extends History {
_startLocation: string

    constructor (router: Router, base: ?string) {
        super(router, base)

        this._startLocation = getLocation(this.base)
    }
    getCurrentLocation (): string {
        return getLocation(this.base)
    }
}

// 获取去除base后的路由
export function getLocation (base: string): string {
    let path = window.location.pathname
    const pathLowerCase = path.toLowerCase()
    const baseLowerCase = base.toLowerCase()
    if (base && ((pathLowerCase === baseLowerCase) ||
        (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
        path = path.slice(base.length)
    }
    return (path || '/') + window.location.search + window.location.hash
}

src/history/base.js

export class History {
    constructor(router: Router, base: ?string) {
    
        this.router = router
        this.base = normalizeBase(base)
        // 挂载router,装载base
        
        this.current = START
        this.pending = null
        // 初始化
        
        this.ready = false
        this.readyCbs = []
        this.readyErrorCbs = []
        this.errorCbs = []
        // 状态回调
        
        this.listeners = []
        // history 监听器
    }
    
    // 包装参数,设置回调并传递至 confirmTransition
    transitionTo(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        let route
        try {
            route = this.router.match(location, this.current)
        } catch (e) {
            this.errorCbs.forEach(cb => {
                cb(e)
            })
            throw e
        }
        
        const prev = this.current
        this.confirmTransition(
        route,
        () => {
            this.updateRoute(route)
            onComplete && onComplete(route)
            this.ensureURL()
            this.router.afterHooks.forEach(hook => {
                hook && hook(route, prev)
            })

            if (!this.ready) {
                this.ready = true
                this.readyCbs.forEach(cb => {
                    cb(route)
                })
            }
        },
        err => {
            if (onAbort) {
                onAbort(err)
            }
            if (err && !this.ready) {
                if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
                this.ready = true
                this.readyErrorCbs.forEach(cb => {
                    cb(err)
                })
            }
        }
        }
    )
    }
    
    confirmTransition(route: Route, onComplete: Function, onAbort?: Function) { 
        const current = this.current
        this.pending = route
    
        const lastRouteIndex = route.matched.length - 1
        const lastCurrentIndex = current.matched.length - 1
    	if (isSameRoute(route, current) && lastRouteIndex === lastCurrentIndex && route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]){
            ...// 相同路由报错,创建执行队列
        }
        
        const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched)
        const queue: Array<?NavigationGuard> = [].concat(
            extractLeaveGuards(deactivated), // 组件内 `beforeRouteLeave`
            this.router.beforeHooks, // router.beforeEach
            extractUpdateHooks(updated), // 组件内 `beforeRouteUpdate`
            activated.map(m => m.beforeEnter), // router.beforeEnter
            resolveAsyncComponents(activated) // 异步解析激活组件
        )
        // 执行队列
        
        const iterator = (hook: NavigationGuard, next) => {
            ...
            try {
                hook(route, current, (to: any) => {}
            }
            ...
        }
        // hook迭代器
        
        runQueue(queue, iterator, () => {
            const enterGuards = extractEnterGuards(activated) // 组件内 `beforeRouteEnter`
            const queue = enterGuards.concat(this.router.resolveHooks)
            runQueue(queue, iterator, () => {
                this.pending = null
                onComplete(route)
                if (this.router.app) {
                    this.router.app.$nextTick(() => {
                        handleRouteEntered(route) 
                    })
                }
            }
        })
    }
    
    
}

function normalizeBase(base: ?string): string {
    // 尝试寻找<base href="..."/>标签
    if (!base) {
        if (inBrowser) {
            const baseEl = document.querySelector('base')
            base = (baseEl && baseEl.getAttribute('href')) || '/'
            base = base.replace(/^https?:\/\/[^\/]+/, '')
        } else {
            base = '/'
        }
    }
    if (base.charAt(0) !== '/') {
        base = '/' + base
    }
    return base.replace(/\/$/, '')
}