vue-router源码浅析

什么是路由

简单来说,路由就是URL到函数的映射关系。并且路由这个概念最早是出现在后端的,因为早期的网页都是服务端渲染的,比如:JSP,PHP,ASP等语言,都是直接返回渲染好的html给客户端显示。

路由分类

后端路由

在web开发早期的年代里,前端的功能远不如现在这么强大,一直是后端路由占据主导地位。无论是jsp,还是php、asp,用户能通过URL访问到的页面,大多是通过后端路由匹配之后再返回给浏览器的。浏览器在地址栏中切换不同的URL时,每次都向后台服务器发出请求,服务器响应请求,在后台拼接html文件返回给前端,并且每次切换页面时,浏览器都会刷新页面。

在后端,路由映射表中就是不同的URL地址与不同的html + css + 后端数据 之间的映射

下面这张图或许能够让你更加清楚的了解后端路由:

后端路由缺点:每次在浏览器中输入url都要发送请求到服务器,服务器解析请求路径,返回对应的页面给前端。造成服务器压力比较大。

前端路由

前端路由是后来发展到SPA(单页应用)时才出现的概念。

前端路由主要是有两种模式:

  • hash模式:带有hash的前端路由,优点是兼容性高。缺点是URL带有#号不好看
  • history模式: 不带hash的前端路由,优点是URL不带#号,好看。缺点是既需要浏览器支持也需要后端服务器支持。否则会出现404。

前端路由应用最广泛的例子就是当今的SPA的web项目。不管是Vue、React还是Angular的页面工程,都离不开相应配套的router工具。

整个SPA页面就只有一整套的HTMLCSS+JS,这一套HTML+CSS+JS中包含了许多个网页,当我们请求不同的URL时,客户端会从这一整套HTML+CSS+JS中找到对应的HTML+CSS+JS,将它们解析执行,渲染在页面上。

前端路由带来的最明显的好处就是,地址栏URL的跳转不会白屏了------这也得益于客户端渲染带来的好处。

vue-router就是vue框架实现前端路由的方法,接下来就开始研究vue-router实现前端路由的原理。

Vue插件注册

插件功能

  • 添加全局指令、全局过滤器、全局组件
  • 通过全局混入来添加一些组件选项(例如:vue-router会通过Vue.mixin给全局混入一些生命周期和销毁的生命周期,同样在原型上增加一些属性)
  • 添加Vue实例方法,通过把它们添加到Vue.prototype上实现

插件注册原理

在Vue中通过Vue.use()方法注册插件。Vue编写插件的时候通常要提供静态的install方法。插件的写法有以下两种:如果插件是一个对象,必须提供install方法,如果插件是一个函数,它会被当作install方法。

js 复制代码
let plugin1 = { install(_Vue,...args) {} } 
let plugin2 = (_Vue,...args) => {} 
Vue.use(plugin1,1,2,3) 
Vue.use(plugin2,1,2,3)

通过分析use方法的源码,我们可以理解插件的注册原理。

js 复制代码
Vue.use = function(plugin:Function | Object) { 
  const installedPlugins = (this._installedPlugins || (this._installedPlugins=[]))      if(installedPlugins.indexOf(plugin) > -1) { 
  return this 
} 
const args = toArray(arguments,1) // 拿到plugin以外的参数 args.unshift(this) 
if(typeof plugin.install === 'function') { 
  // 如果有install方法则执行,并将参数传入 
  plugin.install.apply(plugin,args) 
 } else if (typeof plugin === 'function') { 
   // 如果plugin本身就是一个方法,那么直接执行plugin并将参数传入 
   plugin.apply(null, args) } 
   installedPlugins.push(plugin)
   return this 
}

首先会判断,该插件是否已经注册过了。如果已经注册过的插件那就不执行下面的注册逻辑。如果之前没有注册过该插件,首先通过toArray(arguments,1)方法拿到传入use方法中plugin本身以外的参数,并把Vue实例到参数的最前面(args.unshift(this)),那么install接收到的第一个参数就是Vue,为什么要在install方法执行的时候传入Vue呢?这是因为不可能在插件中再去通过import Vue引入(可能会造成版本不一致),通过参数传递的这种方式就可以让插件拿到Vue,因为插件里面会用到Vue的一些方法。然后判断插件中是否存在install方法,如果有install方法则执行,并将参数传入。如果没有install方法但是plugin本身就是一个方法,那么直接执行plugin并将参数传入。并将插件存入到installedPlugins中缓存插件。这样就完成了插件的注册。

路由注册

路由注册的实现流程

上面分析了Vue注册插件的过程,现在看下对于vue-router而言插件方法是怎么定义的。vue-router的具体使用方法如下:

js 复制代码
import Vue from 'vue' 
import VueRouter from 'vue-router' 
import App from './App.vue' 
Vue.use(VueRouter) 
const routes = [ 
  { 
    path: '/foo', 
    name: 'foo', 
    component:Foo, 
    children: { 
     path: '/bar', 
     name: 'bar' 
     component: Bar 
    }
   } 
 ] 
 const router = new VueRouter({ routes }) 
 const app = new Vue({ el:'#app', render(h) { render h(App) }, router })

也就是我们在执行Vue.use(VueRouter)的时候,就会去执行VueRouter.install方法。

js 复制代码
let _Vue 
export function install(Vue) { 
  if(install.installed && _Vue === Vue) 
  return install.installed = true
  _Vue = Vue Vue.mixin({ 
    beforeCreate() { 
      if(isDef(this.$options.router)) { 
        this._routerRoot = this this._router = this.$options.router   this._router.init(this) // 把_route变成响应式的 Vue.util.defineReactive(this,'_route', this._router.history.current) 
        } else { 
        // 非根组件 
        this._routerRoot=(this.$parent && this.$parent._routerRoot) || this } 
        // 这样每个组件都可以通过this._routerRoot访问到根的实例 
        registerInstance(this,this) // 
     }, 
     destoryed() { registerInstance(this) } })  
     Object.defineProperty(Vue.prototype,'$router', 
       { get() { return this._routerRoot._router } }
     ) 
     Object.defineProperty(Vue.prototype,'$route',{ 
       get() {return this._routerRoot._route} }
    ) 
    Vue.component('RouterView',View) // 全局注册router-view组件  
    Vue.component('RouterLink',Link) // 全局注册router-link组件 }

第一次注册路由的时候会把installed置为true,且把传入到install方法中的Vue赋值给_Vue。如果多次执行路由的注册会直接return。最关键的是通过Vue.mixin方式混入了一个beforeCreate和destoryed的钩子函数。这是整个路由最关键的地方。Vue.mixin的作用其实就是mergeOptions,也就是把mixin方法拓展到全局的options。这样的话每一个组件都会有一个beforeCreate和destoryed钩子函数。beforeCreate首先会判断this. <math xmlns="http://www.w3.org/1998/Math/MathML"> o p t i o n s . r o u t e r 是否为 t r u e , 也就是说我们在使用路由的时候,最终我们会实例化一个 n e w V u e R o u t e r 的 r o u t e r 对象。然后在 n e w V u e 的时候会把 r o u t e r 对象传入。一旦我们把 r o u t e r 传入后,我们就可以在 t h i s . options.router是否为true,也就是说我们在使用路由的时候,最终我们会实例化一个new VueRouter的router对象。然后在new Vue的时候会把router对象传入。一旦我们把router传入后,我们就可以在this. </math>options.router是否为true,也就是说我们在使用路由的时候,最终我们会实例化一个newVueRouter的router对象。然后在newVue的时候会把router对象传入。一旦我们把router传入后,我们就可以在this.options上获取到router对象。也就是this.$options.router为true的条件成立。然后做一些私有属性定义和路由初始化工作,即执行插件上面的init方法。通过Vue.util.defineReactive方法把_route变成响应式的。最后调用registerInstance方法。这些方法具体内容后面介绍。然后会全局注册router-view组件和router-link组件。这就是为什么我们在注册了路由后就可以随意使用router-view组件和router-link组件了。router-view组件会渲染相关组件,router-link组件会渲染成一个a标签。

VueRouter对象

VueRouter是一个class,有一些静态的方法和属性。也有一些实例上的属性。当我们执行new VueRouter的时候就会去执行constructor

js 复制代码
constructor(options:RouterOptions={}) { 
  this.app = null this.apps = [] 
  this.options = options 
  this.beforeHooks = [] 
  this.resolveHookes = [] 
  this.afterHooks = [] 
  this.matcher = createMatcher(options.routes || [], this) 
  let mode = options.mode || 'hash' 
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !==false 
  if(this.fallback) { mode = 'hash' } 
  if(!inBrowser) { mode = 'abstract' } 
  this.mode = mode 
  switch(mode) { 
    // 根据不同的mode对this.history进行实例化,对路由进行管理 
    case 'history': 
      this.history = new HTML5History(this,options.base) 
      break 
   case 'hash': 
     this.histroy = 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') } } }

options就是我们传入的一些配置。beforeHooks、resolveHookes、afterHooks这是跟导航守卫相关的,之后会介绍。对传入的mode(路由的模式)进行处理。浏览器环境中通常使用hash或者history路由。在非浏览器环境中使用抽象abstract路由。history路由其实是利用了html5的一个特性,并不是所有浏览器都支持,即!supportsPushState的情况下(特定浏览器、浏览器是否支持window.history且history上存在pushState方法)把mode设置降级为hash模式。根据不同的mode对this.history进行实例化(new HTML5History、new HashHistory、new AbstractHistory),对路由进行管理。

js 复制代码
function supportsPushState = inBrowser & (
  function() { 
    const ua = window.nagivator.userAgent 
    if((ua.indexOf('Android 2.') !== -1) || us.indexOf('Android 4.0') !== -1) 
    && ua.indexOf('Mobile Safari') !== - 1 && ua.indexOf('Chrome') === -1 
    && ua.indexOf('Windows Phone') === -1 ){
    return false
   } 
   return window.history && 'pushState' in window.history })()

初始化路由

路由初始化的时机是在组件的初始化阶段,执行到beforeCreate钩子函数的时候会执行router.init方法。init是class VueRouter原型上的方法,把每个组件实例传入到init方法中。保存到this.apps这个数组中。然后拿到history对象。判断是HTML5History还是HashHistory,执行不同的逻辑。

js 复制代码
init(app) { 
  this.apps.push(app) 
  if(this.app) { return } 
  this.app = app 
  const history = this.history // 拿到history 
  if (history instanceof HTML5History || history instanceof HashHistory) { 
    var handleInitialScroll = function (routeOrError) { 
      var from = history.current; 
      var expectScroll = this$1$1.options.scrollBehavior; 
      var supportsScroll = supportsPushState && expectScroll; 
      if (supportsScroll && 'fullPath' in routeOrError) { 
        handleScroll(this$1$1, routeOrError, from, false); 
      } 
    }; 
    var setupListeners = function (routeOrError) { 
      history.setupListeners(); 
      handleInitialScroll(routeOrError); 
     }; 
     history.transitionTo(
       history.getCurrentLocation(), 
       setupListeners, 
       setupListeners 
     ); 
   } 
   history.listen(route => { 
     this.apps.forEach(
       app => { 
         app._route = route 
       }
     ) 
   }) 
 }

然后会执行history.transitionTo方法做路径切换。首先会通过执行this.router.match拿到路径route,然后执行confirmTransition。具体实现后续介绍。即路由在初始化的时候就会去调用histroy.transitionTo方法做路由过渡。

js 复制代码
History.prototype.transitionTo = function transitionTo ( location, onComplete, onAbort ) { 
  var this$1$1 = this; 
  var route; 
  try { 
    route = this.router.match(location, this.current); 
  } catch (e) { 
    this.errorCbs.forEach(function (cb) { cb(e); }); // Exception should still be   thrown throw e 
  } 
  var prev = this.current; 
  this.confirmTransition( 
    route, 
    function () { 
      this$1$1.updateRoute(route); 
      onComplete && onComplete(route); 
      this$1$1.ensureURL(); 
      this$1$1.router.afterHooks.forEach(
        function (hook) { 
          hook && hook(route, prev); 
         }
       ); // fire ready cbs once 
       if (!this$1$1.ready) { 
         this$1$1.ready = true; 
         this$1$1.readyCbs.forEach(function (cb) { cb(route); }); 
       } 
     }, 
     function (err) { 
       if (onAbort) { onAbort(err); } 
       if (err && !this$1$1.ready) { 
        if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) { 
          this$1$1.ready = true; 
          this$1$1.readyErrorCbs.forEach(function (cb) { cb(err); }); 
        } 
     } 
   } 
 ); 
};

matcher

addRoutes函数

在实例化router的过程中会执行createMatcher函数(最终就是创建一个路由映射表),并将返回结果赋值给this.matcher。传入的就是我们定义的路由routes,即path到组件的映射关系,以及子路由的描述等。最终会返回一个对象{match,addRoutes}。addRoutes其实是vue-router提供的一个能力。可以动态的去添加路由配置。通常情况下我们会写死路由,但有些情况下是从后端动态下发的路由。就可以通过addRoutes函数再次执行createRouteMap,就会在原来的路由映射基础上做添加。这也是vue-router设计比较巧妙的地方。

js 复制代码
function createMatcher(routes,router) { 
  const {pathList,pathMap,nameMap} = createRouteMap(routes) 
  function addRoutes(routes) { 
    createRouteMap(routes,pathListh,pathMap,nameMap) }
    function match(raw,currentRoute,redirectedFrom){ } 
    reutrn { match, addRoutes } 
 } 

首先通过createRouteMap方法创建路由映射表,定义pathList、pathMap、nameMap三个变量,优先从参数中去取。参数中没有传入的话就赋一个默认值([]或者Object.create(null))。然后对路由配置做遍历,执行addRouteRecord方法。如果路径是通配符,会调整到结尾,即优先级最低。最终返回pathList、pathMap、nameMap组成的对象。

js 复制代码
function createRouteMap(routes,oldPathList?,oldPathMap?,oldNameMap?) { 
  const pathList = oldPathList || [] 
  const pathMap = oldPathMap || Object.create(null) 
  const nameMap = oldNameMap || Object.create(null) 
  routes.forEach(route => { 
    addRouteRecord(pathList,pathMap,nameMap,route) 
  }) 
  for(let i = 0; l = pathList.length;i<l;i++) { 
    if(pathList[i]==='*') { // 如果路径是通配符,会调整到结尾,即优先级最低    pathList.push(pathList.splice(i,1)[0]) l-- i-- 
    } 
   } 
 return { pathList, pathMap, nameMap } }

addRouteRecord首先从路由定义中拿到path和name(路由是可以取名称的)。会限制path不能为空,同时传入的component不是string类型的,必须是一个真实的组件。pathToRegexpOptions是路径的正则匹配。通过normalizePath根据当前传入的path、parent、strict拼接成最后的path。如果是非strict,那么会把结尾的'/'替换为空。如果path[0]是'/',说明是一个绝对路径直接返回path。如果没有parent直接返回path。否则会根据parent.path和path进行拼接。

然后声明一个record变量重新描述路由,为之后的路径匹配提供比较好的依据。如果配置了子路由children,就会去遍历children。递归调用addRouteRecord创建record。如果pathList不存在record.path,就将record.path存到pathList中,并将record.path作为key,record作为value添加到pathMap中。如果传入了name,那么nameMap也会去记录这样一个record。这样就构造出完整的路由映射表。之后就能很方便的根据path或者name查到对应的record。

js 复制代码
function addRouteRecord(pathList,pathMap,nameMap,route,parent?,matchAs?) {
  const {path,name} = route 
  const pathToRegexpOptions = route.pathToRegexpOptions || {} 
  const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict )  if(typeof route.caseSensitive === 'boolean') { 
  pathToRegexpOptions.sensitive = route.caseSensitive 
 } 
 const record = { // 对传入路由配置项重新做规范 
   path: normalizedPath, 
   regex: compileRouteRegex(normalizedPath,pathToRegexpOptions), // 正则表达式   components: route.components || {default: route.component}, // 传入的component instances: {}, 
   name, 
   parent, 
   matchAs, 
   redirect: route.redirct, 
   beforeEnter: router.beforeEnter, 
   meta:route.meta || {}, 
   props: route.props == null ? {}:route.components?route.props:{default:route.props} }  if(route.children) { // 嵌套路由,递归 
   route.children.forEach(child => { 
     const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined  addRouteRecord(pathList,pathMap,nameMap,child,record,childMatchAs) }) } if(!pathMap[record.path]) { 
    pathList.push(record.path) 
    pathMap[record.path] = record 
  } 
  if(name) { 
    if(!nameMap[name]) { 
      nameMap[name] = record 
     } 
    } 
  } 
  function normalizePath(path,parent?,strict?) { 
    if(!strict) path = path.replace(/\/$/,'') // 把结尾的'/'去掉 
    if(path[0]==='/') return path 
    if(parent == null) return path 
    return cleanPath(`${parent.path}/${path}`) // 将parent.path跟path进行拼接 
  }

最终的pathMap结果如下:

nameMap结果如下:

match函数

作用:根据我们传入的路径raw和当前路径currentRoute计算出新的路径,并匹配到对应的路由record,然后根据新的位置和record创建新的路径并返回。

初始化时候this.current如下:

执行transitionTo时候,通过this.router.match得到路径route。比如当前路由是'/foo',那么location就是'/foo'。

js 复制代码
var route = this.router.match(location, this.current);

在match函数中首先通过normalizeLocation对即将跳转的路径做处理,处理的结果location的结构如下:

此时满足没有name,但是有path的情况。然后通过pathMap[path]找到对应的record。找到的record如下:

判断location的path和record的regex是否匹配上。此时匹配上调用 _createRoute,实际就会调用createRoute函数。结果如下:

即通过match函数后拿到的route就是上面的结构。matched就是匹配的路由信息。

js 复制代码
function match(raw,currentRoute?,redirectedFrom?) { 
  const location = normalizeLocation(raw,currentRoute,false,router) 
  const {name} = location 
  if(name) { 
    const record = nameMap[name] 
    if(!record) return _createRoute(null, location) 
    if(record) { 
    // 如果有record,对query参数做一些处理 
      if(currentRoute && typeof currentRoute.params === 'object') { 
        for(const key in currentRoute.params) { 
          if(!(key in location.params)) { 
            location.params[key] = currentRoute.params[key] 
           } 
         } 
       } 
    } 
    if(record) { 
      // 生成path,然后执行_createRoute 
      location.path = fillParams(record.path,location.params,`named route "${name}"`)  return _createRoute(record,location,redictedFrom) 
    } 
  } elseif(location.path) { 
    // 没有name,但是有path的话。 
    loaction.params = {} // 通过path找到对应的
    record for(let i = 0; i< pathListh.length;i++) {
      const path = pathList[i] 
      const record = pathMap[path] 
      // 判断location的path和record的regex是否匹配上。匹配上就调用 _createRoute 
      if(matchRoute(record.regex,location.path,location.params)) { 
        return _createRoute(record,location,redirectedFrom) 
      } 
    } 
  } 
  // 都没有时 
 return _createRoute(null, location) }
js 复制代码
function normalizeLocation(raw,current,append?,router?) { 
  let next = typeof raw === 'string' ? {path:raw}: raw 
  if(next.name || next._normalized) return next 
  if(!next.path && next.params && current) { 
    next = assign({}, next) 
    next._normalized = true 
    const params = assign(assign({},current.params),next.params) 
    if(current.name) { 
      next.name = current.name next.params = params 
    } elseif (current.matched.length) { } 
    const parsedPath = parsePath(next.path || '') // 对路径做解析 
    const basePath = (current && current.path) || '/' 
    const path = parsedPath.path ? resolvePath(parsedPath.path,basePath,append || next.append) :basePath // 算出一个新的path 
    const query = resolveQuery( parsedPath.query, next.query, router && router.options.parseQuery ) // 得到一个新的query 
    let hash = next.hash || parsedPath.hash 
    if(hash && hash.charAt(0) !== '#') { 
      hash = `#${hash} 
    } 
  return { 
    _normalized: true, 
    path, 
    query, 
    hash 
  } 
}}

normalizeLocation的功能可以看下下面的两个测试案例

即如果输入'/abc?foo=bar&baz=qux#hello',通过normalizeLocation后返回 return {

_normalized: true,

path:'/abc',

query: {foo:'bar',baz:'qux'}

hash: '#hello'

}

如果有name就会去nameMap中获取对应的record。同样执行_createRoute生成以上结构的route。如果不存在这个record的话就执行_createRoute(null,location)。即没有与之匹配的路由,此时的结构如下:其中matched是[]。

路径切换

路径切换是整个路由中最核心的环节,因为它关系到导航守卫的管理、url的变化、视图的渲染。在路由初始化的时候会执行history.transitionTo(history.getCurrentLocation())。这个就是路径切换的方法,路径切换的入口还有就是push和replace API的执行,实际上也会去执行transitionTo方法。transitionTo是所有history基类的实现。transitionTo从api设计上功能是跳转到一个新的地址。并且有跳转成功和失败的回调。首先调用this.router.match根据跳转的路径和当前的路径算出一个新的路径。然后执行this.comfirmTransition方法进行真正的路径切换。

js 复制代码
transitionTo(location,onComplete,onAbort) { 
  const route = this.router.match(location,this.current) 
  this.confirmTransition(route,
    () => { 
      this.updateRoute(route) 
      onComplete && onComplete(route) 
      this.ensureURL() 
      if(!this.ready) { 
        this.ready = true 
        this.readyCbs.forEach(cb => {cb(route)}) 
      } 
    },
    err =>{ 
      if(onAbort) { 
       onAbort(err) 
      } 
      if(err && !this.ready) { 
        this.ready = true 
        this.readyErrorCbs.forEach(cb => {cb(err)}) 
       } 
     }) 
   }

在上面已经通过match函数拿到了相关的路由信息route。就会去执行this.confirmTransition进行路由切换。关键就是去执行this.updateRoute(route)。更新_route的值触发组件的渲染逻辑,下面会分析到。

导航守卫的执行逻辑

导航守卫可以理解为在我们执行路径切换的时候执行一些钩子函数。 完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫
  3. 调用全局的 beforeEach 守卫
  4. 在重用的组件里调用 beforeRouteUpdate 守卫
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

添加全局导航守卫

js 复制代码
// 全局导航守卫 
const router = new VueRouter({ routes }) 
router.beforeEach((to,from,next) => { 
  console.log('global before each') 
  next() 
}) 
router.afterEach((to,from,next) => { 
  console.log('global after each') 
  next() 
})

在路由配置中增加导航守卫

js 复制代码
const routes = [ 
  { 
    path: '/foo', 
    name: 'foo', 
    component: Foo, 
    beforeEnter(to,from,next) { 
      console.log('foo before enter') 
      next() 
    } 
    children: [ 
    { 
      path: '/child', 
      component: Bar 
    } 
  ] 
}, 
{ 
  name: 'bar', 
  path: '/bar', 
  component: Bar, 
  beforeEnter(to,from,next) { 
    console.log('bar before enter') 
    next() 
  } 
}]

给组件添加导航守卫

js 复制代码
const Foo = { 
  template: `<div>foo</div>`, 
  beforeRouteEnter(to, from, next) { 
    next((vm) => { console.log(vm) 
   }) 
  }, 
  beforeRouteUpdate(to, from, next) { 
    console.log('foo beforeRouteUpdate') 
    next() 
   }, 
  }

导航守卫的逻辑是在confirmTransition函数中执行的,首先通过resolveQueue拿到updated、deactivated、activated三个数组。

js 复制代码
var ref = resolveQueue( this.current.matched, route.matched ); 
var updated = ref.updated; 
var deactivated = ref.deactivated; 
var activated = ref.activated;

比如我从'/foo'切换到'/bar',上面三个数组的值如下:即当前激活的组件是activated,失活的组件是deactivated,updated为空。后选会根据这3个数组去执行相关的导航守卫。

接下来定义一个queue,把所有导航守卫拆到一个队列中,这个队列依次执行。每执行完一个就调用next执行队列中的下一个。直到执行完毕。

js 复制代码
var queue = [].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(function (m) { return m.beforeEnter; }), 
  // async components
  resolveAsyncComponents(activated) 
 );

调用runQueue执行队列中的任务,runQueue就是取出queue中的任务,放入到iterator去执行相应的hook。参数route、current、function(to) 分别表示导航守卫中的to、from、next。在fuction(to)中最后会执行next(to),也就是step(index + 1)。即执行下一个hook。

js 复制代码
runQueue(queue, iterator, function () { 
  // wait until async components are resolved before
  // extracting in-component enter guards 
  var enterGuards = extractEnterGuards(activated); 
  var queue = enterGuards.concat(this$1$1.router.resolveHooks); 
  runQueue(queue, iterator, function () { 
    if (this$1$1.pending !== route) { 
    return abort(createNavigationCancelledError(current, route)) 
   } 
   this$1$1.pending = null; 
   onComplete(route); 
   if (this$1$1.router.app) { 
     this$1$1.router.app.$nextTick(function () { handleRouteEntered(route); }); 
   } 
  }); 
 });
js 复制代码
var iterator = function (hook, next) { 
  if (this$1$1.pending !== route) {
  return abort(createNavigationCancelledError(current, route)) 
 } 
 try { 
   hook(route, current, function (to) { 
     if (to === false) { 
       // next(false) -> abort navigation, ensure current URL 
       this$1$1.ensureURL(true); 
       abort(createNavigationAbortedError(current, route)); 
      } else if (isError(to)) { 
        this$1$1.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$1$1.replace(to); 
       } else { 
        this$1$1.push(to); 
       } 
      } else { 
      // confirm transition and pass on the value next(to); } }); 
      } catch (e) { abort(e); }
js 复制代码
function runQueue (queue, fn, cb) { 
  var step = function (index) { 
    if (index >= queue.length) { 
      cb(); 
    } else { 
      if (queue[index]) { 
        fn(queue[index], function () { step(index + 1); }); 
     } else { step(index + 1); } 
   } 
 }; 
 step(0); 
}

接下来看都有哪些导航守卫,以及它们执行的逻辑。

首先会执行extractLeaveGuards方法,把deactivated作为参数传入。调用失活组件中beforeRouteLeave的导航守卫。

js 复制代码
function extractLeaveGuards (deactivated) { 
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) 
 }
 function extractGuards ( records, name, bind, reverse ) { 
   var guards = flatMapComponents(records, function (def, instance, match, key) { 
     var guard = extractGuard(def, name); 
     if (guard) { 
       return Array.isArray(guard) ? guard.map(function (guard) { 
         return bind(guard, instance, match, key); }) 
         : bind(guard, instance, match, key) } }); 
         return flatten(reverse ? guards.reverse() : guards) 
       }
 function extractGuard ( def, key ) { 
  if (typeof def !== 'function') { // extend now so that global mixins are applied. 
   def = _Vue.extend(def); 
  }
  return def.options[key] 
 }

通过bind去执行组件上的beforeRouteLeave函数。

上面hook执行完后调用next()执行 this.router.beforeHooks。当我们添加全局导航守卫的时候调用router.beforeEach就会去调用registerHook,把beforeEach接收的函数push到this.beforeHooks中。

js 复制代码
VueRouter.prototype.beforeEach = function beforeEach (fn) { 
  return registerHook(this.beforeHooks, fn) 
 }; 
 function registerHook (list, fn) { 
   list.push(fn); 
   return function () { 
     var i = list.indexOf(fn); 
     if (i > -1) { 
       list.splice(i, 1); 
     } 
   } 
 }

第三步执行 extractUpdateHooks(updated),跟extractLeaveGuards(deactivated)类似。会拿到组件定义的beforeRouteUpdate函数并执行。

第四步执行activated.map(function (m) { return m.beforeEnter; }),即执行路由中定义的beforeEnter

第五步解析异步组件resolveAsyncComponents(activated)。即通过import方式引入的组件。

js 复制代码
{ 
  name: 'bar', 
  path: '/bar', 
  component: () => import('./Bar.vue'), 
  beforeEnter:(to,from,next) =>{ 
    console.log('bar before enter')
    next() 
  } 
}

resolveAsyncComponents也是返回类似导航守卫的函数,如果路由配置的时候component设置为一个函数,通过import引入组件,那么def是函数且def.cid === undefined。就说明当前是异步组件。执行def加载组件,加载完成后执行resolve函数。把加载的结果重新赋值给match.components。然后执行next(),执行后续逻辑。

js 复制代码
function resolveAsyncComponents (matched) { 
  return function (to, from, next) { 
    var hasAsync = false; 
    var pending = 0; 
    var error = null; 
    flatMapComponents(matched, function (def, _, match, key) { 
      if (typeof def === 'function' && def.cid === undefined) { 
        hasAsync = true; 
        pending++; 
        var resolve = once(function (resolvedDef) { 
          if (isESModule(resolvedDef)) { 
           resolvedDef = resolvedDef.default; 
          } 
          def.resolved = typeof resolvedDef === 'function' ? resolvedDef : _Vue.extend(resolvedDef); 
          match.components[key] = resolvedDef; 
          pending--; 
          if (pending <= 0) { next(); } 
         }); 
         }
       }
       var res; 
       try { 
         res = def(resolve, reject); 
       } catch (e) { reject(e); } 
       if (res) { 
         if (typeof res.then === 'function') { 
           res.then(resolve, reject); 
         } else { // new syntax in Vue 2.3 
           var comp = res.component; 
           if (comp && typeof comp.then === 'function') { 
           comp.then(resolve, reject); 
         } } }
     }
    

其他导航守卫执行过程类似。

url的变化逻辑

当点击link-router时候,触发history.push方法。

如果是hash模式,通过pushHash方法修改页面上的hash值。

js 复制代码
HashHistory.prototype.push = function push (location, onComplete, onAbort) { 
  var this$1$1 = this; 
  var ref = this; 
  var fromRoute = ref.current; 
  this.transitionTo( location, function (route) { 
    pushHash(route.fullPath); 
    handleScroll(this$1$1.router, route, fromRoute, false); 
    onComplete && onComplete(route); 
  }, 
  onAbort 
 ); 
}; 
function pushHash (path) { 
  if (supportsPushState) { 
    pushState(getUrl(path)); 
    } else { 
      window.location.hash = path; 
    } }

如果是history模式,就会触发HTML5Histroy.push方法,通过pushState方法调用window.history.pushState或者window.history.replaceState在历史记录里面添加一条新记录或者替换当前记录。

js 复制代码
HTML5History.prototype.push = function push (location, onComplete, onAbort) { 
  var this$1$1 = this; 
  var ref = this; 
  var fromRoute = ref.current; 
  this.transitionTo(location, function (route) { 
    pushState(cleanPath(this$1$1.base + route.fullPath)); 
    handleScroll(this$1$1.router, route, fromRoute, false); 
    onComplete && onComplete(route); }, 
    onAbort); 
   }; 
   function pushState (url, replace) { 
   saveScrollPosition(); 
   var history = window.history; 
   try { 
     if (replace) { 
     var 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); } }

通过上面的操作就会触发hash或者history路由变化的监听函数

路由变化监听

在init方法中会执行history.transitionTo,完成后执行setupListeners监听url的变化。

js 复制代码
var setupListeners = function (routeOrError) { 
  history.setupListeners(); 
  handleInitialScroll(routeOrError); 
 }; 
 history.transitionTo( 
   history.getCurrentLocation(), 
   setupListeners, 
   setupListeners 
 );

如果是hash模式的路由就监听hashchange事件,如果是histroy模式的路由那么就监听popstate事件。路由发生改变执行handleRoutingEvent事件处理函数,又会去执行transitionTo函数更新_route,触发视图重新渲染,渲染新的组件。

js 复制代码
HashHistory.prototype.setupListeners = function setupListeners () { 
  var this$1$1 = this; 
  if (this.listeners.length > 0) { return } 
  var router = this.router; 
  var expectScroll = router.options.scrollBehavior; 
  var supportsScroll = supportsPushState && expectScroll; 
  if (supportsScroll) { 
    this.listeners.push(setupScroll()); 
   } 
   var handleRoutingEvent = function () { 
     var current = this$1$1.current; 
     if (!ensureSlash()) { return } 
     this$1$1.transitionTo(getHash(), function (route) { 
       if (supportsScroll) { 
         handleScroll(this$1$1.router, route, current, true); 
        } 
        if (!supportsPushState) { 
          replaceHash(route.fullPath); 
         } 
       }); 
     }; 
     var eventType = supportsPushState ? 'popstate' : 'hashchange'; window.addEventListener( eventType, handleRoutingEvent ); 
     this.listeners.push(function () { 
       window.removeEventListener(eventType, handleRoutingEvent); 
     }); 
   };

组件的渲染逻辑

上面提到在每个组件渲染之前会执行beforeCreate方法,会设置响应式数据'_route'

js 复制代码
Vue.util.defineReactive(this, '_route', this._router.history.current); // 数据响应式,_route变化触发视图重新更新

并通过Object.defineProperty监听 <math xmlns="http://www.w3.org/1998/Math/MathML"> r o u t e ,当获取 route,当获取 </math>route,当获取route时候触发其get函数,返回上面的响应式数据'_route'

js 复制代码
Object.defineProperty(Vue.prototype, '$route', { 
  get: function get () { return this._routerRoot._route } 
 });

自定义全局组件router-view拿到$route也就拿到了响应式数据'_route'。然后在matched中去寻找匹配的component,通过h函数进行渲染。当通过router-link切换路由的时候,会调用this.transitionTo然后调用update(route),就会去更新app._route的值。触发视图重新更新。router-view根据新的_route值获取需要渲染的组件。

js 复制代码
History.prototype.listen = function listen (cb) { this.cb = cb; };

在初始化的时候执行history.listen,对this.cb进行赋值。

js 复制代码
history.listen(function (route) { 
  this$1$1.apps.forEach(function (app) { 
    app._route = route; 
   }); 
 });

调用updateRoute时候会执行this.cb,更新app._route的值,重新渲染视图。

js 复制代码
History.prototype.updateRoute = function updateRoute (route) { 
  this.current = route; 
  this.cb && this.cb(route); 
};
相关推荐
昨天;明天。今天。4 分钟前
案例-任务清单
前端·javascript·css
zqx_71 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己2 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称2 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2343 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河3 小时前
CSS总结
前端·css
BigYe程普3 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H3 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍3 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发