zoukankan      html  css  js  c++  java
  • 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)
    }
    
  • 相关阅读:
    sklearn 数据预处理1: StandardScaler
    Gitlab利用Webhook实现Push代码后的Jenkins自动构建
    Shell脚本-自动化部署WEB
    Jenkins可用环境变量以及使用方法
    Docker常用命令
    nginx中root和alias的区别
    gitlab+jenkins=自动化构建
    Spring Boot2.0:使用Docker部署Spring Boot
    Maven内置属性、POM属性
    navicat连接mysql出现Client does not support authentication protocol requested by server解决方案
  • 原文地址:https://www.cnblogs.com/everlose/p/12608978.html
Copyright © 2011-2022 走看看