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

  • Post author:
  • Post category:vue




前言

在正式进入

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.pushState



history.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.pushState



    history.replaceState

    的后面.


vue-router

的底层逻辑同样如此,比如平时中经常使用的

<router-link to="home">

类似于上面案例中的

a

标签,它的底层就是利用

history.pushState



history.replaceState

(当路由

mode

选择

'history'

时)改变

url

的.而

<router-view/>

相当于上方

app

容器元素,监听不同路径再根据路径值拿到相应的页面组件并渲染出来.



路径解析

现有页面模板如下.

router-link



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

    ,

    pathMap



    nameMap

    继续添加路由信息.官方提供的

    this.$router.addRoutes

    动态路由出处正是在此.

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

createMatcher

函数返回的

match

函数可以解决这个问题.

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

  • 一个是

    match

    函数最后命中了某个路由配置后,为什么还要使用

    _createRoute

    函数重新创建一个新的路由,直接将命中的那个路由配置返回不行吗?
  • 第二个是我们现在已经知道

    createRouteMap

    将静态的路由表转化成了

    pathList

    ,

    pathMap



    nameMap

    这三种数据结构,但它内部到底是怎么做到的?



_createRoute实现

通过正则校验后命中的路由对象

record



match

函数里被传入下面的

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

函数处理返回

pathList



pathMap



nameMap

.


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

函数,它拿到链接以后,去

pathList



pathMap



nameMap

里面寻找相匹配的路由对象,找到后返回结果.

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



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

来完成插件的安装.接下来我们重点看一下

VueRouter



install

方法.


install

函数的参数

Vue



Vue

的构造函数,并不是实例对象.源码中给

Vue



mixin

里面添加了一个生命周期钩子函数

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.$router



this.$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;
    });
  });
};


transitionTo



history

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

/

根路径做一下跳转.


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

对象的

push



replace

方法.

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

中路由守卫的实现.



版权声明:本文为brokenkay原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。