vivipure / blog

Funkun's blog.
https://funkun.hashnode.dev/
2 stars 0 forks source link

Vue Router 源码学习 #8

Open vivipure opened 2 years ago

vivipure commented 2 years ago

1. 我的疑问

Vue Router 的使用频率还是很高的,作为开发者,我们可能知道 hash路由和 history路由 的区别和实现原理。但是一些还是东西是值得理解的。

  1. 内置组件 router-view 是怎么实现的
  2. 路由守卫是怎么实现的
  3. 和Transition是怎么搭配合作的
  4. routes 数据是怎么解析的
  5. 和keep-alive 是怎么配合

2. 基本介绍

这里就不介绍基本的使用

项目地址:https://github.com/vuejs/vue-router 构建工具: Rollup 入口文件:src/index.js

3. 入口文件分析

3.1 install

看下 install 函数执行的逻辑

export function install (Vue) {
    // 避免重复注册
    if (install.installed && _Vue === Vue) return
    install.installed = true
    _Vue = Vue
    const isDef = v => v !== undefined
    // 暂时不知道少用
    const registerInstance = (vm, callVal) => {
        let i = vm.$options._parentVnode
        if (
            isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)
        ){
            i(vm, callVal)
        }
    }
    // 混入逻辑,
    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 {
                // 由于是树形结构,因此子组件会找到离自己最近的 带有router的组件
                this._routerRoot = 
                    (this.$parent && this.$parent._routerRoot) || this
            }
            registerInstance(this, this)
        },
        destroyed () {
            registerInstance(this)
        }
    })

    // 定义全局属性
    Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
    })
    Object.defineProperty(Vue.prototype, '$route', {
        get () { return this._routerRoot._route }
    })
    // 组件注册
    Vue.component('RouterView', View)
    Vue.component('RouterLink', Link)
}

相关注释我都写在代码中了,主要逻辑就是在组件中混入了路由的属性,定义全局的属性,注册了两个内置组件。

这里比较巧妙的是通过树形结构的特性,保证了拥有 options.router 的组件进行了路由初始化,子组件根据父组件层层查找,找到离自己最近的带有 router 的组件。

然后 registerInstance 方法暂时不知道用法,

3.2 VueRouter

VueRouter 类在 src/index.js 中,默认导出的就是 VueRouter, 我们在业务开发时也通过实例化 VueRouter 来生成 router 给应用使用。

3.2.1 构造函数

constructor (options: RouterOptions = {}) {
  this.app = null
  this.apps = []
  this.options = options
  this.beforeHooks = []
  this.resolveHooks = []
  this.afterHooks = []

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

  // 根据options结合实际浏览器确定 路由模式
  let mode = options.mode || 'hash'
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
  if (this.fallback) {
    mode = 'hash'
  }
    // 如果不在浏览器则为 abstract模式
  if (!inBrowser) {
    mode = 'abstract'
  }
  this.mode = mode
  // 根据路由模式生成 history 对象
  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}`)
      }
  }
}

这里主要的逻辑就是通过options.router 生成 matcher, 和通过路由模式生成对应的 history对象. 通过这里我才知道原来 路由还有 abstract 模式, 提供给服务端或者ssr模式使用,应该和V4版本的 Memory mode是一样的。

3.2.2 init

install的过程中,VueRouterVuebeforeCreated 逻辑, 对有 routeroptions 进行了路由初始化

this._router.init(this)

因此我们再看看 routerinit 方法

init (app: any) {
  // 将当前组件推入 app 中
  this.apps.push(app)
  if (this.app) {
    return
  }
  this.app = app

  const history = this.history
  // 属于hash 和 history 模式
  if (history instanceof HTML5History || history instanceof HashHistory) {
     // scrollBehavior支持
    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()
        handleInitialScroll(routeOrError)
    }
    // 切换到当前链接对应的路由
    history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
    )
  }
  // 路由更新后,更新组件的_route
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

初始化时将当前组件进行保存,将当前路由切换到当前链接对应路由,也设置了订阅,当 history 改变时,会更新组件的 route

3.2.3 transitionTo

看下 tansitionTo 的实际逻辑

// 匹配到路由
route = this.router.match(location, this.current)
const prev = this.current

// 进行切换动画
this.confirmTransition(
    route,
    () => {
        // 更新当前路由
        this.updateRoute(route)
        onComplete && onComplete(route)
        // 更改url
        this.ensureURL()
        // hook
        this.router.afterHooks.forEach(hook => {
            hook && hook(route, prev)
        })
        // 初始化回调
        if (!this.ready) {
            ...
        }
    },
    err => {
    // 错误处理
})

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    this.pending = route
    // 错误处理
    const abort = err => {}
    // 当前路由重复导航处理
    if (isDuplicatedRoute) {
        this.ensureURL()
        if (route.hash) {
            handleScroll(this.router, current, route, false)
        }
        return abort(createNavigationDuplicatedError(current, route))
    }

    const { updated, deactivated, activated } = resolveQueue(
        this.current.matched,
        route.matched
    )
    const queue: Array<?NavigationGuard> = [].concat(
        // in-component leave guards
        extractLeaveGuards(deactivated),
        // global before hooks
        this.router.beforeHooks,
        // in-component update hooks
        extractUpdateHooks(updated),
        // in-config enter guards
        activated.map(m => m.beforeEnter),
        // async components
        resolveAsyncComponents(activated)
    )
    const iterator = (hook: NavigationGuard, next) => {
        if (this.pending !== route) {
            return abort(createNavigationCancelledError(current, route))
        }
        try {
            hook(route, current, (to: any) => {
                if (to === false) {
                    // next(false) -> abort navigation, ensure current URL
                    this.ensureURL(true)
                    abort(createNavigationAbortedError(current, route))
                } else if (isError(to)) {
                    this.ensureURL(true)
                    abort(to)
                } else if (
                    typeof to === 'string' ||
                    (typeof to === 'object' &&
                    (typeof to.path === 'string' || typeof to.name === 'string'))
                ) {
                    // next('/') or next({ path: '/' }) -> redirect
                    abort(createNavigationRedirectedError(current, route))
                    if (typeof to === 'object' && to.replace) {
                        this.replace(to)
                    } else {
                        this.push(to)
                    }
                } else {
                    // confirm transition and pass on the value
                    next(to)
                }
            })
        } catch (e) {
            abort(e)
        }
    }

    runQueue(queue, iterator, () => {
        // wait until async components are resolved before
        // extracting in-component enter guards
        const enterGuards = extractEnterGuards(activated)
        const queue = enterGuards.concat(this.router.resolveHooks)
        runQueue(queue, iterator, () => {
            if (this.pending !== route) {
                return abort(createNavigationCancelledError(current, route))
            }
            this.pending = null
            onComplete(route)
            if (this.router.app) {
                this.router.app.$nextTick(() => {
                    handleRouteEntered(route)
                })
            }
        })
    })
}

这里主要执行 confirmTransition 方法,主要逻辑分为几步:

  1. 检查是否重复路由,进行处理
  2. reolveQuene 筛选出当前路由和跳转路由的差异
  3. 然后对迭代一个队列,队列包含
    1. 激活失效组件 beforeLeave 路由守卫
    2. 全局路由 beforHooks
    3. 重用组件 update 路由守卫
    4. 激活路由 配置的beforeEnter hook
    5. 处理异步组件加载逻辑 如果顺序执行中有一个任务失败,则不会继续下面的任务
  4. 队列跑完之后,执行新的队列任务。队列包括
    1. 激活组件的 beforeEnter hook
    2. 全局路由的 resolveHooks 执行完成后,在 nextTick 后执行路由完成后的回调操作,调用全局的 afterEach 钩子。

3.2.4 matcher

上面的路由切换方法中,进场会比较 routematched 属性,而在构造函数中也有

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

因此我们来研究下 matcher

function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {

const { pathList, pathMap, nameMap } = createRouteMap(routes)
...
return {
    match,
    addRoute,
    getRoutes,
    addRoutes
}

首先是根据传入的 routes 通过 createRouteMap 解析为 listmap .

在平时的业务开发中,路由的配置不是固定的,根据用户的权限生产对应的路由才是合理的。因此会使用两个方法:

  1. addRoutes
    function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
    }
  2. addRoute

    function addRoute (parentOrRoute, route) {
    const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
    
    createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)
    if (parent && parent.alias.length) {
        createRouteMap(
            parent.alias.map(alias => ({ path: alias, children: [route] })),
            pathList,
            pathMap,
            nameMap,
            parent
        )
    }
    }

    逻辑都很简单,直接调用 createRouteMap 的方法即可。在添加单个 route 时则会处理父级路由和别名的相关逻辑。

createMatcher 的返回值中还包括 match 方法, 通过这个方法将 Location 转化为 Route. 这里的 Location 就是我们平时使用 push 等方法传入的参数。

 type Location = {
    _normalized?: boolean;
    name?: string;
    path?: string;
    hash?: string;
    query?: Dictionary<string>;
    params?: Dictionary<string>;
    append?: boolean;
    replace?: boolean;
}

 type RawLocation = string | Location

现在看看 match 方法的执行逻辑

function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
): Route {
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location
    if (name) {
        const record = nameMap[name]

        if (process.env.NODE_ENV !== 'production') {
            warn(record, `Route with name '${name}' does not exist`)
        }

        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]
                }
            }
        }

        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)
            }
        }
    }
    // no match
    return _createRoute(null, location)
}

最终的到的东西就是一个 Route对象

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) : [] // 这里通过遍历父级,得到完整的路径数组
  }

由于 这个对象最终被 Object.freeze() ,因此实际使用时,我们无法更改上面的属性

4 内置组件

4. 1 router-link

router-link 自动处理了 a 标签 点击跳转的情况,在点击时会触发路由跳转的事件

const handler = e => {
    // 默认事件
    if (guardEvent(e)) {
        if (this.replace) {
        router.replace(location, noop)

        } else {

        router.push(location, noop)

        }
    }
}

4.2 router-view

render (_, { props, children, parent, data }) {
    // routerView标识
    data.routerView = true
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route

    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
        const vnodeData = parent.$vnode ? parent.$vnode.data : {}
        if (vnodeData.routerView) {
            depth++
        }
        // keep-alive 逻辑
        if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
            inactive = true
        }
        parent = parent.$parent

    }
    // 得到routerview的深度,确定当前的路由
    data.routerViewDepth = depth

    // render previous view if the tree is inactive and kept-alive
    if (inactive) {
        const cachedData = cache[name]
        const cachedComponent = cachedData && cachedData.component

        if (cachedComponent) {
            if (cachedData.configProps) {
                fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
            }
            return h(cachedComponent, data, children)
        } else {
            return h()
        }
    }

    const matched = route.matched[depth]
    const component = matched && matched.components[name]
    // cache component
    cache[name] = { component }

    // 这个方法也是在初始化的时候进行的调用
    // 用来设置当前路由匹配的组件实例
    data.registerRouteInstance = (vm, val) => {
        const current = matched.instances[name]
        if (
        (val && current !== vm) ||
        (!val && current === vm)
        ) {
            matched.instances[name] = val
        }
    }
    // hook注入
    data.hook.prepatch = (_, vnode) => {
        matched.instances[name] = vnode.componentInstance
    }
    data.hook.init = (vnode) => {
        if (vnode.data.keepAlive &&
        vnode.componentInstance &&
        vnode.componentInstance !== matched.instances[name]
        ) {
        matched.instances[name] = vnode.componentInstance
        }
        handleRouteEntered(route)
    }

    const configProps = matched.props && matched.props[name]
    return h(component, data, children)

}

主要逻辑就是:

  1. 标识当前路由为 routerview
  2. 往父级遍历,得到当前的 routerview 深度,确定 route
  3. 兼容 keep-alive 的逻辑,处理缓存逻辑
  4. 渲染组件

5. 最后总结

看完核心逻辑后,我最初的疑问基本得到了解答。感觉还是挺有收获的。知道了路由的第三种模式,路由切换的整体过程,路由的匹配逻辑。也了解了 router-view 这种函数式组件的实现。 我还有一个问题未得到答案

3. 和Transition是怎么搭配合作的

看来只有到时候看 Vue 源码时才能有收获了.