vue-vueRouter原理分析

182 阅读1分钟

VueRouter使用

创建router

  • Vue.use使用VueRouter插件,默认调用VueRouter的install方法
  • 创建VueRouter实例,传入参数options
Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About,
      children: [
        {
          path: 'a',
          component: {
            render(h){ return <h1>this is about/a</h1>}
          },
        },
        {
          path: 'b',
          component: {
            render(h){ return <h1>this is about/b</h1>}
          },
        }
      ]
    }
  ]
})

注入

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

install

  • 当执行Vue.use时,默认执行插件的install方法
import RouterView from './components/router-view.js'
import RouterLink from './components/router-link.js'
const install = (Vue) => {
  Vue.mixin({
    beforeCreate() {
      if (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;
      }
    }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this._routerRoot._route
    }
  })
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot._router
    }
  })
  Vue.component('RouterView', RouterView)
  Vue.component('RouterLink', RouterLink)
}

export default install

整体架构图

VueRouter构造函数

  • 初始化时,创建匹配器matcher和history实例
  • init:获取当前location,找到匹配的record;监听hash变化;
  • match:根据location获取对应的record
  • push:进行跳转
  • beforeEach:订阅路由钩子
import install from './install.js'
import HashHistory from './history/hash.js'
import createMatcher from './create-matcher.js'
class VueRouter {
  constructor(options) {
    this.options = options;
   this.matcher = createMatcher(options.routes || [])

   this.history = new HashHistory(this)
   this.beforeEachs = []
  }
  match(location) {
    return this.matcher.match(location)
  }
  push(location) {
    this.history.transitionTo(location, () => {
      window.location.hash = location
    });
  }
  init (app) {
    const history = this.history;
    const setupHashListener = () => {
      history.setupListener();
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener
    )
    history.listen((route) => {
      app._route = route
    })
  }
  beforeEach(cb) { 
    this.beforeEachs.push(cb);
  }

}

VueRouter.install = install;
export default VueRouter
createMatcher
  • addRoutes:routes格式化后合并到pathList,pathMap中;
  • match:根据location在pathMap中找到对应的record,将当前record树中父子关联的record放在matched数组中,返回matched+path
import createRouteMap from './create-route-map.js';
import {createRoute} from './history/base.js'
export default function createMatcher(routes) {
  let {pathList, pathMap} = createRouteMap(routes)
  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap)
  }
  function match(location) { 
    let record = pathMap[location]
    return createRoute(record, {
      path: location
    })
  }

  return {
    addRoutes,
    match
  }
}
createRouteMap
  • 遍历route,格式化后保存在pathList和pathMap上
export default function createRouteMap(routes, oldPathList, oldPathMap) {
  let pathList = oldPathList || [];
  let pathMap = oldPathMap || Object.create(null);
  routes.forEach(route => {
    addRouteRecord(route, pathList, pathMap)
  })
  
  return {
    pathList,
    pathMap
  }
}
function addRouteRecord(route, pathList, pathMap, parent) {
  let path = parent ? parent.path + '/' + route.path : route.path;

  let record = {
    path,
    component: route.component,
    parent,
  }
  if (!pathMap[path]) {
    pathList.push(path);
    pathMap[path] = record;
  }
  if (route.children) {
    route.children.forEach(item => {
      addRouteRecord(item, pathList, pathMap, record)
    })
  }

}

router组件

router-link
  • 函数组件+jsx,点击跳转
export default {
  props: {
    to: {
      type: String,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    }
  },
  methods:{
    handler() {
      this.$router.push(this.to)
    }
  },
  render(h) {
    let tag = this.tag;
  return <tag onClick={this.handler}>{this.$slots.default}</tag>
  }
}
router-view
  • 递归渲染:自上而下,父级得到匹配的record渲染后,depth+1
export default {
  functional: true,
  render(h, {parent, data}){
    let route = parent.$route;
    let depth = 0;
    while (parent) {
      if(parent.$vnode && parent.$vnode.data.routerView) {
        depth++;
      }
      parent = parent.$parent
    }
    data.routerView = true
    let record = route.matched[depth];
    if (!record) {
      return h();
    }
    return h(record.component, data)
  }
}

history构造函数

base构造函数
  • createRoute:根据record树,将父子关系中的record都放在matched数组中
  • runQuene:按照iterator方式遍历执行路由钩子;因为可能存在异步,通过step函数实现
  • setupListener:监听hash变化,执行transitionTo
  • transitionTo:根据路径找到对应record,如果路径改变,执行路由钩子;路由钩子执行完后,会执行updateRoute
  • updateRoute:路径变化时,更新app._route;执行回调;将路径匹配的record绑定在current上,通过RouterView进行渲染
  • listen:订阅函数fn
export function createRoute(record, location) {
  let res = [];
  if(record){
    while(record) {
      res.unshift(record)
      record = record.parent
    }
  }

  return {
    ...location,
    matched: res
  }
}
function runQuene(quene, iterator, callback){
  function step(index) {
    if (index == quene.length) return callback()
    let hook = quene[index];
    iterator(hook, () => step(index+1))
  }
  step(0)
}
class History{
  constructor(router) {
    this.router = router;
    this.current = createRoute(null, {
      path: '/'
    })
    this.cb = undefined
  }
  transitionTo(location, callback){
    let r = this.router.match(location);
    if (location == this.current.path && r.matched.length == this.current.matched.length) {
      return
    }
    let quene = this.router.beforeEachs
    const iterator = (hook, next) => {
      hook(this.current, r, next);
    }
    runQuene(quene, iterator, () => {
      this.updateRoute(r, callback)
    })
    
  }
  updateRoute(r, callback) {
    this.current = r;
    this.cb && this.cb(r);
    callback && callback();
  }
  setupListener() {
    window.addEventListener('hashchange', () => {
      this.transitionTo(window.location.hash.slice(1))
    })
  }
  listen(cb) {
    this.cb = cb
  }
}

export default History

hash构造函数
  • 初始化时,调用ensureSlash保证页面加载后就有hash
  • getCurrentLocation:获取当前的路径
import History from './base'

function ensureSlash() {
  if (window.location.hash) {
    return
  }
  window.location.hash = '/'
}
class HashHistory extends History {
  constructor(router) {
    super(router)
    this.router = router
    ensureSlash();
  }
  getCurrentLocation() {
    return window.location.hash.slice(1)
  }
}
export default HashHistory

hash改变视图刷新的原理