vue-router源码学习笔记

223 阅读3分钟

背景

这两天在学习vue-router的源码,通过造轮子,把学到的东西进行巩固吧。

Router类的设计

在使用vue-router的时候我们通常会传入这样的参数:

 new Router({
        base: '/example',
        mode: 'hash',
        routes: [
            {
                path: '/',
                name: 'home',
                component: '<div>home</div>'
            }
        ]
    })

很显然Router是一个类,传入的参数就是构造函数的参数因此我们的构造函数设计如下:

class Router {
    constructor (options) {
        this.base = options.base
        this.routes = options.routes
        this.mode = options.mode || 'hash'
        this.init()
    }
    
    init() {}
}

hash和history两种模式基类

设计完了Router类,我们还需要根据不同的mode采取不同的处理方式,即hash模式和history模式。但是这两种模式都有一些共同的属性,例如:path,query,params,name,fullPath, route等属性,还有对路由匹配的处理,因此我们将这些通用方法设计成一个基类。

class Base {
  constructor (router) {
    this.router = router
    this.current = {
      path: '/',
      query: {},
      params: {},
      name: '',
      fullPath: '/',
      route: {}
    }
  }
 // 这里的taregt就是浏览器中获取path,如:/foo /bar
  transitionTo(target, cb) {
    // 通过对比传入的 routes 获取匹配到的 targetRoute 对象
    const targetRoute = match(target, this.router.routes)
    this.confirmTransition(targetRoute, () => {
      this.current.route = targetRoute
      this.current.name = targetRoute.name
      this.current.path = targetRoute.path
      this.current.query = targetRoute.query || getQuery()
      this.current.fullPath = getFullPath(this.current)
      cb && cb()
    })
  }
  
  confirmTransition (route, cb) {
    cb()
  }
}

function getFullPath ({ path, query = {}, hash = '' }, _stringifyQuery){
  const stringify = _stringifyQuery || stringifyQuery
  return (path || '/') + stringify(query) + hash
}

// 用这些path从而筛选出对应的route
export function match(path, routeMap) {
  let match = {}
  if (typeof path === 'string' || path.name === undefined) {
    for(let route of routeMap) {
      if (route.path === path || route.path === path.path) {
        match = route
        break;
      }
    }
  } else {
    for(let route of routeMap) {
      if (route.name === path.name) {
        match = route
        if (path.query) {
          match.query = path.query
        }
        break;
      }
    }
  }
  return match
}
// 获取url中的参数
export function getQuery() {
  const hash = location.hash
  const queryStr = hash.indexOf('?') !== -1 ? hash.substring(hash.indexOf('?') + 1) : ''
  const queryArray = queryStr ? queryStr.split('&') : []
  let query = {}
  queryArray.forEach((q) => {
    let qArray = q.split('=')
    query[qArray[0]] = qArray[1]
  })
  return query
}

function stringifyQuery (obj) {
  const res = obj ? Object.keys(obj).map(key => {
    const val = obj[key]

    if (val === undefined) {
      return ''
    }

    if (val === null) {
      return key
    }

    if (Array.isArray(val)) {
      const result = []
      val.forEach(val2 => {
        if (val2 === undefined) {
          return
        }
        if (val2 === null) {
          result.push(key)
        } else {
          result.push(key + '=' + val2)
        }
      })
      return result.join('&')
    }

    return key + '=' + val
  }).filter(x => x.length > 0).join('&') : null
  return res ? `?${res}` : ''
}

HashHistory实现

class HashHistory extends Base {
  constructor (router) {
    super(router)
    this.ensureSlash()
    // 监听hashchange事件
    window.addEventListener('hashchange', () => {
      this.transitionTo(this.getCurrentLocation())
    })
  }

  push (location) {
    const targetRoute = match(location, this.router.routes)

    this.transitionTo(targetRoute, () => {
      changeUrl(this.current.fullPath.substring(1))
    })
  }

  replaceState (location) {
    const targetRoute = match(location, this.router.routes)

    this.transitionTo(targetRoute, () => {
      changeUrl(this.current.fullPath.substring(1), true)
    })
  }


  ensureSlash () {
    const path = this.getCurrentLocation()
    if (path.charAt(0) === '/') {
      return true
    }
    changeUrl(path)
    return false
  }
  // 获取当前路由
  getCurrentLocation() {
    const href = window.location.href
    const index = href.indexOf('#')
    return index === -1 ? '' : href.slice(index + 1)
  }
}
// 处理浏览器路由跳转变化 
// 这里使用了:
// window.history.replaceState({}, '', url)
// window.history.pushState({}, '', url)
function changeUrl(path, replace) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  if (replace) {
    window.history.replaceState({}, '', `${base}#/${path}`)
  } else {
    window.history.pushState({}, '', `${base}#/${path}`)
  }
}

window.history.replaceState和window.history.pushState的区别

Html5History实现

 class HTML5History extends Base {
  constructor (router) {
    super(router)
    window.addEventListener('popstate', () => {
      this.transitionTo(getLocation())
    })
  }
  push (location) {
    const targetRoute = match(location, this.router.routes)

    this.transitionTo(targetRoute, () => {
      changeUrl(this.router.base, this.current.fullPath)
    })
  }
  getCurrentLocation () {
    return getLocation(this.router.base)
  }
}

function getLocation (base = ''){
  let path = window.location.pathname
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

function changeUrl(base, path, replace) {
  if (replace) {
    window.history.replaceState({}, '', (base + path).replace(/\/\//g, '/'))
  } else {
    window.history.pushState({}, '', (base + path).replace(/\/\//g, '/'))
  }
}

整合Router

前面我聊了Router类,hash模式下如何处理,history模式下如何处理,但是如何与我们的构造函数Router类整合呢?或者说Router中对这两种路由模式如何处理呢?

import { HTML5History } from './history/HTML5History'
import { HashHistory } from './history/HashHistory'
class Router {
    constructor (options) {
        // 省略之前代码
        this.mode = options.mode || 'hash'
        this.history = this.mode === 'hash' ? 
                new HashHistory(options) :
                new HTML5History(options)
    }
}

这样就实现了针对不同的mode对应不同的路由解析办法, 但是还有一个问题,我们上面的代码实现了可以动态切换路由,但是切换路由的同时视图如何跟着渲染呢?我们可以利用vue的双向绑定,从而实现当路由切换的同时视图也跟着切换

import { Watcher } from './utils/Watcher'
class Router {
    constructor (options) {
        // 省略代码
        this.init()
        this.history = this.mode === 'hash' ? 
                new HashHistory(options) :
                new HTML5History(options)
    }
    // 简单搞个render()说明问题就好
    render () {
        let i
        if ((i = this.history.current) && (i = i.route) && (i = i.component)) {
          document.getElementById(this.container).innerHTML = i
        }
    }
    init () {
        const history = this.history
        observer.call(this, this.history.current)
        new Watcher(this.history.current, 'route', this.render.bind(this))
        history.transitionTo(history.getCurrentLocation())
    }
}

接下来我们来依次实现这几个函数,顺便也学习了vue中的双向绑定

  • observer函数实现
class Observer {
    constructor (value) {
        this.walk(value)
    }
    walk (obj) {
        Object.keys(obj).forEach(key => {
            if (typeof obj[key] === 'object') {
                this.walk(obj[key])
            }
            defineRective(obj, key, obj[key])
        })
    }
}

function defineRective (obj, key, value) {
    let dep = new Dep()
    Object.defineProperty(obj, key, {
        get () {
            if (dep.target) {
                dep.add()
            }
            return value
        },
        set (newVal) {
            // 注意这两个顺序
            value = newVal
            dep.notify()
        }
    })
}
  • Dep类的实现
class Dep {
    constructor () {
        this.listeners = []
    }
    add () {
        this.listeners.push(Dep.target)
    }
    notify () {
        this.listeners.forEach(listen => listen.update())
    }
}

export function setTarget (target) {
  Dep.target = target
}

export function cleanTarget() {
  Dep.target = null
}
  • watcher函数实现
import {setTarget, cleanTarget} from './dep'

export class Watcher {
  constructor (vm, expression, callback) {
    this.vm = vm
    this.callbacks = []
    this.expression = expression
    this.callbacks.push(callback)
    this.value = this.getVal()

  }
  getVal () {
    setTarget(this)
    let val = this.vm
    this.expression.split('.').forEach((key) => {
      val = val[key]
    })
    cleanTarget()
    return val
  }

  update () {
    this.callbacks.forEach((cb) => {
      cb()
    })
  }
}

参考资料: