vue-router 源码分析——5.编程式导航

这是对vue-router 3 版本的源码分析。
本次分析会按以下方法进行:

  1. 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
  2. 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
  3. 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。

本章讲解router中编程式导航是如何实现的。
另外我的vuex3源码分析也发布完了,欢迎大家学习:
vuex3 最全面最透彻的源码分析
还有vue-router的源码分析:
vue-router 源码分析——1. 路由匹配
vue-router 源码分析——2. router-link 组件是如何实现导航的
vue-router 源码分析——3. 动态路由匹配
vue-router 源码分析——4.嵌套路由

router.push(location, onComplete?, onAbort?)

  1. 官网内容
    1 想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
    2 该方法的参数可以是一个字符串路径,或者一个描述地址的对象。
    3.注意:如果提供了 path,params 会被忽略。
  2. 通过下面对router.push方法的分析,发现router.push实际上是调用了一个名为 HashHistory 类的push方法,同时如果支持promise的话会返回一个promise。
// ./router.js
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'

export default class VueRouter {
    ...
    // 这里只看默认的hash-mode对应的内容
    history: HashHistory | HTML5History | AbstractHistory
    constructor(options: RouterOptions = {}) {
        ...
        switch(mode) {
            case 'hash':
                this.history = new HashHistory(this, options.base, this.fallback)
                break
        }
    }
    push(location: RawLocation, onComplete?: Function, onAbort?: Function)) {
        if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
            // 以下对应官网的“此时如果支持 Promise,router.push 或 router.replace 将返回一个 Promise”
            return new Promise((resolve, reject) => {
                this.history.push(location, resolve, reject)            
            })        
        } else {
            this.history.push(location, onComplete, onAbort)        
        }
    }
}
  1. 看HashHistory类是如何定义push方法的
    1 HashHistory.push实际上是调用了 History.transitionTo 方法,自身并没有做太多处理,接着看 History 类的代码。
    2.History.transitionTo 先通过调用 this.router.match 创建了一个新的路由对象(即跳转到的路由),然后调用了 confirmTransition 方法。调用了 confirmTransition 方法时的第二个参数,即onComplete,除了传入最开始的onComplete以外,还加入了其他逻辑处理,例如更新路由,触发钩子函数等等。
    3.History.confirmTransition 方法:构建了一个队列,里面除了路由信息,还包含路由守卫、钩子函数的解析。之后使用 runQueue 方法来顺序执行队列中的相关内容。执行完成后调用 onComplete 回调,更新路由。
// ./history/hash.js
import { History } from './base'
export class HashHistory extends History {
    constructor(roter: Router, base: ?string, fallback: boolean) {
        super(router, base)
        ...    
    }
    push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        const { current: fromRoute } = this
        this.transitionTo(
            location,
            route => {
                pushHash(route.fullPath),
                onComplete && onComplete(route)
                ...
            },
            onAbort        
        )    
    }
}

// ./history/base.js
export class History {
    router: Router
    current: Route
    ...
    constructor(router: Router, base: ?string) {
        this.router = router
        ...    
    }
    transitionTo(
        location: RawLocation,
        onComplete?: Function,
        onAbort?: Function   
    ) {
        let route
        try {
            route = this.router.match(location, this.current)        
        } catch(e) {...}
        const prev = this.current
        this.confirmTransition(
            route,
            () => {
                this.updateRoute(route)
                onComplete && onComplete(route)
                ...                            
            },
            err => {
                if (onAbort) {
                    onAbort(err)                
                }
                ...      
            }
        )
    }
}
  1. 最后,调用pushHash函数来更新浏览器的URL
    1 pushHash会根据浏览器是否支持 pushState 来决定是使用 history.pushState 还是 window.location.hash。
    2 二者的区别在于pushState 可用于管理或者监听浏览器的历史记录的相关操作,都不会重新加载页面。
    3 pushState本质调用的是 window.history.pushState。
// ./history/hash.js
import { History } from './base'
export class HashHistory extends History {
    constructor(roter: Router, base: ?string, fallback: boolean) {
        super(router, base)
        ...    
    }
    push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        const { current: fromRoute } = this
        this.transitionTo(
            location,
            route => {
                // 解析完成后,更新url,执行onComplete
                pushHash(route.fullPath),
                onComplete && onComplete(route)
                ...
            },
            onAbort        
        )    
    }
}

function pushHash(path) {
    if (supportsPushState) {
        pushState(getUrl(path))    
    } else {
        window.location.hash = path    
    }
}
  1. “洋葱模型”设计
    1 最后这里再多分析一点,就是对 onComplete 的处理。这里很好地体现了VUE的"洋葱模型"设计。
    2 在最开始执行router.push时,onComplete只是用户自己定义的函数。
    3.在 transitionTo 方法中,对用户定义的onComplete执行前,加入了更新浏览器URL,处理滚动的函数,组成一个新的 onComplete 函数传入transitionTo中。
    4.在 transitionTo 中,又在上一层的 onComplete 函数前后,加入了更新路由、执行相关钩子函数的包裹,作为一个新的 onComplete 传入 confirmTransition 方法。
    5.这种模式有很多好处,个人认为最主要的还是代码的模块性,维护性以及业务解耦。
// ./history/hash.js
...
    push(location, onComplete, onAbort) {
        ...
        // 这里的 onComplete 是最开始用户定义的函数
        this.transitionTo(
            location,
            // 将用户定义的 onComplete 做了一层包裹,加入了其他业务处理,传入transitionTo的onComplete
            route => {
                pushHash(route, fullPath)
                handleScroll(this.router, route, fromRoute, false)
                onComplete && onComplete(route)            
            }        
        )    
    }

// ./history/base.js
...
    transitionTo(location, onComplete, onAbort) {
        ...
        this.confirmTransition(
            route,
            () => {
                // 在 transitionTo 中,又对 onComplete 裹上一层逻辑传入 confirmTransition
                this.updateRoute(route)
                onComplete && onComplete(route)
                this.ensureURL()
                this.router.afterHooks.forEach(hook =>{...})                            
            }        
        )    
    }

router.replace(location, onComplete?, onAbort?)

  1. 官网内容:
    1.router.replace(…)
    2.跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
  2. 区别就在于replace调用的是window.history.replaceState,而push调用的是window.history.pushState。
// ./reouter.js
...
    replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
            return new Promise((resolve, reject) => {
                this.history.replace(location, resolve, reject)            
            }        
        } else {
            this.history.replace(location, onComplete, onAbort)
        }
    }

 // ./history/hash.js
import { pushState, replaceState, supportsPushState } from '../util/push-state'
...
    replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        ...
        this.transitionTo(
            location,
            reoute => {
                replaceHash(route.fullPath),
                ...             
            })     
    }

function replaceHash(path) {
    if (supportsPushState) {
        replaceState(getUrl(path))
    } else {
        window.location.replace(getUrl(path))
    }
}

// ./util/push-state.js
export function replaceStaet(url?: string) {
    pushState(url, true)
}

export function pushState(url?: string, replace?: boolean) {
    const history = window.history
    try {
        if (replace) {
            const stateCopy = extend({}, history.state)
            history.replaceState(stateCopy, '', url)        
        } else {
            history.pushState({ key: setStateKey(genStateKey()) }, '', url)
        }
    }
}

router.go(n)

  1. 这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
// ./router.js
...
    go(n: number) {
        this.history.go(n)    
    }

// ./history/hash.js
...
    go(n: number) {
        window.history.go(n)    
    }

总结

1.Vue-Router的push、replace、go都是对window.history的pushState、replaceState、go的封装。
2.利用了“洋葱模型"将router的相关功能(路由守卫、钩子函数等)加入到url跳转的过程中
3.利用上面的设计模式,在保证业务解耦的前提下,完全可以达到官网说的“Vue Router 的导航方法 (push、 replace、 go) 在各类路由模式 (history、 hash 和 abstract) 下表现一致。”

  • 30
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值