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标签

参考文章

  • window History
  • hash路由和history路由的区别
  • webpack动静加载原理/webpack异步加载原理/webpack动静解析import()原理