Vue源碼學(xué)習(xí)(二):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>

由前一篇可以知道,在掛載的時(shí)候,<div id="app"><router-view></router-view></div>會(huì)被編譯成以下渲染函數(shù):

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

_c函數(shù)是什么呢?

在initMixin函數(shù)中定義了Vue的初始化函數(shù)_init,在初始化的過(guò)程執(zhí)行了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方法,其實(shí)就是調(diào)用了createElement函數(shù)。

我們?cè)倏纯?code>_c('router-view')發(fā)生了什么。

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

經(jīng)過(guò)一系列調(diào)用 來(lái)到下面的方法


這里的tag就是'router-view'

resolveAsset的源碼:


這里的主要工作就是取出RouterView的構(gòu)造函數(shù)。

把options打印出來(lái),可以看到components是空的。


這里最終通過(guò)components的原型取到RouterView,那原型里面的值是什么時(shí)候定義的呢?


resolveAsset中的options即vm.options。這里又要回到初始化的時(shí)候調(diào)用的mergeOptions方法。

上面一系列調(diào)用的意思就是取出Vue.options并通過(guò)Object.create()設(shè)置到vm.$options的原型中。Object.create()

那Vue.options中的components又是什么時(shí)候定義的呢?

全局搜索一下RouterView,在Vue.component('RouterView', View);打個(gè)斷點(diǎn)

這行代碼是在VueRouter的install函數(shù)里面的,我們并沒(méi)有主動(dòng)調(diào)用Vue.use(VueRouter),那是怎么來(lái)到這里的呢?其實(shí)跟蹤調(diào)用棧就可以看到下面這行代碼:



其實(shí)是VueRouter主動(dòng)調(diào)用的。

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


image.png

就是這里定義了Vue.options的。

再回到前面,這時(shí)我們已經(jīng)取到RouterView的構(gòu)造函數(shù)了。接下來(lái)是創(chuàng)建虛擬節(jié)點(diǎn):

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

只看關(guān)鍵的代碼,可以看到最后來(lái)到了RouterView定義的render函數(shù),它的源碼可以在VueRouter的src/components/view.js中找到。
直接看它的最后一行代碼:

return h(component, data, children)

這個(gè)h函數(shù)是什么呢?可以看看這個(gè)函數(shù)的前面:

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

其實(shí)就是$createElement函數(shù)。

component又是怎么取到的呢?



這里先取到route,那parent的$route又是什么是定義的呢?這里要回到VueRouter的install函數(shù)。

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

可以看到這里通過(guò)mixin在組件beforeCreate的時(shí)候設(shè)置根節(jié)點(diǎn)的_router,_route,再設(shè)置子組件的_routerRoot,之后給Vue的原型定義兩個(gè)變量_router和_route讓他們從_routerRoot取值。
注意_route的定義是響應(yīng)式的:

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

意思就是在渲染的時(shí)候調(diào)用到_route的會(huì)被依賴收集,當(dāng)_route變的時(shí)候會(huì)通知改組件。RouterView就是這樣更新的。

拿到route就可以拿到對(duì)應(yīng)的component了,之后就是調(diào)用$createElement創(chuàng)建虛擬節(jié)點(diǎn)。

_route是什么時(shí)候更新的呢?可以在defineReactive$$1定義的setter里面打個(gè)斷點(diǎn),看調(diào)用??梢哉业絖route被設(shè)置值的地方:


來(lái)看看HashHistory的原理。
首先HashHistory在初始化的時(shí)候給window加上popstate和hashchange的監(jiān)聽(tīng)

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

當(dāng)監(jiān)聽(tīng)到地址變化的時(shí)候就執(zhí)行transitionTo然后confirmTransition,這其中經(jīng)過(guò)一系列的調(diào)用,包括路由守衛(wèi)的處理,到最后調(diào)用updateRoute(route):



cb就是在下面的代碼定義的:

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

至此_route修改,然后通知RouterView進(jìn)行更新。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容