源碼這個東西對於實際的工做其實沒有立竿見影的效果,不會像那些針對性極強的文章同樣看了以後就立馬能夠運用到實際項目中,產生什麼樣的效果,源碼的做用是一個潛移默化的過程,它的理念、設計模式、代碼結構等看了以後可能不會當即知識變現(或者說變現不多),而是在往後的工做過程當中悄無聲息地發揮出來,你甚至都感受不到這個過程html
另外,優秀的源碼案例,例如 vue
、react
這種,內容量比較龐大,根本不是三篇五篇十篇八篇文章就能說完的,並且寫起來也很難寫得清楚,也挺浪費時間的,而若是隻是分析其中一個點,例如 vue
的響應式,相似的文章也已經夠多了,不必再 repeat
vue
因此我以前沒專門寫過源碼分析的文章,只是本身看看,不過最近閒來無事看了 vue-router
的源碼,發現這種插件級別的東西,相比 vue
這種框架級別的東西,邏輯簡單清晰,沒有那麼多道道,代碼量也很少,可是其中包含的理念等東西卻很精煉,值得一寫,固然,文如其名,只是概覽,不會一行行代碼分析過去,細節的東西仍是要本身看看的html5
vue
插件必須經過 vue.use
進行註冊,vue.use
的代碼位於 vue
源碼的 src/core/global-api/use.js
文件中,此方法的主要做用有兩個:node
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
複製代碼
install
方法或者直接運行插件,以實現插件的 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
中 主要是經過 vue.minxin
混入 beforeCreate
和 destroyed
鉤子函數,並全局註冊 router-view
和 router-link
組件react
// 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)
}
})
...
// 全局註冊 `router-view` 和 `router-link`組件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
複製代碼
vue-router
支持三種路由模式(mode
):hash
、history
、abstract
,其中 abstract
是在非瀏覽器環境下使用的路由模式,例如weex
git
路由內部會對外部指定傳入的路由模式進行判斷,例如當前環境是非瀏覽器環境,則不管傳入何種mode
,最後都會被強制指定爲 abstract
,若是判斷當前環境不支持 HTML5 History
,則最終會被降級爲 hash
模式github
// 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'
}
複製代碼
最後會對符合要求的 mode
進行對應的初始化操做vue-router
// 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}`)
}
}
複製代碼
經過遞歸的方式來解析嵌套路由設計模式
// 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)
})
...
}
複製代碼
解析完畢以後,會經過 key-value
對的形式對解析好的路由進行記錄,因此若是聲明多個相同路徑(path
)的路由映射,只有第一個會起做用,後面的會被忽略api
// src/create-route-map.js
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
複製代碼
例如以下路由配置,路由 /bar
只會匹配 Bar1
,Bar2
這一條配置會被忽略
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar1 },
{ path: '/bar', component: Bar2 },
];
複製代碼
當訪問一個 url
的時候,vue-router
會根據路徑進行匹配,建立出一個 route
對象,可經過 this.$route
進行訪問
// 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, () => {
...
}
複製代碼
路由實例的push
和 replace
等路由切換方法,都是基於此方法實現路由切換的,例如 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
方法內部經過一種異步函數隊列化執⾏的模式來更新切換路由,經過 next
函數執行異步回調,並在異步回調方法中執行相應的鉤子函數(即 導航守衛) beforeEach
、beforeRouteUpdate
、beforeRouteEnter
、beforeRouteLeave
經過 queue
這個數組保存相應的路由參數:
// 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)
)
複製代碼
經過 runQueue
以一種遞歸回調的方式來啓動異步函數隊列化的執⾏:
// src/history/base.js
// 異步回調函數
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)
// 遞歸執行
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() })
})
}
})
})
複製代碼
經過 next
進行導航守衛的回調迭代,因此若是在代碼中顯式聲明瞭導航鉤子函數,那麼就必須在最後調用 next()
,不然回調不執行,導航將沒法繼續
// src/history/base.js
const iterator = (hook: NavigationGuard, next) => {
...
hook(route, current, (to: any) => {
...
} else {
// confirm transition and pass on the value
next(to)
}
})
...
}
複製代碼
在路由切換的時候,vue-router
會調用 push
、go
等方法實現視圖與地址url
的同步
url
與視圖的同步當進行點擊頁面上按鈕等操做進行路由切換時,vue-router
會經過改變 window.location.href
來保持視圖與 url
的同步,例如 hash
模式的路由切換:
// src/history/hash.js
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
複製代碼
上述代碼,先檢測當前瀏覽器是否支持 html5
的 History API
,若是支持則調用此 API
進行 href
的修改,不然直接對window.location.hash
進行賦值 history
的原理與此相同,也是利用了 History API
url
的同步當點擊瀏覽器的前進後退按鈕時,一樣能夠實現視圖的同步,這是由於在路由初始化的時候,設置了對瀏覽器前進後退的事件監聽器
下述是 hash
模式的事件監聽:
// 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
模式與此相似:
// 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)
}
})
})
複製代碼
不管是 hash
仍是 history
,都是經過監聽事件最後來調用 transitionTo
這個方法,從而實現路由與視圖的統一
另外,當第一次訪問頁面,路由進行初始化的時候,若是是 hash
模式,則會對url
進行檢查,若是發現訪問的 url
沒有帶 #
字符,則會自動追加,例如初次訪問 http://localhost:8080
這個 url
,vue-router
會自動置換爲 http://localhost:8080/#/
,方便以後的路由管理:
// src/history/hash.js
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
複製代碼
當從一個路由 /a
跳轉到另外的路由 /b
後,若是在路由 /a
的頁面中進行了滾動條的滾動行爲,那麼頁面跳轉到/b
時,會發現瀏覽器的滾動條位置和 /a
的同樣(若是 /b
也能滾動的話),或者刷新當前頁面,瀏覽器的滾動條位置依舊不變,不會直接返回到頂部的 而若是是經過點擊瀏覽器的前進、後退按鈕來控制路由切換時,則部分瀏覽器(例如微信)滾動條在路由切換時都會自動返回到頂部,即scrollTop=0
的位置 這些都是瀏覽器默認的行爲,若是想要定製頁面切換時的滾動條位置,則能夠藉助 scrollBehavior
這個 vue-router
的 options
當路由初始化時,vue-router
會對路由的切換事件進行監聽,監聽邏輯的一部分就是用於控制瀏覽器滾動條的位置:
// src/history/hash.js
setupListeners () {
...
if (supportsScroll) {
// 進行瀏覽器滾動條的事件控制
setupScroll()
}
...
}
複製代碼
這個 set
方法定義在 src/util/scroll.js
,這個文件就是專門用於控制滾動條位置的,經過監聽路由切換事件從而進行滾動條位置控制:
// src/util/scroll.js
window.addEventListener('popstate', e => {
saveScrollPosition()
if (e.state && e.state.key) {
setStateKey(e.state.key)
}
})
複製代碼
經過 scrollBehavior
能夠定製路由切換的滾動條位置,vue-router
的 github上的源碼中,有相關的 example
,源碼位置在 vue-router/examples/scroll-behavior/app.js
router-view
和 router-link
這兩個 vue-router
的內置組件,源碼位於 src/components
下
router-view
是無狀態(沒有響應式數據)、無實例(沒有 this
上下文)的函數式組件,其經過路由匹配獲取到對應的組件實例,經過 h
函數動態生成組件,若是當前路由沒有匹配到任何組件,則渲染一個註釋節點
// 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)
複製代碼
每次路由切換都會觸發 router-view
從新 render
從而渲染出新的視圖,這個觸發的動做是在 vue-router
初始化 init
的時候就聲明瞭的:
// src/install.js
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// 觸發 router-view重渲染
Vue.util.defineReactive(this, '_route', this._router.history.current)
...
})
複製代碼
將 this._route
經過 defineReactive
變成一個響應式的數據,這個defineReactive
就是 vue
中定義的,用於將數據變成響應式的一個方法,源碼在 vue/src/core/observer/index.js
中,其核心就是經過 Object.defineProperty
方法修改數據的 getter
和 setter
:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 進行依賴收集
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
...
// 通知訂閱當前數據 watcher的觀察者進行響應
dep.notify()
}
複製代碼
當路由發生變化時,將會調用 router-view
的 render
函數,此函數中訪問了 this._route
這個數據,也就至關因而調用了 this._route
的 getter
方法,觸發依賴收集,創建一個 Watcher
,執行 _update
方法,從而讓頁面從新渲染
// 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
// 觸發依賴收集,創建 render watcher
const route = parent.$route
...
}
複製代碼
這個 render watcher
的派發更新,也就是 setter
的調用,位於 src/index.js
:
history.listen(route => {
this.apps.forEach((app) => {
// 觸發 setter
app._route = route
})
})
複製代碼
router-link
在執行 render
函數的時候,會根據當前的路由狀態,給渲染出來的active
元素添加 class
,因此你能夠藉助此給active
路由元素設置樣式等:
// 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
默認渲染出來的元素是 <a>
標籤,其會給這個 <a>
添加 href
屬性值,以及一些用於監聽可以觸發路由切換的事件,默認是 click
事件:
// src/components/link.js
data.on = on
data.attrs = { href }
複製代碼
另外,你能夠能夠經過傳入 tag
這個 props
來定製 router-link
渲染出來的元素標籤:
<router-link to="/foo" tag="div">Go to foo</router-link>
複製代碼
若是 tag
值不爲 a
,則會遞歸遍歷 router-link
的子元素,直到找到一個 a
標籤,則將事件和路由賦值到這個 <a>
上,若是沒找到a
標籤,則將事件和路由放到 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)
...
}
}
複製代碼
當觸發這些路由切換事件時,會調用相應的方法來切換路由刷新視圖:
// src/components/link.js
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
// replace路由
router.replace(location)
} else {
// push 路由
router.push(location)
}
}
}
複製代碼
能夠看到,vue-router
的源碼是很簡單的,比較適合新手進行閱讀分析
源碼這種東西,個人理解是不必非要專門騰出時間來看,只要你熟讀文檔,能正確而熟練地運用 API
實現各類需求那就好了,輪子的出現本就是爲實際開發所服務而不是用來折騰開發者的,注意,我不是說不要去看,有時間仍是要看看的,就算弄不明白其中的道道,但看了一遍總會有收穫的,好比我在看 vue
源碼的時候,常常看到相似於這種的賦值寫法:
// vue/src/core/vdom/create-functional-component.js
(clone.data || (clone.data = {})).slot = data.slot
複製代碼
若是是以前,對於這段邏輯我一般會這麼寫:
if (clone.data) {
clone.data.slot = data.slot
} else {
clone.data = {
slot: data.slot
}
}
複製代碼
也不是說第一種寫法有什麼難度或者看不明白,只是習慣了第二種寫法,平時寫代碼的過程當中天然而然不假思索地就寫出來了,習慣成天然了,可是當看到第一種寫法的時候纔會一拍腦殼想着原來這麼寫也能夠,之前白敲了那麼屢次鍵盤,因此沒事要多看看別人優秀的源碼,避免沉迷於本身的世界閉門造車,這樣才能查漏補缺,這一樣也是我認爲代碼 review
比較重要的緣由,本身很難發現的問題,別人可能一眼就看出來了,此之謂當局者迷旁觀者清也