vue@3.3.4源码解析

前端路由主要有2部分组成:1 、url的处理; 2、 组件的加载

[toc]

install 函数

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 () {
      if (isDef(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) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  // $router 和 $route 绑定在Vue的原型对象上,方便全局访问
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // 注册router-view和router-link组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

vuex一样,vue-router的注入时机也是在beforeCreated.不过在destroyed的生命周期钩子中多了一步

VueRouter 类

History类

export class History {
  // 定义私有变量
  router: Router
  base: string
  current: Route
  pending: ?Route
  cb: (r: Route) => void
  ready: boolean
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  listeners: Array<Function>
  cleanupListeners: Function

  // implemented by sub-classes
  +go: (n: number) => void
  +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
  +replace: (
    loc: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) => void
  +ensureURL: (push?: boolean) => void
  +getCurrentLocation: () => string
  +setupListeners: Function

  constructor (router: Router, base: ?string) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
    this.listeners = []
  }

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

  onReady (cb: Function, errorCb: ?Function) {
    if (this.ready) {
      cb()
    } else {
      this.readyCbs.push(cb)
      if (errorCb) {
        this.readyErrorCbs.push(errorCb)
      }
    }
  }

  onError (errorCb: Function) {
    this.errorCbs.push(errorCb)
  }
  // 重点: 
  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    // catch redirect option https://github.com/vuejs/vue-router/issues/3201
    try {
      // 获取匹配的路由对象
      route = this.router.match(location, this.current)
    } catch (e) {
      this.errorCbs.forEach(cb => {
        cb(e)
      })
      // Exception should still be thrown
      throw e
    }
    // 保存旧路由对象
    const prev = this.current
    this.confirmTransition(
      route,
      () => {
        // 更新路由对象
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })

        // fire ready cbs once
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          // Initial redirection should not mark the history as ready yet
          // because it's triggered by the redirection instead
          // https://github.com/vuejs/vue-router/issues/3225
          // https://github.com/vuejs/vue-router/issues/3331
          if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
            this.ready = true
            this.readyErrorCbs.forEach(cb => {
              cb(err)
            })
          }
        }
      }
    )
  }

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    this.pending = route
    const abort = err => {
      // changed after adding errors with
      // https://github.com/vuejs/vue-router/pull/3047 before that change,
      // redirect and aborted navigation would produce an err == null
      if (!isNavigationFailure(err) && isError(err)) {
        if (this.errorCbs.length) {
          this.errorCbs.forEach(cb => {
            cb(err)
          })
        } else {
          if (process.env.NODE_ENV !== 'production') {
            warn(false, 'uncaught error during route navigation:')
          }
          console.error(err)
        }
      }
      onAbort && onAbort(err)
    }
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      if (route.hash) {
        handleScroll(this.router, current, route, false)
      }
      return abort(createNavigationDuplicatedError(current, route))
    }

    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )

    const queue: Array<?NavigationGuard> = [].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(m => m.beforeEnter),
      // async components
      resolveAsyncComponents(activated)
    )

    const iterator = (hook: NavigationGuard, next) => {
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      try {
        hook(route, current, (to: any) => {
          if (to === false) {
            // next(false) -> abort navigation, ensure current URL
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) {
            this.ensureURL(true)
            abort(to)
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            // next('/') or next({ path: '/' }) -> redirect
            abort(createNavigationRedirectedError(current, route))
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // confirm transition and pass on the value
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }

    runQueue(queue, iterator, () => {
      // wait until async components are resolved before
      // extracting in-component enter guards
      const enterGuards = extractEnterGuards(activated)
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            handleRouteEntered(route)
          })
        }
      })
    })
  }

  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }

  setupListeners () {
    // Default implementation is empty
  }

  teardown () {
    // clean up event listeners
    // https://github.com/vuejs/vue-router/issues/2341
    this.listeners.forEach(cleanupListener => {
      cleanupListener()
    })
    this.listeners = []

    // reset current history route
    // https://github.com/vuejs/vue-router/issues/3294
    this.current = START
    this.pending = null
  }
}

HTML5History

export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {
    super(router, base)
     // 
    this._startLocation = getLocation(this.base)
  }
  // 设置监听器
  setupListeners () {
    // 如果存在listener,直接返回
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    // 是否支持history.pushState api
    const supportsScroll = supportsPushState && expectScroll
        
    // 如果浏览器支持pushState的api,则设置popstate的监听事件
    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }
    
    // 处理路由监听事件函数
    const handleRoutingEvent = () => {
      const current = this.current

      // Avoiding first `popstate` event dispatched in some browsers but first
      // history route not updated since async guard at the same time.
      // 避免某些浏览器首次加载触发popstate事件,导致路由状态更新不同步
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return
      }

      this.transitionTo(location, route => {
        if (supportsScroll) {
          // 保存页面滚动条位置
          handleScroll(router, route, current, true)
        }
      })
    }
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }
  // 封装history go方法
  go (n: number) {
    window.history.go(n)
  }
  // 
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      // 
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }
  // 
  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  ensureURL (push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath)
      push ? pushState(current) : replaceState(current)
    }
  }

  getCurrentLocation (): string {
    return getLocation(this.base)
  }
}

export function getLocation (base: string): string {
  let path = window.location.pathname
  const pathLowerCase = path.toLowerCase()
  const baseLowerCase = base.toLowerCase()
  // base="/a" shouldn't turn path="/app" into "/a/pp"
  // https://github.com/vuejs/vue-router/issues/3555
  // so we ensure the trailing slash in the base
  if (base && ((pathLowerCase === baseLowerCase) ||
    (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

getLocation

获取url的path,不包含base

export function getLocation (base: string): string {
  let path = window.location.pathname
  const pathLowerCase = path.toLowerCase()
  const baseLowerCase = base.toLowerCase()
  // base="/a" shouldn't turn path="/app" into "/a/pp"
  // https://github.com/vuejs/vue-router/issues/3555
  // so we ensure the trailing slash in the base
  if (base && ((pathLowerCase === baseLowerCase) ||
    (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

setupScroll 函数

export function setupScroll () {
  // Prevent browser scroll behavior on History popstate
  if ('scrollRestoration' in window.history) {
    window.history.scrollRestoration = 'manual'
  }
  // Fix for #1585 for Firefox
  // Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
  // Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
  // window.location.protocol + '//' + window.location.host
  // location.host contains the port and location.hostname doesn't
  const protocolAndPath = window.location.protocol + '//' + window.location.host
  const absolutePath = window.location.href.replace(protocolAndPath, '')
  // preserve existing history state as it could be overriden by the user
  const stateCopy = extend({}, window.history.state)
  stateCopy.key = getStateKey()
  window.history.replaceState(stateCopy, '', absolutePath)
  window.addEventListener('popstate', handlePopState)
  return () => {
    window.removeEventListener('popstate', handlePopState)
  }
}

pushState函数

export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      // preserve existing history state as it could be overriden by the user
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

replaceState函数

export function replaceState (url?: string) {
  pushState(url, true)
}

HashHistory

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    // 如果支持popstate则,使用popstate监听,否则使用hashchange监听,注意vue-router4已经不支持hashchange监听
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

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

  go (n: number) {
    window.history.go(n)
  }

  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }

  getCurrentLocation () {
    return getHash()
  }
}

pushHash

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

replaceHash

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

Hash路由与History的区别

  1. hash路由在地址栏URL上有#,而history路由没有会好看一点
  2. 我们进行回车刷新操作,hash路由会加载到地址栏对应的页面,而history路由一般就404报错了,所以history 在部署的时候,如 nginx, 需要只渲染⾸⻚,让⾸⻚根据路径重新跳转。
  3. hash路由支持低版本的浏览器,而history路由是HTML5新增的API。(IE10及以上)
  4. 默认改变hash路由,浏览器端不会发出请求,主要用于锚点;history路由 go/back/forward以及浏览器中的前进,后退按钮,一般都会向服务器发起请求
  5. hash 模式,是不⽀持SSR的,但是 history 模式可以做 SSR

History对象

  • pushState / replaceState都不会触发popState事件
  • popState什么时候触发

    • 点击浏览器的前进/后退按钮
    • back/forward/go

导航守卫的触发顺序

  1. 【组件】- 前一个组件的beforeRouteLeave
  2. 【全局】- router.beforeEach
  3. 【组件】- 如果是路由的参数变化,会触发beforeRouteUpdate
  4. 【配置文件】- beforeEnter
  5. 【组件】内部声明的beforeRouteEnter
  6. 【全局】beforeResolve
  7. 【全局】router.afterEach

手写Hsitory路由

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>History 路由</title>
  </head>
  <body>
    <div id="container">
      <button onclick="window.location.hash = '#'">首页</button>
      <button onclick="window.location.hash = '#about'">关于我们</button>
      <button onclick="window.location.hash = '#user'">用户列表</button>
    </div>

    <div id="context"></div>
    <script>
      class HistoryRouter {
        constructor() {
          this.routes = {};
          this._bindPopstate();
          // this.init();
        }

        init(path) {
          window.history.replaceState({ path }, null, path);
          const cb = this.routes[path];
          if (cb) {
            cb();
          }
        }

        route(path, callback) {
          this.routes[path] = callback || function () {};
        }

        go(path) {
          window.history.pushState({ path }, null, path);
          const cb = this.routes[path];
          if (cb) {
            cb();
          }
        }

        _bindPopstate() {
          window.addEventListener("popstate", (e) => {
            const path = e.state && e.state.path;
            this.routes[path] && this.routes[path]();
          });
        }
      }

      const Route = new HistoryRouter();

      Route.route("./about", () => changeText("关于我们页面"));
      Route.route("./user", () => changeText("用户列表页"));
      Route.route("./", () => changeText("首页"));

      function changeText(arg) {
        document.getElementById("context").innerHTML = arg;
      }

      container.addEventListener("click", (e) => {
        if (e.target.tagName === "A") {
          e.preventDefault();
          Route.go(e.target.getAttribute("href"));
        }
      });
    </script>
  </body>
</html>

手写Hash路由

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hash 路由</title>
  </head>
  <body>
    <div id="container">
      <button onclick="window.location.hash = '#'">首页</button>
      <button onclick="window.location.hash = '#about'">关于我们</button>
      <button onclick="window.location.hash = '#user'">用户列表</button>
    </div>

    <div id="context"></div>
    <script>
      class HashRouter {
        constructor() {
          this.routes = {};
          this.refresh = this.refresh.bind(this);
          window.addEventListener("load", this.refresh);
          window.addEventListener("hashchange", this.refresh);
        }

        route(path, callback) {
          this.routes[path] = callback || function () {};
        }

        refresh() {
          const path = `/${window.location.hash.slice(1) || ""}`;
          this.routes[path]();
        }
      }
      const Route = new HashRouter();

      Route.route("/about", () => changeText("关于我们页面"));
      Route.route("/user", () => changeText("用户列表页"));
      Route.route("/", () => changeText("首页"));
      
      function changeText(arg) {
        document.getElementById("context").innerHTML = arg;
      }

    </script>
  </body>
</html>

webpack import函数

vue 路由懒加载时通过Vue的异步组件和Webpack的代码分割功能来实现的。

代码分割是webpack最引人注目的特性之一。这个特性允许您将代码分割成各种包,然后可以按需或并行加载这些包。它可以用来实现更小的bundle和控制资源加载优先级,如果使用正确,会对加载时间产生重大影响。

这里主要分析import()这种代码分割方式,其他方式忽略。

  1. 首先,webpack遇到import方法时,会将其当成一个代码分割点,也就是说碰到import方法了,那么就去解析import方法
  2. 然后,import引用的文件,webpack会将其编译成一个jsonp,也就是一个自执行函数,然后函数内部是引用的文件的内容,因为到时候是通过jsonp的方法去加载的
  3. 具体就是,import引用文件,会先调用require.ensure方法(打包的结果来看叫require.e),这个方法主要是构造一个promise,会将resolverejectpromise放到一个数组中,将promise放到一个队列中。
  4. 然后,调用require.load(打包结果来看叫require.l)方法,这个方法主要是创建一个jsonp,也就是创建一个script标签,标签的url就是文件加载地址,然后塞到document.head中,一塞进去,就会加载该文件了。
  5. 加载完,就去执行这段jsonp,主要就是把moduleIdmodule内容存到modules数组中,然后再去走webpack内置的require
  6. webpack内置的require,主要是先判断缓存,这个moduleId是否缓存过了,如果缓存过了,就直接返回。如果没有缓存,再继续往下走,也就是加载module内容,然后最终内容会挂在都module,exports上,返回module.exports就返回了引用文件的最终执行结果。

简单讲就是:promise.all +jsonp +动态创建script标签

参考文章


看见了
876 声望16 粉丝

前端开发,略懂后台;


下一篇 »
微前端