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标签
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。