精华内容
下载资源
问答
  • Vue Router 源码分析

    2021-04-27 11:50:54
    Vue Router源码分析 Vue Router 是一个插件 需要用到 vue.use() 挂载 首先分析一下 vue.use 源码位置:vue/src/core/global-api/use.js export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: ...

    Vue Router源码分析

    Vue Router 是一个插件 需要用到 vue.use() 挂载 首先分析一下 vue.use

    源码位置:vue/src/core/global-api/use.js

    export function initUse (Vue: GlobalAPI) {
      Vue.use = function (plugin: Function | Object) {
        // 维护了一个 _installedPlugins 数组,它存储所有注册过的 plugin
        const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
        // 防止重复注册
        if (installedPlugins.indexOf(plugin) > -1) {
          return this
        }
        // 附加参数 一般为[]
        const args = toArray(arguments, 1)
        // 第一个参数是 Vue
        args.unshift(this)
        // 每个插件都需要实现一个静态的 install 方法
        if (typeof plugin.install === 'function') {
          plugin.install.apply(plugin, args)
        } else if (typeof plugin === 'function') {
          plugin.apply(null, args)
        }
        // 添加缓存
        installedPlugins.push(plugin)
        return this
      }
    }
    
    路由安装

    源码位置:vue/src/core/global-api/use.js

    // Vue-Router 的入口文件是 src/index.js其中定义了 VueRouter 类,
    // 也实现了 install 的静态方法:VueRouter.install = install,
    // 它的定义在 src/install.js 中。
    export let _Vue
    export function install (Vue) {
      // 确保install() 只执行一次
      if (install.installed && _Vue === Vue) return
      install.installed = true
      // 全局的 _Vue 来接收参数 Vue,因为作为 Vue 的插件对 Vue 对象是有依赖的
      // 但又不能去单独去 import Vue,因为那样会增加包体积,
      // 所以就通过这种方式拿到 Vue 对象。
      _Vue = Vue
    
      const isDef = v => v !== undefined
    
      const registerInstance = (vm, callVal) => {
        let i = vm.$options._parentVnode
        if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
          i(vm, callVal)
        }
      }
    
      // Vue-Router 安装最重要的一步就是利用
      // Vue.mixin 去把 beforeCreate 和 destroyed 钩子函数注入到每一个组件中
      Vue.mixin({
        beforeCreate () {
          if (isDef(this.$options.router)) {
            // 表示Vue自身
            this._routerRoot = this
            // 表示vueRouter实例
            this._router = this.$options.router
            // 初始化 vueRouter
            this._router.init(this)
            // this._route 变成响应式对象
            Vue.util.defineReactive(this, '_route', this._router.history.current)
          } else {
            this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
          }
          registerInstance(this, this)
        },
        destroyed () {
          registerInstance(this)
        }
      })
      
      // vue原型上定义$router、$route
      // 这就是为什么组件中可以访问this.$router 以及 this.$route
      Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
      })
    
      Object.defineProperty(Vue.prototype, '$route', {
        get () { return this._routerRoot._route }
      })
      
      // 挂载全局组件
      Vue.component('RouterView', View)
      Vue.component('RouterLink', Link)
    
      const strats = Vue.config.optionMergeStrategies
      strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
    }
    
    VueRouter 对象
    export default class VueRouter {
      static install: () => void;
      static version: string;
    
      app: any;
      apps: Array<any>;
      ready: boolean;
      readyCbs: Array<Function>;
      options: RouterOptions;
      mode: string;
      history: HashHistory | HTML5History | AbstractHistory;
      matcher: Matcher;
      fallback: boolean;
      beforeHooks: Array<?NavigationGuard>;
      resolveHooks: Array<?NavigationGuard>;
      afterHooks: Array<?AfterNavigationHook>;
    
      constructor (options: RouterOptions = {}) {
        // 表示根 Vue 实例
        this.app = null
        // 保存持有 $options.router 属性的 Vue 实例
        this.apps = []
        // 保存传入的路由配置
        this.options = options
        // 表示一些钩子函数
        this.beforeHooks = []
        this.resolveHooks = []
        this.afterHooks = []
        // 创建路由列表 【会单独分析】
        this.matcher = createMatcher(options.routes || [], this)
    
        let mode = options.mode || 'hash'
        // this.fallback 表示在浏览器不支持 history.pushState 的情况下
        // 根据传入的 fallback 配置参数,决定是否回退到hash模式,
        this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
        if (this.fallback) {
          mode = 'hash'
        }
        if (!inBrowser) {
          mode = 'abstract'
        }
        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:
            if (process.env.NODE_ENV !== 'production') {
              assert(false, `invalid mode: ${mode}`)
            }
        }
        // 初始化方法 【下面分析】
        init (app: any) {}
        // ....
      }
    }
    

    实例化 VueRouter 后会返回它的实例 router,我们在 new Vue 的时候会把 router 作为配置的属性传入

    beforeCreate() {if (isDef(this.$options.router)) {
        // ...
        this._router = this.$options.router
        this._router.init(this)
        // ...
      }
    } 
    
    分析 init
    init (app: any) {
      process.env.NODE_ENV !=='production' && assert(
        install.installed,
        `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
        `before creating root instance.`
      )
      // 当前vue
      this.apps.push(app)
      // 避免重复  
      if (this.app) {
        return
      }
    
      this.app = app
    
      const history = this.history
     
      // 拿到当前的 this.history,根据它的不同类型来执行不同逻辑,
      // 由于我们平时使用 hash 路由多一些,所以我们先看这部分逻辑,
      // 先定义了 setupHashListener 函数,接着执行了 history.transitionTo 方法
      if (history instanceof HTML5History) {
        history.transitionTo(history.getCurrentLocation())
      } else if (history instanceof HashHistory) {
        const setupHashListener = () => {
          history.setupListeners()
        }
        history.transitionTo(
          history.getCurrentLocation(),
          setupHashListener,
          setupHashListener
        )
      }
    
      history.listen(route => {
        this.apps.forEach((app) => {
          app._route = route
        })
      })
    }
    
    history.transitionTo

    源码地址:src/history/base.js

    transitionTo (location: RawLocation, onComplete?:Function, onAbort?: Function) {
      const route = this.router.match(location, this.current)
      // ...
    }
    
    我们先不着急去看 transitionTo 的具体实现,先看第一行代码,
    它调用了 this.router.match 函数,它是在createMatcher函数中调用
    const { pathList, pathMap, nameMap } = createRouteMap(routes)
        function addRoutes (routes) {
        createRouteMap(routes, pathList, pathMap, nameMap)
        }
        function createRouteMap () {} // 单独分析
        function match () {} // 单独分析
        function _createRoute () {} // 单独分析
        return {
          match,
          addRoutes
        }
    
    createRouteMap 定义一个路由映射表
    // 可以创建路由映射表,也可以动态添加
    export function createRouteMap (
      routes: Array<RouteConfig>,
      oldPathList?: Array<string>,
      oldPathMap?: Dictionary<RouteRecord>,
      oldNameMap?: Dictionary<RouteRecord>
    ): {
      pathList: Array<string>;
      pathMap: Dictionary<RouteRecord>;
      nameMap: Dictionary<RouteRecord>;
    } {
      const pathList: Array<string> = oldPathList || []
      const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
      const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
       
      // 遍历router列表
        /* const routes = [
          { path: '/foo', component: Foo },
          { path: '/bar', component: Bar }
        ] */
      routes.forEach(route => {
        // 根据参数生成一条记录 如果有子节点循环调用生成树结构的记录
        addRouteRecord(pathList, pathMap, nameMap, route)
      })
      
      // 匹配通配符    
      for (let i = 0, l = pathList.length; i < l; i++) {
        if (pathList[i] === '*') {
          pathList.push(pathList.splice(i, 1)[0])
          l--
          i--
        }
      }
    
      return {
        pathList, // 存储所有的 path存储所有的 path
        pathMap, // 表示一个 path 到 RouteRecord 的映射关系
        nameMap // name 到 RouteRecord 的映射关系
      }
    }
    
    match
    function match (
      raw: RawLocation, // 是 RawLocation 类型,它可以是一个 url 字符串,也可以是一个 Location 对象
      currentRoute?: Route, // 是 Route 类型,它表示当前的路径
      redirectedFrom?: Location // 和重定向相关
    ): Route {
      // 方法的作用是根据 raw,current 计算出新的 location
      const location = normalizeLocation(raw, currentRoute, false, router)
      const { name } = location
    
      if (name) {
        // 匹配到 record
        const record = nameMap[name]
        if (process.env.NODE_ENV !== 'production') {
          warn(record, `Route with name '${name}' does not exist`)
        }
        // 失败 创建一个空的 record
        if (!record) return _createRoute(null, location)
       // 然后拿到 record 对应的 paramNames,再对比 currentRoute 中的 params,
        const paramNames = record.regex.keys
          .filter(key => !key.optional)
          .map(key => key.name)
    
        if (typeof location.params !== 'object') {
          location.params = {}
        }
         // 把交集部分的 params 添加到 location 中,
        if (currentRoute && typeof currentRoute.params === 'object') {
          for (const key in currentRoute.params) {
            if (!(key in location.params) && paramNames.indexOf(key) > -1) {
              location.params[key] = currentRoute.params[key]
            }
          }
        }
           // 然后在通过 fillParams 方法根据 record.path 和 location.path 计算出 location.path,
        if (record) {
          location.path = fillParams(record.path, location.params, `named route "${name}"`)
          // 最后调用 _createRoute(record, location, redirectedFrom) 去生成一条新路径,
          return _createRoute(record, location, redirectedFrom)
        }
      } else if (location.path) {
        // 通过 name 我们可以很快的找到 record,但是通过 path 并不能,
        // 因为我们计算后的 location.path 是一个真实路径,
        // 而 record 中的 path 可能会有 param,因此需要对所有的 pathList 做顺序遍历,
        // 然后通过 matchRoute 方法根据 record.regex、location.path、location.params 匹配,
        location.params = {}
        for (let i = 0; i < pathList.length; i++) {
          const path = pathList[i]
          const record = pathMap[path]
          if (matchRoute(record.regex, location.path, location.params)) {
            return _createRoute(record, location, redirectedFrom)
          }
        }
      }
      
      return _createRoute(null, location)
    }
    
    _createRoute
    function _createRoute (
      record: ?RouteRecord,
      location: Location,
      redirectedFrom?: Location
    ): Route {
      if (record && record.redirect) {
        return redirect(record, redirectedFrom || location)
      }
      if (record && record.matchAs) {
        return alias(record, location, record.matchAs)
      }
      return createRoute(record, location, redirectedFrom, router)
    }
    // 我们先不考虑 record.redirect 和 record.matchAs 的情况,
    // 最终会调用 createRoute 方法
    export function createRoute (
      record: ?RouteRecord,
      location: Location,
      redirectedFrom?: ?Location,
      router?: VueRouter
    ): Route {
      const stringifyQuery = router && router.options.stringifyQuery
    
      let query: any = location.query || {}
      try {
        query = clone(query)
      } catch (e) {}
      
      // 最终返回的是一条 Route 路径,
      // 在 Vue-Router 中,所有的 Route 最终都会通过 createRoute 函数创建,
      // 并且它最后是不可以被外部修改的。Route 对象中有一个非常重要属性是 matched,
      // 它通过 formatMatch(record) 计算而来:
      const route: Route = {
        name: location.name || (record && record.name),
        meta: (record && record.meta) || {},
        path: location.path || '/',
        hash: location.hash || '',
        query,
        params: location.params || {},
        fullPath: getFullPath(location, stringifyQuery),
        matched: record ? formatMatch(record) : []
      }
      if (redirectedFrom) {
        route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
      }
      return Object.freeze(route)
    }
    
    formatMatch
    function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
      const res = []
      while (record) {
        res.unshift(record)
        record = record.parent
      }
      return res
    }
    // 可以看它是通过 record 循环向上找 parent,直到找到最外层,
    // 并把所有的 record 都 push 到一个数组中,最终返回的就是 record 的数组,
    // 它记录了一条线路上的所有 record。matched 属性非常有用,它为之后渲染组件提供了依据
    
    展开全文
  • VueRouter源码分析

    2020-07-19 11:46:46
    VueRouter源码分析 感谢 funfish,玩弄心里的鬼,Vue.js 技术揭秘的文章,对我的帮助 前言 vue-router的源码不算很多, 但是内容也不算少。本文谈不上逐行分析, 但是会尽量详尽的说明主流程和原理。对一些工具...

    VueRouter源码分析

     

    image

    感谢

    funfish玩弄心里的鬼Vue.js 技术揭秘的文章,对我的帮助

    前言

    vue-router的源码不算很多, 但是内容也不算少。本文谈不上逐行分析, 但是会尽量详尽的说明主流程和原理。对一些工具函数和边缘条件的处理会略过,因为我也没有逐行去了解它们,请见谅。

    前置基础知识

    我们在学习VueRouter源码前,先来复习下hash以及histroy相关的知识。更多细节请参考mdn文档,本节内容节选自mdn文档。

    hash

    onhashchange

    当URL的片段标识符更改时,将触发hashchange事件 (跟在#符号后面的URL部分,包括#符号)。注意 histroy.pushState() 绝对不会触发 hashchange 事件,即使新的URL与旧的URL仅哈希不同也是如此。

    histroy

    pushState

    pushState()需要三个参数: 一个状态对象, 一个标题(目前被忽略), 和一个URL。

    • state, 状态对象state是一个JavaScript对象,popstate事件触发时,该对象会传入回调函数
    • title, 目前所有浏览器忽略
    • url, 新的url记录

    replaceState

    history.replaceState()的使用与history.pushState()非常相似,区别在于replaceState()是修改了当前的历史记录项而不是新建一个。

    onpopstate

    调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法)。

    如果当前处于激活状态的历史记录条目是由history.pushState()方法创建, 或者由history.replaceState()方法修改过的, 则popstate事件对象的state属性包含了这个历史记录条目的state对象的一个拷贝。

    应用初始化

    通常构建一个Vue应用的时候, 我们会使用Vue.use以插件的形式安装VueRouter。同时会在Vue的实例上挂载router的实例。

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    
    Vue.config.productionTip = false
    
    let a = new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
    
    import Vue from 'vue'
    import Router from 'vue-router'
    import Home from './views/Home.vue'
    
    Vue.use(Router)
    
    export default new Router({
      mode: 'history',
      base: process.env.BASE_URL,
      routes: [
        {
          path: '/',
          name: 'home',
          component: Home
        },
        {
          path: '/about',
          name: 'about',
          component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
        }
      ]
    })

    插件的安装

    在Vue的文档中指出Vue.js 的插件应该有一个公开方法 install。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象, 我们首先查看源码中install.js的文件。

    在install文件中, 我们在Vue的实例上初始化了一些私有属性

    • _routerRoot, 指向了Vue的实例
    • _router, 指向了VueRouter的实例

    在Vue的prototype上初始化了一些getter

    • $router, 当前Router的实例
    • $route, 当前Router的信息

    并且在全局混入了mixin, 已经全局注册了RouterView, RouterLink组件.

    
    import View from './components/view'
    import Link from './components/link'
    
    export let _Vue
    
    export function install (Vue) {
      if (install.installed && _Vue === Vue) return
      install.installed = true
    
      _Vue = Vue
    
      const isDef = v => v !== undefined
    
      const registerInstance = (vm, callVal) => {
        let i = vm.$options._parentVnode
        if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
          i(vm, callVal)
        }
      }
    
      Vue.mixin({
        beforeCreate () {
          // 判断是否实例是否挂载了router
          if (isDef(this.$options.router)) {
            this._routerRoot = this
            this._router = this.$options.router
            this._router.init(this)
            // _router, 劫持的是当前的路由
            Vue.util.defineReactive(this, '_route', this._router.history.current)
          } else {
            this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
          }
          registerInstance(this, this)
        },
        destroyed () {
          registerInstance(this)
        }
      })
    
      Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
      })
    
      Object.defineProperty(Vue.prototype, '$route', {
        get () { return this._routerRoot._route }
      })
    
      Vue.component('RouterView', View)
      Vue.component('RouterLink', Link)
    
      const strats = Vue.config.optionMergeStrategies
      strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
    }

    Vue.util.defineReactive, 这是Vue里面观察者劫持数据的方法,劫持_route,当_route触发setter方法的时候,则会通知到依赖的组件。而RouterView, 需要访问parent.$route所以形成了依赖(我们在后面会看到)

    👀我们到Vue中看一下defineReactive的源码, 在defineReactive, 会对_route使用Object.defineProperty劫持setter方法。set时会通知观察者。

    
    
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        // ...
      },
      set: function reactiveSetter (newVal) {
        // ...
        childOb = !shallow && observe(newVal)
        dep.notify()
      }
    })

    VueRouter实例

    
    export default class VueRouter {
      constructor (options: RouterOptions = {}) {
        this.app = null
        this.apps = []
        this.options = options
        this.beforeHooks = []
        this.resolveHooks = []
        this.afterHooks = []
        this.matcher = createMatcher(options.routes || [], this)
    
        let mode = options.mode || 'hash'
        // fallback会在不支持history环境的情况下, 回退到hash模式
        this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
        if (this.fallback) {
          mode = 'hash'
        }
        if (!inBrowser) {
          mode = 'abstract'
        }
        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:
            if (process.env.NODE_ENV !== 'production') {
              assert(false, `invalid mode: ${mode}`)
            }
        }
      }
    }

    matcher

    matcher对象中包含了两个属性, addRoutes, match。

    pathList, pathMap, nameMap

    pathList, pathMap, nameMap分别是路径的列表, 路径和路由对象的映射, 路由名称和路由对象的映射。vue-router目标支持动态路由, pathList, pathMap, nameMap可以在初始化后动态的被修改。它们由createRouteMap方法创建, 我们来看看createRouteMap的源码。

    
    export function createRouteMap (
      routes,
      oldPathList,
      oldPathMap,
      oldNameMap
    ) {
      // pathList,pathMap,nameMap支持后续的动态添加
      const pathList: Array<string> = oldPathList || []
      const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
      const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
    
      // 遍历路由列表
      routes.forEach(route => {
        addRouteRecord(pathList, pathMap, nameMap, route)
      })
    
      // 将通配符的路径, push到pathList的末尾
      for (let i = 0, l = pathList.length; i < l; i++) {
        if (pathList[i] === '*') {
          pathList.push(pathList.splice(i, 1)[0])
          l--
          i--
        }
      }
    
      return {
        pathList,
        pathMap,
        nameMap
      }
    }

    routes为一组路由, 所以我们循环routes, 但是route可能存在children所以我们通过递归的形式创建route。返回一个route的树🌲

    
    function addRouteRecord (
      pathList,
      pathMap,
      nameMap,
      route,
      parent,
      matchAs
    ) {
      const { path, name } = route
     
      const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
    
      // normalizePath, 会对path进行格式化
      // 会删除末尾的/,如果route是子级,会连接父级和子级的path,形成一个完整的path
      const normalizedPath = normalizePath(
        path,
        parent,
        pathToRegexpOptions.strict
      )
    
      if (typeof route.caseSensitive === 'boolean') {
        pathToRegexpOptions.sensitive = route.caseSensitive
      }
    
      // 创建一个完整的路由对象
      const record: RouteRecord = {
        path: normalizedPath,
        regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
        components: route.components || { default: route.component },
        instances: {},
        name,
        parent,
        matchAs,
        redirect: route.redirect,
        beforeEnter: route.beforeEnter,
        meta: route.meta || {},
        props: route.props == null
          ? {}
          : route.components
            ? route.props
            : { default: route.props }
      }
    
      // 如果route存在children, 我们会递归的创建路由对象
      // 递归的创建route对象
      if (route.children) {
        route.children.forEach(child => {
          const childMatchAs = matchAs
            ? cleanPath(`${matchAs}/${child.path}`)
            : undefined
          addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
        })
      }
    
      // 这里是对路由别名的处理
      if (route.alias !== undefined) {
        const aliases = Array.isArray(route.alias)
          ? route.alias
          : [route.alias]
    
        aliases.forEach(alias => {
          const aliasRoute = {
            path: alias,
            children: route.children
          }
          addRouteRecord(
            pathList,
            pathMap,
            nameMap,
            aliasRoute,
            parent,
            record.path || '/' // matchAs
          )
        })
      }
    
      // 填充pathMap,nameMap,pathList
      if (!pathMap[record.path]) {
        pathList.push(record.path)
        pathMap[record.path] = record
      }
    
      if (name) {
        if (!nameMap[name]) {
          nameMap[name] = record
        }
      }
    }

    addRoutes

    动态添加更多的路由规则, 并动态的修改pathList,pathMap,nameMap

    function addRoutes (routes) {
      createRouteMap(routes, pathList, pathMap, nameMap)
    }

    match

    match方法根据参数raw(可以是字符串也可以Location对象), 以及currentRoute(当前的路由对象返回Route对象),在nameMap中查找对应的Route,并返回。

    如果location包含name, 我通过nameMap找到了对应的Route, 但是此时path中可能包含params, 所以我们会通过fillParams函数将params填充到patch,返回一个真实的路径path。

    
    function match (
      raw,
      currentRoute,
      redirectedFrom
    ) {
      // 会对raw,currentRoute处理,返回格式化后path, hash, 以及params
      const location = normalizeLocation(raw, currentRoute, false, router)
    
      const { name } = location
    
      if (name) {
        const record = nameMap[name]
        if (!record) return _createRoute(null, location)
        
        // 获取所有必须的params。如果optional为true说明params不是必须的
        const paramNames = record.regex.keys
          .filter(key => !key.optional)
          .map(key => key.name)
    
        if (typeof location.params !== 'object') {
          location.params = {}
        }
    
        if (currentRoute && typeof currentRoute.params === 'object') {
          for (const key in currentRoute.params) {
            if (!(key in location.params) && paramNames.indexOf(key) > -1) {
              location.params[key] = currentRoute.params[key]
            }
          }
        }
    
        if (record) {
          // 使用params对path进行填充返回一个真实的路径
          location.path = fillParams(record.path, location.params, `named route "${name}"`)
          // 创建Route对象
          return _createRoute(record, location, redirectedFrom)
        }
      } else if (location.path) {
        location.params = {}
        for (let i = 0; i < pathList.length; i++) {
          const path = pathList[i]
          const record = pathMap[path]
          // 使用pathList中的每一个regex,对path进行匹配
          if (matchRoute(record.regex, location.path, location.params)) {
            return _createRoute(record, location, redirectedFrom)
          }
        }
      }
      return _createRoute(null, location)
    }

    我们接下来继续看看_createRoute中做了什么。

    
    function _createRoute (
      record: ?RouteRecord,
      location: Location,
      redirectedFrom?: Location
    ): Route {
      if (record && record.redirect) {
        return redirect(record, redirectedFrom || location)
      }
      if (record && record.matchAs) {
        return alias(record, location, record.matchAs)
      }
      return createRoute(record, location, redirectedFrom, router)
    }

    其中redirect,alias最终都会调用createRoute方法。我们再将视角转向createRoute函数。createRoute函数会返回一个冻结的Router对象。

    其中matched属性为一个数组,包含当前路由的所有嵌套路径片段的路由记录。数组的顺序为从外向里(树的外层到内层)。

    
    export function createRoute (
      record: ?RouteRecord,
      location: Location,
      redirectedFrom?: ?Location,
      router?: VueRouter
    ): Route {
      const stringifyQuery = router && router.options.stringifyQuery
    
      let query: any = location.query || {}
      try {
        query = clone(query)
      } catch (e) {}
    
      const route: Route = {
        name: location.name || (record && record.name),
        meta: (record && record.meta) || {},
        path: location.path || '/',
        hash: location.hash || '',
        query,
        params: location.params || {},
        fullPath: getFullPath(location, stringifyQuery),
        matched: record ? formatMatch(record) : []
      }
      if (redirectedFrom) {
        route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
      }
      return Object.freeze(route)
    }

    init

    init中。会挂载cb的回调,这关乎到RouteView的渲染。我们根据当前的url,在Vue根实例的beforeCreate生命周期钩子中完成路由的初始化,完成第一次的路由导航。

    
    init (app) {
    
      // app为Vue的实例
      this.apps.push(app)
    
      if (this.app) {
        return
      }
    
      // 在VueRouter上挂载app属性
      this.app = app
    
      const history = this.history
    
      // 初始化当前的路由,完成第一次导航,在hash模式下会在transitionTo的回调中调用setupListeners
      // setupListeners里会对hashchange事件进行监听
      // transitionTo是进行路由导航的函数,我们将会在下面介绍
      if (history instanceof HTML5History) {
        history.transitionTo(history.getCurrentLocation())
      } else if (history instanceof HashHistory) {
        const setupHashListener = () => {
          history.setupListeners()
        }
        history.transitionTo(
          history.getCurrentLocation(),
          setupHashListener,
          setupHashListener
        )
      }
    
      // 挂载了回调的cb, 每次更新路由更好更新_route
      history.listen(route => {
        this.apps.forEach((app) => {
          app._route = route
        })
      })
    }

    history

    history一共有三个模式hash, histroy, abstract, 这三个类都继承至base类

    base

    我们首先看下base的构造函数, 其中router是VueRouter的实例, base是路由的基础路径。current是当前的路由默认为"/", ready是路由的状态, readyCbs是ready的回调的集合, readyErrorCbs是raday失败的回调。errorCbs导航出错的回调的集合。

    
    export class History {
      constructor (router: Router, base: ?string) {
        this.router = router
        // normalizeBase会对base路径做出格式化的处理,会为base开头自动添加‘/’,删除结尾的‘/’,默认返回’/‘
        this.base = normalizeBase(base)
        // 初始化的当前路由对象
        this.current = START
        this.pending = null
        this.ready = false
        this.readyCbs = []
        this.readyErrorCbs = []
        this.errorCbs = []
      }
    }
    
    export const START = createRoute(null, {
      path: '/'
    })
    
    function normalizeBase (base: ?string): string {
      if (!base) {
        // inBrowser判断是否为浏览器环境
        if (inBrowser) {
          const baseEl = document.querySelector('base')
          base = (baseEl && baseEl.getAttribute('href')) || '/'
          base = base.replace(/^https?:\/\/[^\/]+/, '')
        } else {
          base = '/'
        }
      }
      if (base.charAt(0) !== '/') {
        base = '/' + base
      }
      return base.replace(/\/$/, '')
    }

    base中的listen的方法,会在VueRouter的init方法中使用到,listen会给每一次的路由的更新,添加回调

    
    listen (cb: Function) {
      this.cb = cb
    }   

    base类中还有一些其他方法比如,transitionTo,confirmTransition,updateRoute它们在base子类中被使用。我们马上在hashrouter中再看看它们的具体实现。

    HashRouter

    构造函数

    在HashHistory的构造函数中。我们会判断当前的fallback是否为true。如果为true,使用checkFallback,添加’#‘,并使用window.location.replace替换文档。

    如果fallback为false,我们会调用ensureSlash,ensureSlash会为没有“#”的url,添加“#”,并且使用histroy的API或者replace替换文档。

    所以我们在访问127.0.0.1的时候,会自动替换为127.0.0.1/#/

    
    export class HashHistory extends History {
      constructor (router: Router, base: ?string, fallback: boolean) {
        super(router, base)
        // 如果是回退hash的情况,并且判断当前路径是否有/#/。如果没有将会添加'/#/'
        if (fallback && checkFallback(this.base)) {
          return
        }
        ensureSlash()
      }
    }

    checkFallback

    
    // 检查url是否包含‘/#/’
    function checkFallback (base) {
      // 获取hash值
      const location = getLocation(base)
      // 如果location不是以/#,开头。添加/#,使用window.location.replace替换文档
      if (!/^\/#/.test(location)) {
        window.location.replace(
          cleanPath(base + '/#' + location)
        )
        return true
      }
    }
    // 返回hash
    export function getLocation (base) {
      let path = decodeURI(window.location.pathname)
      if (base && path.indexOf(base) === 0) {
        path = path.slice(base.length)
      }
      return (path || '/') + window.location.search + window.location.hash
    }
    
    // 删除 //, 替换为 /
    export function cleanPath (path) {
      return path.replace(/\/\//g, '/')
    }

    ensureSlash

    
    function ensureSlash (): boolean {
      // 判断是否包含#,并获取hash值。如果url没有#,则返回‘’
      const path = getHash()
      // 判断path是否以/开头
      if (path.charAt(0) === '/') {
        return true
      }
      // 如果开头不是‘/’, 则添加/
      replaceHash('/' + path)
      return false
    }
    // 获取“#”后面的hash
    export function getHash (): string {
      const href = window.location.href
      const index = href.indexOf('#')
      return index === -1 ? '' : decodeURI(href.slice(index + 1))
    }
    function replaceHash (path) {
      // supportsPushState判断是否存在history的API
      // 使用replaceState或者window.location.replace替换文档
      // getUrl获取完整的url
      if (supportsPushState) {
        replaceState(getUrl(path))
      } else {
        window.location.replace(getUrl(path))
      }
    }
    // getUrl返回了完整了路径,并且会添加#, 确保存在/#/
    function getUrl (path) {
      const href = window.location.href
      const i = href.indexOf('#')
      const base = i >= 0 ? href.slice(0, i) : href
      return `${base}#${path}`
    }

    在replaceHash中,我们调用了replaceState方法,在replaceState方法中,又调用了pushState方法。在pushState中我们会调用saveScrollPosition方法,它会记录当前的滚动的位置信息。然后使用histroyAPI,或者window.location.replace完成文档的更新。

    
    export function replaceState (url?: string) {
      pushState(url, true)
    }
    
    export function pushState (url?: string, replace?: boolean) {
      // 记录当前的x轴和y轴,以发生导航的时间为key,位置信息记录在positionStore中
      saveScrollPosition()
      const history = window.history
      try {
        if (replace) {
          history.replaceState({ key: _key }, '', url)
        } else {
          _key = genKey()
          history.pushState({ key: _key }, '', url)
        }
      } catch (e) {
        window.location[replace ? 'replace' : 'assign'](url)
      }
    }

    push, replace,

    我们把push,replace放在一起说,因为它们实现的源码都是类似的。在push和replace中,调用transitionTo方法,transitionTo方法在基类base中,我们现在转过头来看看transitionTo的源码(👇往下两节,代码不是很难,但是callback嵌套callback, 如蜜传如蜜,看起来还是比较恶心的)

    
    push (location, onComplete, onAbort) {
      const { current: fromRoute } = this
      this.transitionTo(
        location,
        route => {
          pushHash(route.fullPath)
          handleScroll(this.router, route, fromRoute, false)
          onComplete && onComplete(route)
        },
        onAbort
      )
    }
    
    replace (location, onComplete, onAbort) {
      const { current: fromRoute } = this
      this.transitionTo(
        location,
        route => {
          replaceHash(route.fullPath)
          handleScroll(this.router, route, fromRoute, false)
          onComplete && onComplete(route)
        },
        onAbort
      )
    }

    transitionTo, confirmTransition, updateRoute

    image

    transitionTo的location参数是我们的目标路径, 可以是string或者RawLocation对象。我们通过router.match方法(我们在在matcher介绍过),router.match会返回我们的目标路由对象。紧接着我们会调用confirmTransition函数。

    
    transitionTo (location, onComplete, onAbort) {
      const route = this.router.match(location, this.current)
      this.confirmTransition(
        route,
        () => {
          // ...
        },
        err => {
          // ...
        }
      )
    }

    confirmTransition函数中会使用,isSameRoute会检测是否导航到相同的路由,如果导航到相同的路由会停止🤚导航,并执行终止导航的回调。

    
    if (
      isSameRoute(route, current) &&
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      return abort()
    }

    接着我们调用resolveQueue方法,resolveQueue接受当前的路由和目标的路由的matched属性作为参数,resolveQueue的工作方式可以如下图所示。我们会逐一比较两个数组的路由,寻找出需要销毁的,需要更新的,需要激活的路由,并返回它们(因为我们需要执行它们不同的路由守卫)

    image

    function resolveQueue (
      current
      next
    ) {
      let i
      // 依次比对当前的路由和目标的路由的matched属性中的每一个路由
      const max = Math.max(current.length, next.length)
      for (i = 0; i < max; i++) {
        if (current[i] !== next[i]) {
          break
        }
      }
      return {
        updated: next.slice(0, i),
        activated: next.slice(i),
        deactivated: current.slice(i)
      }
    }

    下一步,我们会逐一提取出,所有要执行的路由守卫,将它们concat到队列queue。queue里存放里所有需要在这次路由更新中执行的路由守卫。

    第一步,我们使用extractLeaveGuards函数,提取出deactivated中所有需要销毁的组件内的“beforeRouteLeave”的守卫。extractLeaveGuards函数中会调用extractGuards函数,extractGuards函数,会调用flatMapComponents函数,flatMapComponents函数会遍历records(resolveQueue返回deactivated), 在遍历过程中我们将组件,组件的实例,route对象,传入了fn(extractGuards中传入flatMapComponents的回调), 在fn中我们会获取组件中beforeRouteLeave守卫。

    
    // 返回每一个组件中导航的集合
    function extractLeaveGuards (deactivated) {
      return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
    }
    
    function extractGuards (
      records,
      name,
      bind,
      reverse?
    ) {
      const guards = flatMapComponents(
        records,
        // def为组件
        // instance为组件的实例
        (def, instance, match, key) => {
          // 返回每一个组件中定义的路由守卫
          const guard = extractGuard(def, name)
          if (guard) {
            // bindGuard函数确保了guard(路由守卫)的this指向的是Component中的实例
            return Array.isArray(guard)
              ? guard.map(guard => bind(guard, instance, match, key))
              : bind(guard, instance, match, key)
          }
        }
      )
      // 返回导航的集合
      return flatten(reverse ? guards.reverse() : guards)
    }
    
    export function flatMapComponents (
      matched,
      fn
    ) {
      // 遍历matched,并返回matched中每一个route中的每一个Component
      return flatten(matched.map(m => {
        // 如果没有设置components则默认是components{ default: YouComponent },可以从addRouteRecord函数中看到
        // 将每一个matched中所有的component传入fn中
        // m.components[key]为components中的key键对应的组件
        // m.instances[key]为组件的实例,这个属性是在routerview组件中beforecreated中被赋值的
        return Object.keys(m.components).map(key => fn(
          m.components[key],
          m.instances[key],
          m,
          key
        ))
      }))
    }
    
    // 返回一个新数组
    export function flatten (arr) {
      return Array.prototype.concat.apply([], arr)
    }
    
    // 获取组件中的属性
    function extractGuard (def, key) {
      if (typeof def !== 'function') {
        def = _Vue.extend(def)
      }
      return def.options[key]
    }
    
    // 修正函数的this指向
    function bindGuard (guard, instance) {
      if (instance) {
        return function boundRouteGuard () {
          return guard.apply(instance, arguments)
        }
      }
    }

    第二步,获取全局VueRouter对象beforeEach的守卫

    第三步, 使用extractUpdateHooks函数,提取出update组件中所有的beforeRouteUpdate的守卫。过程同第一步类似。

    第四步, 获取activated的options配置中beforeEach守卫

    第五部, 获取所有的异步组件


    在获取所有的路由守卫后我们定义了一个迭代器iterator。接着我们使用runQueue遍历queue队列。将queue队列中每一个元素传入fn(迭代器iterator)中,在迭代器中会执行路由守卫,并且路由守卫中必须明确的调用next方法才会进入下一个管道,进入下一次迭代。迭代完成后,会执行runQueue的callback。

    在runQueue的callback中,我们获取激活组件内的beforeRouteEnter的守卫,并且将beforeRouteEnter守卫中next的回调存入postEnterCbs中,在导航被确认后遍历postEnterCbs执行next的回调。

    在queue队列执行完成后,confirmTransition函数会执行transitionTo传入的onComplete的回调。往下看👇

    // queue为路由守卫的队列
    // fn为定义的迭代器
    export function runQueue (queue, fn, cb) {
      const step = index => {
        if (index >= queue.length) {
          cb()
        } else {
          if (queue[index]) {
            // 使用迭代器处理每一个钩子
            // fn是迭代器
            fn(queue[index], () => {
              step(index + 1)
            })
          } else {
            step(index + 1)
          }
        }
      }
      step(0)
    }
    
    // 迭代器
    const iterator = (hook, next) => {
      if (this.pending !== route) {
        return abort()
      }
      try {
        // 传入路由守卫三个参数,分别分别对应to,from,next
        hook(route, current, (to: any) => {
          if (to === false || isError(to)) {
            // 如果next的参数为false
            this.ensureURL(true)
            abort(to)
          } else if (
            // 如果next需要重定向到其他路由
            typeof to === 'string' ||
            (typeof to === 'object' && (
              typeof to.path === 'string' ||
              typeof to.name === 'string'
            ))
          ) {
            abort()
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // 进入下个管道
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }
    
    runQueue(
      queue,
      iterator,
      () => {
        const postEnterCbs = []
        const isValid = () => this.current === route
        // 获取所有激活组件内部的路由守卫beforeRouteEnter,组件内的beforeRouteEnter守卫,是无法获取this实例的
        // 因为这时激活的组件还没有创建,但是我们可以通过传一个回调给next来访问组件实例。
        // beforeRouteEnter (to, from, next) {
        //   next(vm => {
        //     // 通过 `vm` 访问组件实例
        //   })
        // }
        const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
        // 获取全局的beforeResolve的路由守卫
        const queue = enterGuards.concat(this.router.resolveHooks)
        // 再一次遍历queue
        runQueue(queue, iterator, () => {
          // 完成过渡
          if (this.pending !== route) {
            return abort()
          }
          // 正在过渡的路由设置为null
          this.pending = null
          // 
          onComplete(route)
          // 导航被确认后,我们执行beforeRouteEnter守卫中,next的回调
          if (this.router.app) {
            this.router.app.$nextTick(() => {
              postEnterCbs.forEach(cb => { cb() })
            })
          }
        }
      )
    })
    
    // 获取组件中的beforeRouteEnter守卫
    function extractEnterGuards (
      activated,
      cbs,
      isValid
    ) {
      return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
        // 这里没有修改guard(守卫)中this的指向
        return bindEnterGuard(guard, match, key, cbs, isValid)
      })
    }
    
    // 将beforeRouteEnter守卫中next的回调push到postEnterCbs中
    function bindEnterGuard (
      guard,
      match,
      key,
      cbs,
      isValid
    ) {
      // 这里的next参数是迭代器中传入的参数
      return function routeEnterGuard (to, from, next) {
        return guard(to, from, cb => {
          // 执行迭代器中传入的next,进入下一个管道
          next(cb)
          if (typeof cb === 'function') {
            // 我们将next的回调包装后保存到cbs中,next的回调会在导航被确认的时候执行回调
            cbs.push(() => {
              poll(cb, match.instances, key, isValid)
            })
          }
        })
      }
    }

    在confirmTransition的onComplete回调中,我们调用updateRoute方法, 参数是导航的路由。在updateRoute中我们会更新当前的路由(history.current), 并执行cb(更新Vue实例上的_route属性,🌟这会触发RouterView的重新渲染

    
    updateRoute (route: Route) {
      const prev = this.current
      this.current = route
      this.cb && this.cb(route)
      // 执行after的钩子
      this.router.afterHooks.forEach(hook => {
        hook && hook(route, prev)
      })
    }

    接着我们执行transitionTo的回调函数onComplete。在回调中会调用replaceHash或者pushHash方法。它们会更新location的hash值。如果兼容historyAPI,会使用history.replaceState或者history.pushState。如果不兼容historyAPI会使用window.location.replace或者window.location.hash。而handleScroll方法则是会更新我们的滚动条的位置我们这里就不在细说了。

    
    // replaceHash方法
    (route) => {
      replaceHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }
    
    // push方法
    route => {
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }

    好了,现在我们就把,replace或者push方法的流程说完了。

    🎉🎉🎉🎉🎉🎉 以下是transitionTo,confirmTransition中完整的代码。 🎉🎉🎉🎉🎉🎉

    
    // onComplete 导航成功的回调
    // onAbort 导航终止的回调
    transitionTo (location, onComplete, onAbort) {
      const route = this.router.match(location, this.current)
      this.confirmTransition(route,
        () => {
          this.updateRoute(route)
          onComplete && onComplete(route)
          this.ensureURL()
          if (!this.ready) {
            this.ready = true
            this.readyCbs.forEach(cb => { cb(route) })
          }
        },
        err => {
          if (onAbort) {
            onAbort(err)
          }
          if (err && !this.ready) {
            this.ready = true
            this.readyErrorCbs.forEach(cb => { cb(err) })
          }
        }
      )
    }
    
    // onComplete 导航成功的回调
    // onAbort 导航终止的回调
    confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    
      // 当前的路由
      const current = this.current
    
      const abort = err => {
        if (isError(err)) {
          if (this.errorCbs.length) {
            this.errorCbs.forEach(cb => { cb(err) })
          }
        }
        onAbort && onAbort(err)
      }
      
      // 判断是否导航到相同的路由,如果是我们终止导航
      if (
        isSameRoute(route, current) &&
        route.matched.length === current.matched.length
      ) {
        this.ensureURL()
        return abort()
      }
    
      // 获取所有需要激活,更新,销毁的路由
      const {
        updated,
        deactivated,
        activated
      } = resolveQueue(this.current.matched, route.matched)
    
      // 获取所有需要执行的路由守卫
      const queue = [].concat(
        extractLeaveGuards(deactivated),
        this.router.beforeHooks,
        extractUpdateHooks(updated), 
        activated.map(m => m.beforeEnter),
        resolveAsyncComponents(activated)
      )
    
      this.pending = route
    
      // 定义迭代器
      const iterator = (hook: NavigationGuard, next) => {
        if (this.pending !== route) {
          return abort()
        }
        try {
          hook(route, current, (to: any) => {
            if (to === false || isError(to)) {
              this.ensureURL(true)
              abort(to)
            } else if (
              typeof to === 'string' ||
              (typeof to === 'object' && (
                typeof to.path === 'string' ||
                typeof to.name === 'string'
              ))
            ) {
              abort()
              if (typeof to === 'object' && to.replace) {
                this.replace(to)
              } else {
                this.push(to)
              }
            } else {
              next(to)
            }
          })
        } catch (e) {
          abort(e)
        }
      }
    
      // 迭代所有的路由守卫
      runQueue(
        queue,
        iterator, 
        () => {
          const postEnterCbs = []
          const isValid = () => this.current === route
          const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
          const queue = enterGuards.concat(this.router.resolveHooks)
          runQueue(queue, iterator, () => {
            if (this.pending !== route) {
              return abort()
            }
            this.pending = null
            onComplete(route)
            if (this.router.app) {
              this.router.app.$nextTick(() => {
                postEnterCbs.forEach(cb => { cb() })
              })
            }
          }
        )
      })
    }

    go, forward, back

    在VueRouter上定义的go,forward,back方法都是调用history的属性的go方法

    // index.js
    
    go (n) {
      this.history.go(n)
    }
    
    back () {
      this.go(-1)
    }
    
    forward () {
      this.go(1)
    }

    而hash上go方法调用的是history.go,它是如何更新RouteView的呢?答案是hash对象在setupListeners方法中添加了对popstate或者hashchange事件的监听。在事件的回调中会触发RoterView的更新

    // go方法调用history.go
    go (n) {
      window.history.go(n)
    }

    setupListeners

    我们在通过点击后退, 前进按钮或者调用back, forward, go方法的时候。我们没有主动更新_app.route和current。我们该如何触发RouterView的更新呢?通过在window上监听popstate,或者hashchange事件。在事件的回调中,调用transitionTo方法完成对_route和current的更新。

    或者可以这样说,在使用push,replace方法的时候,hash的更新在_route更新的后面。而使用go, back时,hash的更新在_route更新的前面。

    
    setupListeners () {
      const router = this.router
    
      const expectScroll = router.options.scrollBehavior
      const supportsScroll = supportsPushState && expectScroll
    
      if (supportsScroll) {
        setupScroll()
      }
    
      window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
        const current = this.current
        if (!ensureSlash()) {
          return
        }
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      })
    }

    HistoryRouter

    HistoryRouter的实现基本于HashRouter一致。差异在于HistoryRouter不会做一些容错处理,不会判断当前环境是否支持historyAPI。默认监听popstate事件,默认使用histroyAPI。感兴趣的同学可以看/history/html5.js中关于HistoryRouter的定义。

    组件

    RouterView

    RouterView是可以互相嵌套的,RouterView依赖了parent.属性,route即this._routerRoot._route。我们使用Vue.util.defineReactive将_router设置为响应式的。在transitionTo的回调中会更新_route, 这会触发RouteView的渲染。(渲染机制目前不是很了解,目前还没有看过Vue的源码,猛男落泪)。

    export default {
      name: 'RouterView',
      functional: true,
      // RouterView的name, 默认是default
      props: {
        name: {
          type: String,
          default: 'default'
        }
      },
      render (_, { props, children, parent, data }) {
        data.routerView = true
    
        // h为渲染函数
        const h = parent.$createElement
        const name = props.name
        const route = parent.$route
        const cache = parent._routerViewCache || (parent._routerViewCache = {})
    
        let depth = 0
        let inactive = false
        // 使用while循环找到Vue的根节点, _routerRoot是Vue的根实例
        // depth为当前的RouteView的深度,因为RouteView可以互相嵌套,depth可以帮组我们找到每一级RouteView需要渲染的组件
        while (parent && parent._routerRoot !== parent) {
          if (parent.$vnode && parent.$vnode.data.routerView) {
            depth++
          }
          if (parent._inactive) {
            inactive = true
          }
          parent = parent.$parent
        }
        data.routerViewDepth = depth
    
        if (inactive) {
          return h(cache[name], data, children)
        }
    
        const matched = route.matched[depth]
        if (!matched) {
          cache[name] = null
          return h()
        }
    
        // 获取到渲染的组件
        const component = cache[name] = matched.components[name]
    
        // registerRouteInstance会在beforeCreated中调用,又全局的Vue.mixin实现
        // 在matched.instances上注册组件的实例, 这会帮助我们修正confirmTransition中执行路由守卫中内部的this的指向
        data.registerRouteInstance = (vm, val) => {
          const current = matched.instances[name]
          if (
            (val && current !== vm) ||
            (!val && current === vm)
          ) {
            matched.instances[name] = val
          }
        }
    
        ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
          matched.instances[name] = vnode.componentInstance
        }
    
        let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
        if (propsToPass) {
          propsToPass = data.props = extend({}, propsToPass)
          const attrs = data.attrs = data.attrs || {}
          for (const key in propsToPass) {
            if (!component.props || !(key in component.props)) {
              attrs[key] = propsToPass[key]
              delete propsToPass[key]
            }
          }
        }
        // 渲染组件
        return h(component, data, children)
      }
    }

    结语

    我们把VueRouter源码看完了。总体来说不是很复杂。总的来说就是使用Vue.util.defineReactive将实例的_route属性设置为响应式。而push, replace方法会主动更新属性_route。而go,back,或者点击前进后退的按钮则会在onhashchange或者onpopstate的回调中更新_route,而_route的更新会触发RoterView的重新渲染

    但是也略过了比如keep-live,滚动行为的处理。我打算接下来,结合VueRouter核心原理实现了一个简易版的VueRouter,当然现在还没有开始。

    其他

    从3月中下旬左右一直在学一些库的源码,本身学习源码对工作帮助并不是很大。因为像VueRouter,Preact都有着完善的文档。看源码单纯是个人的兴趣,不过学习了这些库的源码,自己实现一个简易版本,还是挺有成就感的一件事情。

    展开全文
  • 针对于VueRouter源码分析,其实可以分为两部分来分析,或者说大部分的Vue插件都可以按照这样的方式去区分:安装插件的install方法和插件本身实例(Class)的实现。 install方法 在install方法中的操作其实可以分为...

    针对于VueRouter的源码分析,其实可以分为两部分来分析,或者说大部分的Vue插件都可以按照这样的方式去区分:安装插件的install方法和插件本身实例(Class)的实现。

    1. install方法
      在install方法中的操作其实可以分为三部分:
      1. 封装了一个mixin
      2. 注册了两个全局组件
      3. 定义了两个原型属性
    2. 插件类的实现
      每一个插件类的实现,其代码内容是相当庞大的,必须借助拆分的方法继续进行拆分,才有继续学下去勇气。
      VueRouter类的实现可以分为三部分,之后再细分,这三部分的内容:
      1. VueRouter的构造函数(constructor)
        1.1. 初始化路由钩子队列
        1.2. 初始化matcher用于处理路由匹配逻辑并创建了路由对象
        1.3. 初始化history用于执行过渡逻辑并执行钩子队列
      2. VueRouter的路由切换逻辑的执行(首次执行)
        2.1. init方法中调用history对象的transitionTo方法
        2.2. 在transitionTo方法中通过Router实例的match方法获取当前路由匹配的数据并赋值给一个新对象route
        2.3. 把route传递给confirmTransition方法,执行钩子队列中的事件
        2.4. 在confirmTransition的成功回调函数中,更新current对象,引起被响应式化的_route更新从而触发组件重新渲染,
        2.5 调用ensureURL触发各自模式的路由更新方法,切换切面
      3. 路由的页面切换(非首次执行)
        3.1 在各自模式的push、replace、back等可以改变页面路由的方法,都是先调用transitionTo方法,然后在其成功回调函数中调用各自模式的更新路由的方法(pushHash、pushState)
    展开全文
  • class vueRouter { constructor(options) { this.mode = options.mode || 'hash'; //模式 this.routes = options.routes || []; //路由 this.history = new historyRouter; //current值 t.
    
    class historyRoute{
      construcor() {
        this.current = null;
      }
    }
    class vueRouter {
      constructor(options) {
        this.mode = options.mode || 'hash'; //模式
        this.routes = options.routes || []; //路由
        this.history = new historyRouter; //current值
        this.routesMap = this.createMap(this.routes) //转换成key-vaule
        this.init();
      }
      init() {
        if(this.mode =='hash') {
          location.hash ? '' : location.hash = '/';
          window.addEventListener('load',()=>{
            this.history.current = location.hash.slice(1)
          })
          window.addEventListener('hashchange',()=>{
            this.history.current = location.hash.slice(1)
          })
        }else {
          location.pathname ? '' : location.pathname = '/';
          window.addEventListener('load',()=>{
            this.history.current = location.pathname
          })
          window.addEventListener('popstate',()=>{
            this.history.current = location.pathname
          })
        }
      }
      createMap(routes) {
        return routes.reduce((pre,current)=>{
          pre[current.path] = current.component;
          return pre 
        })
      }
    }
    //Vue监视current变量
    vueRouter.install = function(Vue) {
      Vue.mixin({
        beforeCreate() {
          if(this.$options && this.$options.router) {
            this._root = this; //就是把_root绑定到Vue实例
            this._router = this.$options.router;
            Vue.util.defineReactive(this, 'current', this._router.history)
          }else {
            this._root = this.$parent._root
          }
        }
      })
      //获取新组建以及render
      Vue.component('router-view',{
        //渲染新组件
        render(h) {
          let current = this._self._root._router.history.current;
          let routesMap = this._self._root._router.routesMap;
          return h(routesMap[current])
        }
      })
    }
    export default vueRouter
      
    
    展开全文
  • VueRouter源码index.js中有一小部分是关于install方法的,也是我们分析install方法的入口。 import { install } from './install' // 挂载install; VueRouter.install = install VueRouter.version = '__VERSION_...
  • class VueRouter { static install(Vue) { // 1. 判断当前插件是否已经被安装 if (VueRouter.install.installed) return; VueRouter.install.installed = true; // 2.把Vue构造函数记录到全局变量 _Vue = Vue;...
  • 主要介绍了vue router 源码概览的案例分析,本文通过实例代码案例分析给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
  • 我们首先用new VueRouter()创建一个VueRouter实例,之后在创建根Vue实例时把这个VueRouter实例作为router属性传递进去,在这个过程中一般有两种方法可以获取到该VueRouter的实例 第一种是node环境下,我们一般在项目...
  • vue-router源码分析

    2018-12-03 15:52:24
    vue-router源码分析 上一篇我们写了前端路由,这几天比较闲,抽空研究了vue.js官方路由的vue-router的实现。 本文主要是以vue-router2.7.0(https://github.com/vuejs/vue-router)版本的源代码进行分析。 首先...
  • 深入Vue-Router源码分析路由实现原理 使用Vue开发SPA应用,离不开vue-router,那么vuevue-router是如何协作运行的呢,下面从使用的角度,大白话帮大家一步步梳理下vue-router的整个实现流程。 到发文时使用的版本...
  • 深入Vue-Router源码分析路由实现原理

    万次阅读 多人点赞 2018-03-02 11:19:48
    深入Vue-Router源码分析路由实现原理使用Vue开发SPA应用,离不开vue-router,那么vuevue-router是如何协作运行的呢,下面从使用的角度,大白话帮大家一步步梳理下vue-router的整个实现流程。
  • 相信用过 Vue 框架的朋友,一定听过 Vue Router。要想写一个复杂的单页应用,...本文将从源码的角度分析 hash 与 history 这两种模式的实现,本文基于 Vue Router 3.5.1 版本。 单页应用 当我们在浏览器地址栏输入一个
  • 前言 虽然最近需求着实不少,但是感觉自己学习劲头还是蛮足的,并没有被需求压垮。...Vue-Router源码分析之index.js Vue-Router源码分析之install方法 正文 vue-router类里面都做了什么? index.js是vue-rout...
  • Vue-Router源码分析之index.js Vue-Router源码分析之install方法 Vue是怎么注册插件的呢? 使用过Vue的coder都知道,如果想注册一个vue的插件,在vue对象上能够使用的话(并不是绑在Vue.prototype上的那种暴力方式)...
  • 1,vue中使用vueRouter需要通过vue.use(vueRouter),众所周知,这里实际上调用了VueRouter的install方法,对Vue进行了扩展,vueRouter的install方法如下: function install (Vue) { //如果已经挂载了,跳过 if ...
  • VueRouter 源码深度解析

    2018-08-01 18:55:00
    VueRouter 源码深度解析 该文章内容节选自团队的开源项目InterviewMap。项目目前内容包含了 JS、网络、浏览器相关、性能优化、安全、框架、Git、数据结构、算法等内容,无论是基础还是进阶,亦或是源码解读,你...
  • 万变不离其宗,看源码先找入口,首先看vue-router的入口,定义在src/index中。 vue-router是基于类(class)实现。 类中提供了构造方法和一些其他方法,类定义完后给类添加了install方法。 vue-router其实是一个vue...
  • vue-router的注册
  • vue-router源码分析笔记

    2020-04-21 11:26:52
    Vue.use ⽤户执⾏Vue.use的时候,实际执⾏的是模块的install⽅ ...class Router{ } function install(_Vue){ Vue = _Vue; Vue.mixin({ beforeCreate() { const options = this.$options; console....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,526
精华内容 1,410
关键字:

vuerouter源码分析

vue 订阅