Vue源码学习(二):VueRouter

<html>

<head>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
</head>

<body>
    <div id="app">
        <router-view></router-view>
    </div>
</body>

<script>
    const routes = [
        { path: '/page', component: { template: '<div>/page</div>' } }
    ]
    const router = new VueRouter({
        routes
    })

    new Vue({
        el: '#app',
        router
    })
</script>

</html>

由前一篇可以知道,在挂载的时候,<div id="app"><router-view></router-view></div>会被编译成以下渲染函数:

        (function anonymous(
        ) {
            with (this) { return _c('div', { attrs: { "id": "app" } }, [_c('router-view')], 1) }
        })

_c函数是什么呢?

在initMixin函数中定义了Vue的初始化函数_init,在初始化的过程执行了initRender(vm);:

initRender的源码:

  function initRender (vm) {
    vm._vnode = null; // the root of the child tree
    vm._staticTrees = null; // v-once cached trees
    var options = vm.$options;
    var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
    var renderContext = parentVnode && parentVnode.context;
    vm.$slots = resolveSlots(options._renderChildren, renderContext);
    vm.$scopedSlots = emptyObject;
    // bind the createElement fn to this instance
    // so that we get proper render context inside it.
    // args order: tag, data, children, normalizationType, alwaysNormalize
    // internal version is used by render functions compiled from templates
    vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
    // normalization is always applied for the public version, used in
    // user-written render functions.
    vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };

    // $attrs & $listeners are exposed for easier HOC creation.
    // they need to be reactive so that HOCs using them are always updated
    var parentData = parentVnode && parentVnode.data;

    /* istanbul ignore else */
    {
      defineReactive$$1(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
        !isUpdatingChildComponent && warn("$attrs is readonly.", vm);
      }, true);
      defineReactive$$1(vm, '$listeners', options._parentListeners || emptyObject, function () {
        !isUpdatingChildComponent && warn("$listeners is readonly.", vm);
      }, true);
    }
  }

可以看到这里面定义了_c方法,其实就是调用了createElement函数。

我们再看看_c('router-view')发生了什么。

_c('router-view')createElement(vm, 'router-view')

经过一系列调用 来到下面的方法


这里的tag就是'router-view'

resolveAsset的源码:


这里的主要工作就是取出RouterView的构造函数。

把options打印出来,可以看到components是空的。


这里最终通过components的原型取到RouterView,那原型里面的值是什么时候定义的呢?


resolveAsset中的options即vm.options。这里又要回到初始化的时候调用的mergeOptions方法。

上面一系列调用的意思就是取出Vue.options并通过Object.create()设置到vm.$options的原型中。Object.create()

那Vue.options中的components又是什么时候定义的呢?

全局搜索一下RouterView,在Vue.component('RouterView', View);打个断点

这行代码是在VueRouter的install函数里面的,我们并没有主动调用Vue.use(VueRouter),那是怎么来到这里的呢?其实跟踪调用栈就可以看到下面这行代码:



其实是VueRouter主动调用的。

那再看看Vue.component('RouterView', View);


image.png

就是这里定义了Vue.options的。

再回到前面,这时我们已经取到RouterView的构造函数了。接下来是创建虚拟节点:

vnode = createComponent(Ctor, data, context, children, tag);

...


if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
}

...

var vnode = options.render.call(null, renderContext._c, renderContext);

只看关键的代码,可以看到最后来到了RouterView定义的render函数,它的源码可以在VueRouter的src/components/view.js中找到。
直接看它的最后一行代码:

return h(component, data, children)

这个h函数是什么呢?可以看看这个函数的前面:

      var parent = ref.parent;
      ...
      var h = parent.$createElement;

其实就是$createElement函数。

component又是怎么取到的呢?



这里先取到route,那parent的$route又是什么是定义的呢?这里要回到VueRouter的install函数。

function install (Vue) {
    if (install.installed && _Vue === Vue) { return }
    install.installed = true;

    _Vue = Vue;

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

    var registerInstance = function (vm, callVal) {
      var i = vm.$options._parentVnode;
      if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
        i(vm, callVal);
      }
    };

    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;
        }
        registerInstance(this, this);
      },
      destroyed: function destroyed () {
        registerInstance(this);
      }
    });

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

    var strats = Vue.config.optionMergeStrategies;
    // use the same hook merging strategy for route hooks
    strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
  }

可以看到这里通过mixin在组件beforeCreate的时候设置根节点的_router,_route,再设置子组件的_routerRoot,之后给Vue的原型定义两个变量_router和_route让他们从_routerRoot取值。
注意_route的定义是响应式的:

Vue.util.defineReactive(this, '_route', this._router.history.current);

意思就是在渲染的时候调用到_route的会被依赖收集,当_route变的时候会通知改组件。RouterView就是这样更新的。

拿到route就可以拿到对应的component了,之后就是调用$createElement创建虚拟节点。

_route是什么时候更新的呢?可以在defineReactive$$1定义的setter里面打个断点,看调用栈可以找到_route被设置值的地方:


来看看HashHistory的原理。
首先HashHistory在初始化的时候给window加上popstate和hashchange的监听

      var eventType = supportsPushState ? 'popstate' : 'hashchange';
      window.addEventListener(
        eventType,
        handleRoutingEvent
      );

当监听到地址变化的时候就执行transitionTo然后confirmTransition,这其中经过一系列的调用,包括路由守卫的处理,到最后调用updateRoute(route):



cb就是在下面的代码定义的:

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

至此_route修改,然后通知RouterView进行更新。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355