vue@3.3.4源码解析
前端路由次要有2局部组成:1 、url的解决; 2、 组件的加载
[toc]
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) 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 和 $route 绑定在Vue的原型对象上,不便全局拜访 Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 注册router-view和router-link组件 Vue.component('RouterView', View) Vue.component('RouterLink', Link) const strats = Vue.config.optionMergeStrategies // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created}
同vuex
一样,vue-router
的注入机会也是在beforeCreated
.不过在destroyed
的生命周期钩子中多了一步
VueRouter 类
History类
export class History { // 定义公有变量 router: Router base: string current: Route pending: ?Route cb: (r: Route) => void ready: boolean readyCbs: Array<Function> readyErrorCbs: Array<Function> errorCbs: Array<Function> listeners: Array<Function> cleanupListeners: Function // implemented by sub-classes +go: (n: number) => void +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void +replace: ( loc: RawLocation, onComplete?: Function, onAbort?: Function ) => void +ensureURL: (push?: boolean) => void +getCurrentLocation: () => string +setupListeners: Function constructor (router: Router, base: ?string) { this.router = router this.base = normalizeBase(base) // start with a route object that stands for "nowhere" this.current = START this.pending = null this.ready = false this.readyCbs = [] this.readyErrorCbs = [] this.errorCbs = [] this.listeners = [] } listen (cb: Function) { this.cb = cb } onReady (cb: Function, errorCb: ?Function) { if (this.ready) { cb() } else { this.readyCbs.push(cb) if (errorCb) { this.readyErrorCbs.push(errorCb) } } } onError (errorCb: Function) { this.errorCbs.push(errorCb) } // 重点: transitionTo ( location: RawLocation, onComplete?: Function, onAbort?: Function ) { let route // catch redirect option https://github.com/vuejs/vue-router/issues/3201 try { // 获取匹配的路由对象 route = this.router.match(location, this.current) } catch (e) { this.errorCbs.forEach(cb => { cb(e) }) // Exception should still be thrown 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) }) // fire ready cbs once if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { if (onAbort) { onAbort(err) } if (err && !this.ready) { // Initial redirection should not mark the history as ready yet // because it's triggered by the redirection instead // https://github.com/vuejs/vue-router/issues/3225 // https://github.com/vuejs/vue-router/issues/3331 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 abort = err => { // changed after adding errors with // https://github.com/vuejs/vue-router/pull/3047 before that change, // redirect and aborted navigation would produce an err == null if (!isNavigationFailure(err) && isError(err)) { if (this.errorCbs.length) { this.errorCbs.forEach(cb => { cb(err) }) } else { if (process.env.NODE_ENV !== 'production') { warn(false, 'uncaught error during route navigation:') } console.error(err) } } onAbort && onAbort(err) } const lastRouteIndex = route.matched.length - 1 const lastCurrentIndex = current.matched.length - 1 if ( isSameRoute(route, current) && // in the case the route map has been dynamically appended to lastRouteIndex === lastCurrentIndex && route.matched[lastRouteIndex] === current.matched[lastCurrentIndex] ) { 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) }) } }) }) } updateRoute (route: Route) { this.current = route this.cb && this.cb(route) } setupListeners () { // Default implementation is empty } teardown () { // clean up event listeners // https://github.com/vuejs/vue-router/issues/2341 this.listeners.forEach(cleanupListener => { cleanupListener() }) this.listeners = [] // reset current history route // https://github.com/vuejs/vue-router/issues/3294 this.current = START this.pending = null }}
HTML5History
export class HTML5History extends History { _startLocation: string constructor (router: Router, base: ?string) { super(router, base) // this._startLocation = getLocation(this.base) } // 设置监听器 setupListeners () { // 如果存在listener,间接返回 if (this.listeners.length > 0) { return } const router = this.router const expectScroll = router.options.scrollBehavior // 是否反对history.pushState api const supportsScroll = supportsPushState && expectScroll // 如果浏览器反对pushState的api,则设置popstate的监听事件 if (supportsScroll) { this.listeners.push(setupScroll()) } // 解决路由监听事件函数 const handleRoutingEvent = () => { const current = this.current // Avoiding first `popstate` event dispatched in some browsers but first // history route not updated since async guard at the same time. // 防止某些浏览器首次加载触发popstate事件,导致路由状态更新不同步 const location = getLocation(this.base) if (this.current === START && location === this._startLocation) { return } this.transitionTo(location, route => { if (supportsScroll) { // 保留页面滚动条地位 handleScroll(router, route, current, true) } }) } window.addEventListener('popstate', handleRoutingEvent) this.listeners.push(() => { window.removeEventListener('popstate', handleRoutingEvent) }) } // 封装history go办法 go (n: number) { window.history.go(n) } // push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { // pushState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } // replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { replaceState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } ensureURL (push?: boolean) { if (getLocation(this.base) !== this.current.fullPath) { const current = cleanPath(this.base + this.current.fullPath) push ? pushState(current) : replaceState(current) } } getCurrentLocation (): string { return getLocation(this.base) }}export function getLocation (base: string): string { let path = window.location.pathname const pathLowerCase = path.toLowerCase() const baseLowerCase = base.toLowerCase() // base="/a" shouldn't turn path="/app" into "/a/pp" // https://github.com/vuejs/vue-router/issues/3555 // so we ensure the trailing slash in the base if (base && ((pathLowerCase === baseLowerCase) || (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) { path = path.slice(base.length) } return (path || '/') + window.location.search + window.location.hash}
getLocation
获取url的path,不蕴含base
export function getLocation (base: string): string { let path = window.location.pathname const pathLowerCase = path.toLowerCase() const baseLowerCase = base.toLowerCase() // base="/a" shouldn't turn path="/app" into "/a/pp" // https://github.com/vuejs/vue-router/issues/3555 // so we ensure the trailing slash in the base if (base && ((pathLowerCase === baseLowerCase) || (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) { path = path.slice(base.length) } return (path || '/') + window.location.search + window.location.hash}
setupScroll 函数
export function setupScroll () { // Prevent browser scroll behavior on History popstate if ('scrollRestoration' in window.history) { window.history.scrollRestoration = 'manual' } // Fix for #1585 for Firefox // Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678 // Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with // window.location.protocol + '//' + window.location.host // location.host contains the port and location.hostname doesn't const protocolAndPath = window.location.protocol + '//' + window.location.host const absolutePath = window.location.href.replace(protocolAndPath, '') // preserve existing history state as it could be overriden by the user const stateCopy = extend({}, window.history.state) stateCopy.key = getStateKey() window.history.replaceState(stateCopy, '', absolutePath) window.addEventListener('popstate', handlePopState) return () => { window.removeEventListener('popstate', handlePopState) }}
pushState函数
export function pushState (url?: string, replace?: boolean) { saveScrollPosition() // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls const history = window.history try { if (replace) { // preserve existing history state as it could be overriden by the user const stateCopy = extend({}, history.state) stateCopy.key = getStateKey() history.replaceState(stateCopy, '', url) } else { history.pushState({ key: setStateKey(genStateKey()) }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) }}
replaceState函数
export function replaceState (url?: string) { pushState(url, true)}
HashHistory
export class HashHistory extends History { constructor (router: Router, base: ?string, fallback: boolean) { super(router, base) // check history fallback deeplinking if (fallback && checkFallback(this.base)) { return } ensureSlash() } // this is delayed until the app mounts // to avoid the hashchange listener being fired too early setupListeners () { if (this.listeners.length > 0) { return } const router = this.router const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { this.listeners.push(setupScroll()) } const handleRoutingEvent = () => { const current = this.current if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } if (!supportsPushState) { replaceHash(route.fullPath) } }) } // 如果反对popstate则,应用popstate监听,否则应用hashchange监听,留神vue-router4曾经不反对hashchange监听 const eventType = supportsPushState ? 'popstate' : 'hashchange' window.addEventListener( eventType, handleRoutingEvent ) this.listeners.push(() => { window.removeEventListener(eventType, handleRoutingEvent) }) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { replaceHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } go (n: number) { window.history.go(n) } ensureURL (push?: boolean) { const current = this.current.fullPath if (getHash() !== current) { push ? pushHash(current) : replaceHash(current) } } getCurrentLocation () { return getHash() }}
pushHash
function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path }}
replaceHash
function replaceHash (path) { if (supportsPushState) { replaceState(getUrl(path)) } else { window.location.replace(getUrl(path)) }}
Hash路由与History的区别
- hash路由在地址栏URL上有#,而history路由没有会难看一点
- 咱们进行回车刷新操作,hash路由会加载到地址栏对应的页面,而history路由个别就404报错了,所以history 在部署的时候,如 nginx, 须要只渲染⾸⻚,让⾸⻚依据门路从新跳转。
- hash路由反对低版本的浏览器,而history路由是HTML5新增的API。(IE10及以上)
- 默认扭转hash路由,浏览器端不会发出请求,次要用于锚点;history路由 go/back/forward以及浏览器中的后退,后退按钮,个别都会向服务器发动申请
- hash 模式,是不⽀持SSR的,然而 history 模式能够做 SSR
History对象
- pushState / replaceState都不会触发popState事件
popState什么时候触发
- 点击浏览器的后退/后退按钮
- back/forward/go
导航守卫的触发程序
- 【组件】- 前一个组件的beforeRouteLeave
- 【全局】- router.beforeEach
- 【组件】- 如果是路由的参数变动,会触发beforeRouteUpdate
- 【配置文件】- beforeEnter
- 【组件】外部申明的beforeRouteEnter
- 【全局】beforeResolve
- 【全局】router.afterEach
手写Hsitory路由
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>History 路由</title> </head> <body> <div id="container"> <button onclick="window.location.hash = '#'">首页</button> <button onclick="window.location.hash = '#about'">对于咱们</button> <button onclick="window.location.hash = '#user'">用户列表</button> </div> <div id="context"></div> <script> class HistoryRouter { constructor() { this.routes = {}; this._bindPopstate(); // this.init(); } init(path) { window.history.replaceState({ path }, null, path); const cb = this.routes[path]; if (cb) { cb(); } } route(path, callback) { this.routes[path] = callback || function () {}; } go(path) { window.history.pushState({ path }, null, path); const cb = this.routes[path]; if (cb) { cb(); } } _bindPopstate() { window.addEventListener("popstate", (e) => { const path = e.state && e.state.path; this.routes[path] && this.routes[path](); }); } } const Route = new HistoryRouter(); Route.route("./about", () => changeText("对于咱们页面")); Route.route("./user", () => changeText("用户列表页")); Route.route("./", () => changeText("首页")); function changeText(arg) { document.getElementById("context").innerHTML = arg; } container.addEventListener("click", (e) => { if (e.target.tagName === "A") { e.preventDefault(); Route.go(e.target.getAttribute("href")); } }); </script> </body></html>
手写Hash路由
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Hash 路由</title> </head> <body> <div id="container"> <button onclick="window.location.hash = '#'">首页</button> <button onclick="window.location.hash = '#about'">对于咱们</button> <button onclick="window.location.hash = '#user'">用户列表</button> </div> <div id="context"></div> <script> class HashRouter { constructor() { this.routes = {}; this.refresh = this.refresh.bind(this); window.addEventListener("load", this.refresh); window.addEventListener("hashchange", this.refresh); } route(path, callback) { this.routes[path] = callback || function () {}; } refresh() { const path = `/${window.location.hash.slice(1) || ""}`; this.routes[path](); } } const Route = new HashRouter(); Route.route("/about", () => changeText("对于咱们页面")); Route.route("/user", () => changeText("用户列表页")); Route.route("/", () => changeText("首页")); function changeText(arg) { document.getElementById("context").innerHTML = arg; } </script> </body></html>
webpack import函数
vue 路由懒加载时通过Vue的异步组件和Webpack的代码宰割性能来实现的。
代码宰割是webpack最引人注目的个性之一。这个个性容许您将代码宰割成各种包,而后能够按需或并行加载这些包。它能够用来实现更小的bundle和管制资源加载优先级,如果应用正确,会对加载工夫产生重大影响。
这里次要剖析import()
这种代码宰割形式,其余形式疏忽。
- 首先,
webpack
遇到import办法
时,会将其当成一个代码宰割点,也就是说碰到import办法
了,那么就去解析import办法
。 - 而后,
import
援用的文件,webpack
会将其编译成一个jsonp
,也就是一个自执行函数,而后函数外部是援用的文件的内容,因为到时候是通过jsonp
的办法去加载的 - 具体就是,
import
援用文件,会先调用require.ensure
办法(打包的后果来看叫require.e
),这个办法次要是结构一个promise
,会将resolve
,reject
和promise
放到一个数组中,将promise
放到一个队列中。 - 而后,调用
require.load
(打包后果来看叫require.l
)办法,这个办法次要是创立一个jsonp
,也就是创立一个script
标签,标签的url
就是文件加载地址,而后塞到document.head
中,一塞进去,就会加载该文件了。 - 加载完,就去执行这段
jsonp
,次要就是把moduleId
和module
内容存到modules数组中,而后再去走webpack
内置的require
。 - webpack内置的require,次要是先判断缓存,这个moduleId是否缓存过了,如果缓存过了,就间接返回。如果没有缓存,再持续往下走,也就是加载module内容,而后最终内容会挂在都module,exports上,返回module.exports就返回了援用文件的最终执行后果。
简略讲就是:promise.all +jsonp +动态创建script标签
参考文章
- window History
- hash路由和history路由的区别
- webpack动静加载原理/webpack异步加载原理/webpack动静解析import()原理