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)
}
posted @ 2020-04-12 13:25  Ever-Lose  阅读(2119)  评论(0编辑  收藏  举报