These years, seems to have been so fast, miss my college life.

– even to sb.

As a junior intern, I wrote a simple MVVM framework and a simple VUEX before, but I didn’t write it after reading the source code of VUE-Router (roughly). I didn’t have to work on weekends (I came out for internship before the junior year started, my level is relatively low now, and the code has not been optimized, so I wrote it in a hurry.

Github storage address

use

<template>
  <div id="app">
    <lj-view></lj-view>
    <button @click="go">tagone</button>
    <button @click="back">back</button>
    <button @click="replace">tagtwo</button>
    <div><span>lj-link</span></div>
    <lj-link :to="'/tagtwo'" :tag="'div'">tagtwo</lj-link>
    <lj-link :to="'/tagone'" :tag="'span'">tagone</lj-link>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  created() {
    console.log(this._router);
  },
  methods:{
    go() {
      this.$router.push({path:'/tagone',query:{name:12}})
    },
    back() {
      this.$router.back();
    },
    replace() {
      this.$router.replace({path:'/tagtwo'});
    }
  }
}
</script>

Copy the code
//main.js
import LJRouter from "./lib/LJRouter/src";
Vue.config.productionTip = false
Vue.use(LJRouter)
let router = new LJRouter({
  mode:'hash',
  routes:[
    {
      path:'/',component:hello
    },
    {
      path:'/tagone',component:tagone
    },
    {
      path:'/tagtwo',component:tagtwo
    }
  ]
});
/*router.beforeEach(function(from,to,next) {
  let path = to.path;
  if(path==='/tagtwo') {
    next();
  }else {
    next('/tagtwo')
  }
});*/
Copy the code

The principle of

The core principle of vue-Router is actually very simple, which is divided into hash mode and history mode (there are three of them, and one of them I haven’t learned yet).

Hash pattern

When the hash is changed, the event is triggered, the record is fetched, and the router-view is updated. When you call the router’s push/replace, you actually get the record to trigger the component update and call the render function again

The history mode

Is actually listening popState event (note is call history. Go/history. Only when the forward triggers), and then through pushState and replaceState to change path, note here by HTML 5 new of these two methods to change the path is not heavy The newly initiated request, when changed path, and then get the changed path, so as to obtain the corresponding record, and then trigger component update, re-call render function.

Implement process analysis

If the profile

PS:(I use the LJRouter I wrote here to analyze, the name can be ignored)

Please each big guy small spray, and sincerely seek like-minded friends, and I collect girlfriend.

Install the initialization

function install(vue) {
     let _vue = install._vue;
     if(_vue) {throw new Error('Cannot be reinstalled'); } install._vue = vue; mixinApply(vue); // global mixin}Copy the code

Vue has a feature is plug-in, similar to vue-Router, vuex these are introduced in the way of plug-in, rather than built-in, through vue. use registration (here does not talk about vue. use implementation, very simple, do not understand, you can see vue. use source code implementation), then call install function

function mixinApply(vue) {
    vue.mixin({
        beforeCreate:routerInit
    });
    definedRouter(vue);
    vue.component('ljView',ljView);
    vue.component('ljLink',ljLink);
}
Copy the code

MixinApply is a global mixin that registers two global components, LJ-View and LJ-Link.

function routerInit() {
    if(this.$options.router) {
        this._router = this.$options.router;
        this.$notFoundComponent = notFound;
        install._vue.util.defineReactive(this,'_route',this._router.history.currentRouter);
    } else {
        this._router = this.$parent._router; }}Copy the code

RouterInit is globally mixed through vue. mixin, so each component calls this function. The main purpose of this function is to add a pointer to the LJRouter instance for each component

function definedRouter(vue) {
  Object.defineProperty(vue.prototype,'$router', {get() {
        returnthis._router.router; }}); Object.defineProperty(vue.prototype,'$route', {get() {
      returnthis._router.route; }}); }Copy the code

DefinedRouter is a simpler function, so I won’t cover it

LJRouter instance build

The writing of the LJRouter

import install from './install.js';
import Hashhistory from "./history/Hashhistory";
import Htmlhistory from './history/Htmlhistory';
import {deepCopy} from './util/util.js'
class LJRouter {
  static install = install;
  constructor(options) {
    this.$options = options;
    this.mode = options.mode||'hash';
    switch (this.mode) {
      case "hash":this.history = new Hashhistory(options);break;
      case "history":this.history = new Htmlhistory(options);break;
    }
  }
  addRoutes(routes) {
    this.history.addRoutes(routes);
  }
  get router() {
    return {
      push:this.push.bind(this),
      back:this.back.bind(this),
      replace:this.replace.bind(this)
    };
  }
  get route() {
    let query = this.history.currentRouter.current.query;
    query = deepCopy(query);
    return {query}
  }
  push({path,name,query}) {
    this.history.push({path,name,query});
  }
  replace({path}) {
    this.history.replace({path});
  }
  back() { this.history.back(); } // Global front-guard beforeEach(fn) {if(this.mode === 'hash'||this.mode === 'history') {
      this.history.beforeEach(fn);
    } else {
      throw new TypeError('beforeEach is undefined'); }}}export default LJRouter;

Copy the code

LJRouter constructor, we see first LJRouter constructor is actually very simple, actually is the agent of various parameters on LJRouter instance, and then according to the mode to create the corresponding instance (Hashhistory | | Htmlhistory), and on the LJRouter explicit prototype Some functions are actually proxies. It will essentially call the methods on the history implicit prototype on the instance, giving the general logic first, and then the implementation logic by function

Hashhistory

import createMatcher from ".. /createMatcher";
class Hashhistory {
  constructor(options) {
    this.$options = options;
    let routes = options.routes;
    const {addRoutes,getCurrentRecord} = createMatcher(routes);
    this.addRoutes = addRoutes;
    this.getCurrentRecord = getCurrentRecord;
    this.currentRouter = {current:{}};
    this.pathQueue = [];
    this.beforeEachCallBack = null;
    this.transitionTo();
    this.setUpListener();
  }
  setUpListener() {
    window.onhashchange = function() {
      let hash = location.hash;


      if(hash= = =' ') {
        location.href = location.href+'# /';
        hash = '/';
      } else if(hash= = ='# /') {
        hash = '/'
      }
      else {
        hash = hash.slice(1);
      }
      this.confirmTransitionTo({hash});
    }.bind(this);
  }
  transitionTo() {
    let hash = location.hash;
    if(hash.indexOf(The '#')===-1) {
        location.href = location.href+'# /';
        hash = '/';
    } else {
        hash = hash.slice(1);
    }
    if(hash= = =' ') {
        hash = '/';
    }
    this.pathQueue.push(hash);
    this.confirmTransitionTo({hash});
  }
  confirmTransitionTo({hash,name,query}) {
    let currentRecord;
    currentRecord = this.getCurrentRecord({path:hash,name,query});
    let that = this;
    function next(resolve,reject) {
        return function(path) {
            if(Object.is(undefined,path)) {
               resolve(currentRecord);
            }else {
               location.hash = The '#'+path;
               that.confirmTransitionTo({hash:path});
            }
        }
    }

    new Promise(function(resolve,reject){
            if(this.beforeEachCallBack) {
               this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
            }else {
              resolve(currentRecord);
            }
          }.bind(this)).then(record=>{
             this.currentRouter.current = record
          });
  }
  push({path,name,query}) {
    this.pathQueue.push(path);
    this.confirmTransitionTo({hash:path,name,query});
  }
  back() {
    let pathQueue = this.pathQueue;
    let path = null;
    if(pathQueue.length<=1) {
      console.error('pathQueue value is < 2:redirect to /');
      path = '/'
    } else {
      path = pathQueue[pathQueue.length-2];
      pathQueue.length = pathQueue.length-2;
    }
    this.pathQueue.push(path);
    this.confirmTransitionTo({hash:path});
  }
  replace({path}) {
    this.pathQueue.length = 0;
    this.confirmTransitionTo({hash:path}); } // Global front-guard beforeEach(fn) {this.beforeEachCallBack = fn; }}export default  Hashhistory;
Copy the code

Hashhistory instance build time, resolution of the routes (see createMatcher parsing process), then call transitionTo, transitionTo get hash, then call confirmTransitionTo to switch from the component. Tra NsitionTo makes a logical determination to append the hash automatically if no # is detected.

Htmlhistory

import createMatcher from ".. /createMatcher";
class Htmlhistory {
  constructor(options) {
    this.$options = options;
    let routes = options.routes;
    const {addRoutes,getCurrentRecord} = createMatcher(routes);
    this.addRoutes = addRoutes;
    this.getCurrentRecord = getCurrentRecord;
    this.currentRouter = {current:{}};
    this.beforeEachCallBack = null;
    let path = this.getPath();
    this.transitionTo(path)
    this.setUpListener();

  }
  setUpListener() {
    window.onpopstate = function() {
      let path = this.getPath();
      this.confirmTransitionTo({path});
    }.bind(this);
  }
  getPath() {
    let path = location.href.split('/') [3]; path = path?'/'+path:'/';
    return path;
  }
  transitionTo(path) {
    this.confirmTransitionTo({path});
  }
  confirmTransitionTo({path,name,query}) {
    let currentRecord;
    currentRecord = this.getCurrentRecord({path,name,query});
    let that = this;
    function next(resolve,reject) {
      return function(path) {
        if(Object.is(undefined,path)) {
          resolve(currentRecord);
        }else {
          that.confirmTransitionTo({path});
        }
      }
    }
    new Promise(function(resolve,reject){
      if(this.beforeEachCallBack) {
        this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
      }else {
        resolve(currentRecord);
      }
    }.bind(this)).then(record=>{
      this.currentRouter.current = record
    });
  }
  push({path,name,query}) {
    history.pushState(null,null,path);
    this.confirmTransitionTo({path,name,query});
  }
  back() { history.go(-1); } replace({path}) { history.replaceState(null,null,path); this.confirmTransitionTo({path}); } // Global front-guard beforeEach(fn) {this.beforeEachCallBack = fn; }}export default  Htmlhistory;

Copy the code

HtmlHistory and HashHistory are implemented similarly, except that HtmlHistory takes path, switches path with replaceState and pushState, and listens for popState events. The general logic is about the same, I will not talk about.

confirmTransitionTo

confirmTransitionTo({hash,name,query}) {
    let currentRecord;
    currentRecord = this.getCurrentRecord({path:hash,name,query});
    let that = this;
    function next(resolve,reject) {
        return function(path) {
            if(Object.is(undefined,path)) {
               resolve(currentRecord);
            }else {
               location.hash = The '#'+path;
               that.confirmTransitionTo({hash:path});
            }
        }
    }

    new Promise(function(resolve,reject){
            if(this.beforeEachCallBack) {
               this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
            }else {
              resolve(currentRecord);
            }
          }.bind(this)).then(record=>{
             this.currentRouter.current = record
          });
  }
Copy the code

ConfirmTransitionTo confirmTransitionTo is a core function that retrievalsthe corresponding record based on the hash and updates currentRouter. Current. CurrentRouter is responsive

createMatcher

class Record { constructor(path,url,query,params,name,component) { this.url = url; this.path = path; this.query = query; this.params = params; this.name = name; this.component = component; }}function addRecord(pathList,pathMap,nameMap,route) {
    let record  = new Record(route.path,route.url,route.query,route.params,route.name,route.component);
    if(route.path) {
      pathList.push(route.path);
    }
    if(route.name) {
      nameMap[route.name] = record;
    }
    if(route.path) { pathMap[route.path] = record; }}function addRouterRecord(pathList,pathMap,nameMap,routes) {
  routes.forEach(item=>{
    addRecord(pathList,pathMap,nameMap,item);
  });
}
function createMatcher(routes) {
  let pathList = [];
  let pathMap = {};
  let nameMap = {};
  addRouterRecord(pathList,pathMap,nameMap,routes);
  function getCurrentRecord({path,name,query}) {
    let record =  null;
    if(name) {
        record = nameMap[name];
    }
    if(path) {
      record = pathMap[path];
    }
    if(record) {
      record.query = query;
    }
    return record||{};
  }
  function addRoutes(appendRoutes) {
    addRouterRecord(pathList,pathMap,nameMap,appendRoutes);
  }
  return{getCurrentRecord,addRoutes};
}
export default  createMatcher;
Copy the code

CreateMatcher createMatcher createMatcher createMatcher createMatcher createMatcher createMatcher createMatcher createMatcher createMatcher createMatcher createMatcher Record for value added to the pathMap, here also have nameMap, reference the vue – the router source, return addRoutes, getCurrentRecord addRoutes main function dynamically add routing rules, can use this function when it is here to achieve the query by value, But params is not implemented. You can add it yourself. Note :(I don’t implement subpaths here, you can add them if you want);

API implementation

If the profile

//install,js
function definedRouter(vue) {
  Object.defineProperty(vue.prototype,'$router', {get() {
        returnthis._router.router; }}); Object.defineProperty(vue.prototype,'$route', {get() {
      returnthis._router.route; }}); }Copy the code

In install.js, $router and $route are defined on ue.Prototype via Object.defineProperty, which are actually hijacked by get Router getRoute on LJRouter

//LJRouter
  get router() {
    return {
      push:this.push.bind(this),
      back:this.back.bind(this),
      replace:this.replace.bind(this)
    };
  }
  get route() {
    let query = this.history.currentRouter.current.query;
    query = deepCopy(query);
    return {query}
  }
Copy the code

The method with the same name on the history property on the LJRouter instance is actually called

push

  push({path,name,query}) {
    this.pathQueue.push(path);
    this.confirmTransitionTo({hash:path,name,query});
  }
Copy the code

Hash mode – Get the sent path and call confirmTransitionTo to switch the route. The pathQueue is an array of records stored in the confirmTransitionTo function. Simple logic.

push({path,name,query}) {
    history.pushState(null,null,path);
    this.confirmTransitionTo({path,name,query});
  }
Copy the code

History mode – The main path switch is pushState and then confirmTransitionTo

replace

  replace({path}) {
    this.pathQueue.length = 0;
    this.confirmTransitionTo({hash:path});
  }
Copy the code

Hash mode – This clears the record stack and calls confirmTransitionTo

  replace({path}) {
    history.replaceState(null,null,path);
    this.confirmTransitionTo({path});
  }
Copy the code

History mode – The main path switch is replaceState and then confirmTransitionTo

back

  back() {
    let pathQueue = this.pathQueue;
    let path = null;
    if(pathQueue.length<=1) {
      console.error('pathQueue value is < 2:redirect to /');
      path = '/'
    } else {
      path = pathQueue[pathQueue.length-2];
      pathQueue.length = pathQueue.length-2;
    }
    this.pathQueue.push(path);
    this.confirmTransitionTo({hash:path});
  }
Copy the code

Hash mode – A judgment is made that if there is only one record on the stack, calling back will return the root directory, and if there is more than one record, the last record will be returned.

setUpListener() {
    window.onpopstate = function() {
      let path = this.getPath();
      this.confirmTransitionTo({path});
    }.bind(this);
  }
  back() {
    history.go(-1);
  }  
Copy the code

History mode – This is where you call history,go and manipulate the browser history stack, which triggers the popState event, and then gets the returned path, and then switches the route

beforeEach

  //LJRouter
  beforeEach(fn) {
    if(this.mode === 'hash'||this.mode === 'history') {
      this.history.beforeEach(fn);
    } else {
      throw new TypeError('beforeEach is undefined'); }}Copy the code
/ / the correspondinghistoryExample Hashhistory // global front-guard beforeEach(fn) {this.beforeeachCallback = fn; }Copy the code
 confirmTransitionTo({hash,name,query}) {
    let currentRecord;
    currentRecord = this.getCurrentRecord({path:hash,name,query});
    let that = this;
    function next(resolve,reject) {
        return function(path) {
            if(Object.is(undefined,path)) {
               resolve(currentRecord);
            }else {
               location.hash = The '#'+path;
               that.confirmTransitionTo({hash:path});
            }
        }
    }

    new Promise(function(resolve,reject){
            if(this.beforeEachCallBack) {
               this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
            }else {
              resolve(currentRecord);
            }
          }.bind(this)).then(record=>{
             this.currentRouter.current = record
          });
  }
Copy the code

I wrote koA2 onion rings, which is hard to say. The Express intermediate mechanism is similar.

Component implementation

LJView

//LJView
let LJView = {
  functional:true,
  render(c,context) {
    let root = context.parent.$root;
    let component = root._route.current.component;
    if(! component) {return c(root.$notFoundComponent);
    }

    returnc(root._route.current.component); }}export default LJView;
Copy the code

function routerInit() {
    if(this.$options.router) {
        this._router = this.$options.router;
        this.$notFoundComponent = notFound;
        install._vue.util.defineReactive(this,'_route',this._router.history.currentRouter);
    } else {
        this._router = this.$parent._router; }}Copy the code
 confirmTransitionTo({hash,name,query}) {
    let currentRecord;
    currentRecord = this.getCurrentRecord({path:hash,name,query});
    let that = this;
    function next(resolve,reject) {
        return function(path) {
            if(Object.is(undefined,path)) {
               resolve(currentRecord);
            }else {
               location.hash = The '#'+path;
               that.confirmTransitionTo({hash:path});
            }
        }
    }

    new Promise(function(resolve,reject){
            if(this.beforeEachCallBack) {
               this.beforeEachCallBack(this.currentRouter.current,currentRecord,next(resolve,reject));
            }else {
              resolve(currentRecord);
            }
          }.bind(this)).then(record=>{
             this.currentRouter.current = record
          });
  }
Copy the code

LJView is a functional component, and it’s really about data responsiveness, Defining reactive data via defineReactived and then using it in the render function generates a dependency collection process (it is generally assumed that a template will generate a dependency collection when compiled, but the template will eventually compile into the Render function, so use this variable in the render function to generate a dependency collection (if not, please large) ConfirmTransitionTo, when called, changes this data, triggering component updates.

LJLink

let LJLink = {
    props:{
      to:{
        type:String,
        default:""
      },
      tag:{
        type:String,
        default:'a'
      }
    },
    render(c) {
      let tag = this.tag;
      return c(tag,{on:{click:()=>{this.$router.push({path:this.to})}}},this.$slots.default[0].text); }}export default LJLink;

Copy the code

The implementation of LJLink is relatively simple

At the end

Write more nonsense, the code is not optimized, and some functions do not have time to achieve, and to go to work. I am small white, ask big guy small spray, in addition I want to get to know lovely girl.