vue router 源码概览案例分析

源码这个东西对于实际的工作其实没有立竿见影的效果,不会像那些针对性极强的文章一样看了之后就立马可以运用到实际项目中,产生什么样的效果,源码的作用是一个潜移默化的过程,它的理念、设计模式、代码结构等看了之后可能不会立即知识变现(或者说变现很少),而是在日后的工作过程中悄无声息地发挥出来,你甚至都感觉不到这个过程

另外,优秀的源码案例,例如 vue 、 react 这种,内容量比较庞大,根本不是三篇五篇十篇八篇文章就能说完的,而且写起来也很难写得清楚,也挺浪费时间的,而如果只是分析其中一个点,例如 vue 的响应式,类似的文章也已经够多了,没必要再 repeat

所以我之前没专门写过源码分析的文章,只是自己看看,不过最近闲来无事看了 vue-router 的源码,发现这种插件级别的东西,相比 vue 这种框架级别的东西,逻辑简单清晰,没有那么多道道,代码量也不多,但是其中包含的理念等东西却很精炼,值得一写,当然,文如其名,只是概览,不会一行行代码分析过去,细节的东西还是要自己看看的

vue.use

vue 插件必须通过 vue.use 进行注册, vue.use 的代码位于 vue 源码的 src/core/global-api/use.js 文件中,此方法的主要作用有两个:

  • 对注册的组件进行缓存,避免多次注册同一个插件
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 组件

// 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

路由内部会对外部指定传入的路由模式进行判断,例如当前环境是非浏览器环境,则无论传入何种 mode ,最后都会被强制指定为 abstract ,如果判断当前环境不支持 HTML5 History ,则最终会被降级为 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'
}

最后会对符合要求的 mode 进行对应的初始化操作

// 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 )的路由映射,只有第一个会起作用,后面的会被忽略

// 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
}

scrollBehavior

当从一个路由 /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

router-view 和 router-link 这两个 vue-router 的内置组件,源码位于 src/components 下

router-view

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

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 比较重要的原因,自己很难发现的问题,别人可能一眼就看出来了,此之谓 当局者迷旁观者清也

以上所述是小编给大家介绍的vue router 源码概览,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • vue-router 源码实现前端路由的两种方式

    在学习 vue-router 的代码之前,先来简单了解一下前端路由. 前端路由主要有两种实现方法: Hash 路由 History 路由 先来看看这两种方法的实现原理. 接着我们将用它们来简单实现一个自己的前端路由. 前端路由 Hash 路由 url 的 hash 是以 # 开头,原本是用来作为锚点,从而定位到页面的特定区域.当 hash 改变时,页面不会因此刷新,浏览器也不会向服务器发送请求. http://www.xxx.com/#/home 同时, hash 改变时,并会触发相应的 has

  • Vue 2.0+Vue-router构建一个简单的单页应用(附源码)

    一.介绍 vue.js 是 目前 最火的前端框架,vue.js 兼具 angular.js 和 react.js 的优点,并剔除它们的缺点,并且提供了很多的周边配套工具 如vue-router .vue-resource .vuex等等 ,通过他们我们可以很轻松的构建一个大型单页应用. 目前Vue版本为:Vue2.0 官网地址:http://vuejs.org.cn/ 查看API文档:https://vuefe.cn/v2/api/ 对比其他框架:http://vuejs.org.cn/guid

  • vue-router 源码之实现一个简单的 vue-router

    前言 通过上篇,我们知道前端理由的两种实现方法,Hash 路由与 History 路由,并且用它们分别实现了一个前端路由. 接下来我们就将 Vue 与 Hash 路由结合,实现一个非常简单的 vue-router 吧. 开始实现 想象一下,如果自己实现了一个 vue-router,会怎么去使用呢?参考 vue-router 官方的使用方式,看看 html 的使用: <div id="app"> <p> <router-link to="#/&qu

  • vue router 源码概览案例分析

    源码这个东西对于实际的工作其实没有立竿见影的效果,不会像那些针对性极强的文章一样看了之后就立马可以运用到实际项目中,产生什么样的效果,源码的作用是一个潜移默化的过程,它的理念.设计模式.代码结构等看了之后可能不会立即知识变现(或者说变现很少),而是在日后的工作过程中悄无声息地发挥出来,你甚至都感觉不到这个过程 另外,优秀的源码案例,例如 vue . react 这种,内容量比较庞大,根本不是三篇五篇十篇八篇文章就能说完的,而且写起来也很难写得清楚,也挺浪费时间的,而如果只是分析其中一个点,例如

  • Vue.js源码分析之自定义指令详解

    前言 除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令. 官网介绍的比较抽象,显得很高大上,我个人对自定义指令的理解是:当自定义指令作用在一些DOM元素或组件上时,该元素在初次渲染.插入到父节点.更新.解绑时可以执行一些特定的操作(钩子函数() 自定义指令有两种注册方式,一种是全局注册,使用Vue.directive(指令名,配置参数)注册,注册之后所有的Vue实例都可以使用,另一种是局部注册,在创建Vue实例时通过directives属性创建局部指

  • Vue编译器源码分析compileToFunctions作用详解

    目录 引言 Vue.prototype.$mount函数体 源码出处 options.delimiters & options.comments compileToFunctions函数逐行分析 createFunction 函数源码 引言 Vue编译器源码分析 接上篇文章我们来分析:compileToFunctions的作用. 经过前面的讲解,我们已经知道了 compileToFunctions 的真正来源你可能会问为什么要弄的这么复杂?为了搞清楚这个问题,我们还需要继续接触完整的代码. 下面

  • Vue.use源码分析

    我想有过vue开发经验的,对于vue.use并不陌生.当使用vue-resource或vue-router等全局组件时,必须通过Vue.use方法引入,才起作用.那么vue.use在组件引入之前到底做了那些事情呢?让我们一窥究竟. 先上vue.use源码 // javascript的方法是可以传递的,哈哈 Vue.use = function (plugin) { /* istanbul ignore if */ if (plugin.installed) { return } // addit

  • 详解vue mint-ui源码解析之loadmore组件

    本文介绍了vue mint-ui源码解析之loadmore组件,分享给大家,具体如下: 接入 官方接入文档mint-ui loadmore文档 接入使用Example html <div id="app"> <mt-loadmore :top-method="loadTop" :bottom-method="loadBottom" :bottom-all-loaded="allLoaded" :max-dis

  • Vue 2源码解读$mount函数原理

    目录 1. $mount 函数来源 2. runtime 运行时的 $mount 函数 2.1 mountComponent 函数 2.2 _update 函数(首次渲染) 3. runtime-with-compiler 的 $mount 函数 4. runtime 对 Vue 构造函数的其他修改 1. $mount 函数来源 上一节虽然直接从 core 目录下找到了 Vue 的构造函数定义,但是缺少 $mount 方法.所以直接从开发过程中使用的 vue.esm.js 找到对应的源码入口.

  • 详解从Vue.js源码看异步更新DOM策略及nextTick

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/answershuto/learnVue. 在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助. 可能会有理解存在偏差的地方,欢迎提issue指出,

  • 深入理解Vue.js源码之事件机制

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/answershuto/learnVue. 在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助. 可能会有理解存在偏差的地方,欢迎提issue指出

  • JVM堆外内存源码完全解读分析

    概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们大家接触最多的,我们在jvm参数里通常设置-Xmx来指定我们的堆的最大值,不过这还不是我们理解的Java堆,-Xmx的值是新生代和老生代的和的最大值,我们在jvm参数里通常还会加一个参数-XX:MaxPermSize来指定持久代的最大值,那么我们认识的Java堆的最大值其实是-Xmx和-XX:MaxPermSize的总和,在分代算法下,新生代,老生代和持久代是连续的虚拟地址,因为它们是一起分配的,那么剩下的都可以认为是堆外内存

  • vue parseHTML源码解析hars end comment钩子函数

    目录 引言 chars源码: parseText end 源码: 引言 接上文  parseHTML 函数源码解析(六) start钩子函数 接下来我们主要讲解当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,又会如何对文本节点做哪些特殊的处理. parseHTML(template, { chars: function(){ //... }, //... }) chars源码: chars: function chars(text) { if (!currentParent) { {

随机推荐