溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

vue router 源碼概覽案例分析

發(fā)布時(shí)間:2020-10-02 08:50:07 來(lái)源:腳本之家 閱讀:211 作者:清夜 欄目:web開(kāi)發(fā)

源碼這個(gè)東西對(duì)于實(shí)際的工作其實(shí)沒(méi)有立竿見(jiàn)影的效果,不會(huì)像那些針對(duì)性極強(qiáng)的文章一樣看了之后就立馬可以運(yùn)用到實(shí)際項(xiàng)目中,產(chǎn)生什么樣的效果,源碼的作用是一個(gè)潛移默化的過(guò)程,它的理念、設(shè)計(jì)模式、代碼結(jié)構(gòu)等看了之后可能不會(huì)立即知識(shí)變現(xiàn)(或者說(shuō)變現(xiàn)很少),而是在日后的工作過(guò)程中悄無(wú)聲息地發(fā)揮出來(lái),你甚至都感覺(jué)不到這個(gè)過(guò)程

另外,優(yōu)秀的源碼案例,例如 vue 、 react 這種,內(nèi)容量比較龐大,根本不是三篇五篇十篇八篇文章就能說(shuō)完的,而且寫(xiě)起來(lái)也很難寫(xiě)得清楚,也挺浪費(fèi)時(shí)間的,而如果只是分析其中一個(gè)點(diǎn),例如 vue 的響應(yīng)式,類(lèi)似的文章也已經(jīng)夠多了,沒(méi)必要再 repeat

所以我之前沒(méi)專(zhuān)門(mén)寫(xiě)過(guò)源碼分析的文章,只是自己看看,不過(guò)最近閑來(lái)無(wú)事看了 vue-router 的源碼,發(fā)現(xiàn)這種插件級(jí)別的東西,相比 vue 這種框架級(jí)別的東西,邏輯簡(jiǎn)單清晰,沒(méi)有那么多道道,代碼量也不多,但是其中包含的理念等東西卻很精煉,值得一寫(xiě),當(dāng)然,文如其名,只是概覽,不會(huì)一行行代碼分析過(guò)去,細(xì)節(jié)的東西還是要自己看看的

vue.use

vue 插件必須通過(guò) vue.use 進(jìn)行注冊(cè), vue.use 的代碼位于 vue 源碼的 src/core/global-api/use.js 文件中,此方法的主要作用有兩個(gè):

  • 對(duì)注冊(cè)的組件進(jìn)行緩存,避免多次注冊(cè)同一個(gè)插件
if (installedPlugins.indexOf(plugin) > -1) {
 return this
}
  • 調(diào)用插件的 install 方法或者直接運(yùn)行插件,以實(shí)現(xiàn)插件的 install
if (typeof plugin.install === 'function') {
 plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
 plugin.apply(null, args)
}

路由安裝

vue-router 的 install 方法位于 vue-router 源碼的 src/install.js 中 主要是通過(guò) vue.minxin 混入 beforeCreate 和 destroyed 鉤子函數(shù),并全局注冊(cè) router-view 和 router-link 組件

// src/install.js
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)
 }
})
...
// 全局注冊(cè) `router-view` 和 `router-link`組件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)

路由模式

vue-router 支持三種路由模式( mode ): hash 、 history 、 abstract ,其中 abstract 是在非瀏覽器環(huán)境下使用的路由模式,例如 weex

路由內(nèi)部會(huì)對(duì)外部指定傳入的路由模式進(jìn)行判斷,例如當(dāng)前環(huán)境是非瀏覽器環(huán)境,則無(wú)論傳入何種 mode ,最后都會(huì)被強(qiáng)制指定為 abstract ,如果判斷當(dāng)前環(huán)境不支持 HTML5 History ,則最終會(huì)被降級(jí)為 hash 模式

// src/index.js
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
 mode = 'hash'
}
if (!inBrowser) {
 mode = 'abstract'
}

最后會(huì)對(duì)符合要求的 mode 進(jìn)行對(duì)應(yīng)的初始化操作

// src/index.js
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}`)
  }
}

路由解析

通過(guò)遞歸的方式來(lái)解析嵌套路由

// src/create-route-map.js
function addRouteRecord (
 pathList: Array<string>,
 pathMap: Dictionary<RouteRecord>,
 nameMap: Dictionary<RouteRecord>,
 route: RouteConfig,
 parent?: RouteRecord,
 matchAs?: string
) {
 ...
 route.children.forEach(child => {
  const childMatchAs = matchAs
   ? cleanPath(`${matchAs}/${child.path}`)
   : undefined
  addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
 })
 ...
}

解析完畢之后,會(huì)通過(guò) key-value 對(duì)的形式對(duì)解析好的路由進(jìn)行記錄,所以如果聲明多個(gè)相同路徑( path )的路由映射,只有第一個(gè)會(huì)起作用,后面的會(huì)被忽略

// src/create-route-map.js
if (!pathMap[record.path]) {
 pathList.push(record.path)
 pathMap[record.path] = record
}

例如如下路由配置,路由 /bar 只會(huì)匹配 Bar1 , Bar2 這一條配置會(huì)被忽略

const routes = [
 { path: '/foo', component: Foo },
 { path: '/bar', component: Bar1 },
 { path: '/bar', component: Bar2 },
];

路由切換

當(dāng)訪問(wèn)一個(gè) url 的時(shí)候, vue-router 會(huì)根據(jù)路徑進(jìn)行匹配,創(chuàng)建出一個(gè) route 對(duì)象,可通過(guò) this.$route 進(jìn)行訪問(wèn)

// src/util/route.js
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) : []
}

src/history/base.js 源碼文件中的 transitionTo() 是路由切換的核心方法

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
 const route = this.router.match(location, this.current)
 this.confirmTransition(route, () => {
 ...
}

路由實(shí)例的 push 和 replace 等路由切換方法,都是基于此方法實(shí)現(xiàn)路由切換的,例如 hash 模式的 push 方法:

// src/history/hash.js
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
 const { current: fromRoute } = this
 // 利用了 transitionTo 方法
 this.transitionTo(location, route => {
  pushHash(route.fullPath)
  handleScroll(this.router, route, fromRoute, false)
  onComplete && onComplete(route)
 }, onAbort)
}

transitionTo 方法內(nèi)部通過(guò)一種異步函數(shù)隊(duì)列化執(zhí)⾏的模式來(lái)更新切換路由,通過(guò) next 函數(shù)執(zhí)行異步回調(diào),并在異步回調(diào)方法中執(zhí)行相應(yīng)的鉤子函數(shù)(即 導(dǎo)航守衛(wèi)) beforeEach 、 beforeRouteUpdate 、 beforeRouteEnter 、 beforeRouteLeave

通過(guò) queue 這個(gè)數(shù)組保存相應(yīng)的路由參數(shù):

// src/history/base.js
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)
)

通過(guò) runQueue 以一種遞歸回調(diào)的方式來(lái)啟動(dòng)異步函數(shù)隊(duì)列化的執(zhí)⾏:

// src/history/base.js
// 異步回調(diào)函數(shù)
runQueue(queue, iterator, () => {
 const postEnterCbs = []
 const isValid = () => this.current === route
 // wait until async components are resolved before
 // extracting in-component enter guards
 const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
 const queue = enterGuards.concat(this.router.resolveHooks)
 // 遞歸執(zhí)行
 runQueue(queue, iterator, () => {
  if (this.pending !== route) {
   return abort()
  }
  this.pending = null
  onComplete(route)
  if (this.router.app) {
   this.router.app.$nextTick(() => {
    postEnterCbs.forEach(cb => { cb() })
   })
  }
 })
})

通過(guò) next 進(jìn)行導(dǎo)航守衛(wèi)的回調(diào)迭代,所以如果在代碼中顯式聲明了導(dǎo)航鉤子函數(shù),那么就必須在最后調(diào)用 next() ,否則回調(diào)不執(zhí)行,導(dǎo)航將無(wú)法繼續(xù)

// src/history/base.js
const iterator = (hook: NavigationGuard, next) => {
 ...
 hook(route, current, (to: any) => {
  ...
  } else {
   // confirm transition and pass on the value
   next(to)
  }
 })
...
}

路由同步

在路由切換的時(shí)候, vue-router 會(huì)調(diào)用 push 、 go 等方法實(shí)現(xiàn)視圖與地址 url 的同步

地址欄 url 與視圖的同步

當(dāng)進(jìn)行點(diǎn)擊頁(yè)面上按鈕等操作進(jìn)行路由切換時(shí), vue-router 會(huì)通過(guò)改變 window.location.href 來(lái)保持視圖與 url 的同步,例如 hash 模式的路由切換:

// src/history/hash.js
function pushHash (path) {
 if (supportsPushState) {
  pushState(getUrl(path))
 } else {
  window.location.hash = path
 }
}

上述代碼,先檢測(cè)當(dāng)前瀏覽器是否支持 html5 的 History API ,如果支持則調(diào)用此 API 進(jìn)行 href 的修改,否則直接對(duì) window.location.hash 進(jìn)行賦值 history 的原理與此相同,也是利用了 History API

視圖與地址欄 url 的同步

當(dāng)點(diǎn)擊瀏覽器的前進(jìn)后退按鈕時(shí),同樣可以實(shí)現(xiàn)視圖的同步,這是因?yàn)樵诼酚沙跏蓟臅r(shí)候,設(shè)置了對(duì)瀏覽器前進(jìn)后退的事件監(jiān)聽(tīng)器

下述是 hash 模式的事件監(jiān)聽(tīng):

// src/history/hash.js
setupListeners () {
 ...
 window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
  const current = this.current
  if (!ensureSlash()) {
   return
  }
  this.transitionTo(getHash(), route => {
   if (supportsScroll) {
    handleScroll(this.router, route, current, true)
   }
   if (!supportsPushState) {
    replaceHash(route.fullPath)
   }
  })
 })
}

history 模式與此類(lèi)似:

// src/history/html5.js
window.addEventListener('popstate', e => {
 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.
 const location = getLocation(this.base)
 if (this.current === START && location === initLocation) {
  return
 }

 this.transitionTo(location, route => {
  if (supportsScroll) {
   handleScroll(router, route, current, true)
  }
 })
})

無(wú)論是 hash 還是 history ,都是通過(guò)監(jiān)聽(tīng)事件最后來(lái)調(diào)用 transitionTo 這個(gè)方法,從而實(shí)現(xiàn)路由與視圖的統(tǒng)一

另外,當(dāng)?shù)谝淮卧L問(wèn)頁(yè)面,路由進(jìn)行初始化的時(shí)候,如果是 hash 模式,則會(huì)對(duì) url 進(jìn)行檢查,如果發(fā)現(xiàn)訪問(wèn)的 url 沒(méi)有帶 # 字符,則會(huì)自動(dòng)追加,例如初次訪問(wèn) http://localhost:8080 這個(gè) url , vue-router 會(huì)自動(dòng)置換為 http://localhost:8080/#/ ,方便之后的路由管理:

// src/history/hash.js
function ensureSlash (): boolean {
 const path = getHash()
 if (path.charAt(0) === '/') {
  return true
 }
 replaceHash('/' + path)
 return false
}

scrollBehavior

當(dāng)從一個(gè)路由 /a 跳轉(zhuǎn)到另外的路由 /b 后,如果在路由 /a 的頁(yè)面中進(jìn)行了滾動(dòng)條的滾動(dòng)行為,那么頁(yè)面跳轉(zhuǎn)到 /b 時(shí),會(huì)發(fā)現(xiàn)瀏覽器的滾動(dòng)條位置和 /a 的一樣(如果 /b 也能滾動(dòng)的話),或者刷新當(dāng)前頁(yè)面,瀏覽器的滾動(dòng)條位置依舊不變,不會(huì)直接返回到頂部的 而如果是通過(guò)點(diǎn)擊瀏覽器的前進(jìn)、后退按鈕來(lái)控制路由切換時(shí),則部門(mén)瀏覽器(例如微信)滾動(dòng)條在路由切換時(shí)都會(huì)自動(dòng)返回到頂部,即 scrollTop=0 的位置 這些都是瀏覽器默認(rèn)的行為,如果想要定制頁(yè)面切換時(shí)的滾動(dòng)條位置,則可以借助 scrollBehavior 這個(gè) vue-router 的 options

當(dāng)路由初始化時(shí), vue-router 會(huì)對(duì)路由的切換事件進(jìn)行監(jiān)聽(tīng),監(jiān)聽(tīng)邏輯的一部分就是用于控制瀏覽器滾動(dòng)條的位置:

// src/history/hash.js
setupListeners () {
 ...
 if (supportsScroll) {
  // 進(jìn)行瀏覽器滾動(dòng)條的事件控制
  setupScroll()
 }
 ...
}

這個(gè) set 方法定義在 src/util/scroll.js ,這個(gè)文件就是專(zhuān)門(mén)用于控制滾動(dòng)條位置的,通過(guò)監(jiān)聽(tīng)路由切換事件從而進(jìn)行滾動(dòng)條位置控制:

// src/util/scroll.js
window.addEventListener('popstate', e => {
 saveScrollPosition()
 if (e.state && e.state.key) {
  setStateKey(e.state.key)
 }
})

通過(guò) scrollBehavior 可以定制路由切換的滾動(dòng)條位置, vue-router 的github上的源碼中,有相關(guān)的 example ,源碼位置在 vue-router/examples/scroll-behavior/app.js

router-view & router-link

router-view 和 router-link 這兩個(gè) vue-router 的內(nèi)置組件,源碼位于 src/components 下

router-view

router-view 是無(wú)狀態(tài)(沒(méi)有響應(yīng)式數(shù)據(jù))、無(wú)實(shí)例(沒(méi)有 this 上下文)的函數(shù)式組件,其通過(guò)路由匹配獲取到對(duì)應(yīng)的組件實(shí)例,通過(guò) h 函數(shù)動(dòng)態(tài)生成組件,如果當(dāng)前路由沒(méi)有匹配到任何組件,則渲染一個(gè)注釋節(jié)點(diǎn)

// vue-router/src/components/view.js
...
const matched = route.matched[depth]
// render empty node if no matched route
if (!matched) {
 cache[name] = null
 return h()
}
const component = cache[name] = matched.components[name]
...
return h(component, data, children)

每次路由切換都會(huì)觸發(fā) router-view 重新 render 從而渲染出新的視圖,這個(gè)觸發(fā)的動(dòng)作是在 vue-router 初始化 init 的時(shí)候就聲明了的:

// src/install.js
Vue.mixin({
 beforeCreate () {
  if (isDef(this.$options.router)) {
   this._routerRoot = this
   this._router = this.$options.router
   this._router.init(this)
   // 觸發(fā) router-view重渲染
   Vue.util.defineReactive(this, '_route', this._router.history.current)
   ...
})

將 this._route 通過(guò) defineReactive 變成一個(gè)響應(yīng)式的數(shù)據(jù),這個(gè) defineReactive 就是 vue 中定義的,用于將數(shù)據(jù)變成響應(yīng)式的一個(gè)方法,源碼在 vue/src/core/observer/index.js 中,其核心就是通過(guò) Object.defineProperty 方法修改數(shù)據(jù)的 getter 和 setter :

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
   const value = getter ? getter.call(obj) : val
   if (Dep.target) {
    // 進(jìn)行依賴(lài)收集
    dep.depend()
    if (childOb) {
     childOb.dep.depend()
     if (Array.isArray(value)) {
      dependArray(value)
     }
    }
   }
   return value
  },
  set: function reactiveSetter (newVal) {
   ...
   // 通知訂閱當(dāng)前數(shù)據(jù) watcher的觀察者進(jìn)行響應(yīng)
   dep.notify()
  }

當(dāng)路由發(fā)生變化時(shí),將會(huì)調(diào)用 router-view 的 render 函數(shù),此函數(shù)中訪問(wèn)了 this._route 這個(gè)數(shù)據(jù),也就相當(dāng)于是調(diào)用了 this._route 的 getter 方法,觸發(fā)依賴(lài)收集,建立一個(gè) Watcher ,執(zhí)行 _update 方法,從而讓頁(yè)面重新渲染

// vue-router/src/components/view.js
render (_, { props, children, parent, data }) {
 // used by devtools to display a router-view badge
 data.routerView = true

 // directly use parent context's createElement() function
 // so that components rendered by router-view can resolve named slots
 const h = parent.$createElement
 const name = props.name
 // 觸發(fā)依賴(lài)收集,建立 render watcher
 const route = parent.$route
 ...
}

這個(gè) render watcher 的派發(fā)更新,也就是 setter 的調(diào)用,位于 src/index.js :

history.listen(route => {
 this.apps.forEach((app) => {
  // 觸發(fā) setter
  app._route = route
 })
})

router-link

router-link 在執(zhí)行 render 函數(shù)的時(shí)候,會(huì)根據(jù)當(dāng)前的路由狀態(tài),給渲染出來(lái)的 active 元素添加 class ,所以你可以借助此給 active 路由元素設(shè)置樣式等:

// src/components/link.js
render (h: Function) {
 ...
 const globalActiveClass = router.options.linkActiveClass
 const globalExactActiveClass = router.options.linkExactActiveClass
 // Support global empty active class
 const activeClassFallback = globalActiveClass == null
  ? 'router-link-active'
  : globalActiveClass
 const exactActiveClassFallback = globalExactActiveClass == null
  ? 'router-link-exact-active'
  : globalExactActiveClass
  ...
}

router-link 默認(rèn)渲染出來(lái)的元素是 <a> 標(biāo)簽,其會(huì)給這個(gè) <a> 添加 href 屬性值,以及一些用于監(jiān)聽(tīng)能夠觸發(fā)路由切換的事件,默認(rèn)是 click 事件:

// src/components/link.js
data.on = on
data.attrs = { href }

另外,你可以可以通過(guò)傳入 tag 這個(gè) props 來(lái)定制 router-link 渲染出來(lái)的元素標(biāo)簽:

<router-link to="/foo" tag="div">Go to foo</router-link>

如果 tag 值不為 a ,則會(huì)遞歸遍歷 router-link 的子元素,直到找到一個(gè) a 標(biāo)簽,則將事件和路由賦值到這個(gè) <a> 上,如果沒(méi)找到 a 標(biāo)簽,則將事件和路由放到 router-link 渲染出的本身元素上:

if (this.tag === 'a') {
  data.on = on
  data.attrs = { href }
 } else {
  // find the first <a> child and apply listener and href
  // findAnchor即為遞歸遍歷子元素的方法
  const a = findAnchor(this.$slots.default)
  ...
 }
}

當(dāng)觸發(fā)這些路由切換事件時(shí),會(huì)調(diào)用相應(yīng)的方法來(lái)切換路由刷新視圖:

// src/components/link.js
const handler = e => {
 if (guardEvent(e)) {
  if (this.replace) {
   // replace路由
   router.replace(location)
  } else {
   // push 路由
   router.push(location)
  }
 }
}

總結(jié)

可以看到, vue-router 的源碼是很簡(jiǎn)單的,比較適合新手進(jìn)行閱讀分析

源碼這種東西,我的理解是沒(méi)必要非要 專(zhuān)門(mén)騰出時(shí)間來(lái)看 ,只要你熟讀文檔,能正確而熟練地運(yùn)用 API 實(shí)現(xiàn)各種需求那就行了,輪子的出現(xiàn)本就是為實(shí)際開(kāi)發(fā)所服務(wù)而不是用來(lái)折騰開(kāi)發(fā)者的,注意,我不是說(shuō)不要去看,有時(shí)間還是要看看的,就算弄不明白其中的道道,但看了一遍總會(huì)有收獲的,比如我在看 vue 源碼的時(shí)候,經(jīng)??吹筋?lèi)似于這種的賦值寫(xiě)法:

// vue/src/core/vdom/create-functional-component.js
(clone.data || (clone.data = {})).slot = data.slot

如果是之前,對(duì)于這段邏輯我通常會(huì)這么寫(xiě):

if (clone.data) {
 clone.data.slot = data.slot
} else {
 clone.data = {
  slot: data.slot
 }
}

也不是說(shuō)第一種寫(xiě)法有什么難度或者看不明白,只是習(xí)慣了第二種寫(xiě)法,平時(shí)寫(xiě)代碼的過(guò)程中自然而然不假思索地就寫(xiě)出來(lái)了,習(xí)慣成自然了,但是當(dāng)看到第一種寫(xiě)法的時(shí)候才會(huì)一拍腦袋想著原來(lái)這么寫(xiě)也可以,以前白敲了那么多次鍵盤(pán),所以沒(méi)事要多看看別人優(yōu)秀的源碼,避免沉迷于自己的世界閉門(mén)造車(chē),這樣才能查漏補(bǔ)缺,這同樣也是我認(rèn)為代碼 review 比較重要的原因,自己很難發(fā)現(xiàn)的問(wèn)題,別人可能一眼就看出來(lái)了,此之謂 當(dāng)局者迷旁觀者清也

以上所述是小編給大家介紹的vue router 源碼概覽,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)億速云網(wǎng)站的支持!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI