vue-router实现原理

近期面试,遇到关于vue-router实现原理的问题,在查阅了相关资料后,根据自己理解,来记录下。
我们知道vue-router是vue的核心插件,而当前vue项目一般都是单页面应用,也就是说vue-router是应用在单页面应用中的。
那么,什么是单页面应用呢?在单页面应用出现之前,多页面应用又是什么样子呢?

单页面应用与多页面应用

单页面

即 第一次进入页面的时候会请求一个html文件,刷新清除一下。切换到其他组件,此时路径也相应变化,但是并没有新的html文件请求,页面内容也变化了。

原理是:JS会感知到url的变化,通过这一点,可以用js动态的将当前页面的内容清除掉,然后将下一个页面的内容挂载到当前页面上,这个时候的路由不是后端来做了,而是前端来做,判断页面到底是显示哪个组件,清除不需要的,显示需要的组件。这种过程就是单页应用,每次跳转的时候不需要再请求html文件了。

多页面

即 每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用。
原理是:传统的页面应用,是用一些超链接来实现页面切换和跳转的

其实刚才单页面应用跳转原理即 vue-router实现原理

vue-router实现原理

原理核心就是 更新视图但不重新请求页面。

vue-router实现单页面路由跳转,提供了三种方式:hash模式、history模式、abstract模式,根据mode参数来决定采用哪一种方式。

路由模式

vue-router 提供了三种运行模式:
● hash: 使用 URL hash 值来作路由。默认模式。
● history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。
● abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端

Hash模式

hash即浏览器url中#后面的内容,包含#。hash是URL中的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会加载相应位置的内容,不会重新加载页面。
也就是说

  • 即#是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中,不包含#。
  • 每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。

所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

History模式

HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面;

由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

有时,history模式下也会出问题:
eg:
hash模式下:xxx.com/#/id=5 请求地址为 xxx.com,没有问题。
history模式下:xxx.com/id=5 请求地址为 xxx.com/id=5,如果后端没有对应的路由处理,就会返回404错误;

为了应对这种情况,需要后台配置支持:
在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

abstract模式

abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。
根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)。

具体更加详细的文章,请参考:
Vue番外篇 -- vue-router浅析原理
vue-router的原理

hash模式
hash模式背后的原理是onhashchange事件

因为hash发生变化的url都会被浏览器记录下来,从而你会发现浏览器的前进后退都可以用了,同时点击后退时,页面字体颜色也会发生变化。这样一来,尽管浏览器没有请求服务器,但是页面状态和url一一关联起来,后来人们给它起了一个霸气的名字叫前端路由,成为了单页应用标配。

在hash模式下,前端路由修改的是#中的信息,而浏览器请求时是不带它玩的,仅hash符号之前的url会被包含在请求中所以没有问题.

history模式
通过history api,我们丢掉了丑陋的#,但是它也有个毛病:
不怕前进,不怕后退,就怕刷新,f5,(如果服务器中没有相应的响应或者资源,会分分钟刷出一个404来。),因为刷新是实实在在地去请求服务器的,不玩虚的。

history模式下,前端的url必须和实际向后端发起请求的url一致,如http://abc.com/user/id,后端如果没有对user/id的路由处理,将返回404错误。

vue-router底层实现方式

2.1实例化 Vue/Vue组件创建

new Vue({
router,
template: `
<div id="app">
<h1>Basic</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<router-link tag="li" to="/bar">/bar</router-link>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')​

2.2执行 vue-router 注入的 beforeCreate 钩子函数

通过Vue.mixin()

// ...
Vue.mixin({
beforeCreate () {
// 判断是否有 router
if (this.$options.router) {
// 赋值 _router
this._router = this.$options.router
// 初始化 init
this._router.init(this)
// 定义响应式的 _route 对象
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
}
})​

具体来说,首先判断实例化时 options 是否包含 router,如果包含也就意味着是一个带有路由配置的实例被创建了,此时才有必要继续初始化路由相关逻辑。然后给当前实例赋值_router,这样在访问原型上的 $router 的时候就可以得到 router 了。

下边来看里边两个关键:router.init 和 定义响应式的 _route 对象。

2.3执行 router.init(vm)
router.init 
然后来看 router 的 init 方法就干了哪些事情

(注册了对地址变更的监听,history.setupListeners())

依旧是在 src/index.js 中:

/* @flow */
import { install } from './install'
import { createMatcher } from './create-matcher'
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
import { inBrowser, supportsHistory } from './util/dom'
import { assert } from './util/warn'
export default class VueRouter {
// ...
init (app: any /* Vue component instance */) {
// ...
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(getLocation(history.base))
} else if (history instanceof HashHistory) {
history.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () => {
history.onHashChange()
})
})
}
history.listen(route => {
this.app._route = route
})
}
// ...
}
// ...​

可以看到初始化主要就是给 app 赋值,针对于 HTML5History 和 HashHistory 特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页,需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由,此时就是通过调用 transitionTo 来达到目的;

而且此时还有个注意点是针对于 HashHistory 有特殊处理,

2.3.1为什么不直接在初始化 HashHistory 的时候监听 hashchange 事件呢?
答:这个是为了修复vuejs/vue-router#725这个 bug 而这样做的,

原因是因为在初始化的时候如果此时的 hash 值不是以 / 开头的话就会补上 #/,这个过程会触发 hashchange 事件,所以会再走一次生命周期钩子,也就意味着会再次调用 beforeEnter 钩子函数。即beforeEnter 钩子就会被触发两次。

2.3.2transitionTo 方法的大概逻辑
在 src/history/base.js 中:

/* @flow */
import type VueRouter from '../index'
import { warn } from '../util/warn'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { START, isSameRoute } from '../util/route'
import { _Vue } from '../install'
export class History {
// ...
transitionTo (location: RawLocation, cb?: Function) {
// 调用 match 得到匹配的 route 对象
const route = this.router.match(location, this.current)
// 确认过渡
this.confirmTransition(route, () => {
// 更新当前 route 对象
this.updateRoute(route)
cb && cb(route)
// 子类实现的更新url地址
// 对于 hash 模式的话 就是更新 hash 的值
// 对于 history 模式的话 就是利用 pushstate / replacestate 来更新
// 浏览器地址
this.ensureURL()
})
}​

2.hashchange 或者路由跳转时,执行 history.transitionTo(...),在这个过程中,会进行地址匹配,得到一个对应当前地址的 route,然后将其设置到对应的 vm._route上。updateRoute.

只是进行了赋值,

2.3.3为什么 vm 就可以感知到路由的改变了呢?
答案在 vue-router 注入 Vue 的 beforeCreate 钩子函数中:

Vue.util.defineReactive(this, '_route', this._router.history.current)
采用与 Vue 本身数据相同的“数据劫持”方式,这样对 vm._route 的赋值会被 Vue 拦截到,并且触发 Vue 组件的更新渲染流程。

2.4视图更新
地址变更如何同步视图更新?

接上一步,vm._route 已经接收到路由的变更,从而触发视图更新。而当视图更新进一步调用到 <router-view> 的 render() 时,即进入了 <router-view> 的处理。

<router-view> 的 render() 采用函数调用(h())模式,而非通过模板生成。这也是 Vue2 支持的定义组件渲染逻辑的方式,类似 React 的 render()。

采用这种模式的好处是

可以完全利用 JavaScript 的能力来编写逻辑,不必受制于 Vue 的类 HTML 模板语法。

这里的主要处理逻辑是从根组件中取出当前的路由对象(parent.$route),然后取得该路由下对应的组件,然后交由该组件进行渲染:

return h(component, data, children)

这其中还涉及 <router-view> 嵌套的处理,不过主要逻辑就是这样了。

小结
vue-router 以插件方式“侵入”Vue,从而支持一个额外的 router 属性,以提供监听并改变组件路由数据的能力。这样每次路由发生改变后,可以同步到数据,进而“响应式”地触发组件的更新。

<router-view> 作为根组件下的子组件,从根组件那里可以获取到当前的路由对象,进而根据路由对象的组件配置,取出组件并用其替换当前位置的内容。这样,也就完成整个路由变更到视图变更的过程。

① 路由变更到视图变更的过程整理为:

hashchange
-->
match route
-->
set vm._route
-->
<router-view> render()
-->
render matched component
② 实现过程中的技术点包括:

Vue 插件机制
响应式数据机制
Vue 渲染机制
地址变更监听
 
————————————————
版权声明:本文为CSDN博主「chenzeze0707」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/chenzeze0707/article/details/90287466

vue-router实现原理的更多相关文章

  1. 「进阶篇」Vue Router 核心原理解析

    前言 此篇为进阶篇,希望读者有 Vue.js,Vue Router 的使用经验,并对 Vue.js 核心原理有简单了解: 不会大篇幅手撕源码,会贴最核心的源码,对应的官方仓库源码地址会放到超上,可以配 ...

  2. Vue Router的原理及history模式源码实现

    Hash 模式 URL中 # 后面的内容作为路径地址,可以通过location.url直接切换路由地址,如果只改变了#后面的内容,浏览器不会向服务器请求这个地址,会把这个地址 记录到浏览器的访问历史中 ...

  3. vue router.push(),router.replace(),router.go()和router.replace后需要返回两次的问题

    转载:https://www.cnblogs.com/lwwen/p/7245083.html https://blog.csdn.net/qq_15385627/article/details/83 ...

  4. 深入浅出的webpack构建工具--webpack4+vue+router项目架构(十四)

    阅读目录 一:vue-router是什么? 二:vue-router的实现原理 三:vue-router使用及代码配置 四:理解vue设置路由导航的两种方法. 五:理解动态路由和命名视图 六:理解嵌套 ...

  5. vue router的浏览器跳转行为

    最近做的项目中,涉及vue router 路由操作,其操作方法不同,产生的行为亦不同.本文通过对比实验,对其行为进行实验对比及总结,避免混淆. vue router的单页跳转的history模式,类似 ...

  6. [Vue 牛刀小试]:第十二章 - 使用 Vue Router 实现 Vue 中的前端路由控制

    一.前言 前端路由是什么?如果你之前从事的是后端的工作,或者虽然有接触前端,但是并没有使用到单页面应用的话,这个概念对你来说还是会很陌生的.那么,为什么会在单页面应用中存在这么一个概念,以及,前端路由 ...

  7. [Vue 牛刀小试]:第十三章 - Vue Router 基础使用再探(命名路由、命名视图、路由传参)

    一.前言 在上一章的学习中,我们简单介绍了前端路由的概念,以及如何在 Vue 中通过使用 Vue Router 来实现我们的前端路由.但是在实际使用中,我们经常会遇到路由传参.或者一个页面是由多个组件 ...

  8. 深入Vue响应式原理

    深入Vue.js响应式原理 一.创建一个Vue应用 new Vue({ data() { return { name: 'yjh', }; }, router, store, render: h =& ...

  9. 04 Vue Router路由管理器

    路由的基本概念与原理 Vue Router Vue Router (官网: https://router.vuejs.org/zh/)是Vue.js 官方的路由管理器. 它和vue.js的核心深度集成 ...

  10. 06 Vue路由简介,原理,实现及嵌套路由,动态路由

    路由概念 路由的本质就是一种对应关系,比如说我们在url地址中输入我们要访问的url地址之后,浏览器要去请求这个url地址对应的资源. 那么url地址和真实的资源之间就有一种对应的关系,就是路由. 路 ...

随机推荐

  1. nginx中location匹配规则介绍

    一,匹配规则 1,= 表示精确匹配 例子:http://localhost/  将匹配到 location = / {...} http://localhost/aaa  可以匹配到 location ...

  2. nginx环境准备

    一.环境调试确认 1.四项确认 确认公网已连通. 确认yum源可用. 确认iptables已经关闭. 确认selinux已经关闭. a.确认是否连通公网. ping www.baidu.com b.确 ...

  3. Zabbix - LINUX下CPU,硬盘,流量,内存监控

    转载自:https://blog.csdn.net/jxzhfei/article/details/47191431 1.LINUX下zabbix客户端安装 [root@mongodb114 ~]# ...

  4. Redis集群配置和常见异常解决

    前文 Redis的Cluster集群,是在分布式且开源环境下最佳的高可用解决方案,可以有效的解决服务器宕机下或高并发下,数据的完整性. 文档前提 Redis 3.0版本或更高版本.(3.0版本开始支持 ...

  5. C++ 学习笔记整理

    目录 STL 容器 set的用法简介 unordered_set 变量类型转换 stringstream 类型转换 数据结构 哈希表 日常小技巧 C++输入输出流加速器,关闭同步流,ios::sync ...

  6. flannel下k8s pod及容器无法跨主机互通问题

    参照文档    https://blog.csdn.net/a610786189/article/details/80340556 https://blog.csdn.net/weixin_43092 ...

  7. 【MM系列】SAP技巧之更改布局

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[MM系列]SAP技巧之更改布局   前言部分 ...

  8. 关于SQL关键字"having "

    HAVING 子句 在 SQL 中增加 HAVING 子句原因是,WHERE 关键字无法与合计函数一起使用. SQL HAVING 语法 SELECT column_name, aggregate_f ...

  9. 【VS开发】【DSP开发】地址对齐

    组成原理说明------地址对齐 1.引入 1.1如下面的代码,定义了三个变量,int,char,int,并输出他们的十六进制的内存地址. #include<stdio.h> int ma ...

  10. Akka入门

    原文:http://doc.akka.io/docs/akka/2.3.6/intro/getting-started.html 预备知识 AKKA要求你的计算机已经安装了Java1.6或更高版本. ...