Vue-Router의 구현 원리에 대해 이야기하는 기사

青灯夜游
풀어 주다: 2022-12-01 20:24:05
앞으로
3447명이 탐색했습니다.

Vue-Router의 구현 원리에 대해 이야기하는 기사

저희는 Vue를 사용해 실제 프로젝트를 개발할 때 공식 플러그인 Vue-Router를 사용하게 되는데, 대부분의 학생들이 라우팅 개념에 대해 잘 알고 있을 거라 생각합니다. > 라우팅 문제를 해결하는 데 도움을 드립니다. 그 기능은 다양한 경로에 따라 다양한 보기로 매핑하는 것입니다. 이 문서에서는 더 이상 라우팅 및 API의 기본 사용에 대해 설명하지 않습니다. 잘 모르는 학생은 공식 문서를 직접 참조할 수 있습니다. vue-router3은 vue2에 해당합니다vue-router4는 vue3에 해당합니다. Vue 开发过实际项目的时候都会用到 Vue-Router 这个官方插件来帮我们解决路由的问题。它的作用就是根据不同的路径映射到不同的视图。本文不再讲述路由的基础使用和API,不清楚的同学可以自行查阅官方文档vue-router3 对应 vue2 和 vue-router4 对应 vue3

今天我们主要是谈谈Vue-Router的实现原理,感兴趣的小伙伴可以继续往下看,大佬请止步。

本文 vue-router 版本为 3.5.3

路由

既然我们在分析路由,我们首先来说说什么是路由,什么是后端路由、什么是前端路由。

路由就是根据不同的 url 地址展示不同的内容或页面,早期路由的概念是在后端出现的,通过服务器端渲染后返回页面,随着页面越来越复杂,服务器端压力越来越大。后来ajax异步刷新的出现使得前端也可以对url进行管理,此时,前端路由就出现了。【学习视频分享:vue视频教程web前端视频

我们先来说说后端路由

后端路由

后端路由又可称之为服务器端路由,因为对于服务器来说,当接收到客户端发来的HTTP请求,就会根据所请求的URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。

对于最简单的静态资源服务器,可以认为,所有URL的映射函数就是一个文件读取操作。 对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理,等等。

然后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的HTML页面。早期的jsp就是这种模式。

前端路由

刚刚也介绍了,在前后端没有分离的时候,服务端都是直接将整个 HTML 返回,用户每次一个很小的操作都会引起页面的整个刷新(再加上之前的网速还很慢,所以用户体验可想而知)。

在90年代末的时候,微软首先实现了 ajax(Asynchronous JavaScript And XML) 这个技术,这样用户每次的操作就可以不用刷新整个页面了,用户体验就大大提升了。

虽然数据能异步获取不用每个点击都去请求整个网页,但是页面之间的跳转还是会加载整个网页,体验不是特别好,还有没有更好的方法呢?

至此异步交互体验的更高级版本 SPA单页应用 就出现了。单页应用不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的。既然页面的跳转是无刷新的,也就是不再向后端请求返回 HTML页面。

页面跳转都不从后端获取新的HTML页面,那应该怎么做呢?所以就有了现在的前端路由。

可以理解为,前端路由就是将之前服务端根据 url 的不同返回不同的页面的任务交给前端来做。在这个过程中,js会实时检测url的变化,从而改变显示的内容。

前端路由优点是用户体验好,用户操作或页面跳转不会刷新页面,并且能快速展现给用户。缺点是首屏加载慢,因为需要js动态渲染展示内容。而且由于内容是js动态渲染的所以不利于SEO

下面我们正式进入Vue-Router原理分析阶段。

分析Vue-Router.install方法

我们先来看看install.js,这个方法会在Vue.use(VueRouter)

오늘은 주로 Vue-Router의 구현 원리에 대해 이야기하겠습니다. 관심 있는 친구들은 계속 읽으세요.

이 기사의 vue-router 버전은 3.5.3🎜

라우팅

🎜입니다. 라우팅 분석, 먼저 라우팅이 무엇인지, 백엔드 라우팅이 무엇인지, 프런트엔드 라우팅이 무엇인지 이야기해 보겠습니다. 🎜🎜라우팅은 서로 다른 url 주소를 기반으로 서로 다른 콘텐츠나 페이지를 표시하는 것입니다. 초기 라우팅 개념이 백엔드에 나타났고, 페이지가 많아지면서 페이지가 반환되었습니다. 더욱 복잡해지고 서버 측 압력이 증가하고 있습니다. 나중에 ajax 비동기 새로 고침이 등장하면서 프런트 엔드에서 url을 관리할 수 있게 되었습니다. 이때 프런트 엔드 라우팅이 등장했습니다. [학습 동영상 공유: vue 동영상 튜토리얼, 웹 프런트엔드 동영상]🎜🎜하자 먼저 얘기해보세요 백엔드 라우팅🎜

백엔드 라우팅

🎜백엔드 라우팅은 서버 측 라우팅이라고도 합니다. 서버의 경우 클라이언트로부터 HTTP 요청을 받은 후 요청된 URL을 기반으로 해당 매핑 함수를 찾은 다음 함수를 실행하고 함수의 값을 반환하기 때문입니다. 반환 값 클라이언트로 전송됩니다. 🎜🎜가장 간단한 정적 리소스 서버의 경우 모든 URL의 매핑 기능은 파일 읽기 작업이라고 볼 수 있습니다. 동적 리소스의 경우 매핑 기능은 데이터베이스 읽기 작업일 수도 있고 일부 데이터 처리 등을 수행할 수도 있습니다. 🎜🎜그런 다음 읽은 데이터를 기반으로 서버 측에서 해당 템플릿을 사용하여 페이지를 렌더링한 다음 렌더링된 HTML 페이지가 반환됩니다. 초기 jsp는 이 모델이었습니다. 🎜

프런트엔드 라우팅

🎜방금 소개한 대로, 프런트엔드와 백엔드가 분리되지 않은 경우 서버가 직접 라우팅합니다. 전체 HTML이 반환되면 사용자가 작은 작업을 할 때마다 전체 페이지가 새로 고쳐집니다. 또한 이전 네트워크 속도는 여전히 매우 느렸기 때문에 사용자 경험을 상상할 수 있습니다. 🎜🎜1990년대 후반에 Microsoft는 처음으로 ajax(Asynchronous JavaScript And XML) 기술을 구현하여 사용자가 각 작업마다 전체 페이지를 새로 고칠 필요가 없었고 사용자 경험이 크게 향상되었습니다. . 🎜🎜클릭할 때마다 전체 웹페이지를 요청하지 않고도 데이터를 비동기적으로 얻을 수 있지만 페이지 간 이동 시에는 여전히 전체 웹페이지가 로드됩니다. 더 좋은 방법이 없을까요? 🎜🎜이 시점에서 비동기식 대화형 환경 SPA 단일 페이지 애플리케이션의 고급 버전이 나타납니다. 단일 페이지 애플리케이션은 페이지 상호 작용 중에 새로 고침이 없을 뿐만 아니라 페이지 점프도 새로 고침이 없습니다. 페이지 점프가 새로 고쳐지지 않으므로 HTML 페이지가 더 이상 백엔드 요청에 반환되지 않습니다. 🎜🎜페이지 점프를 하면 백엔드에서 새 HTML 페이지를 가져오지 못하는데 어떻게 해야 하나요? 따라서 현재 프런트 엔드 라우팅이 있습니다. 🎜🎜프런트엔드 라우팅은 서로 다른 URL에 따라 서로 다른 페이지를 반환하는 서버의 이전 작업을 프런트엔드에 넘겨주는 것으로 이해할 수 있습니다. 이 과정에서 js는 URL의 변경 사항을 실시간으로 감지하여 표시되는 콘텐츠를 변경합니다. 🎜🎜프런트 엔드 라우팅의 장점은 사용자 작업이나 페이지 이동이 페이지를 새로 고치지 않고 사용자에게 빠르게 표시될 수 있다는 것입니다. 단점은 표시 콘텐츠를 동적으로 렌더링하려면 js가 필요하기 때문에 첫 번째 화면이 느리게 로드된다는 것입니다. 그리고 콘텐츠가 js에 의해 동적으로 렌더링되기 때문에 SEO에 도움이 되지 않습니다. 🎜🎜이제 공식적으로 Vue-Router 원리 분석 단계에 들어갑니다. 🎜

Vue-Router.install 메소드 분석

🎜 먼저 install.js를 살펴보겠습니다. 이 메소드는 Vue.(VueRouter)를 사용할 때 호출됩니다. 🎜
// install.js

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

  // 为router-view组件关联路由组件
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    // 调用vm.$options._parentVnode.data.registerRouteInstance方法
    // 而这个方法只在router-view组件中存在,router-view组件定义在(../components/view.js @71行)
    // 所以,如果vm的父节点为router-view,则为router-view关联当前vm,即将当前vm做为router-view的路由组件
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      // 这里只会进来一次,因为只有Vue根实例才会有router属性。
      if (isDef(this.$options.router)) {
        // 所以这里的this就是Vue根实例
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // 将 _route 变成响应式
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 子组件会进入这里,这里也是把Vue根实例保存带_routerRoot属性上
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 为router-view组件关联路由组件
      registerInstance(this, this)
    },
    destroyed () {
      // destroyed hook触发时,取消router-view和路由组件的关联
      registerInstance(this)
    }
  })

  // 在原型上注入$router、$route属性,方便快捷访问
  Object.defineProperty(Vue.prototype, '$router', {
    // 上面说到每个组件的_routerRoot都是Vue根实例,所以都能访问_router
    get () { return this._routerRoot._router }
  })

  // 每个组件访问到的$route,其实最后访问的都是Vue根实例的_route
  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
}
로그인 후 복사
🎜주로 다음과 같은 일을 했습니다:🎜

避免重复安装

为了确保 install 逻辑只执行一次,用了 install.installed 变量做已安装的标志位。

传递Vue引用减少打包体积

用一个全局的 _Vue 来接收参数 Vue,因为作为 Vue 的插件对 Vue 对象是有依赖的,但又不能去单独去 import Vue,因为那样会增加包体积,所以就通过这种方式拿到 Vue 对象。

注册全局混入

Vue-Router 安装最重要的一步就是利用 Vue.mixin,在beforeCreatedestroyed生命周期函数中注入路由逻辑。

Vue.mixin我们知道就是全局 mixin,所以也就相当于每个组件的beforeCreatedestroyed生命周期函数中都会有这些代码,并在每个组件中都会运行。

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)
  }
})
로그인 후 복사

在这两个钩子中,this是指向当时正在调用钩子的vue实例

这两个钩子中的逻辑,在安装流程中是不会被执行的,只有在组件实例化时执行到钩子时才会被调用

先看混入的 beforeCreate 钩子函数

它先判断了this.$options.router是否存在,我们在new Vue({router})时,router才会被保存到到Vue根实例$options上,而其它Vue实例$options上是没有router的,所以if中的语句只在this === new Vue({router})时,才会被执行,由于Vue根实例只有一个,所以这个逻辑只会被执行一次。

对于根 Vue 实例而言,执行该钩子函数时定义了 this._routerRoot 表示它自身(Vue根实例);this._router 表示 VueRouter 的实例 router,它是在 new Vue 的时候传入的;

另外执行了 this._router.init() 方法初始化 router,这个逻辑在后面讲初始化的时候再介绍。

然后用 defineReactive 方法把 this._route 变成响应式对象,保证_route变化时,router-view会重新渲染,这个我们后面在router-view组件中会细讲。

我们再看下else中具体干了啥

主要是为每个组件定义_routerRoot,对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候 this._routerRoot 始终指向的离它最近的传入了 router 对象作为配置而实例化的父实例(也就是永远等于根实例)。

所以我们可以得到,在每个vue组件都有 this._routerRoot === vue根实例this._routerRoot._router === router对象

对于 beforeCreatedestroyed 钩子函数,它们都会执行 registerInstance 方法,这个方法的作用我们也是之后会介绍。

添加$route、$router属性

接着给 Vue 原型上定义了 $router$route 2 个属性的 get 方法,这就是为什么我们可以在任何组件实例上都可以访问 this.$router 以及 this.$route

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

Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
로그인 후 복사

我们可以看到,$router其实返回的是this._routerRoot._router,也就是vue根实例上的router,因此我们可以通过this.$router来使用router的各种方法。

$route其实返回的是this._routerRoot._route,其实就是this._router.history.current,也就是目前的路由对象,这个后面会细说。

注册全局组件

通过 Vue.component 方法定义了全局的 <router-link><router-view> 2 个组件,这也是为什么我们在写模板的时候可以直接使用这两个标签,它们的作用我想就不用笔者再说了吧。

钩子函数的合并策略

最后设置路由组件的beforeRouteEnterbeforeRouteLeavebeforeRouteUpdate守卫的合并策略。

总结

那么到此为止,我们分析了 Vue-Router 的安装过程,Vue 编写插件的时候通常要提供静态的 install 方法,我们通过 Vue.use(plugin) 时候,就是在执行 install 方法。Vue-Routerinstall 方法会给每一个组件注入 beforeCreatedestoryed 钩子函数,在beforeCreate 做一些私有属性定义和路由初始化工作。并注册了两个全局组件,然后设置了钩子函数合并策略。在destoryed 做了一些销毁工作。

下面我们再来看看Vue-Router的实例化。

分析init方法

前面我们提到了在 install 的时候会执行 VueRouterinit 方法( this._router.init(this) ),那么接下来我们就来看一下 init 方法做了什么。

init (app: any /* Vue component instance */) {
  // ...

  this.apps.push(app)

  // ...

  // main app previously initialized
  // return as we don&#39;t need to set up new history listener
  if (this.app) {
    return
  }

  this.app = app

  const history = this.history
  
  if (history instanceof HTML5History || history instanceof HashHistory) {
    const handleInitialScroll = routeOrError => {
      const from = history.current
      const expectScroll = this.options.scrollBehavior
      const supportsScroll = supportsPushState && expectScroll

      if (supportsScroll && &#39;fullPath&#39; in routeOrError) {
        handleScroll(this, routeOrError, from, false)
      }
    }
    
    // 1.setupListeners 里会对 hashchange或popstate事件进行监听
    const setupListeners = routeOrError => {
      history.setupListeners()
      handleInitialScroll(routeOrError)
    }
    // 2.初始化导航
    history.transitionTo(
      history.getCurrentLocation(),
      setupListeners,
      setupListeners
    )
  }

  // 3.路由全局监听,维护当前的route 
  // 当路由变化的时候修改app._route的值
  // 由于_route是响应式的,所以修改后相应视图会同步更新
  history.listen(route => {
    this.apps.forEach(app => {
      app._route = route
    })
  })
}
로그인 후 복사

这里主要做了如下几件事情:

设置了路由监听

const setupListeners = routeOrError => {
  history.setupListeners()
  handleInitialScroll(routeOrError)
}
로그인 후 복사

这里会根据当前路由模式监听hashchangepopstate事件,当事件触发的时候,会进行路由的跳转。(后面说到路由模式的时候会细说)

初始化导航

history.transitionTo(
  history.getCurrentLocation(),
  setupListeners,
  setupListeners
)
로그인 후 복사

进入系统会进行初始化路由匹配,渲染对应的组件。因为第一次进入系统,并不会触发hashchange或者popstate事件,所以第一次需要自己手动匹配路径然后进行跳转。

路由全局监听

history.listen(route => {
  this.apps.forEach(app => {
    app._route = route
  })
})
로그인 후 복사

当路由变化的时候修改app._route的值。由于_route是响应式的,所以修改后相应视图会同步更新。

总结

这里主要是做了一些初始化工作。根据当前路由模式监听对应的路由事件。初始化导航,根据当前的url渲染初始页面。最后切换路由的时候修改_route,由于_route是响应式的,所以修改后相应视图会同步更新。

分析VueRouter实例化

实例化就是我们new VueRouter({routes})的过程,我们来重点分析下VueRouter的构造函数。

constructor (options: RouterOptions = {}) {
  // ...
  
  // 参数初始化
  this.app = null
  this.apps = []
  this.options = options
  this.beforeHooks = []
  this.resolveHooks = []
  this.afterHooks = []
  // 创建matcher
  this.matcher = createMatcher(options.routes || [], this)

  // 设置默认模式和做不支持 H5 history 的降级处理
  let mode = options.mode || &#39;hash&#39;
  this.fallback =
    mode === &#39;history&#39; && !supportsPushState && options.fallback !== false
  if (this.fallback) {
    mode = &#39;hash&#39;
  }
  if (!inBrowser) {
    mode = &#39;abstract&#39;
  }
  this.mode = mode

  // 根据不同的 mode 实例化不同的 History 对象
  switch (mode) {
    case &#39;history&#39;:
      this.history = new HTML5History(this, options.base)
      break
    case &#39;hash&#39;:
      this.history = new HashHistory(this, options.base, this.fallback)
      break
    case &#39;abstract&#39;:
      this.history = new AbstractHistory(this, options.base)
      break
    default:
      if (process.env.NODE_ENV !== &#39;production&#39;) {
        assert(false, `invalid mode: ${mode}`)
      }
  }
}
로그인 후 복사

这里主要做了如下几件事情:

初始化参数

我们看到在最开始有些参数的初始化,这些参数到底是什么呢?

this.app 用来保存根 Vue 实例。

this.apps 用来保存持有 $options.router 属性的 Vue 实例。

this.options 保存传入的路由配置,也就是前面说的RouterOptions

this.beforeHooksthis.resolveHooksthis.afterHooks 表示一些钩子函数。

this.fallback 表示在浏览器不支持 historyapi的情况下,根据传入的 fallback 配置参数,决定是否回退到hash模式。

this.mode 表示路由创建的模式。

创建matcher

matcher,匹配器。简单理解就是可以通过url找到我们对应的组件。这一块内容较多,这里笔者就不再详细分析了。

确定路由模式

路由模式平时都会只说两种,其实在vue-router总共实现了 hashhistoryabstract 3 种模式。

VueRouter会根据options.modeoptions.fallbacksupportsPushStateinBrowser来确定最终的路由模式。

如果没有设置mode就默认是hash模式。

确定fallback值,只有在用户设置了mode:history并且当前环境不支持pushState且用户没有主动声明不需要回退(没设置fallback值位undefined),此时this.fallback才为true,当fallbacktrue时会使用hash模式。(简单理解就是如果不支持history模式并且只要没设置fallbackfalse,就会启用hash模式)

如果最后发现处于非浏览器环境,则会强制使用abstract模式。

实例化路由模式

根据mode属性值来实例化不同的对象。VueRouter的三种路由模式,主要由下面的四个核心类实现

  • History

    • 基础类
    • 位于src/history/base.js
  • HTML5History

    • 用于支持pushState的浏览器
    • src/history/html5.js
  • HashHistory

    • 用于不支持pushState的浏览器
    • src/history/hash.js
  • AbstractHistory

    • 用于非浏览器环境(服务端渲染)
    • src/history/abstract.js

HTML5HistoryHashHistoryAbstractHistory三者都是继承于基础类History

这里我们详细分析下HTML5HistoryHashHistory类。

HTML5History类

当我们使用history模式的时候会实例化HTML5History类

// src/history/html5.js

...

export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {
    // 调用父类构造函数初始化
    super(router, base)

    this._startLocation = getLocation(this.base)
  }

  // 设置监听,主要是监听popstate方法来自动触发transitionTo
  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    
    // 若支持scroll,初始化scroll相关逻辑
    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current

      // 某些浏览器,会在打开页面时触发一次popstate 
      // 此时如果初始路由是异步路由,就会出现`popstate`先触发,初始路由后解析完成,进而导致route未更新 
      // 所以需要避免
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return
      }
      
      // 路由地址发生变化,则跳转,如需滚动则在跳转后处理滚动
      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    
    // 监听popstate事件
    window.addEventListener(&#39;popstate&#39;, handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener(&#39;popstate&#39;, handleRoutingEvent)
    })
  }

  // 可以看到 history模式go方法其实是调用的window.history.go(n)
  go (n: number) {
    window.history.go(n)
  }

  // push方法会主动调用transitionTo进行跳转
  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方法会主动调用transitionTo进行跳转
  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&#39;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 + &#39;/&#39;)) === 0))) {
    path = path.slice(base.length)
  }
  return (path || &#39;/&#39;) + window.location.search + window.location.hash
}
로그인 후 복사

可以看到HTML5History类主要干了如下几件事。

  • 继承于History类,并调用父类构造函数初始化。

  • 实现了setupListeners方法,在该方法中检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑,监听了popstate事件,并在popstate触发时自动调用transitionTo方法。

  • 实现了go、push、replace等方法,我们可以看到,history模式其实就是使用的history api

// 可以看到 history模式go方法其实是调用的window.history.go(n)
go (n: number) {
  window.history.go(n)
}

// push、replace调用的是util/push-state.js,里面实现了push和replace方法
// 实现原理也是使用的history api,并且在不支持history api的情况下使用location api

export function pushState (url?: string, replace?: boolean) {
  ...
  const history = window.history
  try {
    if (replace) {
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      // 调用的 history.replaceState
      history.replaceState(stateCopy, &#39;&#39;, url)
    } else {
      // 调用的 history.pushState
      history.pushState({ key: setStateKey(genStateKey()) }, &#39;&#39;, url)
    }
  } catch (e) {
    window.location[replace ? &#39;replace&#39; : &#39;assign&#39;](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}
로그인 후 복사

总结

所以history模式的原理就是在js中路由的跳转(也就是使用pushreplace方法)都是通过history apihistory.pushStatehistory.replaceState两个方法完成,通过这两个方法我们知道了路由的变化,然后根据路由映射关系来实现页面内容的更新。

对于直接点击浏览器的前进后退按钮或者js调用 this.$router.go()this.$router.forward()this.$router.back()、或者原生js方法history.back()history.go()history.forward()的,都会触发popstate事件,通过监听这个事件我们就可以知道路由发生了哪些变化然后来实现更新页面内容。

注意history.pushStatehistory.replaceState这两个方法并不会触发popstate事件。在这两个方法里面他是有手动调用transitionTo方法的。

接下来我们再来看看HashHistory类

HashHistory类

当我们使用hash模式的时候会实例化HashHistory类

//src/history/hash.js

...

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()
  }

  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
    // 判断supportsPushState就是通过return window.history && typeof window.history.pushState === &#39;function&#39;
    const eventType = supportsPushState ? &#39;popstate&#39; : &#39;hashchange&#39;
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
  
  // 其实也是优先使用history的pushState方法来实现,不支持再使用location修改hash值
  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
    )
  }

  // 其实也是优先使用history的replaceState方法来实现,不支持再使用location修改replace方法
  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
    )
  }

  // 也是使用的history go方法
  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()
  }
}

function checkFallback (base) {
  const location = getLocation(base)
  if (!/^\/#/.test(location)) {
    window.location.replace(cleanPath(base + &#39;/#&#39; + location))
    return true
  }
}

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === &#39;/&#39;) {
    return true
  }
  replaceHash(&#39;/&#39; + path)
  return false
}

// 获取 # 后面的内容
export function getHash (): string {
  // We can&#39;t use window.location.hash here because it&#39;s not
  // consistent across browsers - Firefox will pre-decode it!
  let href = window.location.href
  const index = href.indexOf(&#39;#&#39;)
  // empty path
  if (index < 0) return &#39;&#39;

  href = href.slice(index + 1)

  return href
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf(&#39;#&#39;)
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

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

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
로그인 후 복사

可以看到HashHistory类主要干了如下几件事。

  • 继承于History类,并调用父类构造函数初始化。这里比HTML5History多了回退操作,所以,需要将history模式的url替换成hash模式,即添加上#,这个逻辑是由checkFallback实现的

  • 实现了setupListeners方法,在该方法中检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑。 监听了popstate事件或hashchange事件,并在相应事件触发时,调用transitionTo方法实现跳转。

通过const eventType = supportsPushState ? &#39;popstate&#39; : &#39;hashchange&#39;我们可以发现就算是hash模式优先使用的还是popstate事件。

  • 实现了go、push、replace等方法。

我们可以看到,hash模式实现的push、replace方法其实也是优先使用history里面的方法,也就是history api

// 可以看到 hash 模式go方法其实是调用的window.history.go(n)
go (n: number) {
  window.history.go(n)
}

// 在支持新的history api情况下优先使用history.pushState实现
// 否则使用location api
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

// 在支持新的history api情况下优先使用history.replaceState实现
// 否则使用location api
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
로그인 후 복사

总结

在浏览器链接里面我们改变hash值是不会重新向后台发送请求的,也就不会刷新页面。并且每次 hash 值的变化,还会触发hashchange 这个事件。

所以hash模式的原理就是通过监听hashchange事件,通过这个事件我们就可以知道 hash 值发生了哪些变化然后根据路由映射关系来实现页面内容的更新。(这里hash值的变化不管是通过js修改的还是直接点击浏览器的前进后退按钮都会触发hashchange事件)

对于hash模式,如果是在浏览器支持history api情况下,hash模式的实现其实是和history模式一样的。只有在不支持history api情况下才会监听hashchange事件。这个我们可以在源码中看出来。

Vue-Router의 구현 원리에 대해 이야기하는 기사

总结

总的来说就是使用 Vue.util.defineReactive 将实例的 _route 设置为响应式对象。在push, replace方法里会主动更新属性 _route。而 go,back,forward,或者通过点击浏览器前进后退的按钮则会在 hashchange 或者 popstate 的回调中更新 _route_route 的更新会触发 RoterView 的重新渲染。

对于第一次进入系统,并不会触发hashchange或者popstate事件,所以第一次需要自己手动匹配路径然后通过transitionTo方法进行跳转,然后渲染对应的视图。

(学习视频分享:web前端开发编程基础视频

위 내용은 Vue-Router의 구현 원리에 대해 이야기하는 기사의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿