vue router 源码概览案例分析


Posted in Javascript onOctober 09, 2018

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

另外,优秀的源码案例,例如 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 源码概览,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
setTimeout与setInterval在不同浏览器下的差异
Jan 24 Javascript
JavaScript之Getters和Setters 平台支持等详细介绍
Dec 07 Javascript
A标签中通过href和onclick传递的this对象实现思路
Apr 19 Javascript
顶部缓冲下拉菜单导航特效的JS代码
Aug 27 Javascript
js网页实时倒计时精确到秒级
Feb 10 Javascript
js实现每日自动换一张图片的方法
May 04 Javascript
javascript日期操作详解(脚本之家整理)
Sep 05 Javascript
jQuery实现折叠、展开的菜单组效果代码
Sep 16 Javascript
基于jquery日历价格、库存等设置插件
Jul 05 jQuery
详解js类型判断
May 22 Javascript
vue2.0 下拉框默认标题设置方法
Aug 22 Javascript
node事件循环和process模块实例分析
Feb 14 Javascript
Angular4 Select选择改变事件的方法
Oct 09 #Javascript
对angular 监控数据模型变化的事件方法$watch详解
Oct 09 #Javascript
JS判断用户用的哪个浏览器实例详解
Oct 09 #Javascript
vue发送ajax请求详解
Oct 09 #Javascript
AngularJS 监听变量变化的实现方法
Oct 09 #Javascript
对angular4子路由&amp;辅助路由详解
Oct 09 #Javascript
angular 未登录状态拦截路由跳转的方法
Oct 09 #Javascript
You might like
destoon实现资讯信息前面调用它所属分类的方法
2014/07/15 PHP
在线游戏大家来找茬II
2006/09/30 Javascript
List Information About the Binary Files Used by an Application
2007/06/18 Javascript
javascript 有趣而诡异的数组
2009/04/06 Javascript
基于Jquery的温度计动画效果
2010/06/18 Javascript
基于jQuery的投票系统显示结果插件
2011/08/12 Javascript
JavaScript简单表格编辑功能实现方法
2015/04/16 Javascript
JavaScript中innerHTML,innerText,outerHTML的用法及区别
2015/09/01 Javascript
三种带箭头提示框总结实例
2016/06/14 Javascript
jQuery继承extend用法详解
2016/10/10 Javascript
nodeJs链接Mysql做增删改查的简单操作
2017/02/04 NodeJs
js实现产品缩略图效果
2017/03/10 Javascript
Node.js设置CORS跨域请求中多域名白名单的方法
2017/03/28 Javascript
vue 每次渲染完页面后div的滚动条保持在最底部的方法
2018/03/17 Javascript
javascript获取select值的方法完整实例
2019/06/20 Javascript
Smartour 让网页导览变得更简单(推荐)
2019/07/19 Javascript
Vue使用Clipboard.JS在h5页面中复制内容实例详解
2019/09/03 Javascript
vue 实现input表单元素的disabled示例
2019/10/28 Javascript
Vue中keep-alive 实现后退不刷新并保持滚动位置
2020/03/17 Javascript
python通过yield实现数组全排列的方法
2015/03/18 Python
Python字符串和文件操作常用函数分析
2015/04/08 Python
Python优化技巧之利用ctypes提高执行速度
2016/09/11 Python
python 中的list和array的不同之处及转换问题
2018/03/13 Python
Python3 中把txt数据文件读入到矩阵中的方法
2018/04/27 Python
如何使用python切换hosts文件
2020/04/29 Python
详解Python中的文件操作
2021/01/14 Python
html5 学习简单的拾色器
2010/09/03 HTML / CSS
澳大利亚汽车零部件、音响及配件超市:Automotive Superstore
2018/06/19 全球购物
Rodd & Gunn澳大利亚官网:新西兰男装品牌
2018/09/25 全球购物
Derek Rose官网:英国高档睡衣、家居服和内衣品牌
2020/01/18 全球购物
会计师职业生涯规划范文
2014/02/18 职场文书
综治维稳工作汇报
2014/10/27 职场文书
出国留学单位推荐信
2015/03/26 职场文书
幼儿园开学家长寄语(2016春季)
2015/12/03 职场文书
初中思想品德教学反思
2016/02/24 职场文书
实用求职信模板范文
2019/05/13 职场文书