vue-router 工作原理


簡述

hashchange
-->
match route
-->
set vm._route
-->
<router-view> render()
-->
render matched component

監聽hashchange方法

window.addEventListener('hashchange', () => {
    // this.transitionTo(...)
})

進行地址匹配,得到對應當前地址的 route。

將其設置到對應的 vm._route 上。侵入vue監聽_route變量而觸發更新流程

最后是router-view組件調用render函數渲染匹配到的route

測試代碼

<!DOCTYPE html>
<html>
<head>
  <title>vue test</title>
</head>
<body>
<div id="app">
  <h1>Hello App!</h1>
  <button @click="goBack">click me and go back</button>
  <button @click="goIndex">click me and go to index</button>
  <p>
    <router-link to="/foo">Go to Foo</router-link>
  </p>
  <p>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  
  <router-view></router-view>
</div>

  <!-- Vue.js v2.6.11 -->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script src="https://cdn.bootcss.com/vue-router/3.1.3/vue-router.js"></script>
  <script>
    const Foo = { template: '<div>render foo</div>' }
    const Bar = { template: '<div>render bar</div>' }
    const routes = [
      { path: '/foo', component: Foo },
      { path: '/bar', component: Bar }
    ]
    const router = new VueRouter({
      routes // (縮寫) 相當於 routes: routes
    })

    var app = new Vue({
      el: '#app',
      router,
      methods: {
        goBack() {
          this.$router.back();
        },
        goIndex() {
          this.$router.push('/');
        }
      }
    })

    console.log(app);
    // var event = new CustomEvent('test', { 'detail': 5 }); window.dispatchEvent(event);
  </script>
</body>
</html>

怎么注入進的 vue

一個 install 函數,把 $route 掛載到了 Vue.prototype 上,保證 Vue 的所有組件實例,都是取同一份 router。並且在里面注冊了 RouterView 和 RouterLink 組件

function install(Vue) {
  // ...

  Vue.mixin({
    beforeCreate: function beforeCreate() {
      // ...
      
      this._routerRoot = this;
      this._router = this.$options.router;
      this._router.init(this);
      Vue.util.defineReactive(this, '_route', this._router.history.current);

      // ...
    },
    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);

  // ...
}

VueRouter.install = install;

最后進入了 vue 的初始化邏輯里 initUse 函數里去觸發插件的 install 函數執行。

router 是個什么結構

詳見 function VueRouter (options),下面代碼中需要注意三點:

  • app 將會掛上 vue 實例對象
  • mode 代表用戶配置的路由模式,默認是 hash,也就是使用 url 上的 hash 部分作為路由路徑的判定。
  • history 將會掛載上用戶曾經的訪問的記錄數組。
var VueRouter = function VueRouter (options) {
  this.app = null;
  this.apps = [];
  this.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:
      {
        assert(false, ("invalid mode: " + mode));
      }
  }
};

RouterView 組件長什么樣

看下文代碼,總結一下關鍵的步驟:

最關鍵的一步 var component = cache[name] = matched.components[name]; 獲取到具體是那個組件,這里的 component 其實是

{
  template: "<div>render bar</div>"
  _Ctor: {0: ƒ}
  __proto__: Object
}

然后最后面就是調用 h(component, data, children) 完成渲染,h其實是 Vue 實例的 $createElement 函數,它會具體解析此 template 成為視圖渲染。

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;

      // used by devtools to display a router-view badge
      data.routerView = true;

      // 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 = {});

      // ...

      var component = cache[name] = matched.components[name];

      // ...

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

很精妙,此組件的 props 默認把 tag 設置為 a,並且代碼中還支持 slotScope 插槽。

最后一樣 h(this.tag, data, this.$slots.default) 去渲染,所以此組件渲染后的標簽才會默認是 a 標簽呀。。

var Link = {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render: function render(h) {

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

    // ...

    var data = { class: classes };

    var scopedSlot =
      !this.$scopedSlots.$hasNormal &&
      this.$scopedSlots.default
    // ...

    if (scopedSlot) {
      if (scopedSlot.length === 1) {
        return scopedSlot[0]
      } else if (scopedSlot.length > 1 || !scopedSlot.length) {
        // ...
        return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
      }
    }

    if (this.tag === 'a') {
      data.on = on;
      data.attrs = { href: href };
    } else {
      // ...
    }

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

路由控制是怎么做的

本質上就是改變了 hash

hashchange 的事件監聽觸發,接着去觸發 HashHistory 實例里的 updateRoute 函數,updateRoute 函數里觸發回調去更新 route 對象,route 對象更新就走入了 vue 自身的 set 觸發廣播通知被觀察者了。

VueRouter.prototype.back = function back () {
    this.go(-1);
  };
  
VueRouter.prototype.go = function go (n) {
    this.history.go(n);
  };

HashHistory.prototype.go = function go (n) {
  window.history.go(n);
};

// ...

window.addEventListener(
  supportsPushState ? 'popstate' : 'hashchange',
  function () {
    var current = this$1.current;
    // ...
    this$1.transitionTo(getHash(), function (route) {
      if (supportsScroll) {
        handleScroll(this$1.router, route, current, true);
      }
      if (!supportsPushState) {
        replaceHash(route.fullPath);
      }
    });
  }
);

// ...

History.prototype.transitionTo = function transitionTo(
  location,
  onComplete,
  onAbort
) {
  var this$1 = this;

  var route = this.router.match(location, this.current);
  this.confirmTransition(
    route,
    function () {
      this$1.updateRoute(route);
      // ...
    },
    function (err) {
      // ...
    }
  );
};

// ...

History.prototype.updateRoute = function updateRoute(route) {
  var prev = this.current;
  this.current = route;
  // 這里的 cb 就是下面一段的 history.listen
  this.cb && this.cb(route);
  this.router.afterHooks.forEach(function (hook) {
    hook && hook(route, prev);
  });
};

// ...

history.listen(function (route) {
  this$1.apps.forEach(function (app) {
    // 改變 app._route 就會進入 vue 實例自身的 get/set 攔截器中,然后自己觸發更新。
    // 因為上文 install 函數里做了屬性劫持 Vue.util.defineReactive(this, '_route', this._router.history.current);
    app._route = route;
  });
});

鈎子是怎么做的

this.beforeHooks 是個數組,registerHook 函數做的就只是往前面的數組里添加進入這個方法。

VueRouter.prototype.beforeEach = function beforeEach(fn) {
  return registerHook(this.beforeHooks, fn)
};

VueRouter.prototype.beforeResolve = function beforeResolve(fn) {
  return registerHook(this.resolveHooks, fn)
};

VueRouter.prototype.afterEach = function afterEach(fn) {
  return registerHook(this.afterHooks, fn)
};

beforeHooks 在每次觸發更新前的隊列里調用

resolveHooks 執行是在下文的 runQueue 里,也就是是在觸發更新前,但比 beforeHooks 晚,主要用於異步組件

afterHooks 的觸發,是在 updateRoute 函數后,也就是開始觸發 vue 的更新邏輯時,但並不一定視圖已經更新完畢,因為 vue 自身也有不少的隊列操作,不會立即更新。

// beforeHooks
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(queue, iterator, function () {
  // ...
}
    
// resolveHooks
runQueue(queue, iterator, function () {
  // ...

  // wait until async components are resolved before
  // extracting in-component enter guards
  var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
  var queue = enterGuards.concat(this$1.router.resolveHooks);
  runQueue(queue, iterator, function () {
    //...
    onComplete(route);
    //...
  });
});

// afterHooks
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 是怎么做的

hash 模式的路由是采用的 hash change 函數來做監聽,並且操作瀏覽器 hash 做標識,

而 history 模式采用的 popstate event 來記住路由的狀態,而 window.history.state 里的 key 只是用時間來生成的一個緩存。

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

function pushState (url, replace) {
  saveScrollPosition();
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  var history = window.history;
  try {
    if (replace) {
      history.replaceState({ key: getStateKey() }, '', url);
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url);
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url);
  }
}

function genStateKey () {
  return Time.now().toFixed(3)
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM