A B C
" />

小言_互联网的博客

浅显易懂的vue-router源码解析(一)

486人阅读  评论(0)

前言

在正式进入vue-router之前,我们先从框架的思维跳出来,仔细思考一下前端路由的实现原理,观察如下案例.

<body>
    <a onclick="jump('/a')">A</a>
    <a onclick="jump('/b')">B</a>
    <a onclick="jump('/c')">C</a>
    <div id="app">
         
    </div>
 </body>   

html文件里新建三个a标签,并且下面放一个空的div.

在看js代码之前,我们先快速回顾一遍 HTML5 history API.(文章下面部分都将以目前主流的history API作为路由方案讲解,至于hash路由可自行研究)

  • history.pushState可以往路由栈添加一条路由记录,从而伪装成页面的跳转.因为此API调用仅仅只是改变了url,并没有引起页面上的任何内容变化,浏览器也不会刷新.
  • history.replaceState的用法和history.pushState,它们的区别在于history.pushState是往路由栈添加一条路由记录,而history.replaceState是将当前路由替换成一条新的路由.
  • window.onpopstate事件能监听到用户点击浏览器前进和后退按钮时触发的事件.

window.onpopstate事件只能监听前进后退的事件,如果在代码中调用history.pushStatehistory.replaceState改变url时,window.onpopstate事件是不会触发的.

现在再看下面js代码.当用户点击a标签时触发jump函数.在函数内部调用history.pushState({}, "",path),此时浏览器的url会发生改变,但是页面的内容还没有发生任何变化.随后执行render函数时,页面内容才开始发生真正的变化.

render函数根据跳转路径的不同动态改变app容器里面的内容,从而便模拟出了点击不同路径页面似乎发生了跳转的效果.

    // a链接跳转
    function jump(path){
        history.pushState({}, "",path);
        render(path);
    }
    //渲染内容
    function render(path){
        var app = document.getElementById("app");
        switch(path){
           case "/a": 
                app.innerText = "页面A";
                break;
           case "/b": 
                app.innerText = "页面B";
                break;
           case "/c": 
                app.innerText = "页面C";
                break;
           default:
                app.innerText = "其他内容";          
        }
    }
    //监听前进后退事件
    window.onpopstate = function(event) {
       const path = location.pathname;
       render(path);
    };

效果如下:

从上面的案例中我们可以总结出前端路由的实现原理.

  • 采用某种方式使url发生改变。这种方式可能是调用HTML5 history API实现,也可能是点击前进后退或者改变路由hash.但是不管采用哪种方式,它都不能造成浏览器刷新,仅仅只是单纯的url发生变化.
  • 监听到url的变化之后,根据不同的路径获取渲染内容,再把内容填充到div容器里.从上面案例可知,监听url的变化一般在两个地方,第一是在window.onpopstate包裹的回调函数里,第二是在执行history.pushStatehistory.replaceState的后面.

vue-router的底层逻辑同样如此,比如平时中经常使用的<router-link to="home">类似于上面案例中的a标签,它的底层就是利用history.pushStatehistory.replaceState(当路由mode选择'history'时)改变url的.而<router-view/>相当于上方app容器元素,监听不同路径再根据路径值拿到相应的页面组件并渲染出来.

路径解析

现有页面模板如下.router-linkrouter-view都是平时开发中使用比较多的全局组件.

<template>
  <div>
       <router-link :to="{ path: '/home' }">Home</router-link>
       <router-view/>
  </div>
</template>

router-link相当于一个跳转链接,它的源码实现后面再讲.我们先假设该组件的内部通过获取to里面的path属性执行跳转操作.按照上面介绍的前端路由的原理,路由跳转操作第一步通过HTML5 history API单纯改变url,这个在router-link组件内部可以做到.

第二步监听url变化后,需要根据path拿到渲染内容.当前案例下,path对应的路径是/home,那么渲染的内容我们可以很容易想到在路由配置表中寻找,比如如下配置.

const routes = [
	  {
	    path: '/home',
	    component: Home
	  },
	  {
	    path: '/login',
	    component: () => import(/* webpackChunkName: "about" */ '../views/Login/Login.vue')
	  }
  ]

我们可以引入路由文件中的routes数组,循环遍历判端path/home是否匹配,一旦匹配成功了就能知道要渲染的内容就是Home组件.

上面的情况属于最简单的一种,因为实际中我们存在嵌套路由和动态参数,仅仅只用上面的判断方式并不能满足日常开发需求.

我们接下来看看源码是如何处理路径匹配的.源码创建了一个VueRouter的构造函数,在它里面编写了路由所有的实现逻辑.

//页面调用
const router = new VueRouter({
  mode: 'history',
  routes
})

// VueRouter构造函数
var VueRouter = function VueRouter (options) {
  this.app = null;
  this.apps = [];
  this.options = options; //options对应上面页面调用里传入的参数
  this.beforeHooks = [];
  this.resolveHooks = [];
  this.afterHooks = [];
  
  this.matcher = createMatcher(options.routes || [], this); 

  var mode = options.mode || 'hash';

  this.mode = mode;

  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));
      }
  }
};

在上面VueRouter构造函数里主要做了两件事.

  • 第一它会根据mode配置不同生成相对应的history对象,history对象是具体执行各种路由操作的执行者.
  • 第二它通过调用createMatcher函数生成了this.matcher对象.这个this.matcher正是解决我们上面遭遇的路径匹配的难题.

我们接下来看一下createMatcher内部是怎么根据路径寻找到要渲染的组件.

createMatcher实现

假设开发者配置的路由表routes如下,它被传入下方createMatcher函数内部执行.

const routes = [
	  {
	    path: '/home',
	    component: Home, // 首页
	    name:"home",
	    children:  [{
		   path: 'list',
		   name:"list",
           component: List  // 列表页
        }]
	  },
	  {
	    path: '/login',
	    name:"login",
	    component: Login // 登录页
	  }
  ]

如果用户访问/home/list,很明显匹配的路由组件应该是List,但通过上面的数据结构并不能快速方便找出与路径相匹配的路由组件.

因此createRouteMap函数对routes树形结构数组作了转换,转化后能更方便找出匹配的组件.createRouteMap(routes)执行完毕后返回三条数据,转化后的数据结构如下:

   pathList = ["/home","/home/list","/login"];
   pathMap = {
       "/home":{
            path: "/home",
            name: "home", 
            regex: /^\/home(?:\/(?=$))?$/i,
            parent: undefined,
            components: {default: {…}}, //对应的页面组件Home
            meta: {}
        },
       "/home/list":{
          path: "/home/list",
          meta: {}
          name: "list",
          parent:{ path:"/home",... }, // 对应上面的home路由
          regex:/^\/home\/list(?:\/(?=$))?$/i,
          components: {default: {…}}, //对应的页面组件List
        },
        "/login":{ ... }
   }
   nameMap = { // 和上面数据结构类似,只不过这里将name作为key
	"home":{...},
	"list":{...},
	"login":{...}
   }

下面的createRouteMap做的事情就是将路由表routes转换成上面的数据结构.我们仔细观察上面三条数据的特征,有三个属性非常关键.

  • parent属性用来描述当前路由是否存在父级路由.如果每一个子路由都用parent属性存着父路由,那么不管嵌套路由的层级有多深,通过某一级子路由的parent属性一直往上寻找,直到找到它的所有祖先路由,这点特征将会在后面渲染嵌套路由时用到.
  • regex是根据当前配置路径动态生成的正则表达式.如何判断当前访问路径是否命中这个路由呢?就是拿访问路径与正则相匹配.使用正则匹配还能解决另外一个问题,比如path配置成动态形式/detail/:id,那么要求访问路径如果为/detail/7也是能够匹配上该路由的,而它的正则为/^\/detail\/((?:[^\/]+?))(?:\/(?=$))?$/i就能与访问路径匹配上.
  • components是一个对象,里面包含一个default属性对应着要渲染的页面组件.

现在将静态的配置routes转化成了上述三种数据结构,那现在根据访问路径找出匹配路由就容易多了.匹配的逻辑写在下面match函数中.

function createMatcher (
  routes,
  router
) {
  const {  pathList,pathMap,nameMap }  = createRouteMap(routes);
  
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap);
  }

  function match (
    raw,
    currentRoute,
    redirectedFrom
  ) {
    var location = normalizeLocation(raw, currentRoute, false, router);
    var name = location.name;

    if (name) {
     // 根据路由name匹配
     ...
    } else if (location.path) { // 根据path匹配
      location.params = {};
      for (var i = 0; i < pathList.length; i++) {
        var path = pathList[i];
        var record$1 = pathMap[path];
        if (matchRoute(record$1.regex, location.path, location.params)) {
          return _createRoute(record$1, location, redirectedFrom)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }

  return {
     match: match,
     addRoutes: addRoutes
  }

我们主要看根据path匹配路由的方式(name匹配可下去自行研究),它会直接遍历一维数组pathList,取出每一个path值.

再通过path值在pathMap找出相应的路由配置,取出其中的正则表达式regex与访问路径相匹配,正则如果校验成功就表示成功获取到了路由配置record$1,再将它传入_createRoute函数生成一个页面上即将要展现的路由.

createMatcher函数执行完会返回两个参数.

  • 第一个是match函数,它的作用是拿访问路径和路由配置表去一一匹配,最后返回一个正确的路由对象.
  • 第二个是addRoutes函数,这个函数可以往pathList,pathMapnameMap继续添加路由信息.官方提供的this.$router.addRoutes动态路由出处正是在此.

回到我们最初提出的问题,怎么根据访问路径找出匹配的路由对象.最关键的地方是需要兼容开发者在编写路由表时可能采用嵌套路由和动态参数的写法,最后createMatcher函数返回的match函数可以解决这个问题.

在上述整个路径解析流程中,我们有两个细节没有展开讲.

  • 一个是match函数最后命中了某个路由配置后,为什么还要使用_createRoute函数重新创建一个新的路由,直接将命中的那个路由配置返回不行吗?
  • 第二个是我们现在已经知道createRouteMap将静态的路由表转化成了pathList,pathMapnameMap这三种数据结构,但它内部到底是怎么做到的?

_createRoute实现

通过正则校验后命中的路由对象recordmatch函数里被传入下面的createRoute执行.该函数内重新创建了一个route新对象,并将原来路由配置信息一一添加上去,但有一个属性matched需要引起注意.

function createRoute (
  record,
  location,
  redirectedFrom,
  router
) {
  var stringifyQuery = router && router.options.stringifyQuery;

  var query = location.query || {};
  try {
    query = clone(query);
  } catch (e) {}

  var route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query: query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  };
  
  return Object.freeze(route);//禁止route被修改
}

matched属性里面存储着所有祖先路由的配置对象,这在后面界面渲染嵌套路由时会用到.它通过formatMatch函数得到.

formatMatch函数实现如下,函数内创建一个数组res,通过循环遍历不断获取路由的父级并存储到res中.最终返回的route对象除了包含基础的配置信息,还在matched属性里存储着所有祖先路由的配置对象

function formatMatch (record) {
  var res = [];
  while (record) {
    res.unshift(record);
    record = record.parent;
  }
  return res
}

createRouteMap实现

routes是开发者编写的静态路由表,最后通过createRouteMap函数处理返回pathListpathMapnameMap.

createRouteMap函数代码里,首先创建三个变量,再传入到addRouteRecord执行,由此可见真正执行数据转换操作的逻辑全放在了addRouteRecord函数里.

createRouteMap单独对path:"*"的情况做了处理,它将*移到了pathList数组的最后面.因为path:"*"对应的路由配置里的正则表达式是/^((?:.*))(?:\/(?=$))?$/i,它能匹配上所有的正规路径,因此要把它放到最后去匹配.只有前面路由都没匹配上时才轮的到它出场,这种场景就是常见的404页面.

function createRouteMap (
  routes,
  oldPathList,
  oldPathMap,
  oldNameMap
) {

  var pathList = oldPathList || [];

  var pathMap = oldPathMap || Object.create(null);

  var nameMap = oldNameMap || Object.create(null);

  routes.forEach(function (route) {
    addRouteRecord(pathList, pathMap, nameMap, route);
  });

  for (var i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0]); //塞到数组最后面
      l--;
      i--;
    }
  }

  return {
    pathList: pathList,
    pathMap: pathMap,
    nameMap: nameMap
  }
}

addRouteRecord函数内部代码如下.record是重新创建的路由配置,它里面有几个属性值得关注.

  • path: path不是从原来中的route直接获取的,而是通过normalizePath做了中间处理.目的就是为了在存在嵌套路由的情况下,子路由的path能够和祖先路由的path拼接后再返回.
  • regex: compileRouteRegex函数针对每个不同path动态生成与之匹配的正则表达式.
  • parent:parent属性会记录父级路由.下面代码里会判断route.children存不存在,如果存在说明存在子级路由.然后将route.children遍历循环,每一个子级路由都会递归调用addRouteRecord函数,子级调用的时候会将record作为parent参数传递过去.那么递归调用时,子级创建的record记录的parent就会有值.
function addRouteRecord (
  pathList,
  pathMap,
  nameMap,
  route,
  parent,
  matchAs
) {
  var path = route.path;
  var name = route.name;


  var pathToRegexpOptions =
    route.pathToRegexpOptions || {};
  var normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict);

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive;
  }

  var record = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name: name,
    parent: parent,
    matchAs: matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  };

  if (route.children) {
    route.children.forEach(function (child) {
      var 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 (route.alias !== undefined) {
    var aliases = Array.isArray(route.alias) ? route.alias : [route.alias];
    for (var i = 0; i < aliases.length; ++i) {
      var alias = aliases[i];
      if (process.env.NODE_ENV !== 'production' && alias === path) {
        warn(
          false,
          ("Found an alias with the same value as the path: \"" + path + "\". You have to remove that alias. It will be ignored in development.")
        );
        // skip in dev to make it work
        continue
      }

      var aliasRoute = {
        path: alias,
        children: route.children
      };
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      );
    }
  }

  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record;
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        "Duplicate named routes definition: " +
          "{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
      );
    }
  }
}

页面渲染

上面已经将路径解析的流程介绍了一遍,总结起来做了这样的一件事.点击某个<router-link to="/home/list"/>将要跳转的链接传递给VueRoute里面matcher对象的match函数,它拿到链接以后,去pathListpathMapnameMap里面寻找相匹配的路由对象,找到后返回结果.match函数返回的路由对象除了包含一些基础配置信息:path,name,meta,params等等.另外它还包含了一个特别重要的属性matched,装载着当前以及所有祖先路由的配置信息.

比如上面案例访问/home/list最终返回的路由数据如下:

{
	fullPath: "/home/list"
	hash: ""
	matched: Array(2)
	            0: {path: "/home", regex: /^\/home(?:\/(?=$))?$/i, components: {…}, instances: {…}, name: "home", …}
	            1: {path: "/home/list", regex: /^\/home\/list(?:\/(?=$))?$/i, components: {…}, instances: {…}, name: "list", …}
	meta: {}
	name: "list"
	params: {}
	path: "/home/list"
	query: {}
}

我们现在再回到最初的问题,点击下面<router-link/>已经可以拿到路由对象,对象的matched属性装载着待渲染的组件.

<template>
  <div>
       <router-link :to="{ path: '/home' }">Home</router-link>
       <router-view/>
  </div>
</template>

现在存在一个非常棘手的问题.router-linkrouter-view是两个不同的组件,即使router-link知道要渲染的组件是什么,那它怎么把数据传递给router-view,另外它怎么能触发router-view组件重新渲染.

router-link虽然不能直接与router-view通信,但是它们两个都可以获取Vue的根组件实例.如果router-link将数据传给根组件实例,并且修改根组件的响应式状态,那样所有子组件的render函数都会重新执行一遍.

router-view也不例外,它在执行render函数时,可以拿到根组件上存放的路由对象,这样就可以正常渲染组件页面内容了.

现在看一下vue-router源码如何一步步实现.我们在初始化应用创建路由对象的时候一般这样写.

//使用vue-router插件
Vue.use(VueRouter);

//创建router实例
const router = new VueRouter({
  mode: 'history',
  routes
})

//创建根组件实例
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

Vue.use(ob)api是给Vue安装插件,它内部一般都是调用ob对象的install来完成插件的安装.接下来我们重点看一下VueRouterinstall方法.

install函数的参数VueVue的构造函数,并不是实例对象.源码中给Vuemixin里面添加了一个生命周期钩子函数beforeCreate.这将意味着使用当前Vue构造函数创建的所有实例对象在创建过程中都会运行一遍beforeCreate钩子函数.

var _Vue;
function install (Vue) {
  if (install.installed && _Vue === Vue) { return } // 确保插件只安装一次
  install.installed = true;

  _Vue = Vue;

  var isDef = function (v) { return v !== undefined; };

  Vue.mixin({
    beforeCreate: function 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;
      }
    },
    destroyed: function destroyed () {
     ...
    }
  });

  Object.defineProperty(Vue.prototype, '$router', {
    get: function get () { return this._routerRoot._router }
  });

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

  Vue.component('RouterView', View);
  Vue.component('RouterLink', Link);
}

根实例在创建的过程中也会运行beforeCreate函数,我们在创建根实例的时候是有把router对象传递给它的(代码如下),那么在beforeCreate函数里,这个this指的就是等待创建的vue实例.

vue源码里创建每个vue实例时都会把配置对象赋值给$options.因此执行beforeCreate函数时,如果发现this.$options.router存在,那么说明这个this一定是根实例.然后它将VueRouter创建那个router对象赋值给根实例的_router属性,并执行了init方法.

这里存在一句很关键的代码Vue.util.defineReactive(this, '_route', this._router.history.current),他在根实例上创建了一个响应式的状态_route,这将意味着如果谁修改根实例的_route就可以让所有子组件的render函数重新执行.

最后Vue.prototype原型对象上挂载了两个值分别是$router$route,它们分别指向new VueRouter创建的router对象以及根实例上的响应式状态_route.我们平时开发中在子组件内经常会调用this.$router.push方法以及this.$route.params获取参数,这些api的出处正在于此.因为this.$routerthis.$route都是挂载Vue原型对象上的,所以所有Vue实例都可以直接调用.

//创建根组件实例
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

VueRouter init实现

现在我们看一下this._router.init(this)都做了哪些初始化操作.页面第一次加载时,Vue根实例作为参数传递给init函数执行.

VueRouter的构造函数在上面已经介绍过,它在实例化的时候会创建一个history对象用于路由间参数传递和跳转.

观察下面代码,获取history对象,并判断history对象是哪种模式创建出来的,随后调用transitionTo函数执行跳转.

VueRouter.prototype.init = function init (app) {
   var this$1 = this;
    
  this.apps.push(app);

  if (this.app) {
    return
  }

  this.app = app;

  var history = this.history;

  if (history instanceof HTML5History) { // history模式
    history.transitionTo(history.getCurrentLocation());
  } else if (history instanceof HashHistory) { // hash模式
    ...
  }

  history.listen(function (route) {
    this$1.apps.forEach(function (app) {
      app._route = route;
    });
  });
};

transitionTohistory对象提供的跳转函数.在项目初始化的时候,需要对当前路径做一次跳转,如果当前页面是首页,那就相当于对/根路径做一下跳转.

transitionTo和普通的链接跳转不一样,它里面封装了很强大的能力,能够确保每次执行完毕后,根实例的_route更新为最新的路由对象,从而使页面重新渲染.

init函数的最后面,调用了listen函数,看上去这是一个监听函数.history.listen代码如下,它其实是利用观察者模式将上面listen包裹的函数存到history对象的cb属性上.一旦执行this.history.cb(new_route)可想而知,listen回调函数就会触发.this$1.apps里面是包含根组件实例的,一旦最新的路由对象赋值给根实例的响应式状态就会引起页面重新渲染.

此时大概也能猜得出transitionTo执行后能引起页面重新渲染,肯定是因为内部调用了this.history.cb(new_route).

History.prototype.listen = function listen (cb) {
  this.cb = cb;
};

transitionTo实现

下面代码中出现了熟悉的this.router.match函数,我们在上面第二节花了大量篇幅讲解的match函数终于在这里派上用场了.match函数根据想要跳转的路径location找到了相适配的路由对象route返回.

this.confirmTransition函数传入route参数后 ,它并没有马上执行跳转,而是做了一连串的拦截判断,这正是路由守卫发挥作用的地方.

路由守卫是vue-router源码中的精华部分,后面会单独开一节重点研究this.confirmTransition内部关于路由守卫的实现.

我们现在只需要理解this.confirmTransition里面封装了大量的路由守卫的逻辑,如果通过了路由守卫的层层校验,最后就会执行this.confirmTransition第二个参数(回调函数),这就将意味着路由守卫已经全部放行,可以对当前路由对象route执行跳转.

History.prototype.transitionTo = function transitionTo (
  location,
  onComplete,
  onAbort
) {
  var this$1 = this;
  // this.router.是new VueRouter出来的实例对象
  var route = this.router.match(location, this.current); //获取最新即将要跳转的路由对象.
  this.confirmTransition(
    route,
    function () {
      this$1.updateRoute(route);
      onComplete && onComplete(route);//跳转成功的回调函数
      this$1.ensureURL(); //为了确保浏览器显示的url和route的path保持一致

      // fire ready cbs once
      if (!this$1.ready) {
        this$1.ready = true;
        this$1.readyCbs.forEach(function (cb) {
          cb(route);
        });
      }
    },
    function (err) {
      if (onAbort) {
        onAbort(err); //跳转失败的回调函数
      }
      if (err && !this$1.ready) {
        this$1.ready = true;
        this$1.readyErrorCbs.forEach(function (cb) {
          cb(err);
        });
      }
    }
  );
};

我们接下来看下回调函数this$1.updateRoute(route)里面的实现.

重点来了,函数内执行了history对象的cb函数,正如我们在上面的分析而言,通过调用history.cb(route)让根组件实例的响应式状态_route得到更新,从而使所有子组件render函数重新执行.

History.prototype.updateRoute = function updateRoute (route) {
  var prev = this.current;
  this.current = route;
  this.cb && this.cb(route);
  this.router.afterHooks.forEach(function (hook) {
    hook && hook(route, prev);
  });
};

现在我们可以将整个过程梳理一遍,history.transitionTo(location)是执行跳转的函数.它首先将路径location传给match函数获得匹配的路由对象route.然后穿过this.confirmTransition层层设置的路由守卫,最后执行this$1.updateRoute(route),将路由对象赋值给根组件的响应式状态_route,从而启动页面开始渲染.

我们现在来看一下<router-link />组件的实现.它的点击事件绑定的是handler,而handler调用的是router对象的pushreplace方法.

var Link = {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render: function render (h) {
    var this$1 = this;

    var router = this.$router;
    var current = this.$route;
    var ref = router.resolve(
      this.to,
      current,
      this.append
    );
    var location = ref.location;
    var route = ref.route;
    var href = ref.href;
    
    ...

    var handler = function (e) {
      if (guardEvent(e)) {
        if (this$1.replace) {
          router.replace(location, noop);
        } else {
          router.push(location, noop);
        }
      }
    };

    var on = { click: guardEvent };
    if (Array.isArray(this.event)) {
      this.event.forEach(function (e) {
        on[e] = handler;
      });
    } else {
      on[this.event] = handler;
    }
   
    ...

    return h(this.tag, data, this.$slots.default)
  }
};

我们以push方法为例,router对象的push方法调用的是history对象的push方法.而history对象的push方法调用的也是transitionTo函数.

VueRouter.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;
  if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
    return new Promise(function (resolve, reject) {
      this$1.history.push(location, resolve, reject);
    })
  } else {
    this.history.push(location, onComplete, onAbort);
  }
};

HTML5History.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;
    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) {
      pushState(cleanPath(this$1.base + route.fullPath));
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    }, onAbort);
  };

由此可见路由里面的所有的跳转操作最后运行的都是transitionTo函数,它一方面可以更新根组件实例状态启动页面重新渲染,第二它里面包含了路由守卫的逻辑,每次执行一次跳转都要通过路由守卫的校验.

RouterView实现

transitionTo函数执行成功后,根组件实例的状态_route已经被赋予最新的路由对象,所有子组件的render函数将会重新执行.

<router-view>组件内的render函数也会重新执行,它可以从根实例拿到最新的路由对象并开始渲染页面内容.

我们还是以跳转/home/list为例,最终<router-view>执行render时,从根实例拿到的路由数据如下:

{
	fullPath: "/home/list"
	hash: ""
	matched: Array(2)
	            0: {path: "/home", regex: /^\/home(?:\/(?=$))?$/i, components: {default:{...}}, instances: {…}, name: "home", …}
	            1: {path: "/home/list", regex: /^\/home\/list(?:\/(?=$))?$/i, components: {default:{...}}, instances: {…}, name: "list", …}
	meta: {}
	name: "list"
	params: {}
	path: "/home/list"
	query: {}
}

下面RouterView的函数里,由于设置了functional: true,因此ref.parent才是当前<router-view />的虚拟dom.

我们平时在开发多层级的嵌套路由时,<router-view />也会写多个,它们会分布在不同组件的模板里.每一个<router-view />只负责渲染它自己的那部分.

我们看上面路由对象的的数据结构可知,matched里面存储着两个元素,一个是父级路由name:home,另一个是子级路由name:list.那么对应着页面模板里也会有两个<router-view />,一个渲染父级元素,一个渲染子级元素.

因此下面代码会执行while (parent && parent._routerRoot !== parent)循环,当前的虚拟dom节点会不断往上寻找,直到找到根节点,目的就是为了确认当前这个<router-view />位于第几个层级,对应的值depth是多少.

一旦得到了depth就可以调用route.matched[depth]取出当前<router-view />对应的路由对象,再从路由对象里面调用matched.components[name]获取页面组件(name默认为default),接下来就可以顺利渲染页面组件内容了.

var View = {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref) {
    var props = ref.props;
    var children = ref.children;
    var parent = ref.parent;
    var data = ref.data;

    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    var h = parent.$createElement;
    var name = props.name;
    var route = parent.$route;
    var cache = parent._routerViewCache || (parent._routerViewCache = {});

    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    var depth = 0;
    var inactive = false;
    while (parent && parent._routerRoot !== parent) {
      var vnodeData = parent.$vnode && parent.$vnode.data;
      if (vnodeData) {
        if (vnodeData.routerView) {
          depth++;
        }
        if (vnodeData.keepAlive && parent._inactive) {
          inactive = true;
        }
      }
      parent = parent.$parent;
    }
    data.routerViewDepth = depth;

    var matched = route.matched[depth];

    var component = cache[name] = matched.components[name];
    
    ... //省略

    return h(component, data, children)
  }
};

尾言

本篇文章已经将vue-router的核心工作流程和源码整体梳理了一遍,下一小结将会重点介绍vue-router中路由守卫的实现.


转载:https://blog.csdn.net/brokenkay/article/details/115773964
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场